[
  {
    "path": ".ctags",
    "content": "-R\n--exclude=build\n--exclude=env\n--exclude=.tox\n--python-kinds=-i\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: arsenetar\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Windows 10 / OSX 10.15 / Ubuntu 20.04 / Arch Linux]\n - Version [e.g. 4.1.0]\n\n**Additional context**\nAdd any other context about the problem here. You may include the debug log although it is normally best to attach it as a file.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "content": "name: \"CodeQL\"\n\non:\n  push:\n    branches: [master]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [master]\n  schedule:\n    - cron: \"24 20 * * 2\"\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [\"cpp\", \"python\"]\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      # Initializes the CodeQL tools for scanning.\n      - name: Initialize CodeQL\n        uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9\n        with:\n          languages: ${{ matrix.language }}\n          # If you wish to specify custom queries, you can do so here or in a config file.\n          # By default, queries listed here will override any specified in a config file.\n          # Prefix the list here with \"+\" to use these queries and those in the config file.\n          # queries: ./path/to/local/query, your-org/your-repo/queries@main\n      - if: matrix.language == 'cpp'\n        name: Build Cpp\n        run: |\n          sudo apt-get update\n          sudo apt-get install python3-pyqt5\n          make modules\n      - if: matrix.language == 'python'\n        name: Autobuild\n        uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9\n      # Analysis\n      - name: Perform CodeQL Analysis\n        uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9\n"
  },
  {
    "path": ".github/workflows/default.yml",
    "content": "# Workflow lints, and checks format in parallel then runs tests on all platforms\n\nname: Default CI/CD\n\non:\n  push:\n  pull_request:\n    branches: [master]\n\njobs:\n  pre-commit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      - name: Set up Python 3.12\n        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0\n        with:\n          python-version: \"3.12\"\n      - uses: pre-commit/action@v3.0.1\n  test:\n    needs: [pre-commit]\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest]\n        python-version: [3.8, 3.9, \"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        include:\n          - os: windows-latest\n            python-version: \"3.12\"\n          - os: macos-latest\n            python-version: \"3.12\"\n\n    steps:\n      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install setuptools\n          pip install -r requirements.txt -r requirements-extra.txt\n      - name: Build python modules\n        run: |\n          python build.py --modules\n      - name: Run tests\n        run: |\n          pytest core hscommon\n      - name: Upload Artifacts\n        if: matrix.os == 'ubuntu-latest'\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: modules ${{ matrix.python-version }}\n          path: build/**/*.so\n  merge-artifacts:\n    needs: [test]\n    runs-on: ubuntu-latest\n    steps:\n      - name: Merge Artifacts\n        uses: actions/upload-artifact/merge@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: modules\n          pattern: modules*\n          delete-merged: true\n"
  },
  {
    "path": ".github/workflows/tx-push.yml",
    "content": "# Push translation source to Transifex\nname: Transifex Sync\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - locale/*.pot\n\nenv:\n  TX_VERSION: \"v1.6.10\"\n\njobs:\n  push-source:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n      - name: Get Transifex Client\n        run: |\n          curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash -s -- $TX_VERSION\n      - name: Update & Push Translation Sources\n        env:\n          TX_TOKEN: ${{ secrets.TX_TOKEN }}\n        run: |\n          ./tx push -s --use-git-timestamps\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n#*.pot\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Environments\n.env\n.venv\nenv*/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# macOS\n.DS_Store\n\n# Visual Studio Code\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Local History for Visual Studio Code\n.history/\n\n# Built Visual Studio Code Extensions\n*.vsix\n\n# dupeGuru Specific\n/qt/*_rc.py\n/help/*/conf.py\n/help/*/changelog.rst\ncocoa/autogen\n/cocoa/*/Info.plist\n/cocoa/*/build\n\n*.waf*\n.lock-waf*\n/tags\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.5.0\n    hooks:\n      - id: check-yaml\n      - id: check-toml\n      - id: end-of-file-fixer\n        exclude: \".*.json\"\n      - id: trailing-whitespace\n  - repo: https://github.com/psf/black\n    rev: 24.2.0\n    hooks:\n      - id: black\n  - repo: https://github.com/PyCQA/flake8\n    rev: 7.0.0\n    hooks:\n      - id: flake8\n        exclude: ^(.tox|env|build|dist|help|qt/dg_rc.py|pkg).*\n  - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook\n    rev: v9.11.0\n    hooks:\n      - id: commitlint\n        stages: [commit-msg]\n        additional_dependencies: [\"@commitlint/config-conventional\"]\n"
  },
  {
    "path": ".sonarcloud.properties",
    "content": "sonar.python.version=3.7, 3.8, 3.9, 3.10, 3.11\n"
  },
  {
    "path": ".tx/config",
    "content": "[main]\nhost = https://www.transifex.com\n\n[o:voltaicideas:p:dupeguru-1:r:columns]\nfile_filter = locale/<lang>/LC_MESSAGES/columns.po\nsource_file = locale/columns.pot\nsource_lang = en\ntype        = PO\n\n[o:voltaicideas:p:dupeguru-1:r:core]\nfile_filter = locale/<lang>/LC_MESSAGES/core.po\nsource_file = locale/core.pot\nsource_lang = en\ntype        = PO\n\n[o:voltaicideas:p:dupeguru-1:r:ui]\nfile_filter = locale/<lang>/LC_MESSAGES/ui.po\nsource_file = locale/ui.pot\nsource_lang = en\ntype        = PO\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    // List of extensions which should be recommended for users of this workspace.\n    \"recommendations\": [\n        \"redhat.vscode-yaml\",\n        \"ms-python.vscode-pylance\",\n        \"ms-python.python\",\n        \"ms-python.black-formatter\",\n    ],\n    // List of extensions recommended by VS Code that should not be recommended for\n    // users of this workspace.\n    \"unwantedRecommendations\": []\n}"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"DupuGuru\",\n            \"type\": \"debugpy\",\n            \"request\": \"launch\",\n            \"program\": \"run.py\",\n            \"console\": \"integratedTerminal\",\n            \"subProcess\": true,\n            \"justMyCode\": false\n        },\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"Dupras\",\n        \"hscommon\"\n    ],\n    \"editor.rulers\": [\n        88,\n        120\n    ],\n    \"python.languageServer\": \"Pylance\",\n    \"yaml.schemaStore.enable\": true,\n    \"[python]\": {\n        \"editor.formatOnSave\": true,\n        \"editor.defaultFormatter\": \"ms-python.black-formatter\"\n    },\n    \"python.testing.pytestEnabled\": true\n}"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to dupeGuru\n\nThe following is a set of guidelines and information for contributing to dupeGuru.\n\n#### Table of Contents\n\n[Things to Know Before Starting](#things-to-know-before-starting)\n\n[Ways to Contribute](#ways-to-contribute)\n  * [Reporting Bugs](#reporting-bugs)\n  * [Suggesting Enhancements](#suggesting-enhancements)\n  * [Localization](#localization)\n  * [Code Contribution](#code-contribution)\n  * [Pull Requests](#pull-requests)\n\n[Style Guides](#style-guides)\n  * [Git Commit Messages](#git-commit-messages)\n  * [Python Style Guide](#python-style-guide)\n  * [Documentation Style Guide](#documentation-style-guide)\n\n[Additional Notes](#additional-notes)\n  * [Issue and Pull Request Labels](#issue-and-pull-request-labels)\n\n## Things to Know Before Starting\n**TODO**\n## Ways to contribute\n### Reporting Bugs\n**TODO**\n### Suggesting Enhancements\n**TODO**\n### Localization\n**TODO**\n### Code Contribution\n**TODO**\n### Pull Requests\nPlease follow these steps to have your contribution considered by the maintainers:\n\n1. Keep Pull Request specific to one feature or bug.\n2. Follow the [style guides](#style-guides)\n3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing?</summary>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.</details>\n\nWhile 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.\n\n## Style Guides\n### Git Commit Messages\n- Use the present tense (\"Add feature\" not \"Added feature\")\n- Use the imperative mood (\"Move cursor to...\" not \"Moves cursor to...\")\n- Limit the first line to 72 characters or less\n- Reference issues and pull requests liberally after the first line\n\n### Python Style Guide\n- All files are formatted with [Black](https://github.com/psf/black)\n- Follow [PEP 8](https://peps.python.org/pep-0008/) as much as practical\n- Pass [flake8](https://flake8.pycqa.org/en/latest/) linting\n- Include [PEP 484](https://peps.python.org/pep-0484/) type hints (new code)\n\n### Documentation Style Guide\n**TODO**\n\n## Additional Notes\n### Issue and Pull Request Labels\nThis 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.\n\n#### Issue Type and Status\n| Label name | Search | Description |\n|------------|--------|-------------|\n| `enhancement` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) | Feature requests and enhancements. |\n| `bug` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Abug) | Bug reports. |\n| `duplicate` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aduplicate) | Issue is a duplicate of existing issue. |\n| `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. |\n| `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). |\n| `blocked` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Ablocked) | Issue blocked by other issues. |\n| `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. |\n\n#### Category Labels\n| Label name | Search | Description |\n|------------|--------|-------------|\n| `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. |\n| `crash` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Acrash) | Related to crashes (complete, or unhandled). |\n| `documentation` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Adocumentation) | Related to any documentation. |\n| `linux` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3linux) | Related to running on Linux. |\n| `mac` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Amac) | Related to running on macOS. |\n| `performance` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aperformance) | Related to the performance. |\n| `ui` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Aui)| Related to the visual design. |\n| `windows` | [search](https://github.com/arsenetar/dupeguru/issues?q=is%3Aopen+is%3Aissue+label%3Awindows) | Related to running on Windows. |\n\n#### Pull Request Labels\nNone at this time, if the volume of Pull Requests increase labels may be added to manage.\n"
  },
  {
    "path": "CREDITS",
    "content": "To know who contributed to dupeGuru, you can look at the commit log, but not all contributions\nresult in a commit. This file lists contributors who don't necessarily appear in the commit log.\n\n* Jason Cho, Exchange icon\n* schollidesign (https://findicons.com/pack/1035/human_o2), Zoom-in, Zoom-out, Zoom-best-fit, Zoom-original icons\n* Jérôme Cantin, Main icon\n* Gregor Tätzner, German localization\n* Frank Weber, German localization\n* Eric Dee, Chinese localization\n* Aleš Nehyba, Czech localization\n* Paolo Rossi, Italian localization\n* Hrant Ohanyan, Armenian localization\n* Igor Pavlov, Russian localization\n* Kyrill Detinov, Russian localization\n* Yuri Petrashko, Ukrainian localization\n* Nickolas Pohilets, Ukrainian localization\n* Victor Figueiredo, Brazilian localization\n* Phan Anh, Vietnamese localization\n* Gabriel Koutilellis, Greek localization\n\nThanks!\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-include core *.h\nrecursive-include core *.m\ninclude run.py\ngraft locale\ngraft help\n"
  },
  {
    "path": "Makefile",
    "content": "PYTHON ?= python3\nPYTHON_VERSION_MINOR := $(shell ${PYTHON} -c \"import sys; print(sys.version_info.minor)\")\nPYRCC5 ?= pyrcc5\nREQ_MINOR_VERSION = 7\nPREFIX ?= /usr/local\n\n# Window compatability via Msys2\n# - venv creates Scripts instead of bin\n# - compile generates .pyd instead of .so\n# - venv with --sytem-site-packages has issues on windows as well...\n\nifeq ($(shell ${PYTHON} -c \"import platform; print(platform.system())\"), Windows)\n\tBIN = Scripts\n\tSO = *.pyd\n\tVENV_OPTIONS =\nelse\n\tBIN = bin\n\tSO = *.so\n\tVENV_OPTIONS = --system-site-packages\nendif\n\n# Set this variable if all dependencies are already met on the system. We will then avoid the\n# whole vitualenv creation and pip install dance.\nNO_VENV ?=\n\nifdef NO_VENV\n\tVENV_PYTHON = $(PYTHON)\nelse\n\tVENV_PYTHON = ./env/$(BIN)/python\nendif\n\n# If you're installing into a path that is not going to be the final path prefix (such as a\n# sandbox), set DESTDIR to that path.\n\n# Our build scripts are not very \"make like\" yet and perform their task in a bundle. For now, we\n# use one of each file to act as a representative, a target, of these groups.\n\npackages = hscommon core qt\nlocaledirs = $(wildcard locale/*/LC_MESSAGES)\npofiles = $(wildcard locale/*/LC_MESSAGES/*.po)\nmofiles = $(patsubst %.po,%.mo,$(pofiles))\n\nvpath %.po $(localedirs)\nvpath %.mo $(localedirs)\n\nall: | env i18n modules qt/dg_rc.py\n\t@echo \"Build complete! You can run dupeGuru with 'make run'\"\n\nrun:\n\t$(VENV_PYTHON) run.py\n\npyc: | env\n\t${VENV_PYTHON} -m compileall ${packages}\n\nreqs:\nifneq ($(shell test $(PYTHON_VERSION_MINOR) -ge $(REQ_MINOR_VERSION); echo $$?),0)\n\t$(error \"Python 3.${REQ_MINOR_VERSION}+ required. Aborting.\")\nendif\nifndef NO_VENV\n\t@${PYTHON} -m venv -h > /dev/null || \\\n\t\techo \"Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv.\"\nendif\n\t@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \\\n\t\t{ echo \"PyQt 5.4+ required. Install it and try again. Aborting\"; exit 1; }\n\nenv: | reqs\nifndef NO_VENV\n\t@echo \"Creating our virtualenv\"\n\t${PYTHON} -m venv env\n\t$(VENV_PYTHON) -m pip install -r requirements.txt\n# We can't use the \"--system-site-packages\" flag on creation because otherwise we end up with\n# the system's pip and that messes up things in some cases (notably in Gentoo).\n\t${PYTHON} -m venv --upgrade ${VENV_OPTIONS} env\nendif\n\nbuild/help: | env\n\t$(VENV_PYTHON) build.py --doc\n\nqt/dg_rc.py: qt/dg.qrc\n\t$(PYRCC5) qt/dg.qrc > qt/dg_rc.py\n\ni18n: $(mofiles)\n\n%.mo: %.po\n\tmsgfmt -o $@ $<\n\nmodules: | env\n\t$(VENV_PYTHON) build.py --modules\n\nmergepot: | env\n\t$(VENV_PYTHON) build.py --mergepot\n\nnormpo: | env\n\t$(VENV_PYTHON) build.py --normpo\n\ninstall: all pyc\n\tmkdir -p ${DESTDIR}${PREFIX}/share/dupeguru\n\tcp -rf ${packages} locale ${DESTDIR}${PREFIX}/share/dupeguru\n\tcp -f run.py ${DESTDIR}${PREFIX}/share/dupeguru/run.py\n\tchmod 755 ${DESTDIR}${PREFIX}/share/dupeguru/run.py\n\tmkdir -p ${DESTDIR}${PREFIX}/bin\n\tln -sf ${PREFIX}/share/dupeguru/run.py ${DESTDIR}${PREFIX}/bin/dupeguru\n\tmkdir -p ${DESTDIR}${PREFIX}/share/applications\n\tcp -f pkg/dupeguru.desktop ${DESTDIR}${PREFIX}/share/applications\n\tmkdir -p ${DESTDIR}${PREFIX}/share/pixmaps\n\tcp -f images/dgse_logo_128.png ${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png\n\ninstalldocs: build/help\n\tmkdir -p ${DESTDIR}${PREFIX}/share/dupeguru\n\tcp -rf build/help ${DESTDIR}${PREFIX}/share/dupeguru\n\nuninstall:\n\trm -rf \"${DESTDIR}${PREFIX}/share/dupeguru\"\n\trm -f \"${DESTDIR}${PREFIX}/bin/dupeguru\"\n\trm -f \"${DESTDIR}${PREFIX}/share/applications/dupeguru.desktop\"\n\trm -f \"${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png\"\n\nclean:\n\t-rm -rf build\n\t-rm locale/*/LC_MESSAGES/*.mo\n\t-rm core/pe/*.$(SO) qt/pe/*.$(SO)\n\n.PHONY: clean normpo mergepot modules i18n reqs run pyc install uninstall all\n"
  },
  {
    "path": "README.md",
    "content": "# dupeGuru\n\n[dupeGuru][dupeguru] is a cross-platform (Linux, OS X, Windows) GUI tool to find duplicate files in\na system. It is written mostly in Python 3 and uses [qt](https://www.qt.io/) for the UI.\n\n## Current status\nStill looking for additional help especially with regards to:\n* OSX maintenance: reproducing bugs, packaging verification.\n* Linux maintenance: reproducing bugs, maintaining PPA repository, Debian package, rpm package.\n* Translations: updating missing strings, transifex project at https://www.transifex.com/voltaicideas/dupeguru-1\n* Documentation: keeping it up-to-date.\n\n## Contents of this folder\n\nThis folder contains the source for dupeGuru. Its documentation is in `help`, but is also\n[available online][documentation] in its built form. Here's how this source tree is organized:\n\n* core: Contains the core logic code for dupeGuru. It's Python code.\n* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.\n* images: Images used by the different UI codebases.\n* pkg: Skeleton files required to create different packages\n* help: Help document, written for Sphinx.\n* locale: .po files for localization.\n* hscommon: A collection of helpers used across HS applications.\n\n## How to build dupeGuru from source\n\n### Windows & macOS specific additional instructions\nFor windows instructions see the [Windows Instructions](Windows.md).\n\nFor macos instructions (qt version) see the [macOS Instructions](macos.md).\n\n### Prerequisites\n* [Python 3.7+][python]\n* PyQt5\n\n### System Setup\nWhen running in a linux based environment the following system packages or equivalents are needed to build:\n* python3-pyqt5\n* pyqt5-dev-tools (on some systems, see note)\n* python3-venv (only if using a virtual environment)\n* python3-dev\n* build-essential\n\nNote: 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.\n\nTo create packages the following are also needed:\n* python3-setuptools\n* debhelper\n\n### Building with Make\ndupeGuru comes with a makefile that can be used to build and run:\n\n    $ make && make run\n\n### Building without Make\n\n    $ cd <dupeGuru directory>\n    $ python3 -m venv --system-site-packages ./env\n    $ source ./env/bin/activate\n    $ pip install -r requirements.txt\n    $ python build.py\n    $ python run.py\n\n### Generating Debian/Ubuntu package\nTo generate packages the extra requirements in requirements-extra.txt must be installed, the\nsteps are as follows:\n\n    $ cd <dupeGuru directory>\n    $ python3 -m venv --system-site-packages ./env\n    $ source ./env/bin/activate\n    $ pip install -r requirements.txt -r requirements-extra.txt\n    $ python build.py --clean\n    $ python package.py\n\nThis can be made a one-liner (once in the directory) as:\n\n    $ 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\"\n\n## Running tests\n\nThe complete test suite is run with [Tox 1.7+][tox]. If you have it installed system-wide, you\ndon't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`.\n\nIf you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then\nrun `tox`.\n\nYou can also run automated tests without Tox. Extra requirements for running tests are in\n`requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your\nvirtualenv and then `py.test core hscommon`\n\n[dupeguru]: https://dupeguru.voltaicideas.net/\n[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software\n[documentation]: http://dupeguru.voltaicideas.net/help/en/\n[python]: http://www.python.org/\n[pyqt]: http://www.riverbankcomputing.com\n[tox]: https://tox.readthedocs.org/en/latest/\n"
  },
  {
    "path": "Windows.md",
    "content": "## How to build dupeGuru for Windows\n\n### Prerequisites\n\n- [Python 3.7+][python]\n- [Visual Studio 2019][vs] or [Visual Studio Build Tools 2019][vsBuildTools] with the Windows 10 SDK\n- [nsis][nsis] (for installer creation)\n- [msys2][msys2] (for using makefile method)\n\nNOTE: 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].\n\nAfter installing python it is recommended to update setuptools before compiling packages.  To update run (example is for python launcher and 3.8):\n\n    $ py -3.8 -m pip install --upgrade setuptools\n\nMore 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.\n\n### With build.py (preferred)\nTo 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.\n\n    $ cd <dupeGuru directory>\n    $ py -3.8 -m venv .\\env\n    $ .\\env\\Scripts\\activate\n    $ pip install -r requirements.txt\n    $ python build.py\n    $ python run.py\n\n### With makefile\nIt 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:\n1. Install msys2 or other POSIX environment\n2. Install PyQt5 globally via pip\n3. Use the respective console for msys2 it is `msys2 msys`\n\nThen the following execution of the makefile should work.  Pass the correct value for PYTHON to the makefile if not on the path as python3.\n\n    $ cd <dupeGuru directory>\n    $ make PYTHON='py -3.8'\n    $ make run\n\n### Generate Windows Installer Packages\nYou 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.\n\n    $ python package.py\n\n### Running tests\nThe 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`.\n\n[python]: http://www.python.org/\n[nsis]: http://nsis.sourceforge.net/Main_Page\n[vs]: https://www.visualstudio.com/downloads/#visual-studio-community-2019\n[vsBuildTools]: https://www.visualstudio.com/downloads/#build-tools-for-visual-studio-2019\n[win10sdk]: https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk\n[KB2999226]: https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows\n[pythonWindowsCompilers]: https://wiki.python.org/moin/WindowsCompilers\n[msys2]: http://www.msys2.org/\n"
  },
  {
    "path": "build.py",
    "content": "# Copyright 2017 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom pathlib import Path\nimport sys\nfrom optparse import OptionParser\nimport shutil\nfrom multiprocessing import Pool\n\nfrom hscommon import sphinxgen\nfrom hscommon.build import (\n    add_to_pythonpath,\n    print_and_do,\n    fix_qt_resource_file,\n)\nfrom hscommon import loc\nimport subprocess\n\n\ndef parse_args():\n    usage = \"usage: %prog [options]\"\n    parser = OptionParser(usage=usage)\n    parser.add_option(\n        \"--clean\",\n        action=\"store_true\",\n        dest=\"clean\",\n        help=\"Clean build folder before building\",\n    )\n    parser.add_option(\"--doc\", action=\"store_true\", dest=\"doc\", help=\"Build only the help file (en)\")\n    parser.add_option(\"--alldoc\", action=\"store_true\", dest=\"all_doc\", help=\"Build only the help file in all languages\")\n    parser.add_option(\"--loc\", action=\"store_true\", dest=\"loc\", help=\"Build only localization\")\n    parser.add_option(\n        \"--updatepot\",\n        action=\"store_true\",\n        dest=\"updatepot\",\n        help=\"Generate .pot files from source code.\",\n    )\n    parser.add_option(\n        \"--mergepot\",\n        action=\"store_true\",\n        dest=\"mergepot\",\n        help=\"Update all .po files based on .pot files.\",\n    )\n    parser.add_option(\n        \"--normpo\",\n        action=\"store_true\",\n        dest=\"normpo\",\n        help=\"Normalize all PO files (do this before commit).\",\n    )\n    parser.add_option(\n        \"--modules\",\n        action=\"store_true\",\n        dest=\"modules\",\n        help=\"Build the python modules.\",\n    )\n    (options, args) = parser.parse_args()\n    return options\n\n\ndef build_one_help(language):\n    print(f\"Generating Help in {language}\")\n    current_path = Path(\".\").absolute()\n    changelog_path = current_path.joinpath(\"help\", \"changelog\")\n    tixurl = \"https://github.com/arsenetar/dupeguru/issues/{}\"\n    changelogtmpl = current_path.joinpath(\"help\", \"changelog.tmpl\")\n    conftmpl = current_path.joinpath(\"help\", \"conf.tmpl\")\n    help_basepath = current_path.joinpath(\"help\", language)\n    help_destpath = current_path.joinpath(\"build\", \"help\", language)\n    confrepl = {\"language\": language}\n    sphinxgen.gen(\n        help_basepath,\n        help_destpath,\n        changelog_path,\n        tixurl,\n        confrepl,\n        conftmpl,\n        changelogtmpl,\n    )\n\n\ndef build_help():\n    languages = [\"en\", \"de\", \"fr\", \"hy\", \"ru\", \"uk\"]\n    # Running with Pools as for some reason sphinx seems to cross contaminate the output otherwise\n    with Pool(len(languages)) as p:\n        p.map(build_one_help, languages)\n\n\ndef build_localizations():\n    loc.compile_all_po(\"locale\")\n    locale_dest = Path(\"build\", \"locale\")\n    if locale_dest.exists():\n        shutil.rmtree(locale_dest)\n    shutil.copytree(\"locale\", locale_dest, ignore=shutil.ignore_patterns(\"*.po\", \"*.pot\"))\n\n\ndef build_updatepot():\n    print(\"Building .pot files from source files\")\n    print(\"Building core.pot\")\n    loc.generate_pot([\"core\"], Path(\"locale\", \"core.pot\"), [\"tr\"])\n    print(\"Building columns.pot\")\n    loc.generate_pot([\"core\"], Path(\"locale\", \"columns.pot\"), [\"coltr\"])\n    print(\"Building ui.pot\")\n    loc.generate_pot([\"qt\"], Path(\"locale\", \"ui.pot\"), [\"tr\"], merge=True)\n\n\ndef build_mergepot():\n    print(\"Updating .po files using .pot files\")\n    loc.merge_pots_into_pos(\"locale\")\n\n\ndef build_normpo():\n    loc.normalize_all_pos(\"locale\")\n\n\ndef build_pe_modules():\n    print(\"Building PE Modules\")\n    # Leverage setup.py to build modules\n    subprocess.check_call([sys.executable, \"setup.py\", \"build_ext\", \"--inplace\"])\n\n\ndef build_normal():\n    print(\"Building dupeGuru with UI qt\")\n    add_to_pythonpath(\".\")\n    print(\"Building dupeGuru\")\n    build_pe_modules()\n    print(\"Building localizations\")\n    build_localizations()\n    print(\"Building Qt stuff\")\n    Path(\"qt\", \"dg_rc.py\").unlink(missing_ok=True)\n    print_and_do(\"pyrcc5 {} > {}\".format(Path(\"qt\", \"dg.qrc\"), Path(\"qt\", \"dg_rc.py\")))\n    fix_qt_resource_file(Path(\"qt\", \"dg_rc.py\"))\n    build_help()\n\n\ndef main():\n    if sys.version_info < (3, 7):\n        sys.exit(\"Python < 3.7 is unsupported.\")\n    options = parse_args()\n    if options.clean and Path(\"build\").exists():\n        shutil.rmtree(\"build\")\n    if not Path(\"build\").exists():\n        Path(\"build\").mkdir()\n    if options.doc:\n        build_one_help(\"en\")\n    elif options.all_doc:\n        build_help()\n    elif options.loc:\n        build_localizations()\n    elif options.updatepot:\n        build_updatepot()\n    elif options.mergepot:\n        build_mergepot()\n    elif options.normpo:\n        build_normpo()\n    elif options.modules:\n        build_pe_modules()\n    else:\n        build_normal()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "const Configuration = {\n    /*\n     * Resolve and load @commitlint/config-conventional from node_modules.\n     * Referenced packages must be installed\n     */\n    extends: ['@commitlint/config-conventional'],\n    /*\n     * Any rules defined here will override rules from @commitlint/config-conventional\n     */\n    rules: {\n        'header-max-length': [2, 'always', 72],\n        'subject-case': [2, 'always', 'sentence-case'],\n        'scope-enum': [2, 'always'],\n    },\n};\n\nmodule.exports = Configuration;\n"
  },
  {
    "path": "core/__init__.py",
    "content": "__version__ = \"4.3.1\"\n__appname__ = \"dupeGuru\"\n"
  },
  {
    "path": "core/app.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport cProfile\nimport datetime\nimport os\nimport os.path as op\nimport logging\nimport subprocess\nimport re\nimport shutil\nfrom pathlib import Path\n\nfrom send2trash import send2trash\nfrom hscommon.jobprogress import job\nfrom hscommon.notify import Broadcaster\nfrom hscommon.conflict import smart_move, smart_copy\nfrom hscommon.gui.progress_window import ProgressWindow\nfrom hscommon.util import delete_if_empty, first, escape, nonone, allsame\nfrom hscommon.trans import tr\nfrom hscommon import desktop\n\nfrom core import se, me, pe\nfrom core.pe.photo import get_delta_dimensions\nfrom core.util import cmp_value, fix_surrogate_encoding\nfrom core import directories, results, export, fs, prioritize\nfrom core.ignore import IgnoreList\nfrom core.exclude import ExcludeDict as ExcludeList\nfrom core.scanner import ScanType\nfrom core.gui.deletion_options import DeletionOptions\nfrom core.gui.details_panel import DetailsPanel\nfrom core.gui.directory_tree import DirectoryTree\nfrom core.gui.ignore_list_dialog import IgnoreListDialog\nfrom core.gui.exclude_list_dialog import ExcludeListDialogCore\nfrom core.gui.problem_dialog import ProblemDialog\nfrom core.gui.stats_label import StatsLabel\n\nHAD_FIRST_LAUNCH_PREFERENCE = \"HadFirstLaunch\"\nDEBUG_MODE_PREFERENCE = \"DebugMode\"\n\nMSG_NO_MARKED_DUPES = tr(\"There are no marked duplicates. Nothing has been done.\")\nMSG_NO_SELECTED_DUPES = tr(\"There are no selected duplicates. Nothing has been done.\")\nMSG_MANY_FILES_TO_OPEN = tr(\n    \"You're about to open many files at once. Depending on what those \"\n    \"files are opened with, doing so can create quite a mess. Continue?\"\n)\n\n\nclass DestType:\n    DIRECT = 0\n    RELATIVE = 1\n    ABSOLUTE = 2\n\n\nclass JobType:\n    SCAN = \"job_scan\"\n    LOAD = \"job_load\"\n    MOVE = \"job_move\"\n    COPY = \"job_copy\"\n    DELETE = \"job_delete\"\n\n\nclass AppMode:\n    STANDARD = 0\n    MUSIC = 1\n    PICTURE = 2\n\n\nJOBID2TITLE = {\n    JobType.SCAN: tr(\"Scanning for duplicates\"),\n    JobType.LOAD: tr(\"Loading\"),\n    JobType.MOVE: tr(\"Moving\"),\n    JobType.COPY: tr(\"Copying\"),\n    JobType.DELETE: tr(\"Sending to Trash\"),\n}\n\n\nclass DupeGuru(Broadcaster):\n    \"\"\"Holds everything together.\n\n    Instantiated once per running application, it holds a reference to every high-level object\n    whose reference needs to be held: :class:`~core.results.Results`,\n    :class:`~core.directories.Directories`, :mod:`core.gui` instances, etc..\n\n    It also hosts high level methods and acts as a coordinator for all those elements. This is why\n    some of its methods seem a bit shallow, like for example :meth:`mark_all` and\n    :meth:`remove_duplicates`. These methos are just proxies for a method in :attr:`results`, but\n    they are also followed by a notification call which is very important if we want GUI elements\n    to be correctly notified of a change in the data they're presenting.\n\n    .. attribute:: directories\n\n        Instance of :class:`~core.directories.Directories`. It holds the current folder selection.\n\n    .. attribute:: results\n\n        Instance of :class:`core.results.Results`. Holds the results of the latest scan.\n\n    .. attribute:: selected_dupes\n\n        List of currently selected dupes from our :attr:`results`. Whenever the user changes its\n        selection at the UI level, :attr:`result_table` takes care of updating this attribute, so\n        you can trust that it's always up-to-date.\n\n    .. attribute:: result_table\n\n        Instance of :mod:`meta-gui <core.gui>` table listing the results from :attr:`results`\n    \"\"\"\n\n    # --- View interface\n    # get_default(key_name)\n    # set_default(key_name, value)\n    # show_message(msg)\n    # open_url(url)\n    # open_path(path)\n    # reveal_path(path)\n    # ask_yes_no(prompt) --> bool\n    # create_results_window()\n    # show_results_window()\n    # show_problem_dialog()\n    # select_dest_folder(prompt: str) --> str\n    # select_dest_file(prompt: str, ext: str) --> str\n\n    NAME = PROMPT_NAME = \"dupeGuru\"\n\n    def __init__(self, view, portable=False):\n        if view.get_default(DEBUG_MODE_PREFERENCE):\n            logging.getLogger().setLevel(logging.DEBUG)\n            logging.debug(\"Debug mode enabled\")\n        Broadcaster.__init__(self)\n        self.view = view\n        self.appdata = desktop.special_folder_path(desktop.SpecialFolder.APPDATA, portable=portable)\n        if not op.exists(self.appdata):\n            os.makedirs(self.appdata)\n        self.app_mode = AppMode.STANDARD\n        self.discarded_file_count = 0\n        self.exclude_list = ExcludeList()\n        hash_cache_file = op.join(self.appdata, \"hash_cache.db\")\n        fs.filesdb.connect(hash_cache_file)\n        self.directories = directories.Directories(self.exclude_list)\n        self.results = results.Results(self)\n        self.ignore_list = IgnoreList()\n        # In addition to \"app-level\" options, this dictionary also holds options that will be\n        # sent to the scanner. They don't have default values because those defaults values are\n        # defined in the scanner class.\n        self.options = {\n            \"escape_filter_regexp\": True,\n            \"clean_empty_dirs\": False,\n            \"ignore_hardlink_matches\": False,\n            \"copymove_dest_type\": DestType.RELATIVE,\n            \"include_exists_check\": True,\n            \"rehash_ignore_mtime\": False,\n        }\n        self.selected_dupes = []\n        self.details_panel = DetailsPanel(self)\n        self.directory_tree = DirectoryTree(self)\n        self.problem_dialog = ProblemDialog(self)\n        self.ignore_list_dialog = IgnoreListDialog(self)\n        self.exclude_list_dialog = ExcludeListDialogCore(self)\n        self.stats_label = StatsLabel(self)\n        self.result_table = None\n        self.deletion_options = DeletionOptions()\n        self.progress_window = ProgressWindow(self._job_completed, self._job_error)\n        children = [self.directory_tree, self.stats_label, self.details_panel]\n        for child in children:\n            child.connect()\n\n    # --- Private\n    def _recreate_result_table(self):\n        if self.result_table is not None:\n            self.result_table.disconnect()\n        if self.app_mode == AppMode.PICTURE:\n            self.result_table = pe.result_table.ResultTable(self)\n        elif self.app_mode == AppMode.MUSIC:\n            self.result_table = me.result_table.ResultTable(self)\n        else:\n            self.result_table = se.result_table.ResultTable(self)\n        self.result_table.connect()\n        self.view.create_results_window()\n\n    def _get_picture_cache_path(self):\n        cache_name = \"cached_pictures.db\"\n        return op.join(self.appdata, cache_name)\n\n    def _get_dupe_sort_key(self, dupe, get_group, key, delta):\n        if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == \"folder_path\":\n            dupe_folder_path = getattr(dupe, \"display_folder_path\", dupe.folder_path)\n            return str(dupe_folder_path).lower()\n        if self.app_mode == AppMode.PICTURE and delta and key == \"dimensions\":\n            r = cmp_value(dupe, key)\n            ref_value = cmp_value(get_group().ref, key)\n            return get_delta_dimensions(r, ref_value)\n        if key == \"marked\":\n            return self.results.is_marked(dupe)\n        if key == \"percentage\":\n            m = get_group().get_match_of(dupe)\n            return m.percentage\n        elif key == \"dupe_count\":\n            return 0\n        else:\n            result = cmp_value(dupe, key)\n        if delta:\n            refval = cmp_value(get_group().ref, key)\n            if key in self.result_table.DELTA_COLUMNS:\n                result -= refval\n            else:\n                same = cmp_value(dupe, key) == refval\n                result = (same, result)\n        return result\n\n    def _get_group_sort_key(self, group, key):\n        if self.app_mode in (AppMode.MUSIC, AppMode.PICTURE) and key == \"folder_path\":\n            dupe_folder_path = getattr(group.ref, \"display_folder_path\", group.ref.folder_path)\n            return str(dupe_folder_path).lower()\n        if key == \"percentage\":\n            return group.percentage\n        if key == \"dupe_count\":\n            return len(group)\n        if key == \"marked\":\n            return len([dupe for dupe in group.dupes if self.results.is_marked(dupe)])\n        return cmp_value(group.ref, key)\n\n    def _do_delete(self, j, link_deleted, use_hardlinks, direct_deletion):\n        def op(dupe):\n            j.add_progress()\n            return self._do_delete_dupe(dupe, link_deleted, use_hardlinks, direct_deletion)\n\n        j.start_job(self.results.mark_count)\n        self.results.perform_on_marked(op, True)\n\n    def _do_delete_dupe(self, dupe, link_deleted, use_hardlinks, direct_deletion):\n        if not dupe.path.exists():\n            return\n        logging.debug(\"Sending '%s' to trash\", dupe.path)\n        str_path = str(dupe.path)\n        if direct_deletion:\n            if op.isdir(str_path):\n                shutil.rmtree(str_path)\n            else:\n                os.remove(str_path)\n        else:\n            send2trash(str_path)  # Raises OSError when there's a problem\n        if link_deleted:\n            group = self.results.get_group_of_duplicate(dupe)\n            ref = group.ref\n            linkfunc = os.link if use_hardlinks else os.symlink\n            linkfunc(str(ref.path), str_path)\n        self.clean_empty_dirs(dupe.path.parent)\n\n    def _create_file(self, path):\n        # We add fs.Folder to fileclasses in case the file we're loading contains folder paths.\n        return fs.get_file(path, self.fileclasses + [se.fs.Folder])\n\n    def _get_file(self, str_path):\n        path = Path(str_path)\n        f = self._create_file(path)\n        if f is None:\n            return None\n        try:\n            f._read_all_info(attrnames=self.METADATA_TO_READ)\n            return f\n        except OSError:\n            return None\n\n    def _get_export_data(self):\n        columns = [col for col in self.result_table._columns.ordered_columns if col.visible and col.name != \"marked\"]\n        colnames = [col.display for col in columns]\n        rows = []\n        for group_id, group in enumerate(self.results.groups):\n            for dupe in group:\n                data = self.get_display_info(dupe, group)\n                row = [fix_surrogate_encoding(data[col.name]) for col in columns]\n                row.insert(0, group_id)\n                rows.append(row)\n        return colnames, rows\n\n    def _results_changed(self):\n        self.selected_dupes = [d for d in self.selected_dupes if self.results.get_group_of_duplicate(d) is not None]\n        self.notify(\"results_changed\")\n\n    def _start_job(self, jobid, func, args=()):\n        title = JOBID2TITLE[jobid]\n        try:\n            self.progress_window.run(jobid, title, func, args=args)\n        except job.JobInProgressError:\n            msg = tr(\n                \"A previous action is still hanging in there. You can't start a new one yet. Wait \"\n                \"a few seconds, then try again.\"\n            )\n            self.view.show_message(msg)\n\n    def _job_completed(self, jobid):\n        if jobid == JobType.SCAN:\n            self._results_changed()\n            fs.filesdb.commit()\n            if not self.results.groups:\n                self.view.show_message(tr(\"No duplicates found.\"))\n            else:\n                self.view.show_results_window()\n        if jobid in {JobType.MOVE, JobType.DELETE}:\n            self._results_changed()\n        if jobid == JobType.LOAD:\n            self._recreate_result_table()\n            self._results_changed()\n            self.view.show_results_window()\n        if jobid in {JobType.COPY, JobType.MOVE, JobType.DELETE}:\n            if self.results.problems:\n                self.problem_dialog.refresh()\n                self.view.show_problem_dialog()\n            else:\n                if jobid == JobType.COPY:\n                    msg = tr(\"All marked files were copied successfully.\")\n                elif jobid == JobType.MOVE:\n                    msg = tr(\"All marked files were moved successfully.\")\n                elif jobid == JobType.DELETE and self.deletion_options.direct:\n                    msg = tr(\"All marked files were deleted successfully.\")\n                else:\n                    msg = tr(\"All marked files were successfully sent to Trash.\")\n                self.view.show_message(msg)\n\n    def _job_error(self, jobid, err):\n        if jobid == JobType.LOAD:\n            msg = tr(\"Could not load file: {}\").format(err)\n            self.view.show_message(msg)\n            return False\n        else:\n            raise err\n\n    @staticmethod\n    def _remove_hardlink_dupes(files):\n        seen_inodes = set()\n        result = []\n        for file in files:\n            try:\n                inode = file.path.stat().st_ino\n            except OSError:\n                # The file was probably deleted or something\n                continue\n            if inode not in seen_inodes:\n                seen_inodes.add(inode)\n                result.append(file)\n        return result\n\n    def _select_dupes(self, dupes):\n        if dupes == self.selected_dupes:\n            return\n        self.selected_dupes = dupes\n        self.notify(\"dupes_selected\")\n\n    # --- Protected\n    def _get_fileclasses(self):\n        if self.app_mode == AppMode.PICTURE:\n            return [pe.photo.PLAT_SPECIFIC_PHOTO_CLASS]\n        elif self.app_mode == AppMode.MUSIC:\n            return [me.fs.MusicFile]\n        else:\n            return [se.fs.File]\n\n    def _prioritization_categories(self):\n        if self.app_mode == AppMode.PICTURE:\n            return pe.prioritize.all_categories()\n        elif self.app_mode == AppMode.MUSIC:\n            return me.prioritize.all_categories()\n        else:\n            return prioritize.all_categories()\n\n    # --- Public\n    def add_directory(self, d):\n        \"\"\"Adds folder ``d`` to :attr:`directories`.\n\n        Shows an error message dialog if something bad happens.\n\n        :param str d: path of folder to add\n        \"\"\"\n        try:\n            self.directories.add_path(Path(d))\n            self.notify(\"directories_changed\")\n        except directories.AlreadyThereError:\n            self.view.show_message(tr(\"'{}' already is in the list.\").format(d))\n        except directories.InvalidPathError:\n            self.view.show_message(tr(\"'{}' does not exist.\").format(d))\n\n    def add_selected_to_ignore_list(self):\n        \"\"\"Adds :attr:`selected_dupes` to :attr:`ignore_list`.\"\"\"\n        dupes = self.without_ref(self.selected_dupes)\n        if not dupes:\n            self.view.show_message(MSG_NO_SELECTED_DUPES)\n            return\n        msg = tr(\"All selected %d matches are going to be ignored in all subsequent scans. Continue?\")\n        if not self.view.ask_yes_no(msg % len(dupes)):\n            return\n        for dupe in dupes:\n            g = self.results.get_group_of_duplicate(dupe)\n            for other in g:\n                if other is not dupe:\n                    self.ignore_list.ignore(str(other.path), str(dupe.path))\n        self.remove_duplicates(dupes)\n        self.ignore_list_dialog.refresh()\n\n    def apply_filter(self, result_filter):\n        \"\"\"Apply a filter ``filter`` to the results so that it shows only dupe groups that match it.\n\n        :param str filter: filter to apply\n        \"\"\"\n        self.results.apply_filter(None)\n        if self.options[\"escape_filter_regexp\"]:\n            result_filter = escape(result_filter, set(\"()[]\\\\.|+?^\"))\n            result_filter = escape(result_filter, \"*\", \".\")\n        self.results.apply_filter(result_filter)\n        self._results_changed()\n\n    def clean_empty_dirs(self, path):\n        if self.options[\"clean_empty_dirs\"]:\n            while delete_if_empty(path, [\".DS_Store\"]):\n                path = path.parent\n\n    def clear_picture_cache(self):\n        try:\n            os.remove(self._get_picture_cache_path())\n        except FileNotFoundError:\n            pass  # we don't care\n\n    def clear_hash_cache(self):\n        fs.filesdb.clear()\n\n    def copy_or_move(self, dupe, copy: bool, destination: str, dest_type: DestType):\n        source_path = dupe.path\n        location_path = first(p for p in self.directories if p in dupe.path.parents)\n        dest_path = Path(destination)\n        if dest_type in {DestType.RELATIVE, DestType.ABSOLUTE}:\n            # no filename, no windows drive letter\n            source_base = source_path.relative_to(source_path.anchor).parent\n            if dest_type == DestType.RELATIVE:\n                source_base = source_base.relative_to(location_path.relative_to(location_path.anchor))\n            dest_path = dest_path.joinpath(source_base)\n        if not dest_path.exists():\n            dest_path.mkdir(parents=True)\n        # Add filename to dest_path. For file move/copy, it's not required, but for folders, yes.\n        dest_path = dest_path.joinpath(source_path.name)\n        logging.debug(\"Copy/Move operation from '%s' to '%s'\", source_path, dest_path)\n        # Raises an EnvironmentError if there's a problem\n        if copy:\n            smart_copy(source_path, dest_path)\n        else:\n            smart_move(source_path, dest_path)\n            self.clean_empty_dirs(source_path.parent)\n\n    def copy_or_move_marked(self, copy):\n        \"\"\"Start an async move (or copy) job on marked duplicates.\n\n        :param bool copy: If True, duplicates will be copied instead of moved\n        \"\"\"\n\n        def do(j):\n            def op(dupe):\n                j.add_progress()\n                self.copy_or_move(dupe, copy, destination, desttype)\n\n            j.start_job(self.results.mark_count)\n            self.results.perform_on_marked(op, not copy)\n\n        if not self.results.mark_count:\n            self.view.show_message(MSG_NO_MARKED_DUPES)\n            return\n        destination = self.view.select_dest_folder(\n            tr(\"Select a directory to copy marked files to\")\n            if copy\n            else tr(\"Select a directory to move marked files to\")\n        )\n        if destination:\n            desttype = self.options[\"copymove_dest_type\"]\n            jobid = JobType.COPY if copy else JobType.MOVE\n            self._start_job(jobid, do)\n\n    def delete_marked(self):\n        \"\"\"Start an async job to send marked duplicates to the trash.\"\"\"\n        if not self.results.mark_count:\n            self.view.show_message(MSG_NO_MARKED_DUPES)\n            return\n        if not self.deletion_options.show(self.results.mark_count):\n            return\n        args = [\n            self.deletion_options.link_deleted,\n            self.deletion_options.use_hardlinks,\n            self.deletion_options.direct,\n        ]\n        logging.debug(\"Starting deletion job with args %r\", args)\n        self._start_job(JobType.DELETE, self._do_delete, args=args)\n\n    def export_to_xhtml(self):\n        \"\"\"Export current results to XHTML.\n\n        The configuration of the :attr:`result_table` (columns order and visibility) is used to\n        determine how the data is presented in the export. In other words, the exported table in\n        the resulting XHTML will look just like the results table.\n        \"\"\"\n        colnames, rows = self._get_export_data()\n        export_path = export.export_to_xhtml(colnames, rows)\n        desktop.open_path(export_path)\n\n    def export_to_csv(self):\n        \"\"\"Export current results to CSV.\n\n        The columns and their order in the resulting CSV file is determined in the same way as in\n        :meth:`export_to_xhtml`.\n        \"\"\"\n        dest_file = self.view.select_dest_file(tr(\"Select a destination for your exported CSV\"), \"csv\")\n        if dest_file:\n            colnames, rows = self._get_export_data()\n            try:\n                export.export_to_csv(dest_file, colnames, rows)\n            except OSError as e:\n                self.view.show_message(tr(\"Couldn't write to file: {}\").format(str(e)))\n\n    def get_display_info(self, dupe, group, delta=False):\n        def empty_data():\n            return {c.name: \"---\" for c in self.result_table.COLUMNS[1:]}\n\n        if (dupe is None) or (group is None):\n            return empty_data()\n        try:\n            return dupe.get_display_info(group, delta)\n        except Exception as e:\n            logging.warning(\"Exception (type: %s) on GetDisplayInfo for %s: %s\", type(e), str(dupe.path), str(e))\n            return empty_data()\n\n    def invoke_custom_command(self):\n        \"\"\"Calls command in ``CustomCommand`` pref with ``%d`` and ``%r`` placeholders replaced.\n\n        Using the current selection, ``%d`` is replaced with the currently selected dupe and ``%r``\n        is replaced with that dupe's ref file. If there's no selection, the command is not invoked.\n        If the dupe is a ref, ``%d`` and ``%r`` will be the same.\n        \"\"\"\n        cmd = self.view.get_default(\"CustomCommand\")\n        if not cmd:\n            msg = tr(\"You have no custom command set up. Set it up in your preferences.\")\n            self.view.show_message(msg)\n            return\n        if not self.selected_dupes:\n            return\n        dupes = self.selected_dupes\n        refs = [self.results.get_group_of_duplicate(dupe).ref for dupe in dupes]\n        for dupe, ref in zip(dupes, refs):\n            dupe_cmd = cmd.replace(\"%d\", str(dupe.path))\n            dupe_cmd = dupe_cmd.replace(\"%r\", str(ref.path))\n            match = re.match(r'\"([^\"]+)\"(.*)', dupe_cmd)\n            if match is not None:\n                # This code here is because subprocess. Popen doesn't seem to accept, under Windows,\n                # executable paths with spaces in it, *even* when they're enclosed in \"\". So this is\n                # a workaround to make the damn thing work.\n                exepath, args = match.groups()\n                path, exename = op.split(exepath)\n                p = subprocess.Popen(\n                    exename + args, shell=True, cwd=path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT\n                )\n                output = p.stdout.read()\n                logging.info(\"Custom command %s %s: %s\", exename, args, output)\n            else:\n                p = subprocess.Popen(dupe_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n                output = p.stdout.read()\n                logging.info(\"Custom command %s: %s\", dupe_cmd, output)\n\n    def load(self):\n        \"\"\"Load directory selection and ignore list from files in appdata.\n\n        This method is called during startup so that directory selection and ignore list, which\n        is persistent data, is the same as when the last session was closed (when :meth:`save` was\n        called).\n        \"\"\"\n        self.directories.load_from_file(op.join(self.appdata, \"last_directories.xml\"))\n        self.notify(\"directories_changed\")\n        p = op.join(self.appdata, \"ignore_list.xml\")\n        self.ignore_list.load_from_xml(p)\n        self.ignore_list_dialog.refresh()\n        p = op.join(self.appdata, \"exclude_list.xml\")\n        self.exclude_list.load_from_xml(p)\n        self.exclude_list_dialog.refresh()\n\n    def load_directories(self, filepath):\n        # Clear out previous entries\n        self.directories.__init__()\n        self.directories.load_from_file(filepath)\n        self.notify(\"directories_changed\")\n\n    def load_from(self, filename):\n        \"\"\"Start an async job to load results from ``filename``.\n\n        :param str filename: path of the XML file (created with :meth:`save_as`) to load\n        \"\"\"\n\n        def do(j):\n            self.results.load_from_xml(filename, self._get_file, j)\n\n        self._start_job(JobType.LOAD, do)\n\n    def make_selected_reference(self):\n        \"\"\"Promote :attr:`selected_dupes` to reference position within their respective groups.\n\n        Each selected dupe will become the :attr:`~core.engine.Group.ref` of its group. If there's\n        more than one dupe selected for the same group, only the first (in the order currently shown\n        in :attr:`result_table`) dupe will be promoted.\n        \"\"\"\n        dupes = self.without_ref(self.selected_dupes)\n        changed_groups = set()\n        for dupe in dupes:\n            g = self.results.get_group_of_duplicate(dupe)\n            if g not in changed_groups and self.results.make_ref(dupe):\n                changed_groups.add(g)\n        # It's not always obvious to users what this action does, so to make it a bit clearer,\n        # we change our selection to the ref of all changed groups. However, we also want to keep\n        # the files that were ref before and weren't changed by the action. In effect, what this\n        # does is that we keep our old selection, but remove all non-ref dupes from it.\n        # If no group was changed, however, we don't touch the selection.\n        if not self.result_table.power_marker:\n            if changed_groups:\n                self.selected_dupes = [\n                    d for d in self.selected_dupes if self.results.get_group_of_duplicate(d).ref is d\n                ]\n            self.notify(\"results_changed\")\n        else:\n            # If we're in \"Dupes Only\" mode (previously called Power Marker), things are a bit\n            # different. The refs are not shown in the table, and if our operation is successful,\n            # this means that there's no way to follow our dupe selection. Then, the best thing to\n            # do is to keep our selection index-wise (different dupe selection, but same index\n            # selection).\n            self.notify(\"results_changed_but_keep_selection\")\n\n    def mark_all(self):\n        \"\"\"Set all dupes in the results as marked.\"\"\"\n        self.results.mark_all()\n        self.notify(\"marking_changed\")\n\n    def mark_none(self):\n        \"\"\"Set all dupes in the results as unmarked.\"\"\"\n        self.results.mark_none()\n        self.notify(\"marking_changed\")\n\n    def mark_invert(self):\n        \"\"\"Invert the marked state of all dupes in the results.\"\"\"\n        self.results.mark_invert()\n        self.notify(\"marking_changed\")\n\n    def mark_dupe(self, dupe, marked):\n        \"\"\"Change marked status of ``dupe``.\n\n        :param dupe: dupe to mark/unmark\n        :type dupe: :class:`~core.fs.File`\n        :param bool marked: True = mark, False = unmark\n        \"\"\"\n        if marked:\n            self.results.mark(dupe)\n        else:\n            self.results.unmark(dupe)\n        self.notify(\"marking_changed\")\n\n    def open_selected(self):\n        \"\"\"Open :attr:`selected_dupes` with their associated application.\"\"\"\n        if len(self.selected_dupes) > 10 and not self.view.ask_yes_no(MSG_MANY_FILES_TO_OPEN):\n            return\n        for dupe in self.selected_dupes:\n            desktop.open_path(dupe.path)\n\n    def purge_ignore_list(self):\n        \"\"\"Remove files that don't exist from :attr:`ignore_list`.\"\"\"\n        self.ignore_list.filter(lambda f, s: op.exists(f) and op.exists(s))\n        self.ignore_list_dialog.refresh()\n\n    def remove_directories(self, indexes):\n        \"\"\"Remove root directories at ``indexes`` from :attr:`directories`.\n\n        :param indexes: Indexes of the directories to remove.\n        :type indexes: list of int\n        \"\"\"\n        try:\n            indexes = sorted(indexes, reverse=True)\n            for index in indexes:\n                del self.directories[index]\n            self.notify(\"directories_changed\")\n        except IndexError:\n            pass\n\n    def remove_duplicates(self, duplicates):\n        \"\"\"Remove ``duplicates`` from :attr:`results`.\n\n        Calls :meth:`~core.results.Results.remove_duplicates` and send appropriate notifications.\n\n        :param duplicates: duplicates to remove.\n        :type duplicates: list of :class:`~core.fs.File`\n        \"\"\"\n        self.results.remove_duplicates(self.without_ref(duplicates))\n        self.notify(\"results_changed_but_keep_selection\")\n\n    def remove_marked(self):\n        \"\"\"Removed marked duplicates from the results (without touching the files themselves).\"\"\"\n        if not self.results.mark_count:\n            self.view.show_message(MSG_NO_MARKED_DUPES)\n            return\n        msg = tr(\"You are about to remove %d files from results. Continue?\")\n        if not self.view.ask_yes_no(msg % self.results.mark_count):\n            return\n        self.results.perform_on_marked(lambda x: None, True)\n        self._results_changed()\n\n    def remove_selected(self):\n        \"\"\"Removed :attr:`selected_dupes` from the results (without touching the files themselves).\"\"\"\n        dupes = self.without_ref(self.selected_dupes)\n        if not dupes:\n            self.view.show_message(MSG_NO_SELECTED_DUPES)\n            return\n        msg = tr(\"You are about to remove %d files from results. Continue?\")\n        if not self.view.ask_yes_no(msg % len(dupes)):\n            return\n        self.remove_duplicates(dupes)\n\n    def rename_selected(self, newname):\n        \"\"\"Renames the selected dupes's file to ``newname``.\n\n        If there's more than one selected dupes, the first one is used.\n\n        :param str newname: The filename to rename the dupe's file to.\n        \"\"\"\n        try:\n            d = self.selected_dupes[0]\n            d.rename(newname)\n            return True\n        except (IndexError, fs.FSError) as e:\n            logging.warning(\"dupeGuru Warning: %s\" % str(e))\n        return False\n\n    def reprioritize_groups(self, sort_key):\n        \"\"\"Sort dupes in each group (in :attr:`results`) according to ``sort_key``.\n\n        Called by the re-prioritize dialog. Calls :meth:`~core.engine.Group.prioritize` and, once\n        the sorting is done, show a message that confirms the action.\n\n        :param sort_key: The key being sent to :meth:`~core.engine.Group.prioritize`\n        :type sort_key: f(dupe)\n        \"\"\"\n        count = 0\n        for group in self.results.groups:\n            if group.prioritize(key_func=sort_key):\n                count += 1\n        if count:\n            self.results.refresh_required = True\n        self._results_changed()\n        msg = tr(\"{} duplicate groups were changed by the re-prioritization.\").format(count)\n        self.view.show_message(msg)\n\n    def reveal_selected(self):\n        if self.selected_dupes:\n            desktop.reveal_path(self.selected_dupes[0].path)\n\n    def save(self):\n        if not op.exists(self.appdata):\n            os.makedirs(self.appdata)\n        self.directories.save_to_file(op.join(self.appdata, \"last_directories.xml\"))\n        p = op.join(self.appdata, \"ignore_list.xml\")\n        self.ignore_list.save_to_xml(p)\n        p = op.join(self.appdata, \"exclude_list.xml\")\n        self.exclude_list.save_to_xml(p)\n        self.notify(\"save_session\")\n\n    def close(self):\n        fs.filesdb.close()\n\n    def save_as(self, filename):\n        \"\"\"Save results in ``filename``.\n\n        :param str filename: path of the file to save results (as XML) to.\n        \"\"\"\n        try:\n            self.results.save_to_xml(filename)\n        except OSError as e:\n            self.view.show_message(tr(\"Couldn't write to file: {}\").format(str(e)))\n\n    def save_directories_as(self, filename):\n        \"\"\"Save directories in ``filename``.\n\n        :param str filename: path of the file to save directories (as XML) to.\n        \"\"\"\n        try:\n            self.directories.save_to_file(filename)\n        except OSError as e:\n            self.view.show_message(tr(\"Couldn't write to file: {}\").format(str(e)))\n\n    def start_scanning(self, profile_scan=False):\n        \"\"\"Starts an async job to scan for duplicates.\n\n        Scans folders selected in :attr:`directories` and put the results in :attr:`results`\n        \"\"\"\n        scanner = self.SCANNER_CLASS()\n        fs.filesdb.ignore_mtime = self.options[\"rehash_ignore_mtime\"] is True\n        if not self.directories.has_any_file():\n            self.view.show_message(tr(\"The selected directories contain no scannable file.\"))\n            return\n        # Send relevant options down to the scanner instance\n        for k, v in self.options.items():\n            if hasattr(scanner, k):\n                setattr(scanner, k, v)\n        if self.app_mode == AppMode.PICTURE:\n            scanner.cache_path = self._get_picture_cache_path()\n        self.results.groups = []\n        self._recreate_result_table()\n        self._results_changed()\n\n        def do(j):\n            if profile_scan:\n                pr = cProfile.Profile()\n                pr.enable()\n            j.set_progress(0, tr(\"Collecting files to scan\"))\n            if scanner.scan_type == ScanType.FOLDERS:\n                files = list(self.directories.get_folders(folderclass=se.fs.Folder, j=j))\n            else:\n                files = list(self.directories.get_files(fileclasses=self.fileclasses, j=j))\n            if self.options[\"ignore_hardlink_matches\"]:\n                files = self._remove_hardlink_dupes(files)\n            logging.info(\"Scanning %d files\" % len(files))\n            self.results.groups = scanner.get_dupe_groups(files, self.ignore_list, j)\n            self.discarded_file_count = scanner.discarded_file_count\n            if profile_scan:\n                pr.disable()\n                pr.dump_stats(op.join(self.appdata, f\"{datetime.datetime.now():%Y-%m-%d_%H-%M-%S}.profile\"))\n\n        self._start_job(JobType.SCAN, do)\n\n    def toggle_selected_mark_state(self):\n        selected = self.without_ref(self.selected_dupes)\n        if not selected:\n            return\n        if allsame(self.results.is_marked(d) for d in selected):\n            markfunc = self.results.mark_toggle\n        else:\n            markfunc = self.results.mark\n        for dupe in selected:\n            markfunc(dupe)\n        self.notify(\"marking_changed\")\n\n    def without_ref(self, dupes):\n        \"\"\"Returns ``dupes`` with all reference elements removed.\"\"\"\n        return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]\n\n    def get_default(self, key, fallback_value=None):\n        result = nonone(self.view.get_default(key), fallback_value)\n        if fallback_value is not None and not isinstance(result, type(fallback_value)):\n            # we don't want to end up with garbage values from the prefs\n            try:\n                result = type(fallback_value)(result)\n            except Exception:\n                result = fallback_value\n        return result\n\n    def set_default(self, key, value):\n        self.view.set_default(key, value)\n\n    # --- Properties\n    @property\n    def stat_line(self):\n        result = self.results.stat_line\n        if self.discarded_file_count:\n            result = tr(\"%s (%d discarded)\") % (result, self.discarded_file_count)\n        return result\n\n    @property\n    def fileclasses(self):\n        return self._get_fileclasses()\n\n    @property\n    def SCANNER_CLASS(self):\n        if self.app_mode == AppMode.PICTURE:\n            return pe.scanner.ScannerPE\n        elif self.app_mode == AppMode.MUSIC:\n            return me.scanner.ScannerME\n        else:\n            return se.scanner.ScannerSE\n\n    @property\n    def METADATA_TO_READ(self):\n        if self.app_mode == AppMode.PICTURE:\n            return [\"size\", \"mtime\", \"dimensions\", \"exif_timestamp\"]\n        elif self.app_mode == AppMode.MUSIC:\n            return [\n                \"size\",\n                \"mtime\",\n                \"duration\",\n                \"bitrate\",\n                \"samplerate\",\n                \"title\",\n                \"artist\",\n                \"album\",\n                \"genre\",\n                \"year\",\n                \"track\",\n                \"comment\",\n            ]\n        else:\n            return [\"size\", \"mtime\"]\n"
  },
  {
    "path": "core/directories.py",
    "content": "# Copyright 2017 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os\nfrom xml.etree import ElementTree as ET\nimport logging\nfrom pathlib import Path\n\nfrom hscommon.jobprogress import job\nfrom hscommon.util import FileOrPath\nfrom hscommon.trans import tr\n\nfrom core import fs\n\n__all__ = [\n    \"Directories\",\n    \"DirectoryState\",\n    \"AlreadyThereError\",\n    \"InvalidPathError\",\n]\n\n\nclass DirectoryState:\n    \"\"\"Enum describing how a folder should be considered.\n\n    * DirectoryState.Normal: Scan all files normally\n    * DirectoryState.Reference: Scan files, but make sure never to delete any of them\n    * DirectoryState.Excluded: Don't scan this folder\n    \"\"\"\n\n    NORMAL = 0\n    REFERENCE = 1\n    EXCLUDED = 2\n\n\nclass AlreadyThereError(Exception):\n    \"\"\"The path being added is already in the directory list\"\"\"\n\n\nclass InvalidPathError(Exception):\n    \"\"\"The path being added is invalid\"\"\"\n\n\nclass Directories:\n    \"\"\"Holds user folder selection.\n\n    Manages the selection that the user make through the folder selection dialog. It also manages\n    folder states, and how recursion applies to them.\n\n    Then, when the user starts the scan, :meth:`get_files` is called to retrieve all files (wrapped\n    in :mod:`core.fs`) that have to be scanned according to the chosen folders/states.\n    \"\"\"\n\n    # ---Override\n    def __init__(self, exclude_list=None):\n        self._dirs = []\n        # {path: state}\n        self.states = {}\n        self._exclude_list = exclude_list\n\n    def __contains__(self, path):\n        for p in self._dirs:\n            if path == p or p in path.parents:\n                return True\n        return False\n\n    def __delitem__(self, key):\n        self._dirs.__delitem__(key)\n\n    def __getitem__(self, key):\n        return self._dirs.__getitem__(key)\n\n    def __len__(self):\n        return len(self._dirs)\n\n    # ---Private\n    def _default_state_for_path(self, path):\n        # New logic with regex filters\n        if self._exclude_list is not None and self._exclude_list.mark_count > 0:\n            # We iterate even if we only have one item here\n            for denied_path_re in self._exclude_list.compiled:\n                if denied_path_re.match(str(path.name)):\n                    return DirectoryState.EXCLUDED\n            return DirectoryState.NORMAL\n        # Override this in subclasses to specify the state of some special folders.\n        if path.name.startswith(\".\"):\n            return DirectoryState.EXCLUDED\n        return DirectoryState.NORMAL\n\n    def _get_files(self, from_path, fileclasses, j):\n        try:\n            with os.scandir(from_path) as iter:\n                root_path = Path(from_path)\n                state = self.get_state(root_path)\n                # if we have no un-excluded dirs under this directory skip going deeper\n                skip_dirs = state == DirectoryState.EXCLUDED and not any(\n                    p.parts[: len(root_path.parts)] == root_path.parts for p in self.states\n                )\n                count = 0\n                for item in iter:\n                    j.check_if_cancelled()\n                    try:\n                        if item.is_dir():\n                            if skip_dirs:\n                                continue\n                            yield from self._get_files(item.path, fileclasses, j)\n                            continue\n                        elif state == DirectoryState.EXCLUDED:\n                            continue\n                        # File excluding or not\n                        if (\n                            self._exclude_list is None\n                            or not self._exclude_list.mark_count\n                            or not self._exclude_list.is_excluded(str(from_path), item.name)\n                        ):\n                            file = fs.get_file(item, fileclasses=fileclasses)\n                            if file:\n                                file.is_ref = state == DirectoryState.REFERENCE\n                                count += 1\n                                yield file\n                    except (OSError, fs.InvalidPath):\n                        pass\n                logging.debug(\n                    \"Collected %d files in folder %s\",\n                    count,\n                    str(root_path),\n                )\n        except OSError:\n            pass\n\n    def _get_folders(self, from_folder, j):\n        j.check_if_cancelled()\n        try:\n            for subfolder in from_folder.subfolders:\n                yield from self._get_folders(subfolder, j)\n            state = self.get_state(from_folder.path)\n            if state != DirectoryState.EXCLUDED:\n                from_folder.is_ref = state == DirectoryState.REFERENCE\n                logging.debug(\"Yielding Folder %r state: %d\", from_folder, state)\n                yield from_folder\n        except (OSError, fs.InvalidPath):\n            pass\n\n    # ---Public\n    def add_path(self, path):\n        \"\"\"Adds ``path`` to self, if not already there.\n\n        Raises :exc:`AlreadyThereError` if ``path`` is already in self. If path is a directory\n        containing some of the directories already present in self, ``path`` will be added, but all\n        directories under it will be removed. Can also raise :exc:`InvalidPathError` if ``path``\n        does not exist.\n\n        :param Path path: path to add\n        \"\"\"\n        if path in self:\n            raise AlreadyThereError()\n        if not path.exists():\n            raise InvalidPathError()\n        self._dirs = [p for p in self._dirs if path not in p.parents]\n        self._dirs.append(path)\n\n    @staticmethod\n    def get_subfolders(path):\n        \"\"\"Returns a sorted list of paths corresponding to subfolders in ``path``.\n\n        :param Path path: get subfolders from there\n        :rtype: list of Path\n        \"\"\"\n        try:\n            subpaths = [p for p in path.glob(\"*\") if p.is_dir()]\n            subpaths.sort(key=lambda x: x.name.lower())\n            return subpaths\n        except OSError:\n            return []\n\n    def get_files(self, fileclasses=None, j=job.nulljob):\n        \"\"\"Returns a list of all files that are not excluded.\n\n        Returned files also have their ``is_ref`` attr set if applicable.\n        \"\"\"\n        if fileclasses is None:\n            fileclasses = [fs.File]\n        file_count = 0\n        for path in self._dirs:\n            for file in self._get_files(path, fileclasses=fileclasses, j=j):\n                file_count += 1\n                if not isinstance(j, job.NullJob):\n                    j.set_progress(-1, tr(\"Collected {} files to scan\").format(file_count))\n                yield file\n\n    def get_folders(self, folderclass=None, j=job.nulljob):\n        \"\"\"Returns a list of all folders that are not excluded.\n\n        Returned folders also have their ``is_ref`` attr set if applicable.\n        \"\"\"\n        if folderclass is None:\n            folderclass = fs.Folder\n        folder_count = 0\n        for path in self._dirs:\n            from_folder = folderclass(path)\n            for folder in self._get_folders(from_folder, j):\n                folder_count += 1\n                if not isinstance(j, job.NullJob):\n                    j.set_progress(-1, tr(\"Collected {} folders to scan\").format(folder_count))\n                yield folder\n\n    def get_state(self, path):\n        \"\"\"Returns the state of ``path``.\n\n        :rtype: :class:`DirectoryState`\n        \"\"\"\n        # direct match? easy result.\n        if path in self.states:\n            return self.states[path]\n        state = self._default_state_for_path(path)\n        # Save non-default states in cache, necessary for _get_files()\n        if state != DirectoryState.NORMAL:\n            self.states[path] = state\n            return state\n        # find the longest parent path that is in states and return that state if found\n        # NOTE: path.parents is ordered longest to shortest\n        for parent_path in path.parents:\n            if parent_path in self.states:\n                return self.states[parent_path]\n        return state\n\n    def has_any_file(self):\n        \"\"\"Returns whether selected folders contain any file.\n\n        Because it stops at the first file it finds, it's much faster than get_files().\n\n        :rtype: bool\n        \"\"\"\n        try:\n            next(self.get_files())\n            return True\n        except StopIteration:\n            return False\n\n    def load_from_file(self, infile):\n        \"\"\"Load folder selection from ``infile``.\n\n        :param file infile: path or file pointer to XML generated through :meth:`save_to_file`\n        \"\"\"\n        try:\n            root = ET.parse(infile).getroot()\n        except Exception:\n            return\n        for rdn in root.iter(\"root_directory\"):\n            attrib = rdn.attrib\n            if \"path\" not in attrib:\n                continue\n            path = attrib[\"path\"]\n            try:\n                self.add_path(Path(path))\n            except (AlreadyThereError, InvalidPathError):\n                pass\n        for sn in root.iter(\"state\"):\n            attrib = sn.attrib\n            if not (\"path\" in attrib and \"value\" in attrib):\n                continue\n            path = attrib[\"path\"]\n            state = attrib[\"value\"]\n            self.states[Path(path)] = int(state)\n\n    def save_to_file(self, outfile):\n        \"\"\"Save folder selection as XML to ``outfile``.\n\n        :param file outfile: path or file pointer to XML file to save to.\n        \"\"\"\n        with FileOrPath(outfile, \"wb\") as fp:\n            root = ET.Element(\"directories\")\n            for root_path in self:\n                root_path_node = ET.SubElement(root, \"root_directory\")\n                root_path_node.set(\"path\", str(root_path))\n            for path, state in self.states.items():\n                state_node = ET.SubElement(root, \"state\")\n                state_node.set(\"path\", str(path))\n                state_node.set(\"value\", str(state))\n            tree = ET.ElementTree(root)\n            tree.write(fp, encoding=\"utf-8\")\n\n    def set_state(self, path, state):\n        \"\"\"Set the state of folder at ``path``.\n\n        :param Path path: path of the target folder\n        :param state: state to set folder to\n        :type state: :class:`DirectoryState`\n        \"\"\"\n        if self.get_state(path) == state:\n            return\n        for iter_path in list(self.states.keys()):\n            if path in iter_path.parents:\n                del self.states[iter_path]\n        self.states[path] = state\n"
  },
  {
    "path": "core/engine.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/01/29\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport difflib\nimport itertools\nimport logging\nimport string\nfrom collections import defaultdict, namedtuple\nfrom unicodedata import normalize\n\nfrom hscommon.util import flatten, multi_replace\nfrom hscommon.trans import tr\nfrom hscommon.jobprogress import job\n\n(\n    WEIGHT_WORDS,\n    MATCH_SIMILAR_WORDS,\n    NO_FIELD_ORDER,\n) = range(3)\n\nJOB_REFRESH_RATE = 100\nPROGRESS_MESSAGE = tr(\"%d matches found from %d groups\")\n\n\ndef getwords(s):\n    # We decompose the string so that ascii letters with accents can be part of the word.\n    s = normalize(\"NFD\", s)\n    s = multi_replace(s, \"-_&+():;\\\\[]{}.,<>/?~!@#$*\", \" \").lower()\n    # logging.debug(f\"DEBUG chars for: {s}\\n\"\n    #               f\"{[c for c in s if ord(c) != 32]}\\n\"\n    #               f\"{[ord(c) for c in s if ord(c) != 32]}\")\n    # HACK We shouldn't ignore non-ascii characters altogether. Any Unicode char\n    # above common european characters that cannot be \"sanitized\" (ie. stripped\n    # of their accents, etc.) are preserved as is. The arbitrary limit is\n    # obtained from this one: ord(\"\\u037e\") GREEK QUESTION MARK\n    s = \"\".join(\n        c\n        for c in s\n        if (ord(c) <= 894 and c in string.ascii_letters + string.digits + string.whitespace) or ord(c) > 894\n    )\n    return [_f for _f in s.split(\" \") if _f]  # remove empty elements\n\n\ndef getfields(s):\n    fields = [getwords(field) for field in s.split(\" - \")]\n    return [_f for _f in fields if _f]\n\n\ndef unpack_fields(fields):\n    result = []\n    for field in fields:\n        if isinstance(field, list):\n            result += field\n        else:\n            result.append(field)\n    return result\n\n\ndef compare(first, second, flags=()):\n    \"\"\"Returns the % of words that match between ``first`` and ``second``\n\n    The result is a ``int`` in the range 0..100.\n    ``first`` and ``second`` can be either a string or a list (of words).\n    \"\"\"\n    if not (first and second):\n        return 0\n    if any(isinstance(element, list) for element in first):\n        return compare_fields(first, second, flags)\n    second = second[:]  # We must use a copy of second because we remove items from it\n    match_similar = MATCH_SIMILAR_WORDS in flags\n    weight_words = WEIGHT_WORDS in flags\n    joined = first + second\n    total_count = sum(len(word) for word in joined) if weight_words else len(joined)\n    match_count = 0\n    in_order = True\n    for word in first:\n        if match_similar and (word not in second):\n            similar = difflib.get_close_matches(word, second, 1, 0.8)\n            if similar:\n                word = similar[0]\n        if word in second:\n            if second[0] != word:\n                in_order = False\n            second.remove(word)\n            match_count += len(word) if weight_words else 1\n    result = round(((match_count * 2) / total_count) * 100)\n    if (result == 100) and (not in_order):\n        result = 99  # We cannot consider a match exact unless the ordering is the same\n    return result\n\n\ndef compare_fields(first, second, flags=()):\n    \"\"\"Returns the score for the lowest matching :ref:`fields`.\n\n    ``first`` and ``second`` must be lists of lists of string. Each sub-list is then compared with\n    :func:`compare`.\n    \"\"\"\n    if len(first) != len(second):\n        return 0\n    if NO_FIELD_ORDER in flags:\n        results = []\n        # We don't want to remove field directly in the list. We must work on a copy.\n        second = second[:]\n        for field1 in first:\n            max_score = 0\n            matched_field = None\n            for field2 in second:\n                r = compare(field1, field2, flags)\n                if r > max_score:\n                    max_score = r\n                    matched_field = field2\n            results.append(max_score)\n            if matched_field:\n                second.remove(matched_field)\n    else:\n        results = [compare(field1, field2, flags) for field1, field2 in zip(first, second)]\n    return min(results) if results else 0\n\n\ndef build_word_dict(objects, j=job.nulljob):\n    \"\"\"Returns a dict of objects mapped by their words.\n\n    objects must have a ``words`` attribute being a list of strings or a list of lists of strings\n    (:ref:`fields`).\n\n    The result will be a dict with words as keys, lists of objects as values.\n    \"\"\"\n    result = defaultdict(set)\n    for object in j.iter_with_progress(objects, \"Prepared %d/%d files\", JOB_REFRESH_RATE):\n        for word in unpack_fields(object.words):\n            result[word].add(object)\n    return result\n\n\ndef merge_similar_words(word_dict):\n    \"\"\"Take all keys in ``word_dict`` that are similar, and merge them together.\n\n    ``word_dict`` has been built with :func:`build_word_dict`. Similarity is computed with Python's\n    ``difflib.get_close_matches()``, which computes the number of edits that are necessary to make\n    a word equal to the other.\n    \"\"\"\n    keys = list(word_dict.keys())\n    keys.sort(key=len)  # we want the shortest word to stay\n    while keys:\n        key = keys.pop(0)\n        similars = difflib.get_close_matches(key, keys, 100, 0.8)\n        if not similars:\n            continue\n        objects = word_dict[key]\n        for similar in similars:\n            objects |= word_dict[similar]\n            del word_dict[similar]\n            keys.remove(similar)\n\n\ndef reduce_common_words(word_dict, threshold):\n    \"\"\"Remove all objects from ``word_dict`` values where the object count >= ``threshold``\n\n    ``word_dict`` has been built with :func:`build_word_dict`.\n\n    The exception to this removal are the objects where all the words of the object are common.\n    Because if we remove them, we will miss some duplicates!\n    \"\"\"\n    uncommon_words = {word for word, objects in word_dict.items() if len(objects) < threshold}\n    for word, objects in list(word_dict.items()):\n        if len(objects) < threshold:\n            continue\n        reduced = set()\n        for o in objects:\n            if not any(w in uncommon_words for w in unpack_fields(o.words)):\n                reduced.add(o)\n        if reduced:\n            word_dict[word] = reduced\n        else:\n            del word_dict[word]\n\n\n# Writing docstrings in a namedtuple is tricky. From Python 3.3, it's possible to set __doc__, but\n# some research allowed me to find a more elegant solution, which is what is done here. See\n# http://stackoverflow.com/questions/1606436/adding-docstrings-to-namedtuples-in-python\n\n\nclass Match(namedtuple(\"Match\", \"first second percentage\")):\n    \"\"\"Represents a match between two :class:`~core.fs.File`.\n\n    Regarless of the matching method, when two files are determined to match, a Match pair is created,\n    which holds, of course, the two matched files, but also their match \"level\".\n\n    .. attribute:: first\n\n        first file of the pair.\n\n    .. attribute:: second\n\n        second file of the pair.\n\n    .. attribute:: percentage\n\n        their match level according to the scan method which found the match. int from 1 to 100. For\n        exact scan methods, such as Contents scans, this will always be 100.\n    \"\"\"\n\n    __slots__ = ()\n\n\ndef get_match(first, second, flags=()):\n    # it is assumed here that first and second both have a \"words\" attribute\n    percentage = compare(first.words, second.words, flags)\n    return Match(first, second, percentage)\n\n\ndef getmatches(\n    objects,\n    min_match_percentage=0,\n    match_similar_words=False,\n    weight_words=False,\n    no_field_order=False,\n    j=job.nulljob,\n):\n    \"\"\"Returns a list of :class:`Match` within ``objects`` after fuzzily matching their words.\n\n    :param objects: List of :class:`~core.fs.File` to match.\n    :param int min_match_percentage: minimum % of words that have to match.\n    :param bool match_similar_words: make similar words (see :func:`merge_similar_words`) match.\n    :param bool weight_words: longer words are worth more in match % computations.\n    :param bool no_field_order: match :ref:`fields` regardless of their order.\n    :param j: A :ref:`job progress instance <jobs>`.\n    \"\"\"\n    COMMON_WORD_THRESHOLD = 50\n    LIMIT = 5000000\n    j = j.start_subjob(2)\n    sj = j.start_subjob(2)\n    for o in objects:\n        if not hasattr(o, \"words\"):\n            o.words = getwords(o.name)\n    word_dict = build_word_dict(objects, sj)\n    reduce_common_words(word_dict, COMMON_WORD_THRESHOLD)\n    if match_similar_words:\n        merge_similar_words(word_dict)\n    match_flags = []\n    if weight_words:\n        match_flags.append(WEIGHT_WORDS)\n    if match_similar_words:\n        match_flags.append(MATCH_SIMILAR_WORDS)\n    if no_field_order:\n        match_flags.append(NO_FIELD_ORDER)\n    j.start_job(len(word_dict), PROGRESS_MESSAGE % (0, 0))\n    compared = defaultdict(set)\n    result = []\n    try:\n        word_count = 0\n        # This whole 'popping' thing is there to avoid taking too much memory at the same time.\n        while word_dict:\n            items = word_dict.popitem()[1]\n            while items:\n                ref = items.pop()\n                compared_already = compared[ref]\n                to_compare = items - compared_already\n                compared_already |= to_compare\n                for other in to_compare:\n                    m = get_match(ref, other, match_flags)\n                    if m.percentage >= min_match_percentage:\n                        result.append(m)\n                        if len(result) >= LIMIT:\n                            return result\n            word_count += 1\n            j.add_progress(desc=PROGRESS_MESSAGE % (len(result), word_count))\n    except MemoryError:\n        # This is the place where the memory usage is at its peak during the scan.\n        # Just continue the process with an incomplete list of matches.\n        del compared  # This should give us enough room to call logging.\n        logging.warning(\"Memory Overflow. Matches: %d. Word dict: %d\" % (len(result), len(word_dict)))\n        return result\n    return result\n\n\ndef getmatches_by_contents(files, bigsize=0, j=job.nulljob):\n    \"\"\"Returns a list of :class:`Match` within ``files`` if their contents is the same.\n\n    :param bigsize: The size in bytes over which we consider files big enough to\n                    justify taking samples of the file for hashing. If 0, compute digest as usual.\n    :param j: A :ref:`job progress instance <jobs>`.\n    \"\"\"\n    size2files = defaultdict(set)\n    for f in files:\n        size2files[f.size].add(f)\n    del files\n    possible_matches = [files for files in size2files.values() if len(files) > 1]\n    del size2files\n    result = []\n    j.start_job(len(possible_matches), PROGRESS_MESSAGE % (0, 0))\n    group_count = 0\n    for group in possible_matches:\n        for first, second in itertools.combinations(group, 2):\n            if first.is_ref and second.is_ref:\n                continue  # Don't spend time comparing two ref pics together.\n            if first.size == 0 and second.size == 0:\n                # skip hashing for zero length files\n                result.append(Match(first, second, 100))\n                continue\n            # if digests are the same (and not None) then files match\n            if first.digest_partial is not None and first.digest_partial == second.digest_partial:\n                if bigsize > 0 and first.size > bigsize:\n                    if first.digest_samples is not None and first.digest_samples == second.digest_samples:\n                        result.append(Match(first, second, 100))\n                else:\n                    if first.digest is not None and first.digest == second.digest:\n                        result.append(Match(first, second, 100))\n        group_count += 1\n        j.add_progress(desc=PROGRESS_MESSAGE % (len(result), group_count))\n    return result\n\n\nclass Group:\n    \"\"\"A group of :class:`~core.fs.File` that match together.\n\n    This manages match pairs into groups and ensures that all files in the group match to each\n    other.\n\n    .. attribute:: ref\n\n        The \"reference\" file, which is the file among the group that isn't going to be deleted.\n\n    .. attribute:: ordered\n\n        Ordered list of duplicates in the group (including the :attr:`ref`).\n\n    .. attribute:: unordered\n\n        Set duplicates in the group (including the :attr:`ref`).\n\n    .. attribute:: dupes\n\n        An ordered list of the group's duplicate, without :attr:`ref`. Equivalent to\n        ``ordered[1:]``\n\n    .. attribute:: percentage\n\n        Average match percentage of match pairs containing :attr:`ref`.\n    \"\"\"\n\n    # ---Override\n    def __init__(self):\n        self._clear()\n\n    def __contains__(self, item):\n        return item in self.unordered\n\n    def __getitem__(self, key):\n        return self.ordered.__getitem__(key)\n\n    def __iter__(self):\n        return iter(self.ordered)\n\n    def __len__(self):\n        return len(self.ordered)\n\n    # ---Private\n    def _clear(self):\n        self._percentage = None\n        self._matches_for_ref = None\n        self.matches = set()\n        self.candidates = defaultdict(set)\n        self.ordered = []\n        self.unordered = set()\n\n    def _get_matches_for_ref(self):\n        if self._matches_for_ref is None:\n            ref = self.ref\n            self._matches_for_ref = [match for match in self.matches if ref in match]\n        return self._matches_for_ref\n\n    # ---Public\n    def add_match(self, match):\n        \"\"\"Adds ``match`` to internal match list and possibly add duplicates to the group.\n\n        A duplicate can only be considered as such if it matches all other duplicates in the group.\n        This method registers that pair (A, B) represented in ``match`` as possible candidates and,\n        if A and/or B end up matching every other duplicates in the group, add these duplicates to\n        the group.\n\n        :param tuple match: pair of :class:`~core.fs.File` to add\n        \"\"\"\n\n        def add_candidate(item, match):\n            matches = self.candidates[item]\n            matches.add(match)\n            if self.unordered <= matches:\n                self.ordered.append(item)\n                self.unordered.add(item)\n\n        if match in self.matches:\n            return\n        self.matches.add(match)\n        first, second, _ = match\n        if first not in self.unordered:\n            add_candidate(first, second)\n        if second not in self.unordered:\n            add_candidate(second, first)\n        self._percentage = None\n        self._matches_for_ref = None\n\n    def discard_matches(self):\n        \"\"\"Remove all recorded matches that didn't result in a duplicate being added to the group.\n\n        You can call this after the duplicate scanning process to free a bit of memory.\n        \"\"\"\n        discarded = {m for m in self.matches if not all(obj in self.unordered for obj in [m.first, m.second])}\n        self.matches -= discarded\n        self.candidates = defaultdict(set)\n        return discarded\n\n    def get_match_of(self, item):\n        \"\"\"Returns the match pair between ``item`` and :attr:`ref`.\"\"\"\n        if item is self.ref:\n            return\n        for m in self._get_matches_for_ref():\n            if item in m:\n                return m\n\n    def prioritize(self, key_func, tie_breaker=None):\n        \"\"\"Reorders :attr:`ordered` according to ``key_func``.\n\n        :param key_func: Key (f(x)) to be used for sorting\n        :param tie_breaker: function to be used to select the reference position in case the top\n                            duplicates have the same key_func() result.\n        \"\"\"\n        # tie_breaker(ref, dupe) --> True if dupe should be ref\n        # Returns True if anything changed during prioritization.\n        new_order = sorted(self.ordered, key=lambda x: (-x.is_ref, key_func(x)))\n        changed = new_order != self.ordered\n        self.ordered = new_order\n        if tie_breaker is None:\n            return changed\n        ref = self.ref\n        key_value = key_func(ref)\n        for dupe in self.dupes:\n            if key_func(dupe) != key_value:\n                break\n            if tie_breaker(ref, dupe):\n                ref = dupe\n        if ref is not self.ref:\n            self.switch_ref(ref)\n            return True\n        return changed\n\n    def remove_dupe(self, item, discard_matches=True):\n        try:\n            self.ordered.remove(item)\n            self.unordered.remove(item)\n            self._percentage = None\n            self._matches_for_ref = None\n            if (len(self) > 1) and any(not getattr(item, \"is_ref\", False) for item in self):\n                if discard_matches:\n                    self.matches = {m for m in self.matches if item not in m}\n            else:\n                self._clear()\n        except ValueError:\n            pass\n\n    def switch_ref(self, with_dupe):\n        \"\"\"Make the :attr:`ref` dupe of the group switch position with ``with_dupe``.\"\"\"\n        if self.ref.is_ref:\n            return False\n        try:\n            self.ordered.remove(with_dupe)\n            self.ordered.insert(0, with_dupe)\n            self._percentage = None\n            self._matches_for_ref = None\n            return True\n        except ValueError:\n            return False\n\n    dupes = property(lambda self: self[1:])\n\n    @property\n    def percentage(self):\n        if self._percentage is None:\n            if self.dupes:\n                matches = self._get_matches_for_ref()\n                self._percentage = sum(match.percentage for match in matches) // len(matches)\n            else:\n                self._percentage = 0\n        return self._percentage\n\n    @property\n    def ref(self):\n        if self:\n            return self[0]\n\n\ndef get_groups(matches):\n    \"\"\"Returns a list of :class:`Group` from ``matches``.\n\n    Create groups out of match pairs in the smartest way possible.\n    \"\"\"\n    matches.sort(key=lambda match: -match.percentage)\n    dupe2group = {}\n    groups = []\n    try:\n        for match in matches:\n            first, second, _ = match\n            first_group = dupe2group.get(first)\n            second_group = dupe2group.get(second)\n            if first_group:\n                if second_group:\n                    if first_group is second_group:\n                        target_group = first_group\n                    else:\n                        continue\n                else:\n                    target_group = first_group\n                    dupe2group[second] = target_group\n            else:\n                if second_group:\n                    target_group = second_group\n                    dupe2group[first] = target_group\n                else:\n                    target_group = Group()\n                    groups.append(target_group)\n                    dupe2group[first] = target_group\n                    dupe2group[second] = target_group\n            target_group.add_match(match)\n    except MemoryError:\n        del dupe2group\n        del matches\n        # should free enough memory to continue\n        logging.warning(f\"Memory Overflow. Groups: {len(groups)}\")\n    # Now that we have a group, we have to discard groups' matches and see if there're any \"orphan\"\n    # matches, that is, matches that were candidate in a group but that none of their 2 files were\n    # accepted in the group. With these orphan groups, it's safe to build additional groups\n    matched_files = set(flatten(groups))\n    orphan_matches = []\n    for group in groups:\n        orphan_matches += {\n            m for m in group.discard_matches() if not any(obj in matched_files for obj in [m.first, m.second])\n        }\n    if groups and orphan_matches:\n        groups += get_groups(orphan_matches)  # no job, as it isn't supposed to take a long time\n    return groups\n"
  },
  {
    "path": "core/exclude.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.markable import Markable\nfrom xml.etree import ElementTree as ET\n\n# TODO: perhaps use regex module for better Unicode support? https://pypi.org/project/regex/\n# also https://pypi.org/project/re2/\n# TODO update the Result list with newly added regexes if possible\nimport re\nfrom os import sep\nimport logging\nimport functools\nfrom hscommon.util import FileOrPath\nfrom hscommon.plat import ISWINDOWS\nimport time\n\ndefault_regexes = [\n    r\"^thumbs\\.db$\",  # Obsolete after WindowsXP\n    r\"^desktop\\.ini$\",  # Windows metadata\n    r\"^\\.DS_Store$\",  # MacOS metadata\n    r\"^\\.Trash\\-.*\",  # Linux trash directories\n    r\"^\\$Recycle\\.Bin$\",  # Windows\n    r\"^\\..*\",  # Hidden files on Unix-like\n]\n# These are too broad\nforbidden_regexes = [r\".*\", r\"\\/.*\", r\".*\\/.*\", r\".*\\\\\\\\.*\", r\".*\\..*\"]\n\n\ndef timer(func):\n    @functools.wraps(func)\n    def wrapper_timer(*args):\n        start = time.perf_counter_ns()\n        value = func(*args)\n        end = time.perf_counter_ns()\n        print(f\"DEBUG: func {func.__name__!r} took {end - start} ns.\")\n        return value\n\n    return wrapper_timer\n\n\ndef memoize(func):\n    func.cache = dict()\n\n    @functools.wraps(func)\n    def _memoize(*args):\n        if args not in func.cache:\n            func.cache[args] = func(*args)\n        return func.cache[args]\n\n    return _memoize\n\n\nclass AlreadyThereException(Exception):\n    \"\"\"Expression already in the list\"\"\"\n\n    def __init__(self, arg=\"Expression is already in excluded list.\"):\n        super().__init__(arg)\n\n\nclass ExcludeList(Markable):\n    \"\"\"A list of lists holding regular expression strings and the compiled re.Pattern\"\"\"\n\n    # Used to filter out directories and files that we would rather avoid scanning.\n    # The list() class allows us to preserve item order without too much hassle.\n    # The downside is we have to compare strings every time we look for an item in the list\n    # since we use regex strings as keys.\n    # If _use_union is True, the compiled regexes will be combined into one single\n    # Pattern instead of separate Patterns which may or may not give better\n    # performance compared to looping through each Pattern individually.\n\n    # ---Override\n    def __init__(self, union_regex=True):\n        Markable.__init__(self)\n        self._use_union = union_regex\n        # list([str regex, bool iscompilable, re.error exception, Pattern compiled], ...)\n        self._excluded = []\n        self._excluded_compiled = set()\n        self._dirty = True\n\n    def __iter__(self):\n        \"\"\"Iterate in order.\"\"\"\n        for item in self._excluded:\n            regex = item[0]\n            yield self.is_marked(regex), regex\n\n    def __contains__(self, item):\n        return self.has_entry(item)\n\n    def __len__(self):\n        \"\"\"Returns the total number of regexes regardless of mark status.\"\"\"\n        return len(self._excluded)\n\n    def __getitem__(self, key):\n        \"\"\"Returns the list item corresponding to key.\"\"\"\n        for item in self._excluded:\n            if item[0] == key:\n                return item\n        raise KeyError(f\"Key {key} is not in exclusion list.\")\n\n    def __setitem__(self, key, value):\n        # TODO if necessary\n        pass\n\n    def __delitem__(self, key):\n        # TODO if necessary\n        pass\n\n    def get_compiled(self, key):\n        \"\"\"Returns the (precompiled) Pattern for key\"\"\"\n        return self.__getitem__(key)[3]\n\n    def is_markable(self, regex):\n        return self._is_markable(regex)\n\n    def _is_markable(self, regex):\n        \"\"\"Return the cached result of \"compilable\" property\"\"\"\n        for item in self._excluded:\n            if item[0] == regex:\n                return item[1]\n        return False  # should not be necessary, the regex SHOULD be in there\n\n    def _did_mark(self, regex):\n        self._add_compiled(regex)\n\n    def _did_unmark(self, regex):\n        self._remove_compiled(regex)\n\n    def _add_compiled(self, regex):\n        self._dirty = True\n        if self._use_union:\n            return\n        for item in self._excluded:\n            # FIXME probably faster to just rebuild the set from the compiled instead of comparing strings\n            if item[0] == regex:\n                # no need to test if already present since it's a set()\n                self._excluded_compiled.add(item[3])\n                break\n\n    def _remove_compiled(self, regex):\n        self._dirty = True\n        if self._use_union:\n            return\n        for item in self._excluded_compiled:\n            if regex in item.pattern:\n                self._excluded_compiled.remove(item)\n                break\n\n    # @timer\n    @memoize\n    def _do_compile(self, expr):\n        return re.compile(expr)\n\n    # @timer\n    # @memoize  # probably not worth memoizing this one if we memoize the above\n    def compile_re(self, regex):\n        compiled = None\n        try:\n            compiled = self._do_compile(regex)\n        except Exception as e:\n            return False, e, compiled\n        return True, None, compiled\n\n    def error(self, regex):\n        \"\"\"Return the compilation error Exception for regex.\n        It should have a \"msg\" attr.\"\"\"\n        for item in self._excluded:\n            if item[0] == regex:\n                return item[2]\n\n    def build_compiled_caches(self, union=False):\n        if not union:\n            self._cached_compiled_files = [x for x in self._excluded_compiled if not has_sep(x.pattern)]\n            self._cached_compiled_paths = [x for x in self._excluded_compiled if has_sep(x.pattern)]\n            self._dirty = False\n            return\n\n        marked_count = [x for marked, x in self if marked]\n        # If there is no item, the compiled Pattern will be '' and match everything!\n        if not marked_count:\n            self._cached_compiled_union_all = []\n            self._cached_compiled_union_files = []\n            self._cached_compiled_union_paths = []\n        else:\n            # HACK returned as a tuple to get a free iterator and keep interface\n            # the same regardless of whether the client asked for union or not\n            self._cached_compiled_union_all = (re.compile(\"|\".join(marked_count)),)\n            files_marked = [x for x in marked_count if not has_sep(x)]\n            if not files_marked:\n                self._cached_compiled_union_files = tuple()\n            else:\n                self._cached_compiled_union_files = (re.compile(\"|\".join(files_marked)),)\n            paths_marked = [x for x in marked_count if has_sep(x)]\n            if not paths_marked:\n                self._cached_compiled_union_paths = tuple()\n            else:\n                self._cached_compiled_union_paths = (re.compile(\"|\".join(paths_marked)),)\n        self._dirty = False\n\n    @property\n    def compiled(self):\n        \"\"\"Should be used by other classes to retrieve the up-to-date list of patterns.\"\"\"\n        if self._use_union:\n            if self._dirty:\n                self.build_compiled_caches(self._use_union)\n            return self._cached_compiled_union_all\n        return self._excluded_compiled\n\n    @property\n    def compiled_files(self):\n        \"\"\"When matching against filenames only, we probably won't be seeing any\n        directory separator, so we filter out regexes with os.sep in them.\n        The interface should be expected to be a generator, even if it returns only\n        one item (one Pattern in the union case).\"\"\"\n        if self._dirty:\n            self.build_compiled_caches(self._use_union)\n        return self._cached_compiled_union_files if self._use_union else self._cached_compiled_files\n\n    @property\n    def compiled_paths(self):\n        \"\"\"Returns patterns with only separators in them, for more precise filtering.\"\"\"\n        if self._dirty:\n            self.build_compiled_caches(self._use_union)\n        return self._cached_compiled_union_paths if self._use_union else self._cached_compiled_paths\n\n    # ---Public\n    def add(self, regex, forced=False):\n        \"\"\"This interface should throw exceptions if there is an error during\n        regex compilation\"\"\"\n        if self.has_entry(regex):\n            # This exception should never be ignored\n            raise AlreadyThereException()\n        if regex in forbidden_regexes:\n            raise ValueError(\"Forbidden (dangerous) expression.\")\n\n        iscompilable, exception, compiled = self.compile_re(regex)\n        if not iscompilable and not forced:\n            # This exception can be ignored, but taken into account\n            # to avoid adding to compiled set\n            raise exception\n        else:\n            self._do_add(regex, iscompilable, exception, compiled)\n\n    def _do_add(self, regex, iscompilable, exception, compiled):\n        # We need to insert at the top\n        self._excluded.insert(0, [regex, iscompilable, exception, compiled])\n\n    @property\n    def marked_count(self):\n        \"\"\"Returns the number of marked regexes only.\"\"\"\n        return len([x for marked, x in self if marked])\n\n    def has_entry(self, regex):\n        for item in self._excluded:\n            if regex == item[0]:\n                return True\n        return False\n\n    def is_excluded(self, dirname, filename):\n        \"\"\"Return True if the file or the absolute path to file is supposed to be\n        filtered out, False otherwise.\"\"\"\n        matched = False\n        for expr in self.compiled_files:\n            if expr.fullmatch(filename):\n                matched = True\n                break\n        if not matched:\n            for expr in self.compiled_paths:\n                if expr.fullmatch(dirname + sep + filename):\n                    matched = True\n                    break\n        return matched\n\n    def remove(self, regex):\n        for item in self._excluded:\n            if item[0] == regex:\n                self._excluded.remove(item)\n        self._remove_compiled(regex)\n\n    def rename(self, regex, newregex):\n        if regex == newregex:\n            return\n        found = False\n        was_marked = False\n        is_compilable = False\n        for item in self._excluded:\n            if item[0] == regex:\n                found = True\n                was_marked = self.is_marked(regex)\n                is_compilable, exception, compiled = self.compile_re(newregex)\n                # We overwrite the found entry\n                self._excluded[self._excluded.index(item)] = [newregex, is_compilable, exception, compiled]\n                self._remove_compiled(regex)\n                break\n        if not found:\n            return\n        if is_compilable:\n            self._add_compiled(newregex)\n            if was_marked:\n                # Not marked by default when added, add it back\n                self.mark(newregex)\n\n    # def change_index(self, regex, new_index):\n    # \"\"\"Internal list must be a list, not dict.\"\"\"\n    #     item = self._excluded.pop(regex)\n    #     self._excluded.insert(new_index, item)\n\n    def restore_defaults(self):\n        for _, regex in self:\n            if regex not in default_regexes:\n                self.unmark(regex)\n        for default_regex in default_regexes:\n            if not self.has_entry(default_regex):\n                self.add(default_regex)\n            self.mark(default_regex)\n\n    def load_from_xml(self, infile):\n        \"\"\"Loads the ignore list from a XML created with save_to_xml.\n\n        infile can be a file object or a filename.\n        \"\"\"\n        try:\n            root = ET.parse(infile).getroot()\n        except Exception as e:\n            logging.warning(f\"Error while loading {infile}: {e}\")\n            self.restore_defaults()\n            return e\n\n        marked = set()\n        exclude_elems = (e for e in root if e.tag == \"exclude\")\n        for exclude_item in exclude_elems:\n            regex_string = exclude_item.get(\"regex\")\n            if not regex_string:\n                continue\n            try:\n                # \"forced\" avoids compilation exceptions and adds anyway\n                self.add(regex_string, forced=True)\n            except AlreadyThereException:\n                logging.error(\n                    f'Regex \"{regex_string}\" \\\nloaded from XML was already present in the list.'\n                )\n                continue\n            if exclude_item.get(\"marked\") == \"y\":\n                marked.add(regex_string)\n\n        for item in marked:\n            self.mark(item)\n\n    def save_to_xml(self, outfile):\n        \"\"\"Create a XML file that can be used by load_from_xml.\n        outfile can be a file object or a filename.\"\"\"\n        root = ET.Element(\"exclude_list\")\n        # reversed in order to keep order of entries when reloading from xml later\n        for item in reversed(self._excluded):\n            exclude_node = ET.SubElement(root, \"exclude\")\n            exclude_node.set(\"regex\", str(item[0]))\n            exclude_node.set(\"marked\", (\"y\" if self.is_marked(item[0]) else \"n\"))\n        tree = ET.ElementTree(root)\n        with FileOrPath(outfile, \"wb\") as fp:\n            tree.write(fp, encoding=\"utf-8\")\n\n\nclass ExcludeDict(ExcludeList):\n    \"\"\"Exclusion list holding a set of regular expressions as keys, the compiled\n    Pattern, compilation error and compilable boolean as values.\"\"\"\n\n    # Implemntation around a dictionary instead of a list, which implies\n    # to keep the index of each string-key as its sub-element and keep it updated\n    # whenever insert/remove is done.\n\n    def __init__(self, union_regex=False):\n        Markable.__init__(self)\n        self._use_union = union_regex\n        # { \"regex string\":\n        #   {\n        #       \"index\": int,\n        #       \"compilable\": bool,\n        #       \"error\": str,\n        #       \"compiled\": Pattern or None\n        #   }\n        # }\n        self._excluded = {}\n        self._excluded_compiled = set()\n        self._dirty = True\n\n    def __iter__(self):\n        \"\"\"Iterate in order.\"\"\"\n        for regex in ordered_keys(self._excluded):\n            yield self.is_marked(regex), regex\n\n    def __getitem__(self, key):\n        \"\"\"Returns the dict item correponding to key\"\"\"\n        return self._excluded.__getitem__(key)\n\n    def get_compiled(self, key):\n        \"\"\"Returns the compiled item for key\"\"\"\n        return self.__getitem__(key).get(\"compiled\")\n\n    def is_markable(self, regex):\n        return self._is_markable(regex)\n\n    def _is_markable(self, regex):\n        \"\"\"Return the cached result of \"compilable\" property\"\"\"\n        exists = self._excluded.get(regex)\n        if exists:\n            return exists.get(\"compilable\")\n        return False\n\n    def _add_compiled(self, regex):\n        self._dirty = True\n        if self._use_union:\n            return\n        try:\n            self._excluded_compiled.add(self._excluded.get(regex).get(\"compiled\"))\n        except Exception as e:\n            logging.error(f\"Exception while adding regex {regex} to compiled set: {e}\")\n            return\n\n    def is_compilable(self, regex):\n        \"\"\"Returns the cached \"compilable\" value\"\"\"\n        return self._excluded[regex][\"compilable\"]\n\n    def error(self, regex):\n        \"\"\"Return the compilation error message for regex string\"\"\"\n        return self._excluded.get(regex).get(\"error\")\n\n    # ---Public\n    def _do_add(self, regex, iscompilable, exception, compiled):\n        # We always insert at the top, so index should be 0\n        # and other indices should be pushed by one\n        for value in self._excluded.values():\n            value[\"index\"] += 1\n        self._excluded[regex] = {\"index\": 0, \"compilable\": iscompilable, \"error\": exception, \"compiled\": compiled}\n\n    def has_entry(self, regex):\n        if regex in self._excluded.keys():\n            return True\n        return False\n\n    def remove(self, regex):\n        old_value = self._excluded.pop(regex)\n        # Bring down all indices which where above it\n        index = old_value[\"index\"]\n        if index == len(self._excluded) - 1:  # we start at 0...\n            # Old index was at the end, no need to update other indices\n            self._remove_compiled(regex)\n            return\n\n        for value in self._excluded.values():\n            if value.get(\"index\") > old_value[\"index\"]:\n                value[\"index\"] -= 1\n        self._remove_compiled(regex)\n\n    def rename(self, regex, newregex):\n        if regex == newregex or regex not in self._excluded.keys():\n            return\n        was_marked = self.is_marked(regex)\n        previous = self._excluded.pop(regex)\n        iscompilable, error, compiled = self.compile_re(newregex)\n        self._excluded[newregex] = {\n            \"index\": previous.get(\"index\"),\n            \"compilable\": iscompilable,\n            \"error\": error,\n            \"compiled\": compiled,\n        }\n        self._remove_compiled(regex)\n        if iscompilable:\n            self._add_compiled(newregex)\n            if was_marked:\n                self.mark(newregex)\n\n    def save_to_xml(self, outfile):\n        \"\"\"Create a XML file that can be used by load_from_xml.\n\n        outfile can be a file object or a filename.\n        \"\"\"\n        root = ET.Element(\"exclude_list\")\n        # reversed in order to keep order of entries when reloading from xml later\n        reversed_list = []\n        for key in ordered_keys(self._excluded):\n            reversed_list.append(key)\n        for item in reversed(reversed_list):\n            exclude_node = ET.SubElement(root, \"exclude\")\n            exclude_node.set(\"regex\", str(item))\n            exclude_node.set(\"marked\", (\"y\" if self.is_marked(item) else \"n\"))\n        tree = ET.ElementTree(root)\n        with FileOrPath(outfile, \"wb\") as fp:\n            tree.write(fp, encoding=\"utf-8\")\n\n\ndef ordered_keys(_dict):\n    \"\"\"Returns an iterator over the keys of dictionary sorted by \"index\" key\"\"\"\n    if not len(_dict):\n        return\n    list_of_items = []\n    for item in _dict.items():\n        list_of_items.append(item)\n    list_of_items.sort(key=lambda x: x[1].get(\"index\"))\n    for item in list_of_items:\n        yield item[0]\n\n\nif ISWINDOWS:\n\n    def has_sep(regexp):\n        return \"\\\\\" + sep in regexp\n\nelse:\n\n    def has_sep(regexp):\n        return sep in regexp\n"
  },
  {
    "path": "core/export.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/09/16\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os.path as op\nfrom tempfile import mkdtemp\nimport csv\n\n# Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency\n# and resource problems.\n\nMAIN_TEMPLATE = \"\"\"\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n    <meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\"/>\n        <title>dupeGuru Results</title>\n        <style type=\"text/css\">\nBODY\n{\n    background-color:white;\n}\n\nBODY,A,P,UL,TABLE,TR,TD\n{\n    font-family:Tahoma,Arial,sans-serif;\n    font-size:10pt;\n    color: #4477AA;\n}\n\nTABLE\n{\n    background-color: #225588;\n    margin-left: auto;\n    margin-right: auto;\n    width: 90%;\n}\n\nTR\n{\n    background-color: white;\n}\n\nTH\n{\n    font-weight: bold;\n    color: black;\n    background-color: #C8D6E5;\n}\n\nTH TD\n{\n    color:black;\n}\n\nTD\n{\n    padding-left: 2pt;\n}\n\nTD.rightelem\n{\n    text-align:right;\n    /*padding-left:0pt;*/\n    padding-right: 2pt;\n    width: 17%;\n}\n\nTD.indented\n{\n    padding-left: 12pt;\n}\n\nH1\n{\n    font-family:&quot;Courier New&quot;,monospace;\n    color:#6699CC;\n    font-size:18pt;\n    color:#6da500;\n    border-color: #70A0CF;\n    border-width: 1pt;\n    border-style: solid;\n    margin-top:   16pt;\n    margin-left:  5%;\n    margin-right: 5%;\n    padding-top:  2pt;\n    padding-bottom:2pt;\n    text-align:   center;\n}\n</style>\n</head>\n<body>\n<h1>dupeGuru Results</h1>\n<table>\n<tr>$colheaders</tr>\n$rows\n</table>\n</body>\n</html>\n\"\"\"\n\nCOLHEADERS_TEMPLATE = \"<th>{name}</th>\"\n\nROW_TEMPLATE = \"\"\"\n<tr>\n    <td class=\"{indented}\">{filename}</td>{cells}\n</tr>\n\"\"\"\n\nCELL_TEMPLATE = \"\"\"<td>{value}</td>\"\"\"\n\n\ndef export_to_xhtml(colnames, rows):\n    # a row is a list of values with the first value being a flag indicating if the row should be indented\n    if rows:\n        assert len(rows[0]) == len(colnames) + 1  # + 1 is for the \"indented\" flag\n    colheaders = \"\".join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)\n    rendered_rows = []\n    previous_group_id = None\n    for row in rows:\n        # [2:] is to remove the indented flag + filename\n        if row[0] != previous_group_id:\n            # We've just changed dupe group, which means that this dupe is a ref. We don't indent it.\n            indented = \"\"\n        else:\n            indented = \"indented\"\n        filename = row[1]\n        cells = \"\".join(CELL_TEMPLATE.format(value=value) for value in row[2:])\n        rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))\n        previous_group_id = row[0]\n    rendered_rows = \"\".join(rendered_rows)\n    # The main template can't use format because the css code uses {}\n    content = MAIN_TEMPLATE.replace(\"$colheaders\", colheaders).replace(\"$rows\", rendered_rows)\n    folder = mkdtemp()\n    destpath = op.join(folder, \"export.htm\")\n    fp = open(destpath, \"wt\", encoding=\"utf-8\")\n    fp.write(content)\n    fp.close()\n    return destpath\n\n\ndef export_to_csv(dest, colnames, rows):\n    writer = csv.writer(open(dest, \"wt\", encoding=\"utf-8\"))\n    writer.writerow([\"Group ID\"] + colnames)\n    for row in rows:\n        writer.writerow(row)\n"
  },
  {
    "path": "core/fs.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-10-22\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru\n# and was re-used for dupeGuru. The problem is that hsfs is way over-engineered for dupeGuru,\n# resulting needless complexity and memory usage. It's been a while since I wanted to do that fork,\n# and I'm doing it now.\n\nimport os\n\nfrom math import floor\nimport logging\nimport sqlite3\nfrom sys import platform\nfrom threading import Lock\nfrom typing import Any, AnyStr, Union, Callable\n\nfrom pathlib import Path\nfrom hscommon.util import nonone, get_file_ext\n\nhasher: Callable\ntry:\n    import xxhash\n\n    hasher = xxhash.xxh128\nexcept ImportError:\n    import hashlib\n\n    hasher = hashlib.md5\n\n__all__ = [\n    \"File\",\n    \"Folder\",\n    \"get_file\",\n    \"get_files\",\n    \"FSError\",\n    \"AlreadyExistsError\",\n    \"InvalidPath\",\n    \"InvalidDestinationError\",\n    \"OperationError\",\n]\n\nNOT_SET = object()\n\n# The goal here is to not run out of memory on really big files. However, the chunk\n# size has to be large enough so that the python loop isn't too costly in terms of\n# CPU.\nCHUNK_SIZE = 1024 * 1024  # 1 MiB\n\n# Minimum size below which partial hashing is not used\nMIN_FILE_SIZE = 3 * CHUNK_SIZE  # 3MiB, because we take 3 samples\n\n# Partial hashing offset and size\nPARTIAL_OFFSET_SIZE = (0x4000, 0x4000)\n\n\nclass FSError(Exception):\n    cls_message = \"An error has occured on '{name}' in '{parent}'\"\n\n    def __init__(self, fsobject, parent=None):\n        message = self.cls_message\n        if isinstance(fsobject, str):\n            name = fsobject\n        elif isinstance(fsobject, File):\n            name = fsobject.name\n        else:\n            name = \"\"\n        parentname = str(parent) if parent is not None else \"\"\n        Exception.__init__(self, message.format(name=name, parent=parentname))\n\n\nclass AlreadyExistsError(FSError):\n    \"The directory or file name we're trying to add already exists\"\n    cls_message = \"'{name}' already exists in '{parent}'\"\n\n\nclass InvalidPath(FSError):\n    \"The path of self is invalid, and cannot be worked with.\"\n    cls_message = \"'{name}' is invalid.\"\n\n\nclass InvalidDestinationError(FSError):\n    \"\"\"A copy/move operation has been called, but the destination is invalid.\"\"\"\n\n    cls_message = \"'{name}' is an invalid destination for this operation.\"\n\n\nclass OperationError(FSError):\n    \"\"\"A copy/move/delete operation has been called, but the checkup after the\n    operation shows that it didn't work.\"\"\"\n\n    cls_message = \"Operation on '{name}' failed.\"\n\n\nclass FilesDB:\n    schema_version = 1\n    schema_version_description = \"Changed from md5 to xxhash if available.\"\n\n    create_table_query = \"\"\"CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER,\n        entry_dt DATETIME, digest BLOB, digest_partial BLOB, digest_samples BLOB)\"\"\"\n    drop_table_query = \"DROP TABLE IF EXISTS files;\"\n    select_query = \"SELECT {key} FROM files WHERE path=:path AND size=:size and mtime_ns=:mtime_ns\"\n    select_query_ignore_mtime = \"SELECT {key} FROM files WHERE path=:path AND size=:size\"\n    insert_query = \"\"\"\n        INSERT INTO files (path, size, mtime_ns, entry_dt, {key})\n        VALUES (:path, :size, :mtime_ns, datetime('now'), :value)\n        ON CONFLICT(path) DO UPDATE SET size=:size, mtime_ns=:mtime_ns, entry_dt=datetime('now'), {key}=:value;\n    \"\"\"\n\n    ignore_mtime = False\n\n    def __init__(self):\n        self.conn = None\n        self.lock = None\n\n    def connect(self, path: Union[AnyStr, os.PathLike]) -> None:\n        if platform.startswith(\"gnu0\"):\n            self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None)\n        else:\n            self.conn = sqlite3.connect(path, check_same_thread=False)\n        self.lock = Lock()\n        self._check_upgrade()\n\n    def _check_upgrade(self) -> None:\n        with self.lock, self.conn as conn:\n            has_schema = conn.execute(\n                \"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'\"\n            ).fetchall()\n            version = None\n            if has_schema:\n                version = conn.execute(\"SELECT version FROM schema_version ORDER BY version DESC\").fetchone()[0]\n            else:\n                conn.execute(\"CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)\")\n            if version != self.schema_version:\n                conn.execute(self.drop_table_query)\n                conn.execute(\n                    \"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)\",\n                    {\"version\": self.schema_version, \"description\": self.schema_version_description},\n                )\n            conn.execute(self.create_table_query)\n\n    def clear(self) -> None:\n        with self.lock, self.conn as conn:\n            conn.execute(self.drop_table_query)\n            conn.execute(self.create_table_query)\n\n    def get(self, path: Path, key: str) -> Union[bytes, None]:\n        stat = path.stat()\n        size = stat.st_size\n        mtime_ns = stat.st_mtime_ns\n        try:\n            with self.conn as conn:\n                if self.ignore_mtime:\n                    cursor = conn.execute(\n                        self.select_query_ignore_mtime.format(key=key), {\"path\": str(path), \"size\": size}\n                    )\n                else:\n                    cursor = conn.execute(\n                        self.select_query.format(key=key),\n                        {\"path\": str(path), \"size\": size, \"mtime_ns\": mtime_ns},\n                    )\n                result = cursor.fetchone()\n                cursor.close()\n\n            if result:\n                return result[0]\n        except Exception as ex:\n            logging.warning(f\"Couldn't get {key} for {path} w/{size}, {mtime_ns}: {ex}\")\n\n        return None\n\n    def put(self, path: Path, key: str, value: Any) -> None:\n        stat = path.stat()\n        size = stat.st_size\n        mtime_ns = stat.st_mtime_ns\n        try:\n            with self.lock, self.conn as conn:\n                conn.execute(\n                    self.insert_query.format(key=key),\n                    {\"path\": str(path), \"size\": size, \"mtime_ns\": mtime_ns, \"value\": value},\n                )\n        except Exception as ex:\n            logging.warning(f\"Couldn't put {key} for {path} w/{size}, {mtime_ns}: {ex}\")\n\n    def commit(self) -> None:\n        with self.lock:\n            self.conn.commit()\n\n    def close(self) -> None:\n        with self.lock:\n            self.conn.close()\n\n\nfilesdb = FilesDB()  # Singleton\n\n\nclass File:\n    \"\"\"Represents a file and holds metadata to be used for scanning.\"\"\"\n\n    INITIAL_INFO = {\"size\": 0, \"mtime\": 0, \"digest\": b\"\", \"digest_partial\": b\"\", \"digest_samples\": b\"\"}\n    # Slots for File make us save quite a bit of memory. In a memory test I've made with a lot of\n    # files, I saved 35% memory usage with \"unread\" files (no _read_info() call) and gains become\n    # even greater when we take into account read attributes (70%!). Yeah, it's worth it.\n    __slots__ = (\"path\", \"unicode_path\", \"is_ref\", \"words\") + tuple(INITIAL_INFO.keys())\n\n    def __init__(self, path):\n        for attrname in self.INITIAL_INFO:\n            setattr(self, attrname, NOT_SET)\n        if type(path) is os.DirEntry:\n            self.path = Path(path.path)\n            self.size = nonone(path.stat().st_size, 0)\n            self.mtime = nonone(path.stat().st_mtime, 0)\n        else:\n            self.path = path\n        if self.path:\n            self.unicode_path = str(self.path)\n\n    def __repr__(self):\n        return f\"<{self.__class__.__name__} {str(self.path)}>\"\n\n    def __getattribute__(self, attrname):\n        result = object.__getattribute__(self, attrname)\n        if result is NOT_SET:\n            try:\n                self._read_info(attrname)\n            except Exception as e:\n                logging.warning(\"An error '%s' was raised while decoding '%s'\", e, repr(self.path))\n            result = object.__getattribute__(self, attrname)\n            if result is NOT_SET:\n                result = self.INITIAL_INFO[attrname]\n        return result\n\n    def _calc_digest(self):\n        # type: () -> bytes\n\n        with self.path.open(\"rb\") as fp:\n            file_hash = hasher()\n            # The goal here is to not run out of memory on really big files. However, the chunk\n            # size has to be large enough so that the python loop isn't too costly in terms of\n            # CPU.\n            CHUNK_SIZE = 1024 * 1024  # 1 mb\n            filedata = fp.read(CHUNK_SIZE)\n            while filedata:\n                file_hash.update(filedata)\n                filedata = fp.read(CHUNK_SIZE)\n            return file_hash.digest()\n\n    def _calc_digest_partial(self):\n        # type: () -> bytes\n        with self.path.open(\"rb\") as fp:\n            fp.seek(PARTIAL_OFFSET_SIZE[0])\n            partial_data = fp.read(PARTIAL_OFFSET_SIZE[1])\n            return hasher(partial_data).digest()\n\n    def _calc_digest_samples(self) -> bytes:\n        size = self.size\n        with self.path.open(\"rb\") as fp:\n            # Chunk at 25% of the file\n            fp.seek(floor(size * 25 / 100), 0)\n            file_data = fp.read(CHUNK_SIZE)\n            file_hash = hasher(file_data)\n\n            # Chunk at 60% of the file\n            fp.seek(floor(size * 60 / 100), 0)\n            file_data = fp.read(CHUNK_SIZE)\n            file_hash.update(file_data)\n\n            # Last chunk of the file\n            fp.seek(-CHUNK_SIZE, 2)\n            file_data = fp.read(CHUNK_SIZE)\n            file_hash.update(file_data)\n            return file_hash.digest()\n\n    def _read_info(self, field):\n        # print(f\"_read_info({field}) for {self}\")\n        if field in (\"size\", \"mtime\"):\n            stats = self.path.stat()\n            self.size = nonone(stats.st_size, 0)\n            self.mtime = nonone(stats.st_mtime, 0)\n        elif field == \"digest_partial\":\n            self.digest_partial = filesdb.get(self.path, \"digest_partial\")\n            if self.digest_partial is None:\n                # If file is smaller than partial requirements just use the full digest\n                if self.size < PARTIAL_OFFSET_SIZE[0] + PARTIAL_OFFSET_SIZE[1]:\n                    self.digest_partial = self.digest\n                else:\n                    self.digest_partial = self._calc_digest_partial()\n                filesdb.put(self.path, \"digest_partial\", self.digest_partial)\n        elif field == \"digest\":\n            self.digest = filesdb.get(self.path, \"digest\")\n            if self.digest is None:\n                self.digest = self._calc_digest()\n                filesdb.put(self.path, \"digest\", self.digest)\n        elif field == \"digest_samples\":\n            size = self.size\n            # Might as well hash such small files entirely.\n            if size <= MIN_FILE_SIZE:\n                self.digest_samples = self.digest\n                return\n            self.digest_samples = filesdb.get(self.path, \"digest_samples\")\n            if self.digest_samples is None:\n                self.digest_samples = self._calc_digest_samples()\n                filesdb.put(self.path, \"digest_samples\", self.digest_samples)\n\n    def _read_all_info(self, attrnames=None):\n        \"\"\"Cache all possible info.\n\n        If `attrnames` is not None, caches only attrnames.\n        \"\"\"\n        if attrnames is None:\n            attrnames = self.INITIAL_INFO.keys()\n        for attrname in attrnames:\n            getattr(self, attrname)\n\n    # --- Public\n    @classmethod\n    def can_handle(cls, path):\n        \"\"\"Returns whether this file wrapper class can handle ``path``.\"\"\"\n        return not path.is_symlink() and path.is_file()\n\n    def exists(self) -> bool:\n        \"\"\"Safely check if the underlying file exists, treat error as non-existent\"\"\"\n        try:\n            return self.path.exists()\n        except OSError as ex:\n            logging.warning(f\"Checking {self.path} raised: {ex}\")\n            return False\n\n    def rename(self, newname):\n        if newname == self.name:\n            return\n        destpath = self.path.parent.joinpath(newname)\n        if destpath.exists():\n            raise AlreadyExistsError(newname, self.path.parent)\n        try:\n            self.path.rename(destpath)\n        except OSError:\n            raise OperationError(self)\n        if not destpath.exists():\n            raise OperationError(self)\n        self.path = destpath\n\n    def get_display_info(self, group, delta):\n        \"\"\"Returns a display-ready dict of dupe's data.\"\"\"\n        raise NotImplementedError()\n\n    # --- Properties\n    @property\n    def extension(self):\n        return get_file_ext(self.name)\n\n    @property\n    def name(self):\n        return self.path.name\n\n    @property\n    def folder_path(self):\n        return self.path.parent\n\n\nclass Folder(File):\n    \"\"\"A wrapper around a folder path.\n\n    It has the size/digest info of a File, but its value is the sum of its subitems.\n    \"\"\"\n\n    __slots__ = File.__slots__ + (\"_subfolders\",)\n\n    def __init__(self, path):\n        File.__init__(self, path)\n        self.size = NOT_SET\n        self._subfolders = None\n\n    def _all_items(self):\n        folders = self.subfolders\n        files = get_files(self.path)\n        return folders + files\n\n    def _read_info(self, field):\n        # print(f\"_read_info({field}) for Folder {self}\")\n        if field in {\"size\", \"mtime\"}:\n            size = sum((f.size for f in self._all_items()), 0)\n            self.size = size\n            stats = self.path.stat()\n            self.mtime = nonone(stats.st_mtime, 0)\n        elif field in {\"digest\", \"digest_partial\", \"digest_samples\"}:\n            # What's sensitive here is that we must make sure that subfiles'\n            # digest are always added up in the same order, but we also want a\n            # different digest if a file gets moved in a different subdirectory.\n\n            def get_dir_digest_concat():\n                items = self._all_items()\n                items.sort(key=lambda f: f.path)\n                digests = [getattr(f, field) for f in items]\n                return b\"\".join(digests)\n\n            digest = hasher(get_dir_digest_concat()).digest()\n            setattr(self, field, digest)\n\n    @property\n    def subfolders(self):\n        if self._subfolders is None:\n            with os.scandir(self.path) as iter:\n                subfolders = [p for p in iter if not p.is_symlink() and p.is_dir()]\n            self._subfolders = [self.__class__(p) for p in subfolders]\n        return self._subfolders\n\n    @classmethod\n    def can_handle(cls, path):\n        return not path.is_symlink() and path.is_dir()\n\n\ndef get_file(path, fileclasses=[File]):\n    \"\"\"Wraps ``path`` around its appropriate :class:`File` class.\n\n    Whether a class is \"appropriate\" is decided by :meth:`File.can_handle`\n\n    :param Path path: path to wrap\n    :param fileclasses: List of candidate :class:`File` classes\n    \"\"\"\n    for fileclass in fileclasses:\n        if fileclass.can_handle(path):\n            return fileclass(path)\n\n\ndef get_files(path, fileclasses=[File]):\n    \"\"\"Returns a list of :class:`File` for each file contained in ``path``.\n\n    :param Path path: path to scan\n    :param fileclasses: List of candidate :class:`File` classes\n    \"\"\"\n    assert all(issubclass(fileclass, File) for fileclass in fileclasses)\n    try:\n        result = []\n        with os.scandir(path) as iter:\n            for item in iter:\n                file = get_file(item, fileclasses=fileclasses)\n                if file is not None:\n                    result.append(file)\n        return result\n    except OSError:\n        raise InvalidPath(path)\n"
  },
  {
    "path": "core/gui/__init__.py",
    "content": "\"\"\"\nMeta GUI elements in dupeGuru\n-----------------------------\n\ndupeGuru is designed with a `cross-toolkit`_ approach in mind. It means that its core code\n(which doesn't depend on any GUI toolkit) has elements which preformat core information in a way\nthat makes it easy for a UI layer to consume.\n\nFor example, we have :class:`~core.gui.ResultTable` which takes information from\n:class:`~core.results.Results` and mashes it in rows and columns which are ready to be fetched by\neither Cocoa's ``NSTableView`` or Qt's ``QTableView``. It tells them which cell is supposed to be\nblue, which is supposed to be orange, does the sorting logic, holds selection, etc..\n\n.. _cross-toolkit: http://www.hardcoded.net/articles/cross-toolkit-software\n\"\"\"\n"
  },
  {
    "path": "core/gui/base.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.notify import Listener\n\n\nclass DupeGuruGUIObject(Listener):\n    def __init__(self, app):\n        Listener.__init__(self, app)\n        self.app = app\n\n    def directories_changed(self):\n        # Implemented in child classes\n        pass\n\n    def dupes_selected(self):\n        # Implemented in child classes\n        pass\n\n    def marking_changed(self):\n        # Implemented in child classes\n        pass\n\n    def results_changed(self):\n        # Implemented in child classes\n        pass\n\n    def results_changed_but_keep_selection(self):\n        # Implemented in child classes\n        pass\n"
  },
  {
    "path": "core/gui/deletion_options.py",
    "content": "# Created On: 2012-05-30\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os\n\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.trans import tr\n\n\nclass DeletionOptionsView:\n    \"\"\"Expected interface for :class:`DeletionOptions`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view presents the user with an appropriate way (probably a mix of checkboxes and radio\n    buttons) to set the different flags in :class:`DeletionOptions`. Note that\n    :attr:`DeletionOptions.use_hardlinks` is only relevant if :attr:`DeletionOptions.link_deleted`\n    is true. This is why we toggle the \"enabled\" state of that flag.\n\n    We expect the view to set :attr:`DeletionOptions.link_deleted` immediately as the user changes\n    its value because it will toggle :meth:`set_hardlink_option_enabled`\n\n    Other than the flags, there's also a prompt message which has a dynamic content, defined by\n    :meth:`update_msg`.\n    \"\"\"\n\n    def update_msg(self, msg: str):\n        \"\"\"Update the dialog's prompt with ``str``.\"\"\"\n\n    def show(self):\n        \"\"\"Show the dialog in a modal fashion.\n\n        Returns whether the dialog was \"accepted\" (the user pressed OK).\n        \"\"\"\n\n    def set_hardlink_option_enabled(self, is_enabled: bool):\n        \"\"\"Enable or disable the widget controlling :attr:`DeletionOptions.use_hardlinks`.\"\"\"\n\n\nclass DeletionOptions(GUIObject):\n    \"\"\"Present the user with deletion options before proceeding.\n\n    When the user activates \"Send to trash\", we present him with a couple of options that changes\n    the behavior of that deletion operation.\n    \"\"\"\n\n    def __init__(self):\n        GUIObject.__init__(self)\n        #: Whether symlinks or hardlinks are used when doing :attr:`link_deleted`.\n        #: *bool*. *get/set*\n        self.use_hardlinks = False\n        #: Delete dupes directly and don't send to trash.\n        #: *bool*. *get/set*\n        self.direct = False\n\n    def show(self, mark_count):\n        \"\"\"Prompt the user with a modal dialog offering our deletion options.\n\n        :param int mark_count: Number of dupes marked for deletion.\n        :rtype: bool\n        :returns: Whether the user accepted the dialog (we cancel deletion if false).\n        \"\"\"\n        self._link_deleted = False\n        self.view.set_hardlink_option_enabled(False)\n        self.use_hardlinks = False\n        self.direct = False\n        msg = tr(\"You are sending {} file(s) to the Trash.\").format(mark_count)\n        self.view.update_msg(msg)\n        return self.view.show()\n\n    def supports_links(self):\n        \"\"\"Returns whether our platform supports symlinks.\"\"\"\n        # When on a platform that doesn't implement it, calling os.symlink() (with the wrong number\n        # of arguments) raises NotImplementedError, which allows us to gracefully check for the\n        # feature.\n        try:\n            os.symlink()\n        except NotImplementedError:\n            # Windows XP, not supported\n            return False\n        except OSError:\n            # Vista+, symbolic link privilege not held\n            return False\n        except TypeError:\n            # wrong number of arguments\n            return True\n\n    @property\n    def link_deleted(self):\n        \"\"\"Replace deleted dupes with symlinks (or hardlinks) to the dupe group reference.\n\n        *bool*. *get/set*\n\n        Whether the link is a symlink or hardlink is decided by :attr:`use_hardlinks`.\n        \"\"\"\n        return self._link_deleted\n\n    @link_deleted.setter\n    def link_deleted(self, value):\n        self._link_deleted = value\n        hardlinks_enabled = value and self.supports_links()\n        self.view.set_hardlink_option_enabled(hardlinks_enabled)\n"
  },
  {
    "path": "core/gui/details_panel.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-05\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.base import GUIObject\nfrom core.gui.base import DupeGuruGUIObject\n\n\nclass DetailsPanel(GUIObject, DupeGuruGUIObject):\n    def __init__(self, app):\n        GUIObject.__init__(self, multibind=True)\n        DupeGuruGUIObject.__init__(self, app)\n        self._table = []\n\n    def _view_updated(self):\n        self._refresh()\n        self.view.refresh()\n\n    # --- Private\n    def _refresh(self):\n        if self.app.selected_dupes:\n            dupe = self.app.selected_dupes[0]\n            group = self.app.results.get_group_of_duplicate(dupe)\n        else:\n            dupe = None\n            group = None\n        data1 = self.app.get_display_info(dupe, group, False)\n        # we don't want the two sides of the table to display the stats for the same file\n        ref = group.ref if group is not None and group.ref is not dupe else None\n        data2 = self.app.get_display_info(ref, group, False)\n        columns = self.app.result_table.COLUMNS[1:]  # first column is the 'marked' column\n        self._table = [(c.display, data1[c.name], data2[c.name]) for c in columns]\n\n    # --- Public\n    def row_count(self):\n        return len(self._table)\n\n    def row(self, row_index):\n        return self._table[row_index]\n\n    # --- Event Handlers\n    def dupes_selected(self):\n        self._view_updated()\n"
  },
  {
    "path": "core/gui/directory_tree.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.tree import Tree, Node\n\nfrom core.directories import DirectoryState\nfrom core.gui.base import DupeGuruGUIObject\n\nSTATE_ORDER = [DirectoryState.NORMAL, DirectoryState.REFERENCE, DirectoryState.EXCLUDED]\n\n\n# Lazily loads children\nclass DirectoryNode(Node):\n    def __init__(self, tree, path, name):\n        Node.__init__(self, name)\n        self._tree = tree\n        self._directory_path = path\n        self._loaded = False\n        self._state = STATE_ORDER.index(self._tree.app.directories.get_state(path))\n\n    def __len__(self):\n        if not self._loaded:\n            self._load()\n        return Node.__len__(self)\n\n    def _load(self):\n        self.clear()\n        subpaths = self._tree.app.directories.get_subfolders(self._directory_path)\n        for path in subpaths:\n            self.append(DirectoryNode(self._tree, path, path.name))\n        self._loaded = True\n\n    def update_all_states(self):\n        self._state = STATE_ORDER.index(self._tree.app.directories.get_state(self._directory_path))\n        for node in self:\n            node.update_all_states()\n\n    # The state propery is an index to the combobox\n    @property\n    def state(self):\n        return self._state\n\n    @state.setter\n    def state(self, value):\n        if value == self._state:\n            return\n        self._state = value\n        state = STATE_ORDER[value]\n        self._tree.app.directories.set_state(self._directory_path, state)\n        self._tree.update_all_states()\n\n\nclass DirectoryTree(Tree, DupeGuruGUIObject):\n    # --- model -> view calls:\n    # refresh()\n    # refresh_states() # when only states label need to be refreshed\n    #\n    def __init__(self, app):\n        Tree.__init__(self)\n        DupeGuruGUIObject.__init__(self, app)\n\n    def _view_updated(self):\n        self._refresh()\n        self.view.refresh()\n\n    def _refresh(self):\n        self.clear()\n        for path in self.app.directories:\n            self.append(DirectoryNode(self, path, str(path)))\n\n    def add_directory(self, path):\n        self.app.add_directory(path)\n\n    def remove_selected(self):\n        selected_paths = self.selected_paths\n        if not selected_paths:\n            return\n        to_delete = [path[0] for path in selected_paths if len(path) == 1]\n        if to_delete:\n            self.app.remove_directories(to_delete)\n        else:\n            # All selected nodes or on second-or-more level, exclude them.\n            nodes = self.selected_nodes\n            newstate = DirectoryState.EXCLUDED\n            if all(node.state == DirectoryState.EXCLUDED for node in nodes):\n                newstate = DirectoryState.NORMAL\n            for node in nodes:\n                node.state = newstate\n\n    def select_all(self):\n        self.selected_nodes = list(self)\n        self.view.refresh()\n\n    def update_all_states(self):\n        for node in self:\n            node.update_all_states()\n        self.view.refresh_states()\n\n    # --- Event Handlers\n    def directories_changed(self):\n        self._view_updated()\n"
  },
  {
    "path": "core/gui/exclude_list_dialog.py",
    "content": "# Created On: 2012/03/13\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.gui.exclude_list_table import ExcludeListTable\nfrom core.exclude import has_sep\nfrom os import sep\nimport logging\n\n\nclass ExcludeListDialogCore:\n    def __init__(self, app):\n        self.app = app\n        self.exclude_list = self.app.exclude_list  # Markable from exclude.py\n        self.exclude_list_table = ExcludeListTable(self, app)  # GUITable, this is the \"model\"\n\n    def restore_defaults(self):\n        self.exclude_list.restore_defaults()\n        self.refresh()\n\n    def refresh(self):\n        self.exclude_list_table.refresh()\n\n    def remove_selected(self):\n        for row in self.exclude_list_table.selected_rows:\n            self.exclude_list_table.remove(row)\n            self.exclude_list.remove(row.regex)\n        self.refresh()\n\n    def rename_selected(self, newregex):\n        \"\"\"Rename the selected regex to ``newregex``.\n        If there is more than one selected row, the first one is used.\n        :param str newregex: The regex to rename the row's regex to.\n        :return bool: true if success, false if error.\n        \"\"\"\n        try:\n            r = self.exclude_list_table.selected_rows[0]\n            self.exclude_list.rename(r.regex, newregex)\n            self.refresh()\n            return True\n        except Exception as e:\n            logging.warning(f\"Error while renaming regex to {newregex}: {e}\")\n        return False\n\n    def add(self, regex):\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        self.exclude_list_table.add(regex)\n\n    def test_string(self, test_string):\n        \"\"\"Set the highlight property on each row when its regex matches the\n        test_string supplied. Return True if any row matched.\"\"\"\n        matched = False\n        for row in self.exclude_list_table.rows:\n            compiled_regex = self.exclude_list.get_compiled(row.regex)\n\n            if self.is_match(test_string, compiled_regex):\n                row.highlight = True\n                matched = True\n            else:\n                row.highlight = False\n        return matched\n\n    def is_match(self, test_string, compiled_regex):\n        # This method is like an inverted version of ExcludeList.is_excluded()\n        if not compiled_regex:\n            return False\n        matched = False\n\n        # Test only the filename portion of the path\n        if not has_sep(compiled_regex.pattern) and sep in test_string:\n            filename = test_string.rsplit(sep, 1)[1]\n            if compiled_regex.fullmatch(filename):\n                matched = True\n            return matched\n\n        # Test the entire path + filename\n        if compiled_regex.fullmatch(test_string):\n            matched = True\n        return matched\n\n    def reset_rows_highlight(self):\n        for row in self.exclude_list_table.rows:\n            row.highlight = False\n\n    def show(self):\n        self.view.show()\n"
  },
  {
    "path": "core/gui/exclude_list_table.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.gui.base import DupeGuruGUIObject\nfrom hscommon.gui.table import GUITable, Row\nfrom hscommon.gui.column import Column, Columns\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\n\nclass ExcludeListTable(GUITable, DupeGuruGUIObject):\n    COLUMNS = [Column(\"marked\", \"\"), Column(\"regex\", tr(\"Regular Expressions\"))]\n\n    def __init__(self, exclude_list_dialog, app):\n        GUITable.__init__(self)\n        DupeGuruGUIObject.__init__(self, app)\n        self._columns = Columns(self)\n        self.dialog = exclude_list_dialog\n\n    def rename_selected(self, newname):\n        row = self.selected_row\n        if row is None:\n            return False\n        row._data = None\n        return self.dialog.rename_selected(newname)\n\n    # --- Virtual\n    def _do_add(self, regex):\n        \"\"\"(Virtual) Creates a new row, adds it in the table.\n        Returns ``(row, insert_index)``.\"\"\"\n        # Return index 0 to insert at the top\n        return ExcludeListRow(self, self.dialog.exclude_list.is_marked(regex), regex), 0\n\n    def _do_delete(self):\n        self.dialog.exclude_list.remove(self.selected_row.regex)\n\n    # --- Override\n    def add(self, regex):\n        row, insert_index = self._do_add(regex)\n        self.insert(insert_index, row)\n        self.view.refresh()\n\n    def _fill(self):\n        for enabled, regex in self.dialog.exclude_list:\n            self.append(ExcludeListRow(self, enabled, regex))\n\n    def refresh(self, refresh_view=True):\n        \"\"\"Override to avoid keeping previous selection in case of multiple rows\n        selected previously.\"\"\"\n        self.cancel_edits()\n        del self[:]\n        self._fill()\n        if refresh_view:\n            self.view.refresh()\n\n\nclass ExcludeListRow(Row):\n    def __init__(self, table, enabled, regex):\n        Row.__init__(self, table)\n        self._app = table.app\n        self._data = None\n        self.enabled = str(enabled)\n        self.regex = str(regex)\n        self.highlight = False\n\n    @property\n    def data(self):\n        if self._data is None:\n            self._data = {\"marked\": self.enabled, \"regex\": self.regex}\n        return self._data\n\n    @property\n    def markable(self):\n        return self._app.exclude_list.is_markable(self.regex)\n\n    @property\n    def marked(self):\n        return self._app.exclude_list.is_marked(self.regex)\n\n    @marked.setter\n    def marked(self, value):\n        if value:\n            self._app.exclude_list.mark(self.regex)\n        else:\n            self._app.exclude_list.unmark(self.regex)\n\n    @property\n    def error(self):\n        # This assumes error() returns an Exception()\n        message = self._app.exclude_list.error(self.regex)\n        if hasattr(message, \"msg\"):\n            return self._app.exclude_list.error(self.regex).msg\n        else:\n            return message  # Exception object\n"
  },
  {
    "path": "core/gui/ignore_list_dialog.py",
    "content": "# Created On: 2012/03/13\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.trans import tr\nfrom core.gui.ignore_list_table import IgnoreListTable\n\n\nclass IgnoreListDialog:\n    # --- View interface\n    # show()\n    #\n\n    def __init__(self, app):\n        self.app = app\n        self.ignore_list = self.app.ignore_list\n        self.ignore_list_table = IgnoreListTable(self)  # GUITable\n\n    def clear(self):\n        if not self.ignore_list:\n            return\n        msg = tr(\"Do you really want to remove all %d items from the ignore list?\") % len(self.ignore_list)\n        if self.app.view.ask_yes_no(msg):\n            self.ignore_list.clear()\n            self.refresh()\n\n    def refresh(self):\n        self.ignore_list_table.refresh()\n\n    def remove_selected(self):\n        for row in self.ignore_list_table.selected_rows:\n            self.ignore_list.remove(row.path1_original, row.path2_original)\n        self.refresh()\n\n    def show(self):\n        self.view.show()\n"
  },
  {
    "path": "core/gui/ignore_list_table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2012-03-13\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.table import GUITable, Row\nfrom hscommon.gui.column import Column, Columns\nfrom hscommon.trans import trget\n\ncoltr = trget(\"columns\")\n\n\nclass IgnoreListTable(GUITable):\n    COLUMNS = [\n        # the str concat below saves us needless localization.\n        Column(\"path1\", coltr(\"File Path\") + \" 1\"),\n        Column(\"path2\", coltr(\"File Path\") + \" 2\"),\n    ]\n\n    def __init__(self, ignore_list_dialog):\n        GUITable.__init__(self)\n        self._columns = Columns(self)\n        self.view = None\n        self.dialog = ignore_list_dialog\n\n    # --- Override\n    def _fill(self):\n        for path1, path2 in self.dialog.ignore_list:\n            self.append(IgnoreListRow(self, path1, path2))\n\n\nclass IgnoreListRow(Row):\n    def __init__(self, table, path1, path2):\n        Row.__init__(self, table)\n        self.path1_original = path1\n        self.path2_original = path2\n        self.path1 = str(path1)\n        self.path2 = str(path2)\n"
  },
  {
    "path": "core/gui/prioritize_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-09-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.gui.selectable_list import GUISelectableList\n\n\nclass CriterionCategoryList(GUISelectableList):\n    def __init__(self, dialog):\n        self.dialog = dialog\n        GUISelectableList.__init__(self, [c.NAME for c in dialog.categories])\n\n    def _update_selection(self):\n        self.dialog.select_category(self.dialog.categories[self.selected_index])\n        GUISelectableList._update_selection(self)\n\n\nclass PrioritizationList(GUISelectableList):\n    def __init__(self, dialog):\n        self.dialog = dialog\n        GUISelectableList.__init__(self)\n\n    def _refresh_contents(self):\n        self[:] = [crit.display for crit in self.dialog.prioritizations]\n\n    def move_indexes(self, indexes, dest_index):\n        indexes.sort()\n        prilist = self.dialog.prioritizations\n        selected = [prilist[i] for i in indexes]\n        for i in reversed(indexes):\n            del prilist[i]\n        prilist[dest_index:dest_index] = selected\n        self._refresh_contents()\n\n    def remove_selected(self):\n        prilist = self.dialog.prioritizations\n        for i in sorted(self.selected_indexes, reverse=True):\n            del prilist[i]\n        self._refresh_contents()\n\n\nclass PrioritizeDialog(GUIObject):\n    def __init__(self, app):\n        GUIObject.__init__(self)\n        self.app = app\n        self.categories = [cat(app.results) for cat in app._prioritization_categories()]\n        self.category_list = CriterionCategoryList(self)\n        self.criteria = []\n        self.criteria_list = GUISelectableList()\n        self.prioritizations = []\n        self.prioritization_list = PrioritizationList(self)\n\n    # --- Override\n    def _view_updated(self):\n        self.category_list.select(0)\n\n    # --- Private\n    def _sort_key(self, dupe):\n        return tuple(crit.sort_key(dupe) for crit in self.prioritizations)\n\n    # --- Public\n    def select_category(self, category):\n        self.criteria = category.criteria_list()\n        self.criteria_list[:] = [c.display_value for c in self.criteria]\n\n    def add_selected(self):\n        # Add selected criteria in criteria_list to prioritization_list.\n        if self.criteria_list.selected_index is None:\n            return\n        for i in self.criteria_list.selected_indexes:\n            crit = self.criteria[i]\n            self.prioritizations.append(crit)\n            del crit\n        self.prioritization_list[:] = [crit.display for crit in self.prioritizations]\n\n    def remove_selected(self):\n        self.prioritization_list.remove_selected()\n        self.prioritization_list.select([])\n\n    def perform_reprioritization(self):\n        self.app.reprioritize_groups(self._sort_key)\n"
  },
  {
    "path": "core/gui/problem_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-04-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon import desktop\n\nfrom core.gui.problem_table import ProblemTable\n\n\nclass ProblemDialog:\n    def __init__(self, app):\n        self.app = app\n        self._selected_dupe = None\n        self.problem_table = ProblemTable(self)\n\n    def refresh(self):\n        self._selected_dupe = None\n        self.problem_table.refresh()\n\n    def reveal_selected_dupe(self):\n        if self._selected_dupe is not None:\n            desktop.reveal_path(self._selected_dupe.path)\n\n    def select_dupe(self, dupe):\n        self._selected_dupe = dupe\n"
  },
  {
    "path": "core/gui/problem_table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-04-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.table import GUITable, Row\nfrom hscommon.gui.column import Column, Columns\nfrom hscommon.trans import trget\n\ncoltr = trget(\"columns\")\n\n\nclass ProblemTable(GUITable):\n    COLUMNS = [\n        Column(\"path\", coltr(\"File Path\")),\n        Column(\"msg\", coltr(\"Error Message\")),\n    ]\n\n    def __init__(self, problem_dialog):\n        GUITable.__init__(self)\n        self._columns = Columns(self)\n        self.dialog = problem_dialog\n\n    # --- Override\n    def _update_selection(self):\n        row = self.selected_row\n        dupe = row.dupe if row is not None else None\n        self.dialog.select_dupe(dupe)\n\n    def _fill(self):\n        problems = self.dialog.app.results.problems\n        for dupe, msg in problems:\n            self.append(ProblemRow(self, dupe, msg))\n\n\nclass ProblemRow(Row):\n    def __init__(self, table, dupe, msg):\n        Row.__init__(self, table)\n        self.dupe = dupe\n        self.msg = msg\n        self.path = str(dupe.path)\n"
  },
  {
    "path": "core/gui/result_table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-11\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom operator import attrgetter\n\nfrom hscommon.gui.table import GUITable, Row\nfrom hscommon.gui.column import Columns\n\nfrom core.gui.base import DupeGuruGUIObject\n\n\nclass DupeRow(Row):\n    def __init__(self, table, group, dupe):\n        Row.__init__(self, table)\n        self._app = table.app\n        self._group = group\n        self._dupe = dupe\n        self._data = None\n        self._data_delta = None\n        self._delta_columns = None\n\n    def is_cell_delta(self, column_name):\n        \"\"\"Returns whether a cell is in delta mode (orange color).\n\n        If the result table is in delta mode, returns True if the column is one of the \"delta\n        columns\", that is, one of the columns that display a a differential value rather than an\n        absolute value.\n\n        If not, returns True if the dupe's value is different from its ref value.\n        \"\"\"\n        if not self.table.delta_values:\n            return False\n        if self.isref:\n            return False\n        if self._delta_columns is None:\n            # table.DELTA_COLUMNS are always \"delta\"\n            self._delta_columns = self.table.DELTA_COLUMNS.copy()\n            dupe_info = self.data\n            if self._group.ref is None:\n                return False\n            ref_info = self._group.ref.get_display_info(group=self._group, delta=False)\n            for key, value in dupe_info.items():\n                if (key not in self._delta_columns) and (ref_info[key].lower() != value.lower()):\n                    self._delta_columns.add(key)\n        return column_name in self._delta_columns\n\n    @property\n    def data(self):\n        if self._data is None:\n            self._data = self._app.get_display_info(self._dupe, self._group, False)\n        return self._data\n\n    @property\n    def data_delta(self):\n        if self._data_delta is None:\n            self._data_delta = self._app.get_display_info(self._dupe, self._group, True)\n        return self._data_delta\n\n    @property\n    def isref(self):\n        return self._dupe is self._group.ref\n\n    @property\n    def markable(self):\n        return self._app.results.is_markable(self._dupe)\n\n    @property\n    def marked(self):\n        return self._app.results.is_marked(self._dupe)\n\n    @marked.setter\n    def marked(self, value):\n        self._app.mark_dupe(self._dupe, value)\n\n\nclass ResultTable(GUITable, DupeGuruGUIObject):\n    def __init__(self, app):\n        GUITable.__init__(self)\n        DupeGuruGUIObject.__init__(self, app)\n        self._columns = Columns(self, prefaccess=app, savename=\"ResultTable\")\n        self._power_marker = False\n        self._delta_values = False\n        self._sort_descriptors = (\"name\", True)\n\n    # --- Override\n    def _view_updated(self):\n        self._refresh_with_view()\n\n    def _restore_selection(self, previous_selection):\n        if self.app.selected_dupes:\n            to_find = set(self.app.selected_dupes)\n            indexes = [i for i, r in enumerate(self) if r._dupe in to_find]\n            self.selected_indexes = indexes\n\n    def _update_selection(self):\n        rows = self.selected_rows\n        self.app._select_dupes(list(map(attrgetter(\"_dupe\"), rows)))\n\n    def _fill(self):\n        if not self.power_marker:\n            for group in self.app.results.groups:\n                self.append(DupeRow(self, group, group.ref))\n                for dupe in group.dupes:\n                    self.append(DupeRow(self, group, dupe))\n        else:\n            for dupe in self.app.results.dupes:\n                group = self.app.results.get_group_of_duplicate(dupe)\n                self.append(DupeRow(self, group, dupe))\n\n    def _refresh_with_view(self):\n        self.refresh()\n        self.view.show_selected_row()\n\n    # --- Public\n    def get_row_value(self, index, column):\n        try:\n            row = self[index]\n        except IndexError:\n            return \"---\"\n        if self.delta_values:\n            return row.data_delta[column]\n        else:\n            return row.data[column]\n\n    def rename_selected(self, newname):\n        row = self.selected_row\n        if row is None:\n            # There's all kinds of way the current row can be swept off during rename. When it\n            # happens, selected_row will be None.\n            return False\n        row._data = None\n        row._data_delta = None\n        return self.app.rename_selected(newname)\n\n    def sort(self, key, asc):\n        if self.power_marker:\n            self.app.results.sort_dupes(key, asc, self.delta_values)\n        else:\n            self.app.results.sort_groups(key, asc)\n        self._sort_descriptors = (key, asc)\n        self._refresh_with_view()\n\n    # --- Properties\n    @property\n    def power_marker(self):\n        return self._power_marker\n\n    @power_marker.setter\n    def power_marker(self, value):\n        if value == self._power_marker:\n            return\n        self._power_marker = value\n        key, asc = self._sort_descriptors\n        self.sort(key, asc)\n        # no need to refresh, it has happened in sort()\n\n    @property\n    def delta_values(self):\n        return self._delta_values\n\n    @delta_values.setter\n    def delta_values(self, value):\n        if value == self._delta_values:\n            return\n        self._delta_values = value\n        self.refresh()\n\n    @property\n    def selected_dupe_count(self):\n        return sum(1 for row in self.selected_rows if not row.isref)\n\n    # --- Event Handlers\n    def marking_changed(self):\n        self.view.invalidate_markings()\n\n    def results_changed(self):\n        self._refresh_with_view()\n\n    def results_changed_but_keep_selection(self):\n        # What we want to to here is that instead of restoring selected *dupes* after refresh, we\n        # restore selected *paths*.\n        indexes = self.selected_indexes\n        self.refresh(refresh_view=False)\n        self.select(indexes)\n        self.view.refresh()\n\n    def save_session(self):\n        self._columns.save_columns()\n"
  },
  {
    "path": "core/gui/stats_label.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-11\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.gui.base import DupeGuruGUIObject\n\n\nclass StatsLabel(DupeGuruGUIObject):\n    def _view_updated(self):\n        self.view.refresh()\n\n    @property\n    def display(self):\n        return self.app.stat_line\n\n    def results_changed(self):\n        self.view.refresh()\n\n    marking_changed = results_changed\n"
  },
  {
    "path": "core/ignore.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/05/02\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom xml.etree import ElementTree as ET\n\nfrom hscommon.util import FileOrPath\n\n\nclass IgnoreList:\n    \"\"\"An ignore list implementation that is iterable, filterable and exportable to XML.\n\n    Call Ignore to add an ignore list entry, and AreIgnore to check if 2 items are in the list.\n    When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.\n    \"\"\"\n\n    # ---Override\n    def __init__(self):\n        self.clear()\n\n    def __iter__(self):\n        for first, seconds in self._ignored.items():\n            for second in seconds:\n                yield (first, second)\n\n    def __len__(self):\n        return self._count\n\n    # ---Public\n    def are_ignored(self, first, second):\n        def do_check(first, second):\n            try:\n                matches = self._ignored[first]\n                return second in matches\n            except KeyError:\n                return False\n\n        return do_check(first, second) or do_check(second, first)\n\n    def clear(self):\n        self._ignored = {}\n        self._count = 0\n\n    def filter(self, func):\n        \"\"\"Applies a filter on all ignored items, and remove all matches where func(first,second)\n        doesn't return True.\n        \"\"\"\n        filtered = IgnoreList()\n        for first, second in self:\n            if func(first, second):\n                filtered.ignore(first, second)\n        self._ignored = filtered._ignored\n        self._count = filtered._count\n\n    def ignore(self, first, second):\n        if self.are_ignored(first, second):\n            return\n        try:\n            matches = self._ignored[first]\n            matches.add(second)\n        except KeyError:\n            try:\n                matches = self._ignored[second]\n                matches.add(first)\n            except KeyError:\n                matches = set()\n                matches.add(second)\n                self._ignored[first] = matches\n        self._count += 1\n\n    def remove(self, first, second):\n        def inner(first, second):\n            try:\n                matches = self._ignored[first]\n                if second in matches:\n                    matches.discard(second)\n                    if not matches:\n                        del self._ignored[first]\n                    self._count -= 1\n                    return True\n                else:\n                    return False\n            except KeyError:\n                return False\n\n        if not inner(first, second) and not inner(second, first):\n            raise ValueError()\n\n    def load_from_xml(self, infile):\n        \"\"\"Loads the ignore list from a XML created with save_to_xml.\n\n        infile can be a file object or a filename.\n        \"\"\"\n        try:\n            root = ET.parse(infile).getroot()\n        except Exception:\n            return\n        file_elems = (e for e in root if e.tag == \"file\")\n        for fn in file_elems:\n            file_path = fn.get(\"path\")\n            if not file_path:\n                continue\n            subfile_elems = (e for e in fn if e.tag == \"file\")\n            for sfn in subfile_elems:\n                subfile_path = sfn.get(\"path\")\n                if subfile_path:\n                    self.ignore(file_path, subfile_path)\n\n    def save_to_xml(self, outfile):\n        \"\"\"Create a XML file that can be used by load_from_xml.\n\n        outfile can be a file object or a filename.\n        \"\"\"\n        root = ET.Element(\"ignore_list\")\n        for filename, subfiles in self._ignored.items():\n            file_node = ET.SubElement(root, \"file\")\n            file_node.set(\"path\", filename)\n            for subfilename in subfiles:\n                subfile_node = ET.SubElement(file_node, \"file\")\n                subfile_node.set(\"path\", subfilename)\n        tree = ET.ElementTree(root)\n        with FileOrPath(outfile, \"wb\") as fp:\n            tree.write(fp, encoding=\"utf-8\")\n"
  },
  {
    "path": "core/markable.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/02/23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\nclass Markable:\n    def __init__(self):\n        self.__marked = set()\n        self.__inverted = False\n\n    # ---Virtual\n    # About did_mark and did_unmark: They only happen what an object is actually added/removed\n    # in self.__marked, and is not affected by __inverted. Thus, self.mark while __inverted\n    # is True will launch _DidUnmark.\n    def _did_mark(self, o):\n        # Implemented in child classes\n        pass\n\n    def _did_unmark(self, o):\n        # Implemented in child classes\n        pass\n\n    def _get_markable_count(self):\n        return 0\n\n    def _is_markable(self, o):\n        return True\n\n    # ---Protected\n    def _remove_mark_flag(self, o):\n        try:\n            self.__marked.remove(o)\n            self._did_unmark(o)\n        except KeyError:\n            pass\n\n    # ---Public\n    def is_marked(self, o):\n        if not self._is_markable(o):\n            return False\n        is_marked = o in self.__marked\n        if self.__inverted:\n            is_marked = not is_marked\n        return is_marked\n\n    def mark(self, o):\n        if self.is_marked(o):\n            return False\n        if not self._is_markable(o):\n            return False\n        return self.mark_toggle(o)\n\n    def mark_multiple(self, objects):\n        for o in objects:\n            self.mark(o)\n\n    def mark_all(self):\n        self.mark_none()\n        self.__inverted = True\n\n    def mark_invert(self):\n        self.__inverted = not self.__inverted\n\n    def mark_none(self):\n        for o in self.__marked:\n            self._did_unmark(o)\n        self.__marked = set()\n        self.__inverted = False\n\n    def mark_toggle(self, o):\n        try:\n            self.__marked.remove(o)\n            self._did_unmark(o)\n        except KeyError:\n            if not self._is_markable(o):\n                return False\n            self.__marked.add(o)\n            self._did_mark(o)\n        return True\n\n    def mark_toggle_multiple(self, objects):\n        for o in objects:\n            self.mark_toggle(o)\n\n    def unmark(self, o):\n        if not self.is_marked(o):\n            return False\n        return self.mark_toggle(o)\n\n    def unmark_multiple(self, objects):\n        for o in objects:\n            self.unmark(o)\n\n    # --- Properties\n    @property\n    def mark_count(self):\n        if self.__inverted:\n            return self._get_markable_count() - len(self.__marked)\n        else:\n            return len(self.__marked)\n\n    @property\n    def mark_inverted(self):\n        return self.__inverted\n\n\nclass MarkableList(list, Markable):\n    def __init__(self):\n        list.__init__(self)\n        Markable.__init__(self)\n\n    def _get_markable_count(self):\n        return len(self)\n\n    def _is_markable(self, o):\n        return o in self\n"
  },
  {
    "path": "core/me/__init__.py",
    "content": "from core.me import fs, prioritize, result_table, scanner  # noqa\n"
  },
  {
    "path": "core/me/fs.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-10-23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport mutagen\nfrom hscommon.util import get_file_ext, format_size, format_time\n\nfrom core.util import format_timestamp, format_perc, format_words, format_dupe_count\nfrom core import fs\n\nTAG_FIELDS = {\n    \"audiosize\",\n    \"duration\",\n    \"bitrate\",\n    \"samplerate\",\n    \"title\",\n    \"artist\",\n    \"album\",\n    \"genre\",\n    \"year\",\n    \"track\",\n    \"comment\",\n}\n\n# This is a temporary workaround for migration from hsaudiotag for the can_handle method\nSUPPORTED_EXTS = {\"mp3\", \"wma\", \"m4a\", \"m4p\", \"ogg\", \"flac\", \"aif\", \"aiff\", \"aifc\"}\n\n\nclass MusicFile(fs.File):\n    INITIAL_INFO = fs.File.INITIAL_INFO.copy()\n    INITIAL_INFO.update(\n        {\n            \"audiosize\": 0,\n            \"bitrate\": 0,\n            \"duration\": 0,\n            \"samplerate\": 0,\n            \"artist\": \"\",\n            \"album\": \"\",\n            \"title\": \"\",\n            \"genre\": \"\",\n            \"comment\": \"\",\n            \"year\": \"\",\n            \"track\": 0,\n        }\n    )\n    __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())\n\n    @classmethod\n    def can_handle(cls, path):\n        if not fs.File.can_handle(path):\n            return False\n        return get_file_ext(path.name) in SUPPORTED_EXTS\n\n    def get_display_info(self, group, delta):\n        size = self.size\n        duration = self.duration\n        bitrate = self.bitrate\n        samplerate = self.samplerate\n        mtime = self.mtime\n        m = group.get_match_of(self)\n        if m:\n            percentage = m.percentage\n            dupe_count = 0\n            if delta:\n                r = group.ref\n                size -= r.size\n                duration -= r.duration\n                bitrate -= r.bitrate\n                samplerate -= r.samplerate\n                mtime -= r.mtime\n        else:\n            percentage = group.percentage\n            dupe_count = len(group.dupes)\n        dupe_folder_path = getattr(self, \"display_folder_path\", self.folder_path)\n        return {\n            \"name\": self.name,\n            \"folder_path\": str(dupe_folder_path),\n            \"size\": format_size(size, 2, 2, False),\n            \"duration\": format_time(duration, with_hours=False),\n            \"bitrate\": str(bitrate),\n            \"samplerate\": str(samplerate),\n            \"extension\": self.extension,\n            \"mtime\": format_timestamp(mtime, delta and m),\n            \"title\": self.title,\n            \"artist\": self.artist,\n            \"album\": self.album,\n            \"genre\": self.genre,\n            \"year\": self.year,\n            \"track\": str(self.track),\n            \"comment\": self.comment,\n            \"percentage\": format_perc(percentage),\n            \"words\": format_words(self.words) if hasattr(self, \"words\") else \"\",\n            \"dupe_count\": format_dupe_count(dupe_count),\n        }\n\n    def _read_info(self, field):\n        fs.File._read_info(self, field)\n        if field in TAG_FIELDS:\n            # The various conversions here are to make this look like the previous implementation\n            file = mutagen.File(str(self.path), easy=True)\n            self.audiosize = self.path.stat().st_size\n            self.bitrate = file.info.bitrate / 1000\n            self.duration = file.info.length\n            self.samplerate = file.info.sample_rate\n            self.artist = \", \".join(file.tags.get(\"artist\") or [])\n            self.album = \", \".join(file.tags.get(\"album\") or [])\n            self.title = \", \".join(file.tags.get(\"title\") or [])\n            self.genre = \", \".join(file.tags.get(\"genre\") or [])\n            self.comment = \", \".join(file.tags.get(\"comment\") or [\"\"])\n            self.year = \", \".join(file.tags.get(\"date\") or [])\n            self.track = (file.tags.get(\"tracknumber\") or [\"\"])[0]\n"
  },
  {
    "path": "core/me/prioritize.py",
    "content": "# Created On: 2011/09/16\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.trans import trget\n\nfrom core.prioritize import (\n    KindCategory,\n    FolderCategory,\n    FilenameCategory,\n    NumericalCategory,\n    SizeCategory,\n    MtimeCategory,\n)\n\ncoltr = trget(\"columns\")\n\n\nclass DurationCategory(NumericalCategory):\n    NAME = coltr(\"Duration\")\n\n    def extract_value(self, dupe):\n        return dupe.duration\n\n\nclass BitrateCategory(NumericalCategory):\n    NAME = coltr(\"Bitrate\")\n\n    def extract_value(self, dupe):\n        return dupe.bitrate\n\n\nclass SamplerateCategory(NumericalCategory):\n    NAME = coltr(\"Samplerate\")\n\n    def extract_value(self, dupe):\n        return dupe.samplerate\n\n\ndef all_categories():\n    return [\n        KindCategory,\n        FolderCategory,\n        FilenameCategory,\n        SizeCategory,\n        DurationCategory,\n        BitrateCategory,\n        SamplerateCategory,\n        MtimeCategory,\n    ]\n"
  },
  {
    "path": "core/me/result_table.py",
    "content": "# Created On: 2011-11-27\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.column import Column\nfrom hscommon.trans import trget\n\nfrom core.gui.result_table import ResultTable as ResultTableBase\n\ncoltr = trget(\"columns\")\n\n\nclass ResultTable(ResultTableBase):\n    COLUMNS = [\n        Column(\"marked\", \"\"),\n        Column(\"name\", coltr(\"Filename\")),\n        Column(\"folder_path\", coltr(\"Folder\"), visible=False, optional=True),\n        Column(\"size\", coltr(\"Size (MB)\"), optional=True),\n        Column(\"duration\", coltr(\"Time\"), optional=True),\n        Column(\"bitrate\", coltr(\"Bitrate\"), optional=True),\n        Column(\"samplerate\", coltr(\"Sample Rate\"), visible=False, optional=True),\n        Column(\"extension\", coltr(\"Kind\"), optional=True),\n        Column(\"mtime\", coltr(\"Modification\"), visible=False, optional=True),\n        Column(\"title\", coltr(\"Title\"), visible=False, optional=True),\n        Column(\"artist\", coltr(\"Artist\"), visible=False, optional=True),\n        Column(\"album\", coltr(\"Album\"), visible=False, optional=True),\n        Column(\"genre\", coltr(\"Genre\"), visible=False, optional=True),\n        Column(\"year\", coltr(\"Year\"), visible=False, optional=True),\n        Column(\"track\", coltr(\"Track Number\"), visible=False, optional=True),\n        Column(\"comment\", coltr(\"Comment\"), visible=False, optional=True),\n        Column(\"percentage\", coltr(\"Match %\"), optional=True),\n        Column(\"words\", coltr(\"Words Used\"), visible=False, optional=True),\n        Column(\"dupe_count\", coltr(\"Dupe Count\"), visible=False, optional=True),\n    ]\n    DELTA_COLUMNS = {\"size\", \"duration\", \"bitrate\", \"samplerate\", \"mtime\"}\n"
  },
  {
    "path": "core/me/scanner.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\r\n#\r\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\r\n# which should be included with this package. The terms are also available at\r\n# http://www.gnu.org/licenses/gpl-3.0.html\r\n\r\nfrom hscommon.trans import tr\r\n\r\nfrom core.scanner import Scanner as ScannerBase, ScanOption, ScanType\r\n\r\n\r\nclass ScannerME(ScannerBase):\r\n    @staticmethod\r\n    def _key_func(dupe):\r\n        return (-dupe.bitrate, -dupe.size)\r\n\r\n    @staticmethod\r\n    def get_scan_options():\r\n        return [\r\n            ScanOption(ScanType.FILENAME, tr(\"Filename\")),\r\n            ScanOption(ScanType.FIELDS, tr(\"Filename - Fields\")),\r\n            ScanOption(ScanType.FIELDSNOORDER, tr(\"Filename - Fields (No Order)\")),\r\n            ScanOption(ScanType.TAG, tr(\"Tags\")),\r\n            ScanOption(ScanType.CONTENTS, tr(\"Contents\")),\r\n        ]\r\n"
  },
  {
    "path": "core/pe/__init__.py",
    "content": "from core.pe import (  # noqa\n    block,\n    cache,\n    exif,\n    matchblock,\n    matchexif,\n    photo,\n    prioritize,\n    result_table,\n    scanner,\n)\n"
  },
  {
    "path": "core/pe/block.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/09/01\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.pe._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2  # NOQA\n\n# Converted to C\n# def getblock(image):\n#     \"\"\"Returns a 3 sized tuple containing the mean color of 'image'.\n#\n#     image: a PIL image or crop.\n#     \"\"\"\n#     if image.size[0]:\n#         pixel_count = image.size[0] * image.size[1]\n#         red = green = blue = 0\n#         for r,g,b in image.getdata():\n#             red += r\n#             green += g\n#             blue += b\n#         return (red // pixel_count, green // pixel_count, blue // pixel_count)\n#     else:\n#         return (0,0,0)\n\n# This is not used anymore\n# def getblocks(image,blocksize):\n#     \"\"\"Returns a list of blocks (3 sized tuples).\n#\n#     image: A PIL image to base the blocks on.\n#     blocksize: The size of the blocks to be create. This is a single integer, defining\n#         both width and height (blocks are square).\n#     \"\"\"\n#     if min(image.size) < blocksize:\n#         return ()\n#     result = []\n#     for i in xrange(image.size[1] // blocksize):\n#         for j in xrange(image.size[0] // blocksize):\n#             box = (blocksize * j, blocksize * i, blocksize * (j + 1), blocksize * (i + 1))\n#             crop = image.crop(box)\n#             result.append(getblock(crop))\n#     return result\n\n# Converted to C\n# def getblocks2(image,block_count_per_side):\n#     \"\"\"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#     \"\"\"\n#     if not image.size[0]:\n#         return []\n#     width,height = image.size\n#     block_width = max(width // block_count_per_side,1)\n#     block_height = max(height // block_count_per_side,1)\n#     result = []\n#     for ih in range(block_count_per_side):\n#         top = min(ih * block_height, height - block_height)\n#         bottom = top + block_height\n#         for iw in range(block_count_per_side):\n#             left = min(iw * block_width, width - block_width)\n#             right = left + block_width\n#             box = (left,top,right,bottom)\n#             crop = image.crop(box)\n#             result.append(getblock(crop))\n#     return result\n\n# Converted to C\n# def diff(first, second):\n#     \"\"\"Returns the difference between the first block and the second.\n#\n#     It returns an absolute sum of the 3 differences (RGB).\n#     \"\"\"\n#     r1, g1, b1 = first\n#     r2, g2, b2 = second\n#     return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)\n\n# Converted to C\n# def avgdiff(first, second, limit=768, min_iterations=1):\n#     \"\"\"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#     \"\"\"\n#     if len(first) != len(second):\n#         raise DifferentBlockCountError\n#     if not first:\n#         raise NoBlocksError\n#     count = len(first)\n#     sum = 0\n#     zipped = izip(xrange(1, count + 1), first, second)\n#     for i, first, second in zipped:\n#         sum += diff(first, second)\n#         if sum > limit * i and i >= min_iterations:\n#             return limit + 1\n#     result = sum // count\n#     if (not result) and sum:\n#         result = 1\n#     return result\n\n# This is not used anymore\n# def maxdiff(first,second,limit=768):\n#     \"\"\"Returns the max diff between first blocks and seconds.\n#\n#     If the result surpasses limit, the first max being over limit is returned.\n#     \"\"\"\n#     if len(first) != len(second):\n#         raise DifferentBlockCountError\n#     if not first:\n#         raise NoBlocksError\n#     result = 0\n#     zipped = zip(first,second)\n#     for first,second in zipped:\n#         result = max(result,diff(first,second))\n#         if result > limit:\n#             return result\n#     return result\n"
  },
  {
    "path": "core/pe/block.pyi",
    "content": "from typing import Tuple, List, Union, Sequence\n\n_block = Tuple[int, int, int]\n\nclass NoBlocksError(Exception): ...  # noqa: E302, E701\nclass DifferentBlockCountError(Exception): ...  # noqa E701\n\ndef getblock(image: object) -> Union[_block, None]: ...  # noqa: E302\ndef getblocks2(image: object, block_count_per_side: int) -> Union[List[_block], None]: ...\ndef diff(first: _block, second: _block) -> int: ...\ndef avgdiff(  # noqa: E302\n    first: Sequence[_block], second: Sequence[_block], limit: int = 768, min_iterations: int = 1\n) -> Union[int, None]: ...\n"
  },
  {
    "path": "core/pe/cache.py",
    "content": "# Copyright 2016 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.pe._cache import bytes_to_colors  # noqa\n\n\ndef colors_to_bytes(colors):\n    \"\"\"Transform the 3 sized tuples 'colors' into a bytes string.\n\n    [(0,100,255)] --> b'\\x00d\\xff'\n    [(1,2,3),(4,5,6)] --> b'\\x01\\x02\\x03\\x04\\x05\\x06'\n    \"\"\"\n    return b\"\".join(map(bytes, colors))\n"
  },
  {
    "path": "core/pe/cache.pyi",
    "content": "from typing import Union, Tuple, List\n\n_block = Tuple[int, int, int]\n\ndef colors_to_bytes(colors: List[_block]) -> bytes: ...  # noqa: E302\ndef bytes_to_colors(s: bytes) -> Union[List[_block], None]: ...\n"
  },
  {
    "path": "core/pe/cache_sqlite.py",
    "content": "# Copyright 2016 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os\nimport os.path as op\nimport logging\nimport sqlite3 as sqlite\n\nfrom core.pe.cache import bytes_to_colors, colors_to_bytes\n\n\nclass SqliteCache:\n    \"\"\"A class to cache picture blocks in a sqlite backend.\"\"\"\n\n    schema_version = 2\n    schema_version_description = \"Added blocks for all 8 orientations.\"\n\n    create_table_query = (\n        \"CREATE TABLE IF NOT EXISTS \"\n        \"pictures(path TEXT, mtime_ns INTEGER, blocks BLOB, blocks2 BLOB, blocks3 BLOB, \"\n        \"blocks4 BLOB, blocks5 BLOB, blocks6 BLOB, blocks7 BLOB, blocks8 BLOB)\"\n    )\n    create_index_query = \"CREATE INDEX IF NOT EXISTS idx_path on pictures (path)\"\n    drop_table_query = \"DROP TABLE IF EXISTS pictures\"\n    drop_index_query = \"DROP INDEX IF EXISTS idx_path\"\n\n    def __init__(self, db=\":memory:\", readonly=False):\n        # readonly is not used in the sqlite version of the cache\n        self.dbname = db\n        self.con = None\n        self._create_con()\n\n    def __contains__(self, key):\n        sql = \"select count(*) from pictures where path = ?\"\n        result = self.con.execute(sql, [key]).fetchall()\n        return result[0][0] > 0\n\n    def __delitem__(self, key):\n        if key not in self:\n            raise KeyError(key)\n        sql = \"delete from pictures where path = ?\"\n        self.con.execute(sql, [key])\n\n    # Optimized\n    def __getitem__(self, key):\n        if isinstance(key, int):\n            sql = (\n                \"select blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 \"\n                \"from pictures \"\n                \"where rowid = ?\"\n            )\n        else:\n            sql = (\n                \"select blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 \"\n                \"from pictures \"\n                \"where path = ?\"\n            )\n        blocks = self.con.execute(sql, [key]).fetchone()\n        if blocks:\n            result = [bytes_to_colors(block) for block in blocks]\n            return result\n        else:\n            raise KeyError(key)\n\n    def __iter__(self):\n        sql = \"select path from pictures\"\n        result = self.con.execute(sql)\n        return (row[0] for row in result)\n\n    def __len__(self):\n        sql = \"select count(*) from pictures\"\n        result = self.con.execute(sql).fetchall()\n        return result[0][0]\n\n    def __setitem__(self, path_str, blocks):\n        blocks = [colors_to_bytes(block) for block in blocks]\n        if op.exists(path_str):\n            mtime = int(os.stat(path_str).st_mtime)\n        else:\n            mtime = 0\n        if path_str in self:\n            sql = (\n                \"update pictures set blocks = ?, blocks2 = ?, blocks3 = ?, blocks4 = ?, blocks5 = ?, blocks6 = ?, \"\n                \"blocks7 = ?, blocks8 = ?, mtime_ns = ?\"\n                \"where path = ?\"\n            )\n        else:\n            sql = (\n                \"insert into pictures(blocks,blocks2,blocks3,blocks4,blocks5,blocks6,blocks7,blocks8,mtime_ns,path) \"\n                \"values(?,?,?,?,?,?,?,?,?,?)\"\n            )\n        try:\n            self.con.execute(sql, blocks + [mtime, path_str])\n        except sqlite.OperationalError:\n            logging.warning(\"Picture cache could not set value for key %r\", path_str)\n        except sqlite.DatabaseError as e:\n            logging.warning(\"DatabaseError while setting value for key %r: %s\", path_str, str(e))\n\n    def _create_con(self, second_try=False):\n        try:\n            self.con = sqlite.connect(self.dbname, isolation_level=None)\n            self._check_upgrade()\n        except sqlite.DatabaseError as e:  # corrupted db\n            if second_try:\n                raise  # Something really strange is happening\n            logging.warning(\"Could not create picture cache because of an error: %s\", str(e))\n            self.con.close()\n            os.remove(self.dbname)\n            self._create_con(second_try=True)\n\n    def _check_upgrade(self) -> None:\n        with self.con as conn:\n            has_schema = conn.execute(\n                \"SELECT NAME FROM sqlite_master WHERE type='table' AND name='schema_version'\"\n            ).fetchall()\n            version = None\n            if has_schema:\n                version = conn.execute(\"SELECT version FROM schema_version ORDER BY version DESC\").fetchone()[0]\n            else:\n                conn.execute(\"CREATE TABLE schema_version (version int PRIMARY KEY, description TEXT)\")\n            if version != self.schema_version:\n                conn.execute(self.drop_table_query)\n                conn.execute(\n                    \"INSERT OR REPLACE INTO schema_version VALUES (:version, :description)\",\n                    {\"version\": self.schema_version, \"description\": self.schema_version_description},\n                )\n            conn.execute(self.create_table_query)\n            conn.execute(self.create_index_query)\n\n    def clear(self):\n        self.close()\n        if self.dbname != \":memory:\":\n            os.remove(self.dbname)\n        self._create_con()\n\n    def close(self):\n        if self.con is not None:\n            self.con.close()\n        self.con = None\n\n    def filter(self, func):\n        to_delete = [key for key in self if not func(key)]\n        for key in to_delete:\n            del self[key]\n\n    def get_id(self, path):\n        sql = \"select rowid from pictures where path = ?\"\n        result = self.con.execute(sql, [path]).fetchone()\n        if result:\n            return result[0]\n        else:\n            raise ValueError(path)\n\n    def get_multiple(self, rowids):\n        ids = \",\".join(map(str, rowids))\n        sql = (\n            \"select rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 \"\n            f\"from pictures where rowid in ({ids})\"\n        )\n        cur = self.con.execute(sql)\n        return (\n            (\n                rowid,\n                [\n                    bytes_to_colors(blocks),\n                    bytes_to_colors(blocks2),\n                    bytes_to_colors(blocks3),\n                    bytes_to_colors(blocks4),\n                    bytes_to_colors(blocks5),\n                    bytes_to_colors(blocks6),\n                    bytes_to_colors(blocks7),\n                    bytes_to_colors(blocks8),\n                ],\n            )\n            for rowid, blocks, blocks2, blocks3, blocks4, blocks5, blocks6, blocks7, blocks8 in cur\n        )\n\n    def purge_outdated(self):\n        \"\"\"Go through the cache and purge outdated records.\n\n        A record is outdated if the picture doesn't exist or if its mtime is greater than the one in\n        the db.\n        \"\"\"\n        todelete = []\n        sql = \"select rowid, path, mtime_ns from pictures\"\n        cur = self.con.execute(sql)\n        for rowid, path_str, mtime_ns in cur:\n            if mtime_ns and op.exists(path_str):\n                picture_mtime = os.stat(path_str).st_mtime\n                if int(picture_mtime) <= mtime_ns:\n                    # not outdated\n                    continue\n            todelete.append(rowid)\n        if todelete:\n            sql = \"delete from pictures where rowid in (%s)\" % \",\".join(map(str, todelete))\n            self.con.execute(sql)\n"
  },
  {
    "path": "core/pe/exif.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-04-20\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)\n\nimport logging\n\nEXIF_TAGS = {\n    0x0100: \"ImageWidth\",\n    0x0101: \"ImageLength\",\n    0x0102: \"BitsPerSample\",\n    0x0103: \"Compression\",\n    0x0106: \"PhotometricInterpretation\",\n    0x010A: \"FillOrder\",\n    0x010D: \"DocumentName\",\n    0x010E: \"ImageDescription\",\n    0x010F: \"Make\",\n    0x0110: \"Model\",\n    0x0111: \"StripOffsets\",\n    0x0112: \"Orientation\",\n    0x0115: \"SamplesPerPixel\",\n    0x0116: \"RowsPerStrip\",\n    0x0117: \"StripByteCounts\",\n    0x011A: \"XResolution\",\n    0x011B: \"YResolution\",\n    0x011C: \"PlanarConfiguration\",\n    0x0128: \"ResolutionUnit\",\n    0x012D: \"TransferFunction\",\n    0x0131: \"Software\",\n    0x0132: \"DateTime\",\n    0x013B: \"Artist\",\n    0x013E: \"WhitePoint\",\n    0x013F: \"PrimaryChromaticities\",\n    0x0156: \"TransferRange\",\n    0x0200: \"JPEGProc\",\n    0x0201: \"JPEGInterchangeFormat\",\n    0x0202: \"JPEGInterchangeFormatLength\",\n    0x0211: \"YCbCrCoefficients\",\n    0x0212: \"YCbCrSubSampling\",\n    0x0213: \"YCbCrPositioning\",\n    0x0214: \"ReferenceBlackWhite\",\n    0x828F: \"BatteryLevel\",\n    0x8298: \"Copyright\",\n    0x829A: \"ExposureTime\",\n    0x829D: \"FNumber\",\n    0x83BB: \"IPTC/NAA\",\n    0x8769: \"ExifIFDPointer\",\n    0x8773: \"InterColorProfile\",\n    0x8822: \"ExposureProgram\",\n    0x8824: \"SpectralSensitivity\",\n    0x8825: \"GPSInfoIFDPointer\",\n    0x8827: \"ISOSpeedRatings\",\n    0x8828: \"OECF\",\n    0x9000: \"ExifVersion\",\n    0x9003: \"DateTimeOriginal\",\n    0x9004: \"DateTimeDigitized\",\n    0x9101: \"ComponentsConfiguration\",\n    0x9102: \"CompressedBitsPerPixel\",\n    0x9201: \"ShutterSpeedValue\",\n    0x9202: \"ApertureValue\",\n    0x9203: \"BrightnessValue\",\n    0x9204: \"ExposureBiasValue\",\n    0x9205: \"MaxApertureValue\",\n    0x9206: \"SubjectDistance\",\n    0x9207: \"MeteringMode\",\n    0x9208: \"LightSource\",\n    0x9209: \"Flash\",\n    0x920A: \"FocalLength\",\n    0x9214: \"SubjectArea\",\n    0x927C: \"MakerNote\",\n    0x9286: \"UserComment\",\n    0x9290: \"SubSecTime\",\n    0x9291: \"SubSecTimeOriginal\",\n    0x9292: \"SubSecTimeDigitized\",\n    0xA000: \"FlashPixVersion\",\n    0xA001: \"ColorSpace\",\n    0xA002: \"PixelXDimension\",\n    0xA003: \"PixelYDimension\",\n    0xA004: \"RelatedSoundFile\",\n    0xA005: \"InteroperabilityIFDPointer\",\n    0xA20B: \"FlashEnergy\",  # 0x920B in TIFF/EP\n    0xA20C: \"SpatialFrequencyResponse\",  # 0x920C    -  -\n    0xA20E: \"FocalPlaneXResolution\",  # 0x920E    -  -\n    0xA20F: \"FocalPlaneYResolution\",  # 0x920F    -  -\n    0xA210: \"FocalPlaneResolutionUnit\",  # 0x9210    -  -\n    0xA214: \"SubjectLocation\",  # 0x9214    -  -\n    0xA215: \"ExposureIndex\",  # 0x9215    -  -\n    0xA217: \"SensingMethod\",  # 0x9217    -  -\n    0xA300: \"FileSource\",\n    0xA301: \"SceneType\",\n    0xA302: \"CFAPattern\",  # 0x828E in TIFF/EP\n    0xA401: \"CustomRendered\",\n    0xA402: \"ExposureMode\",\n    0xA403: \"WhiteBalance\",\n    0xA404: \"DigitalZoomRatio\",\n    0xA405: \"FocalLengthIn35mmFilm\",\n    0xA406: \"SceneCaptureType\",\n    0xA407: \"GainControl\",\n    0xA408: \"Contrast\",\n    0xA409: \"Saturation\",\n    0xA40A: \"Sharpness\",\n    0xA40B: \"DeviceSettingDescription\",\n    0xA40C: \"SubjectDistanceRange\",\n    0xA420: \"ImageUniqueID\",\n}\n\nINTR_TAGS = {\n    0x0001: \"InteroperabilityIndex\",\n    0x0002: \"InteroperabilityVersion\",\n    0x1000: \"RelatedImageFileFormat\",\n    0x1001: \"RelatedImageWidth\",\n    0x1002: \"RelatedImageLength\",\n}\n\nGPS_TA0GS = {\n    0x00: \"GPSVersionID\",\n    0x01: \"GPSLatitudeRef\",\n    0x02: \"GPSLatitude\",\n    0x03: \"GPSLongitudeRef\",\n    0x04: \"GPSLongitude\",\n    0x05: \"GPSAltitudeRef\",\n    0x06: \"GPSAltitude\",\n    0x07: \"GPSTimeStamp\",\n    0x08: \"GPSSatellites\",\n    0x09: \"GPSStatus\",\n    0x0A: \"GPSMeasureMode\",\n    0x0B: \"GPSDOP\",\n    0x0C: \"GPSSpeedRef\",\n    0x0D: \"GPSSpeed\",\n    0x0E: \"GPSTrackRef\",\n    0x0F: \"GPSTrack\",\n    0x10: \"GPSImgDirectionRef\",\n    0x11: \"GPSImgDirection\",\n    0x12: \"GPSMapDatum\",\n    0x13: \"GPSDestLatitudeRef\",\n    0x14: \"GPSDestLatitude\",\n    0x15: \"GPSDestLongitudeRef\",\n    0x16: \"GPSDestLongitude\",\n    0x17: \"GPSDestBearingRef\",\n    0x18: \"GPSDestBearing\",\n    0x19: \"GPSDestDistanceRef\",\n    0x1A: \"GPSDestDistance\",\n    0x1B: \"GPSProcessingMethod\",\n    0x1C: \"GPSAreaInformation\",\n    0x1D: \"GPSDateStamp\",\n    0x1E: \"GPSDifferential\",\n}\n\nINTEL_ENDIAN = ord(\"I\")\nMOTOROLA_ENDIAN = ord(\"M\")\n\n# About MAX_COUNT: It's possible to have corrupted exif tags where the entry count is way too high\n# and thus makes us loop, not endlessly, but for heck of a long time for nothing. Therefore, we put\n# an arbitrary limit on the entry count we'll allow ourselves to read and any IFD reporting more\n# entries than that will be considered corrupt.\nMAX_COUNT = 0xFFFF\n\n\ndef s2n_motorola(bytes):\n    x = 0\n    for c in bytes:\n        x = (x << 8) | c\n    return x\n\n\ndef s2n_intel(bytes):\n    x = 0\n    y = 0\n    for c in bytes:\n        x = x | (c << y)\n        y = y + 8\n    return x\n\n\nclass Fraction:\n    def __init__(self, num, den):\n        self.num = num\n        self.den = den\n\n    def __repr__(self):\n        return \"%d/%d\" % (self.num, self.den)\n\n\nclass TIFF_file:\n    def __init__(self, data):\n        self.data = data\n        self.endian = data[0]\n        self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola\n\n    def s2n(self, offset, length, signed=0, debug=False):\n        data_slice = self.data[offset : offset + length]\n        val = self.s2nfunc(data_slice)\n        # Sign extension ?\n        if signed:\n            msb = 1 << (8 * length - 1)\n            if val & msb:\n                val = val - (msb << 1)\n        if debug:\n            logging.debug(self.endian)\n            logging.debug(\n                \"Slice for offset %d length %d: %r and value: %d\",\n                offset,\n                length,\n                data_slice,\n                val,\n            )\n        return val\n\n    def first_IFD(self):\n        return self.s2n(4, 4)\n\n    def next_IFD(self, ifd):\n        entries = self.s2n(ifd, 2)\n        return self.s2n(ifd + 2 + 12 * entries, 4)\n\n    def list_IFDs(self):\n        i = self.first_IFD()\n        a = []\n        while i:\n            a.append(i)\n            i = self.next_IFD(i)\n        return a\n\n    def dump_IFD(self, ifd):\n        entries = self.s2n(ifd, 2)\n        logging.debug(\"Entries for IFD %d: %d\", ifd, entries)\n        if entries > MAX_COUNT:\n            logging.debug(\"Probably corrupt. Aborting.\")\n            return []\n        a = []\n        for i in range(entries):\n            entry = ifd + 2 + 12 * i\n            tag = self.s2n(entry, 2)\n            entry_type = self.s2n(entry + 2, 2)\n            if not 1 <= entry_type <= 10:\n                continue  # not handled\n            typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][entry_type - 1]\n            count = self.s2n(entry + 4, 4)\n            if count > MAX_COUNT:\n                logging.debug(\"Probably corrupt. Aborting.\")\n                return []\n            offset = entry + 8\n            if count * typelen > 4:\n                offset = self.s2n(offset, 4)\n            if entry_type == 2:\n                # Special case: nul-terminated ASCII string\n                values = str(self.data[offset : offset + count - 1], encoding=\"latin-1\")\n            else:\n                values = []\n                signed = entry_type == 6 or entry_type >= 8\n                for _ in range(count):\n                    if entry_type in {5, 10}:\n                        # The type is either 5 or 10\n                        value_j = Fraction(self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed))\n                    else:\n                        # Not a fraction\n                        value_j = self.s2n(offset, typelen, signed)\n                    values.append(value_j)\n                    offset = offset + typelen\n            # Now \"values\" is either a string or an array\n            a.append((tag, entry_type, values))\n        return a\n\n\ndef read_exif_header(fp):\n    # If `fp`'s first bytes are not exif, it tries to find it in the next 4kb\n    def isexif(data):\n        return data[0:4] == b\"\\377\\330\\377\\341\" and data[6:10] == b\"Exif\"\n\n    data = fp.read(12)\n    if isexif(data):\n        return data\n    # ok, not exif, try to find it\n    large_data = fp.read(4096)\n    try:\n        index = large_data.index(b\"Exif\")\n        data = large_data[index - 6 : index + 6]\n        # large_data omits the first 12 bytes, and the index is at the middle of the header, so we\n        # must seek index + 18\n        fp.seek(index + 18)\n        return data\n    except ValueError:\n        raise ValueError(\"Not an Exif file\")\n\n\ndef get_fields(fp):\n    data = read_exif_header(fp)\n    length = data[4] * 256 + data[5]\n    logging.debug(\"Exif header length: %d bytes\", length)\n    data = fp.read(length - 8)\n    data_format = data[0]\n    logging.debug(\"%s format\", {INTEL_ENDIAN: \"Intel\", MOTOROLA_ENDIAN: \"Motorola\"}[data_format])\n    T = TIFF_file(data)\n    # There may be more than one IFD per file, but we only read the first one because others are\n    # most likely thumbnails.\n    main_ifd_offset = T.first_IFD()\n    result = {}\n\n    def add_tag_to_result(tag, values):\n        try:\n            stag = EXIF_TAGS[tag]\n        except KeyError:\n            stag = \"0x%04X\" % tag\n        if stag in result:\n            return  # don't overwrite data\n        result[stag] = values\n\n    logging.debug(\"IFD at offset %d\", main_ifd_offset)\n    IFD = T.dump_IFD(main_ifd_offset)\n    exif_off = gps_off = 0\n    for tag, type, values in IFD:\n        if tag == 0x8769:\n            exif_off = values[0]\n            continue\n        if tag == 0x8825:\n            gps_off = values[0]\n            continue\n        add_tag_to_result(tag, values)\n    if exif_off:\n        logging.debug(\"Exif SubIFD at offset %d:\", exif_off)\n        IFD = T.dump_IFD(exif_off)\n        # Recent digital cameras have a little subdirectory\n        # here, pointed to by tag 0xA005. Apparently, it's the\n        # \"Interoperability IFD\", defined in Exif 2.1 and DCF.\n        intr_off = 0\n        for tag, type, values in IFD:\n            if tag == 0xA005:\n                intr_off = values[0]\n                continue\n            add_tag_to_result(tag, values)\n        if intr_off:\n            logging.debug(\"Exif Interoperability SubSubIFD at offset %d:\", intr_off)\n            IFD = T.dump_IFD(intr_off)\n            for tag, type, values in IFD:\n                add_tag_to_result(tag, values)\n    if gps_off:\n        logging.debug(\"GPS SubIFD at offset %d:\", gps_off)\n        IFD = T.dump_IFD(gps_off)\n        for tag, type, values in IFD:\n            add_tag_to_result(tag, values)\n    return result\n"
  },
  {
    "path": "core/pe/matchblock.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2007/02/25\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\nimport multiprocessing\nfrom itertools import combinations\n\nfrom hscommon.util import extract, iterconsume\nfrom hscommon.trans import tr\nfrom hscommon.jobprogress import job\n\nfrom core.engine import Match\nfrom core.pe.block import avgdiff, DifferentBlockCountError, NoBlocksError\nfrom core.pe.cache_sqlite import SqliteCache\n\n# OPTIMIZATION NOTES:\n# The bottleneck of the matching phase is CPU, which is why we use multiprocessing. However, another\n# bottleneck that shows up when a lot of pictures are involved is Disk IO's because blocks\n# constantly have to be read from disks by subprocesses. This problem is especially big on CPUs\n# with a lot of cores. Therefore, we must minimize Disk IOs. The best way to achieve that is to\n# separate the files to scan in \"chunks\" and it's by chunk that blocks are read in memory and\n# compared to each other. Each file in a chunk has to be compared to each other, of course, but also\n# to files in other chunks. So chunkifying doesn't save us any actual comparison, but the advantage\n# is that instead of reading blocks from disk number_of_files**2 times, we read it\n# number_of_files*number_of_chunks times.\n# Determining the right chunk size is tricky, because if it's too big, too many blocks will be in\n# memory at the same time and we might end up with memory trashing, which is awfully slow. So,\n# because our *real* bottleneck is CPU, the chunk size must simply be enough so that the CPU isn't\n# starved by Disk IOs.\n\nMIN_ITERATIONS = 3\nBLOCK_COUNT_PER_SIDE = 15\nDEFAULT_CHUNK_SIZE = 1000\nMIN_CHUNK_SIZE = 100\n\n# Enough so that we're sure that the main thread will not wait after a result.get() call\n# cpucount+1 should be enough to be sure that the spawned process will not wait after the results\n# collection made by the main process.\ntry:\n    RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() + 1\nexcept Exception:\n    # I had an IOError on app launch once. It seems to be a freak occurrence. In any case, we want\n    # the app to launch, so let's just put an arbitrary value.\n    logging.warning(\"Had problems to determine cpu count on launch.\")\n    RESULTS_QUEUE_LIMIT = 8\n\n\ndef get_cache(cache_path, readonly=False):\n    return SqliteCache(cache_path, readonly=readonly)\n\n\ndef prepare_pictures(pictures, cache_path, with_dimensions, match_rotated, j=job.nulljob):\n    # The MemoryError handlers in there use logging without first caring about whether or not\n    # there is enough memory left to carry on the operation because it is assumed that the\n    # MemoryError happens when trying to read an image file, which is freed from memory by the\n    # time that MemoryError is raised.\n    cache = get_cache(cache_path)\n    cache.purge_outdated()\n    prepared = []  # only pictures for which there was no error getting blocks\n    try:\n        for picture in j.iter_with_progress(pictures, tr(\"Analyzed %d/%d pictures\")):\n            if not picture.path:\n                # XXX Find the root cause of this. I've received reports of crashes where we had\n                # \"Analyzing picture at \" (without a path) in the debug log. It was an iPhoto scan.\n                # For now, I'm simply working around the crash by ignoring those, but it would be\n                # interesting to know exactly why this happens. I'm suspecting a malformed\n                # entry in iPhoto library.\n                logging.warning(\"We have a picture with a null path here\")\n                continue\n            logging.debug(\"Analyzing picture at %s\", picture.unicode_path)\n            if with_dimensions:\n                picture.dimensions  # pre-read dimensions\n            try:\n                if picture.unicode_path not in cache or (\n                    match_rotated and any(block == [] for block in cache[picture.unicode_path])\n                ):\n                    if match_rotated:\n                        blocks = [picture.get_blocks(BLOCK_COUNT_PER_SIDE, orientation) for orientation in range(1, 9)]\n                    else:\n                        blocks = [[]] * 8\n                        blocks[max(picture.get_orientation() - 1, 0)] = picture.get_blocks(BLOCK_COUNT_PER_SIDE)\n                    cache[picture.unicode_path] = blocks\n                prepared.append(picture)\n            except (OSError, ValueError) as e:\n                logging.warning(str(e))\n            except MemoryError:\n                logging.warning(\n                    \"Ran out of memory while reading %s of size %d\",\n                    picture.unicode_path,\n                    picture.size,\n                )\n                if picture.size < 10 * 1024 * 1024:  # We're really running out of memory\n                    raise\n    except MemoryError:\n        logging.warning(\"Ran out of memory while preparing pictures\")\n    cache.close()\n    return prepared\n\n\ndef get_chunks(pictures):\n    min_chunk_count = multiprocessing.cpu_count() * 2  # have enough chunks to feed all subprocesses\n    chunk_count = len(pictures) // DEFAULT_CHUNK_SIZE\n    chunk_count = max(min_chunk_count, chunk_count)\n    chunk_size = (len(pictures) // chunk_count) + 1\n    chunk_size = max(MIN_CHUNK_SIZE, chunk_size)\n    logging.info(\n        \"Creating %d chunks with a chunk size of %d for %d pictures\",\n        chunk_count,\n        chunk_size,\n        len(pictures),\n    )\n    chunks = [pictures[i : i + chunk_size] for i in range(0, len(pictures), chunk_size)]\n    return chunks\n\n\ndef get_match(first, second, percentage):\n    if percentage < 0:\n        percentage = 0\n    return Match(first, second, percentage)\n\n\ndef async_compare(ref_ids, other_ids, dbname, threshold, picinfo, match_rotated=False):\n    # The list of ids in ref_ids have to be compared to the list of ids in other_ids. other_ids\n    # can be None. In this case, ref_ids has to be compared with itself\n    # picinfo is a dictionary {pic_id: (dimensions, is_ref)}\n    cache = get_cache(dbname, readonly=True)\n    limit = 100 - threshold\n    ref_pairs = list(cache.get_multiple(ref_ids))  # (rowid, [b, b2, ..., b8])\n    if other_ids is not None:\n        other_pairs = list(cache.get_multiple(other_ids))\n        comparisons_to_do = [(r, o) for r in ref_pairs for o in other_pairs]\n    else:\n        comparisons_to_do = list(combinations(ref_pairs, 2))\n    results = []\n    for (ref_id, ref_blocks), (other_id, other_blocks) in comparisons_to_do:\n        ref_dimensions, ref_is_ref = picinfo[ref_id]\n        other_dimensions, other_is_ref = picinfo[other_id]\n        if ref_is_ref and other_is_ref:\n            continue\n        if ref_dimensions != other_dimensions:\n            if match_rotated:\n                rotated_ref_dimensions = (ref_dimensions[1], ref_dimensions[0])\n                if rotated_ref_dimensions != other_dimensions:\n                    continue\n            else:\n                continue\n\n        orientation_range = 1\n        if match_rotated:\n            orientation_range = 8\n\n        for orientation_ref in range(orientation_range):\n            try:\n                diff = avgdiff(ref_blocks[orientation_ref], other_blocks[0], limit, MIN_ITERATIONS)\n                percentage = 100 - diff\n            except (DifferentBlockCountError, NoBlocksError):\n                percentage = 0\n            if percentage >= threshold:\n                results.append((ref_id, other_id, percentage))\n                break\n\n    cache.close()\n    return results\n\n\ndef getmatches(pictures, cache_path, threshold, match_scaled=False, match_rotated=False, j=job.nulljob):\n    def get_picinfo(p):\n        if match_scaled:\n            return ((None, None), p.is_ref)\n        else:\n            return (p.dimensions, p.is_ref)\n\n    def collect_results(collect_all=False):\n        # collect results and wait until the queue is small enough to accomodate a new results.\n        nonlocal async_results, matches, comparison_count, comparisons_to_do\n        limit = 0 if collect_all else RESULTS_QUEUE_LIMIT\n        while len(async_results) > limit:\n            ready, working = extract(lambda r: r.ready(), async_results)\n            for result in ready:\n                matches += result.get()\n                async_results.remove(result)\n                comparison_count += 1\n        # About the NOQA below: I think there's a bug in pyflakes. To investigate...\n        progress_msg = tr(\"Performed %d/%d chunk matches\") % (\n            comparison_count,\n            len(comparisons_to_do),\n        )  # NOQA\n        j.set_progress(comparison_count, progress_msg)\n\n    j = j.start_subjob([3, 7])\n    pictures = prepare_pictures(pictures, cache_path, not match_scaled, match_rotated, j=j)\n    j = j.start_subjob([9, 1], tr(\"Preparing for matching\"))\n    cache = get_cache(cache_path)\n    id2picture = {}\n    for picture in pictures:\n        try:\n            picture.cache_id = cache.get_id(picture.unicode_path)\n            id2picture[picture.cache_id] = picture\n        except ValueError:\n            pass\n    cache.close()\n    pictures = [p for p in pictures if hasattr(p, \"cache_id\")]\n    pool = multiprocessing.Pool()\n    async_results = []\n    matches = []\n    chunks = get_chunks(pictures)\n    # We add a None element at the end of the chunk list because each chunk has to be compared\n    # with itself. Thus, each chunk will show up as a ref_chunk having other_chunk set to None once.\n    comparisons_to_do = list(combinations(chunks + [None], 2))\n    comparison_count = 0\n    j.start_job(len(comparisons_to_do))\n    try:\n        for ref_chunk, other_chunk in comparisons_to_do:\n            picinfo = {p.cache_id: get_picinfo(p) for p in ref_chunk}\n            ref_ids = [p.cache_id for p in ref_chunk]\n            if other_chunk is not None:\n                other_ids = [p.cache_id for p in other_chunk]\n                picinfo.update({p.cache_id: get_picinfo(p) for p in other_chunk})\n            else:\n                other_ids = None\n            args = (ref_ids, other_ids, cache_path, threshold, picinfo, match_rotated)\n            async_results.append(pool.apply_async(async_compare, args))\n            collect_results()\n        collect_results(collect_all=True)\n    except MemoryError:\n        # Rare, but possible, even in 64bit situations (ref #264). What do we do now? We free us\n        # some wiggle room, log about the incident, and stop matching right here. We then process\n        # the matches we have. The rest of the process doesn't allocate much and we should be\n        # alright.\n        del (\n            comparisons_to_do,\n            chunks,\n            pictures,\n        )  # some wiggle room for the next statements\n        logging.warning(\"Ran out of memory when scanning! We had %d matches.\", len(matches))\n        del matches[-len(matches) // 3 :]  # some wiggle room to ensure we don't run out of memory again.\n    pool.close()\n    result = []\n    myiter = j.iter_with_progress(\n        iterconsume(matches, reverse=False),\n        tr(\"Verified %d/%d matches\"),\n        every=10,\n        count=len(matches),\n    )\n    for ref_id, other_id, percentage in myiter:\n        ref = id2picture[ref_id]\n        other = id2picture[other_id]\n        if percentage == 100 and ref.digest != other.digest:\n            percentage = 99\n        if percentage >= threshold:\n            ref.dimensions  # pre-read dimensions for display in results\n            other.dimensions\n            result.append(get_match(ref, other, percentage))\n    pool.join()\n    return result\n\n\nmultiprocessing.freeze_support()\n"
  },
  {
    "path": "core/pe/matchexif.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-04-20\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom collections import defaultdict\nfrom itertools import combinations\n\nfrom hscommon.trans import tr\n\nfrom core.engine import Match\n\n\ndef getmatches(files, match_scaled, j):\n    timestamp2pic = defaultdict(set)\n    for picture in j.iter_with_progress(files, tr(\"Read EXIF of %d/%d pictures\")):\n        timestamp = picture.exif_timestamp\n        if timestamp:\n            timestamp2pic[timestamp].add(picture)\n    if \"0000:00:00 00:00:00\" in timestamp2pic:  # very likely false matches\n        del timestamp2pic[\"0000:00:00 00:00:00\"]\n    matches = []\n    for pictures in timestamp2pic.values():\n        for p1, p2 in combinations(pictures, 2):\n            if (not match_scaled) and (p1.dimensions != p2.dimensions):\n                continue\n            matches.append(Match(p1, p2, 100))\n    return matches\n"
  },
  {
    "path": "core/pe/modules/block.c",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-01-30\n * Copyright 2014 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"BSD\" License as described in the\n * \"LICENSE\" file, which should be included with this package. The terms are\n * also available at http://www.hardcoded.net/licenses/bsd_license\n */\n\n#include \"common.h\"\n\n/* avgdiff/maxdiff has been called with empty lists */\nstatic PyObject *NoBlocksError;\n/* avgdiff/maxdiff has been called with 2 block lists of different size. */\nstatic PyObject *DifferentBlockCountError;\n\n/* Returns a 3 sized tuple containing the mean color of 'image'.\n * image: a PIL image or crop.\n */\nstatic PyObject *getblock(PyObject *image) {\n  int i, totr, totg, totb;\n  Py_ssize_t pixel_count;\n  PyObject *ppixels;\n\n  totr = totg = totb = 0;\n  ppixels = PyObject_CallMethod(image, \"getdata\", NULL);\n  if (ppixels == NULL) {\n    return NULL;\n  }\n\n  pixel_count = PySequence_Length(ppixels);\n  for (i = 0; i < pixel_count; i++) {\n    PyObject *ppixel, *pr, *pg, *pb;\n    int r, g, b;\n\n    ppixel = PySequence_ITEM(ppixels, i);\n    pr = PySequence_ITEM(ppixel, 0);\n    pg = PySequence_ITEM(ppixel, 1);\n    pb = PySequence_ITEM(ppixel, 2);\n    Py_DECREF(ppixel);\n    r = PyLong_AsLong(pr);\n    g = PyLong_AsLong(pg);\n    b = PyLong_AsLong(pb);\n    Py_DECREF(pr);\n    Py_DECREF(pg);\n    Py_DECREF(pb);\n\n    totr += r;\n    totg += g;\n    totb += b;\n  }\n\n  Py_DECREF(ppixels);\n\n  if (pixel_count) {\n    totr /= pixel_count;\n    totg /= pixel_count;\n    totb /= pixel_count;\n  }\n\n  return inttuple(3, totr, totg, totb);\n}\n\n/* Returns the difference between the first block and the second.\n * It returns an absolute sum of the 3 differences (RGB).\n */\nstatic int diff(PyObject *first, PyObject *second) {\n  int r1, g1, b1, r2, b2, g2;\n  PyObject *pr, *pg, *pb;\n  pr = PySequence_ITEM(first, 0);\n  pg = PySequence_ITEM(first, 1);\n  pb = PySequence_ITEM(first, 2);\n  r1 = PyLong_AsLong(pr);\n  g1 = PyLong_AsLong(pg);\n  b1 = PyLong_AsLong(pb);\n  Py_DECREF(pr);\n  Py_DECREF(pg);\n  Py_DECREF(pb);\n\n  pr = PySequence_ITEM(second, 0);\n  pg = PySequence_ITEM(second, 1);\n  pb = PySequence_ITEM(second, 2);\n  r2 = PyLong_AsLong(pr);\n  g2 = PyLong_AsLong(pg);\n  b2 = PyLong_AsLong(pb);\n  Py_DECREF(pr);\n  Py_DECREF(pg);\n  Py_DECREF(pb);\n\n  return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2);\n}\n\nPyDoc_STRVAR(block_getblocks2_doc,\n             \"Returns a list of blocks (3 sized tuples).\\n\\\n\\n\\\nimage: A PIL image to base the blocks on.\\n\\\nblock_count_per_side: This integer determine the number of blocks the function will return.\\n\\\nIf it is 10, for example, 100 blocks will be returns (10 width, 10 height). The blocks will not\\n\\\nnecessarely cover square areas. The area covered by each block will be proportional to the image\\n\\\nitself.\\n\");\n\nstatic PyObject *block_getblocks2(PyObject *self, PyObject *args) {\n  int block_count_per_side, width, height, block_width, block_height, ih;\n  PyObject *image;\n  PyObject *pimage_size, *pwidth, *pheight;\n  PyObject *result;\n\n  if (!PyArg_ParseTuple(args, \"Oi\", &image, &block_count_per_side)) {\n    return NULL;\n  }\n\n  pimage_size = PyObject_GetAttrString(image, \"size\");\n  pwidth = PySequence_ITEM(pimage_size, 0);\n  pheight = PySequence_ITEM(pimage_size, 1);\n  width = PyLong_AsLong(pwidth);\n  height = PyLong_AsLong(pheight);\n  Py_DECREF(pimage_size);\n  Py_DECREF(pwidth);\n  Py_DECREF(pheight);\n\n  if (!(width && height)) {\n    return PyList_New(0);\n  }\n\n  block_width = max(width / block_count_per_side, 1);\n  block_height = max(height / block_count_per_side, 1);\n\n  result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side);\n  if (result == NULL) {\n    return NULL;\n  }\n\n  for (ih = 0; ih < block_count_per_side; ih++) {\n    int top, bottom, iw;\n    top = min(ih * block_height, height - block_height);\n    bottom = top + block_height;\n    for (iw = 0; iw < block_count_per_side; iw++) {\n      int left, right;\n      PyObject *pbox;\n      PyObject *pmethodname;\n      PyObject *pcrop;\n      PyObject *pblock;\n\n      left = min(iw * block_width, width - block_width);\n      right = left + block_width;\n      pbox = inttuple(4, left, top, right, bottom);\n      pmethodname = PyUnicode_FromString(\"crop\");\n      pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL);\n      Py_DECREF(pmethodname);\n      Py_DECREF(pbox);\n      if (pcrop == NULL) {\n        Py_DECREF(result);\n        return NULL;\n      }\n      pblock = getblock(pcrop);\n      Py_DECREF(pcrop);\n      if (pblock == NULL) {\n        Py_DECREF(result);\n        return NULL;\n      }\n      PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock);\n    }\n  }\n\n  return result;\n}\n\nPyDoc_STRVAR(block_avgdiff_doc,\n             \"Returns the average diff between first blocks and seconds.\\n\\\n\\n\\\nIf the result surpasses limit, limit + 1 is returned, except if less than min_iterations\\n\\\niterations have been made in the blocks.\\n\");\n\nstatic PyObject *block_avgdiff(PyObject *self, PyObject *args) {\n  PyObject *first, *second;\n  int limit, min_iterations;\n  Py_ssize_t count;\n  int sum, i, result;\n\n  if (!PyArg_ParseTuple(args, \"OOii\", &first, &second, &limit,\n                        &min_iterations)) {\n    return NULL;\n  }\n\n  count = PySequence_Length(first);\n  if (count != PySequence_Length(second)) {\n    PyErr_SetString(DifferentBlockCountError, \"\");\n    return NULL;\n  }\n  if (!count) {\n    PyErr_SetString(NoBlocksError, \"\");\n    return NULL;\n  }\n\n  sum = 0;\n  for (i = 0; i < count; i++) {\n    int iteration_count;\n    PyObject *item1, *item2;\n\n    iteration_count = i + 1;\n    item1 = PySequence_ITEM(first, i);\n    item2 = PySequence_ITEM(second, i);\n    sum += diff(item1, item2);\n    Py_DECREF(item1);\n    Py_DECREF(item2);\n    if ((sum > limit * iteration_count) &&\n        (iteration_count >= min_iterations)) {\n      return PyLong_FromLong(limit + 1);\n    }\n  }\n\n  result = sum / count;\n  if (!result && sum) {\n    result = 1;\n  }\n  return PyLong_FromLong(result);\n}\n\nstatic PyMethodDef BlockMethods[] = {\n    {\"getblocks2\", block_getblocks2, METH_VARARGS, block_getblocks2_doc},\n    {\"avgdiff\", block_avgdiff, METH_VARARGS, block_avgdiff_doc},\n    {NULL, NULL, 0, NULL} /* Sentinel */\n};\n\nstatic struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT,\n                                      \"_block\",\n                                      NULL,\n                                      -1,\n                                      BlockMethods,\n                                      NULL,\n                                      NULL,\n                                      NULL,\n                                      NULL};\n\nPyObject *PyInit__block(void) {\n  PyObject *m = PyModule_Create(&BlockDef);\n  if (m == NULL) {\n    return NULL;\n  }\n\n  NoBlocksError = PyErr_NewException(\"_block.NoBlocksError\", NULL, NULL);\n  PyModule_AddObject(m, \"NoBlocksError\", NoBlocksError);\n  DifferentBlockCountError =\n      PyErr_NewException(\"_block.DifferentBlockCountError\", NULL, NULL);\n  PyModule_AddObject(m, \"DifferentBlockCountError\", DifferentBlockCountError);\n\n  return m;\n}\n"
  },
  {
    "path": "core/pe/modules/block_osx.m",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-02-04\n * Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n * which should be included with this package. The terms are also available at\n * http://www.gnu.org/licenses/gpl-3.0.html\n**/\n\n#include \"common.h\"\n\n#import <Foundation/Foundation.h>\n#import <CoreGraphics/CoreGraphics.h>\n#import <ImageIO/ImageIO.h>\n\n#define RADIANS( degrees ) ( degrees * M_PI / 180 )\n\nstatic CFStringRef\npystring2cfstring(PyObject *pystring)\n{\n    PyObject *encoded;\n    UInt8 *s;\n    CFIndex size;\n    CFStringRef result;\n\n    if (PyUnicode_Check(pystring)) {\n        encoded = PyUnicode_AsUTF8String(pystring);\n        if (encoded == NULL) {\n            return NULL;\n        }\n    } else {\n        encoded = pystring;\n        Py_INCREF(encoded);\n    }\n\n    s = (UInt8*)PyBytes_AS_STRING(encoded);\n    size = PyBytes_GET_SIZE(encoded);\n    result = CFStringCreateWithBytes(NULL, s, size, kCFStringEncodingUTF8, FALSE);\n    Py_DECREF(encoded);\n    return result;\n}\n\nstatic PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)\n{\n    PyObject *path;\n    CFStringRef image_path;\n    CFURLRef image_url;\n    CGImageSourceRef source;\n    CGImageRef image;\n    long width, height;\n    PyObject *pwidth, *pheight;\n    PyObject *result;\n\n    width = 0;\n    height = 0;\n    if (!PyArg_ParseTuple(args, \"O\", &path)) {\n        return NULL;\n    }\n\n    image_path = pystring2cfstring(path);\n    if (image_path == NULL) {\n        return PyErr_NoMemory();\n    }\n    image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE);\n    CFRelease(image_path);\n\n    source = CGImageSourceCreateWithURL(image_url, NULL);\n    CFRelease(image_url);\n    if (source != NULL) {\n        image = CGImageSourceCreateImageAtIndex(source, 0, NULL);\n        if (image != NULL) {\n            width = CGImageGetWidth(image);\n            height = CGImageGetHeight(image);\n            CGImageRelease(image);\n        }\n        CFRelease(source);\n    }\n\n    pwidth = PyLong_FromLong(width);\n    if (pwidth == NULL) {\n        return NULL;\n    }\n    pheight = PyLong_FromLong(height);\n    if (pheight == NULL) {\n        return NULL;\n    }\n    result = PyTuple_Pack(2, pwidth, pheight);\n    Py_DECREF(pwidth);\n    Py_DECREF(pheight);\n    return result;\n}\n\nstatic CGContextRef\nMyCreateBitmapContext(int width, int height)\n{\n    CGContextRef context = NULL;\n    CGColorSpaceRef colorSpace;\n    void *bitmapData;\n    int bitmapByteCount;\n    int bitmapBytesPerRow;\n\n    bitmapBytesPerRow = (width * 4);\n    bitmapByteCount = (bitmapBytesPerRow * height);\n\n    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);\n\n    // calloc() must be used to allocate bitmapData here because the buffer has to be zeroed.\n    // If it's not zeroes, when images with transparency are drawn in the context, this buffer\n    // will stay with undefined pixels, which means that two pictures with the same pixels will\n    // most likely have different blocks (which is not supposed to happen).\n    bitmapData = calloc(bitmapByteCount, 1);\n    if (bitmapData == NULL) {\n        fprintf(stderr, \"Memory not allocated!\");\n        return NULL;\n    }\n\n    context = CGBitmapContextCreate(bitmapData, width, height, 8, bitmapBytesPerRow, colorSpace,\n        (CGBitmapInfo)kCGImageAlphaNoneSkipLast);\n    if (context== NULL) {\n        free(bitmapData);\n        fprintf(stderr, \"Context not created!\");\n        return NULL;\n    }\n    CGColorSpaceRelease(colorSpace);\n    return context;\n}\n\nstatic PyObject* getblock(unsigned char *imageData, int imageWidth, int imageHeight, int boxX, int boxY, int boxW, int boxH)\n{\n    int i,j, totalR, totalG, totalB;\n\n    totalR = totalG = totalB = 0;\n    for(i=boxY; i<boxY+boxH; i++) {\n        for(j=boxX; j<boxX+boxW; j++) {\n            int offset = (i * imageWidth * 4) + (j * 4);\n            totalR += *(imageData + offset);\n            totalG += *(imageData + offset + 1);\n            totalB += *(imageData + offset + 2);\n        }\n    }\n    int pixelCount = boxH * boxW;\n    totalR /= pixelCount;\n    totalG /= pixelCount;\n    totalB /= pixelCount;\n\n    return inttuple(3, totalR, totalG, totalB);\n}\n\nstatic PyObject* block_osx_getblocks(PyObject *self, PyObject *args)\n{\n    PyObject *path, *result;\n    CFStringRef image_path;\n    CFURLRef image_url;\n    CGImageSourceRef source;\n    CGImageRef image;\n    size_t width, height, image_width, image_height;\n    int block_count, block_width, block_height, orientation, i;\n\n    if (!PyArg_ParseTuple(args, \"Oii\", &path, &block_count, &orientation)) {\n        return NULL;\n    }\n\n    if (PySequence_Length(path) == 0) {\n        PyErr_SetString(PyExc_ValueError, \"empty path\");\n        return NULL;\n    }\n\n    if ((orientation > 8) || (orientation < 0)) {\n        orientation = 0; // simplifies checks later since we can only have values in 0-8\n    }\n\n    image_path = pystring2cfstring(path);\n    if (image_path == NULL) {\n        return PyErr_NoMemory();\n    }\n    image_url = CFURLCreateWithFileSystemPath(NULL, image_path, kCFURLPOSIXPathStyle, FALSE);\n    CFRelease(image_path);\n\n    source = CGImageSourceCreateWithURL(image_url, NULL);\n    CFRelease(image_url);\n    if (source == NULL) {\n        return PyErr_NoMemory();\n    }\n\n    image = CGImageSourceCreateImageAtIndex(source, 0, NULL);\n    if (image == NULL) {\n        CFRelease(source);\n        return PyErr_NoMemory();\n    }\n\n\n    width = image_width = CGImageGetWidth(image);\n    height = image_height = CGImageGetHeight(image);\n    if (orientation >= 5) {\n        // orientations 5-8 rotate the photo sideways, so we have to swap width and height\n        width = image_height;\n        height = image_width;\n    }\n\n    CGContextRef context = MyCreateBitmapContext(width, height);\n\n    if (orientation == 2) {\n        // Flip X\n        CGContextTranslateCTM(context, width, 0);\n        CGContextScaleCTM(context, -1, 1);\n    }\n    else if (orientation == 3) {\n        // Rot 180\n        CGContextTranslateCTM(context, width, height);\n        CGContextRotateCTM(context, RADIANS(180));\n    }\n    else if (orientation == 4) {\n        // Flip Y\n        CGContextTranslateCTM(context, 0, height);\n        CGContextScaleCTM(context, 1, -1);\n    }\n    else if (orientation == 5) {\n        // Flip X + Rot CW 90\n        CGContextTranslateCTM(context, width, 0);\n        CGContextScaleCTM(context, -1, 1);\n        CGContextTranslateCTM(context, 0, height);\n        CGContextRotateCTM(context, RADIANS(-90));\n    }\n    else if (orientation == 6) {\n        // Rot CW 90\n        CGContextTranslateCTM(context, 0, height);\n        CGContextRotateCTM(context, RADIANS(-90));\n    }\n    else if (orientation == 7) {\n        // Rot CCW 90 + Flip X\n        CGContextTranslateCTM(context, width, 0);\n        CGContextScaleCTM(context, -1, 1);\n        CGContextTranslateCTM(context, width, 0);\n        CGContextRotateCTM(context, RADIANS(90));\n    }\n    else if (orientation == 8) {\n        // Rot CCW 90\n        CGContextTranslateCTM(context, width, 0);\n        CGContextRotateCTM(context, RADIANS(90));\n    }\n    CGRect myBoundingBox = CGRectMake(0, 0, image_width, image_height);\n    CGContextDrawImage(context, myBoundingBox, image);\n    unsigned char *bitmapData = CGBitmapContextGetData(context);\n    CGContextRelease(context);\n\n    CGImageRelease(image);\n    CFRelease(source);\n    if (bitmapData == NULL) {\n        return PyErr_NoMemory();\n    }\n\n    block_width = max(width/block_count, 1);\n    block_height = max(height/block_count, 1);\n\n    result = PyList_New(block_count * block_count);\n    if (result == NULL) {\n        return NULL;\n    }\n\n    for(i=0; i<block_count; i++) {\n        int j, top;\n        top = min(i*block_height, height-block_height);\n        for(j=0; j<block_count; j++) {\n            int left;\n            left = min(j*block_width, width-block_width);\n            PyObject *block = getblock(bitmapData, width, height, left, top, block_width, block_height);\n            if (block == NULL) {\n                Py_DECREF(result);\n                return NULL;\n            }\n            PyList_SET_ITEM(result, i*block_count+j, block);\n        }\n    }\n\n    free(bitmapData);\n    return result;\n}\n\nstatic PyMethodDef BlockOsxMethods[] = {\n    {\"get_image_size\",  block_osx_get_image_size, METH_VARARGS, \"\"},\n    {\"getblocks\",  block_osx_getblocks, METH_VARARGS, \"\"},\n    {NULL, NULL, 0, NULL} /* Sentinel */\n};\n\nstatic struct PyModuleDef BlockOsxDef = {\n    PyModuleDef_HEAD_INIT,\n    \"_block_osx\",\n    NULL,\n    -1,\n    BlockOsxMethods,\n    NULL,\n    NULL,\n    NULL,\n    NULL\n};\n\nPyObject *\nPyInit__block_osx(void)\n{\n    PyObject *m = PyModule_Create(&BlockOsxDef);\n    if (m == NULL) {\n        return NULL;\n    }\n    return m;\n}\n"
  },
  {
    "path": "core/pe/modules/cache.c",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-01-30\n * Copyright 2014 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"BSD\" License as described in the\n * \"LICENSE\" file, which should be included with this package. The terms are\n * also available at http://www.hardcoded.net/licenses/bsd_license\n */\n\n#include \"common.h\"\n\nstatic PyObject *cache_bytes_to_colors(PyObject *self, PyObject *args) {\n  char *y;\n  Py_ssize_t char_count, i, color_count;\n  PyObject *result;\n  unsigned long r, g, b;\n  Py_ssize_t ci;\n  PyObject *color_tuple;\n\n  if (!PyArg_ParseTuple(args, \"y#\", &y, &char_count)) {\n    return NULL;\n  }\n\n  color_count = char_count / 3;\n  result = PyList_New(color_count);\n  if (result == NULL) {\n    return NULL;\n  }\n\n  for (i = 0; i < color_count; i++) {\n    ci = i * 3;\n    r = (unsigned char)y[ci];\n    g = (unsigned char)y[ci + 1];\n    b = (unsigned char)y[ci + 2];\n\n    color_tuple = inttuple(3, r, g, b);\n    if (color_tuple == NULL) {\n      Py_DECREF(result);\n      return NULL;\n    }\n    PyList_SET_ITEM(result, i, color_tuple);\n  }\n\n  return result;\n}\n\nstatic PyMethodDef CacheMethods[] = {\n    {\"bytes_to_colors\", cache_bytes_to_colors, METH_VARARGS,\n     \"Transform the bytes 's' into a list of 3 sized tuples.\"},\n    {NULL, NULL, 0, NULL} /* Sentinel */\n};\n\nstatic struct PyModuleDef CacheDef = {PyModuleDef_HEAD_INIT,\n                                      \"_cache\",\n                                      NULL,\n                                      -1,\n                                      CacheMethods,\n                                      NULL,\n                                      NULL,\n                                      NULL,\n                                      NULL};\n\nPyObject *PyInit__cache(void) {\n  PyObject *m = PyModule_Create(&CacheDef);\n  if (m == NULL) {\n    return NULL;\n  }\n  return m;\n}\n"
  },
  {
    "path": "core/pe/modules/common.c",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-02-04\n * Copyright 2014 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"BSD\" License as described in the \"LICENSE\" file,\n * which should be included with this package. The terms are also available at\n * http://www.hardcoded.net/licenses/bsd_license\n */\n\n#include \"common.h\"\n\n#ifndef _MSC_VER\nint max(int a, int b)\n{\n    return b > a ? b : a;\n}\n\nint min(int a, int b)\n{\n    return b < a ? b : a;\n}\n#endif\n\nPyObject* inttuple(int n, ...)\n{\n    int i;\n    PyObject *pnumber;\n    PyObject *result;\n    va_list numbers;\n\n    va_start(numbers, n);\n    result = PyTuple_New(n);\n\n    for (i=0; i<n; i++) {\n        pnumber = PyLong_FromUnsignedLong(va_arg(numbers, long));\n        if (pnumber == NULL) {\n            Py_DECREF(result);\n            return NULL;\n        }\n        PyTuple_SET_ITEM(result, i, pnumber);\n    }\n\n    va_end(numbers);\n    return result;\n}\n"
  },
  {
    "path": "core/pe/modules/common.h",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-02-04\n * Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n * which should be included with this package. The terms are also available at\n * http://www.gnu.org/licenses/gpl-3.0.html\n */\n\n#define PY_SSIZE_T_CLEAN\n#include \"Python.h\"\n\n/* It seems like MS VC defines min/max already */\n#ifndef _MSC_VER\nint max(int a, int b);\nint min(int a, int b);\n#endif\n\n/* Create a tuple out of an array of integers. */\nPyObject* inttuple(int n, ...);\n"
  },
  {
    "path": "core/pe/photo.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\nfrom hscommon.util import get_file_ext, format_size\n\nfrom core.util import format_timestamp, format_perc, format_dupe_count\nfrom core import fs\nfrom core.pe import exif\n\n# This global value is set by the platform-specific subclasser of the Photo base class\nPLAT_SPECIFIC_PHOTO_CLASS = None\n\n\ndef format_dimensions(dimensions):\n    return \"%d x %d\" % (dimensions[0], dimensions[1])\n\n\ndef get_delta_dimensions(value, ref_value):\n    return (value[0] - ref_value[0], value[1] - ref_value[1])\n\n\nclass Photo(fs.File):\n    INITIAL_INFO = fs.File.INITIAL_INFO.copy()\n    INITIAL_INFO.update({\"dimensions\": (0, 0), \"exif_timestamp\": \"\"})\n    __slots__ = fs.File.__slots__ + tuple(INITIAL_INFO.keys())\n\n    # These extensions are supported on all platforms\n    HANDLED_EXTS = {\"png\", \"jpg\", \"jpeg\", \"gif\", \"bmp\", \"tiff\", \"tif\", \"webp\"}\n\n    def _plat_get_dimensions(self):\n        raise NotImplementedError()\n\n    def _plat_get_blocks(self, block_count_per_side, orientation):\n        raise NotImplementedError()\n\n    def get_orientation(self):\n        if not hasattr(self, \"_cached_orientation\"):\n            try:\n                with self.path.open(\"rb\") as fp:\n                    exifdata = exif.get_fields(fp)\n                    # the value is a list (probably one-sized) of ints\n                    orientations = exifdata[\"Orientation\"]\n                    self._cached_orientation = orientations[0]\n            except Exception:  # Couldn't read EXIF data, no transforms\n                self._cached_orientation = 0\n        return self._cached_orientation\n\n    def _get_exif_timestamp(self):\n        try:\n            with self.path.open(\"rb\") as fp:\n                exifdata = exif.get_fields(fp)\n                return exifdata[\"DateTimeOriginal\"]\n        except Exception:\n            logging.info(\"Couldn't read EXIF of picture: %s\", self.path)\n        return \"\"\n\n    @classmethod\n    def can_handle(cls, path):\n        return fs.File.can_handle(path) and get_file_ext(path.name) in cls.HANDLED_EXTS\n\n    def get_display_info(self, group, delta):\n        size = self.size\n        mtime = self.mtime\n        dimensions = self.dimensions\n        m = group.get_match_of(self)\n        if m:\n            percentage = m.percentage\n            dupe_count = 0\n            if delta:\n                r = group.ref\n                size -= r.size\n                mtime -= r.mtime\n                dimensions = get_delta_dimensions(dimensions, r.dimensions)\n        else:\n            percentage = group.percentage\n            dupe_count = len(group.dupes)\n        dupe_folder_path = getattr(self, \"display_folder_path\", self.folder_path)\n        return {\n            \"name\": self.name,\n            \"folder_path\": str(dupe_folder_path),\n            \"size\": format_size(size, 0, 1, False),\n            \"extension\": self.extension,\n            \"dimensions\": format_dimensions(dimensions),\n            \"exif_timestamp\": self.exif_timestamp,\n            \"mtime\": format_timestamp(mtime, delta and m),\n            \"percentage\": format_perc(percentage),\n            \"dupe_count\": format_dupe_count(dupe_count),\n        }\n\n    def _read_info(self, field):\n        fs.File._read_info(self, field)\n        if field == \"dimensions\":\n            self.dimensions = self._plat_get_dimensions()\n            if self.get_orientation() in {5, 6, 7, 8}:\n                self.dimensions = (self.dimensions[1], self.dimensions[0])\n        elif field == \"exif_timestamp\":\n            self.exif_timestamp = self._get_exif_timestamp()\n\n    def get_blocks(self, block_count_per_side, orientation: int = None):\n        if orientation is None:\n            return self._plat_get_blocks(block_count_per_side, self.get_orientation())\n        else:\n            return self._plat_get_blocks(block_count_per_side, orientation)\n"
  },
  {
    "path": "core/pe/prioritize.py",
    "content": "# Created On: 2011/09/16\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.trans import trget\n\nfrom core.prioritize import (\n    KindCategory,\n    FolderCategory,\n    FilenameCategory,\n    NumericalCategory,\n    SizeCategory,\n    MtimeCategory,\n)\n\ncoltr = trget(\"columns\")\n\n\nclass DimensionsCategory(NumericalCategory):\n    NAME = coltr(\"Dimensions\")\n\n    def extract_value(self, dupe):\n        return dupe.dimensions\n\n    def invert_numerical_value(self, value):\n        width, height = value\n        return (-width, -height)\n\n\ndef all_categories():\n    return [\n        KindCategory,\n        FolderCategory,\n        FilenameCategory,\n        SizeCategory,\n        DimensionsCategory,\n        MtimeCategory,\n    ]\n"
  },
  {
    "path": "core/pe/result_table.py",
    "content": "# Created On: 2011-11-27\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.column import Column\nfrom hscommon.trans import trget\n\nfrom core.gui.result_table import ResultTable as ResultTableBase\n\ncoltr = trget(\"columns\")\n\n\nclass ResultTable(ResultTableBase):\n    COLUMNS = [\n        Column(\"marked\", \"\"),\n        Column(\"name\", coltr(\"Filename\")),\n        Column(\"folder_path\", coltr(\"Folder\"), optional=True),\n        Column(\"size\", coltr(\"Size (KB)\"), optional=True),\n        Column(\"extension\", coltr(\"Kind\"), visible=False, optional=True),\n        Column(\"dimensions\", coltr(\"Dimensions\"), optional=True),\n        Column(\"exif_timestamp\", coltr(\"EXIF Timestamp\"), visible=False, optional=True),\n        Column(\"mtime\", coltr(\"Modification\"), visible=False, optional=True),\n        Column(\"percentage\", coltr(\"Match %\"), optional=True),\n        Column(\"dupe_count\", coltr(\"Dupe Count\"), visible=False, optional=True),\n    ]\n    DELTA_COLUMNS = {\"size\", \"dimensions\", \"mtime\"}\n"
  },
  {
    "path": "core/pe/scanner.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.trans import tr\n\nfrom core.scanner import Scanner, ScanType, ScanOption\n\nfrom core.pe import matchblock, matchexif\n\n\nclass ScannerPE(Scanner):\n    cache_path = None\n    match_scaled = False\n    match_rotated = False\n\n    @staticmethod\n    def get_scan_options():\n        return [\n            ScanOption(ScanType.FUZZYBLOCK, tr(\"Contents\")),\n            ScanOption(ScanType.EXIFTIMESTAMP, tr(\"EXIF Timestamp\")),\n        ]\n\n    def _getmatches(self, files, j):\n        if self.scan_type == ScanType.FUZZYBLOCK:\n            return matchblock.getmatches(\n                files,\n                cache_path=self.cache_path,\n                threshold=self.min_match_percentage,\n                match_scaled=self.match_scaled,\n                match_rotated=self.match_rotated,\n                j=j,\n            )\n        elif self.scan_type == ScanType.EXIFTIMESTAMP:\n            return matchexif.getmatches(files, self.match_scaled, j)\n        else:\n            raise ValueError(\"Invalid scan type\")\n"
  },
  {
    "path": "core/prioritize.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011/09/07\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.util import dedupe, flatten, rem_file_ext\nfrom hscommon.trans import trget, tr\n\ncoltr = trget(\"columns\")\n\n\nclass CriterionCategory:\n    NAME = \"Undefined\"\n\n    def __init__(self, results):\n        self.results = results\n\n    # --- Virtual\n    def extract_value(self, dupe):\n        raise NotImplementedError()\n\n    def format_criterion_value(self, value):\n        return value\n\n    def sort_key(self, dupe, crit_value):\n        raise NotImplementedError()\n\n    def criteria_list(self):\n        raise NotImplementedError()\n\n\nclass Criterion:\n    def __init__(self, category, value):\n        self.category = category\n        self.value = value\n        self.display_value = category.format_criterion_value(value)\n\n    def sort_key(self, dupe):\n        return self.category.sort_key(dupe, self.value)\n\n    @property\n    def display(self):\n        return f\"{self.category.NAME} ({self.display_value})\"\n\n\nclass ValueListCategory(CriterionCategory):\n    def sort_key(self, dupe, crit_value):\n        # Use this sort key when the order in the list depends on whether or not the dupe meets the\n        # criteria. If it does, we return 0 (top of the list), if it doesn't, we return 1.\n        if self.extract_value(dupe) == crit_value:\n            return 0\n        else:\n            return 1\n\n    def criteria_list(self):\n        dupes = flatten(g[:] for g in self.results.groups)\n        values = sorted(dedupe(self.extract_value(d) for d in dupes))\n        return [Criterion(self, value) for value in values]\n\n\nclass KindCategory(ValueListCategory):\n    NAME = coltr(\"Kind\")\n\n    def extract_value(self, dupe):\n        value = dupe.extension\n        if not value:\n            value = tr(\"None\")\n        return value\n\n\nclass FolderCategory(ValueListCategory):\n    NAME = coltr(\"Folder\")\n\n    def extract_value(self, dupe):\n        return dupe.folder_path\n\n    def format_criterion_value(self, value):\n        return str(value)\n\n    def sort_key(self, dupe, crit_value):\n        value = self.extract_value(dupe)\n        # This is instead of using is_relative_to() which was added in py 3.9\n        try:\n            value.relative_to(crit_value)\n        except ValueError:\n            return 1\n        return 0\n\n\nclass FilenameCategory(CriterionCategory):\n    NAME = coltr(\"Filename\")\n    ENDS_WITH_NUMBER = 0\n    DOESNT_END_WITH_NUMBER = 1\n    LONGEST = 2\n    SHORTEST = 3\n    LONGEST_PATH = 4\n    SHORTEST_PATH = 5\n\n    def format_criterion_value(self, value):\n        return {\n            self.ENDS_WITH_NUMBER: tr(\"Ends with number\"),\n            self.DOESNT_END_WITH_NUMBER: tr(\"Doesn't end with number\"),\n            self.LONGEST: tr(\"Longest\"),\n            self.SHORTEST: tr(\"Shortest\"),\n            self.LONGEST_PATH: tr(\"Longest Path\"),\n            self.SHORTEST_PATH: tr(\"Shortest Path\"),\n        }[value]\n\n    def extract_value(self, dupe):\n        return rem_file_ext(dupe.name)\n\n    def sort_key(self, dupe, crit_value):\n        value = self.extract_value(dupe)\n        if crit_value in {self.ENDS_WITH_NUMBER, self.DOESNT_END_WITH_NUMBER}:\n            ends_with_digit = value.strip()[-1:].isdigit()\n            if crit_value == self.ENDS_WITH_NUMBER:\n                return 0 if ends_with_digit else 1\n            else:\n                return 1 if ends_with_digit else 0\n        elif crit_value == self.LONGEST_PATH:\n            return len(str(dupe.folder_path)) * -1\n        elif crit_value == self.SHORTEST_PATH:\n            return len(str(dupe.folder_path))\n        else:\n            value = len(value)\n            if crit_value == self.LONGEST:\n                value *= -1  # We want the biggest values on top\n            return value\n\n    def criteria_list(self):\n        return [\n            Criterion(self, crit_value)\n            for crit_value in [\n                self.ENDS_WITH_NUMBER,\n                self.DOESNT_END_WITH_NUMBER,\n                self.LONGEST,\n                self.SHORTEST,\n                self.LONGEST_PATH,\n                self.SHORTEST_PATH,\n            ]\n        ]\n\n\nclass NumericalCategory(CriterionCategory):\n    HIGHEST = 0\n    LOWEST = 1\n\n    def format_criterion_value(self, value):\n        return tr(\"Highest\") if value == self.HIGHEST else tr(\"Lowest\")\n\n    def invert_numerical_value(self, value):  # Virtual\n        return value * -1\n\n    def sort_key(self, dupe, crit_value):\n        value = self.extract_value(dupe)\n        if crit_value == self.HIGHEST:  # we want highest values on top\n            value = self.invert_numerical_value(value)\n        return value\n\n    def criteria_list(self):\n        return [Criterion(self, self.HIGHEST), Criterion(self, self.LOWEST)]\n\n\nclass SizeCategory(NumericalCategory):\n    NAME = coltr(\"Size\")\n\n    def extract_value(self, dupe):\n        return dupe.size\n\n\nclass MtimeCategory(NumericalCategory):\n    NAME = coltr(\"Modification\")\n\n    def extract_value(self, dupe):\n        return dupe.mtime\n\n    def format_criterion_value(self, value):\n        return tr(\"Newest\") if value == self.HIGHEST else tr(\"Oldest\")\n\n\ndef all_categories():\n    return [KindCategory, FolderCategory, FilenameCategory, SizeCategory, MtimeCategory]\n"
  },
  {
    "path": "core/results.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/02/23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\nimport re\nimport os\nimport os.path as op\nfrom errno import EISDIR, EACCES\nfrom xml.etree import ElementTree as ET\n\nfrom hscommon.jobprogress.job import nulljob\nfrom hscommon.conflict import get_conflicted_name\nfrom hscommon.util import flatten, nonone, FileOrPath, format_size\nfrom hscommon.trans import tr\n\nfrom core import engine\nfrom core.markable import Markable\n\n\nclass Results(Markable):\n    \"\"\"Manages a collection of duplicate :class:`~core.engine.Group`.\n\n    This class takes care or marking, sorting and filtering duplicate groups.\n\n    .. attribute:: groups\n\n        The list of :class:`~core.engine.Group` contained managed by this instance.\n\n    .. attribute:: dupes\n\n        A list of all duplicates (:class:`~core.fs.File` instances), without ref, contained in the\n        currently managed :attr:`groups`.\n    \"\"\"\n\n    # ---Override\n    def __init__(self, app):\n        Markable.__init__(self)\n        self.__groups = []\n        self.__group_of_duplicate = {}\n        self.__groups_sort_descriptor = None  # This is a tuple (key, asc)\n        self.__dupes = None\n        self.__dupes_sort_descriptor = None  # This is a tuple (key, asc, delta)\n        self.__filters = None\n        self.__filtered_dupes = None\n        self.__filtered_groups = None\n        self.__recalculate_stats()\n        self.__marked_size = 0\n        self.app = app\n        self.problems = []  # (dupe, error_msg)\n        self.is_modified = False\n        self.refresh_required = False\n\n    def _did_mark(self, dupe):\n        self.__marked_size += dupe.size\n\n    def _did_unmark(self, dupe):\n        self.__marked_size -= dupe.size\n\n    def _get_markable_count(self):\n        return self.__total_count\n\n    def _is_markable(self, dupe):\n        if dupe.is_ref:\n            return False\n        g = self.get_group_of_duplicate(dupe)\n        if not g:\n            return False\n        if dupe is g.ref:\n            return False\n        if self.__filtered_dupes and dupe not in self.__filtered_dupes:\n            return False\n        return True\n\n    def mark_all(self):\n        if self.__filters:\n            self.mark_multiple(self.__filtered_dupes)\n        else:\n            Markable.mark_all(self)\n\n    def mark_invert(self):\n        if self.__filters:\n            self.mark_toggle_multiple(self.__filtered_dupes)\n        else:\n            Markable.mark_invert(self)\n\n    def mark_none(self):\n        if self.__filters:\n            self.unmark_multiple(self.__filtered_dupes)\n        else:\n            Markable.mark_none(self)\n\n    # ---Private\n    def __get_dupe_list(self):\n        if self.__dupes is None or self.refresh_required:\n            self.__dupes = flatten(group.dupes for group in self.groups)\n            self.refresh_required = False\n            if None in self.__dupes:\n                # This is debug logging to try to figure out #44\n                logging.warning(\n                    \"There is a None value in the Results' dupe list. dupes: %r groups: %r\",\n                    self.__dupes,\n                    self.groups,\n                )\n            if self.__filtered_dupes:\n                self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]\n            sd = self.__dupes_sort_descriptor\n            if sd:\n                self.sort_dupes(sd[0], sd[1], sd[2])\n        return self.__dupes\n\n    def __get_groups(self):\n        if self.__filtered_groups is None:\n            return self.__groups\n        else:\n            return self.__filtered_groups\n\n    def __get_stat_line(self):\n        if self.__filtered_dupes is None:\n            mark_count = self.mark_count\n            marked_size = self.__marked_size\n            total_count = self.__total_count\n            total_size = self.__total_size\n        else:\n            mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)])\n            marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe))\n            total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)])\n            total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe))\n        if self.mark_inverted:\n            marked_size = self.__total_size - marked_size\n        result = tr(\"%d / %d (%s / %s) duplicates marked.\") % (\n            mark_count,\n            total_count,\n            format_size(marked_size, 2),\n            format_size(total_size, 2),\n        )\n        if self.__filters:\n            result += tr(\" filter: %s\") % \" --> \".join(self.__filters)\n        return result\n\n    def __recalculate_stats(self):\n        self.__total_size = 0\n        self.__total_count = 0\n        for group in self.groups:\n            markable = [dupe for dupe in group.dupes if self._is_markable(dupe)]\n            self.__total_count += len(markable)\n            self.__total_size += sum(dupe.size for dupe in markable)\n\n    def __set_groups(self, new_groups):\n        self.mark_none()\n        self.__groups = new_groups\n        self.__group_of_duplicate = {}\n        for g in self.__groups:\n            for dupe in g:\n                self.__group_of_duplicate[dupe] = g\n                if not hasattr(dupe, \"is_ref\"):\n                    dupe.is_ref = False\n        self.is_modified = bool(self.__groups)\n        old_filters = nonone(self.__filters, [])\n        self.apply_filter(None)\n        for filter_str in old_filters:\n            self.apply_filter(filter_str)\n\n    # ---Public\n    def apply_filter(self, filter_str):\n        \"\"\"Applies a filter ``filter_str`` to :attr:`groups`\n\n        When you apply the filter, only  dupes with the filename matching ``filter_str`` will be in\n        in the results. To cancel the filter, just call apply_filter with ``filter_str`` to None,\n        and the results will go back to normal.\n\n        If call apply_filter on a filtered results, the filter will be applied\n        *on the filtered results*.\n\n        :param str filter_str: a string containing a regexp to filter dupes with.\n        \"\"\"\n        if not filter_str:\n            self.__filtered_dupes = None\n            self.__filtered_groups = None\n            self.__filters = None\n        else:\n            if not self.__filters:\n                self.__filters = []\n            try:\n                filter_re = re.compile(filter_str, re.IGNORECASE)\n            except re.error:\n                return  # don't apply this filter.\n            self.__filters.append(filter_str)\n            if self.__filtered_dupes is None:\n                self.__filtered_dupes = flatten(g[:] for g in self.groups)\n            self.__filtered_dupes = {dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path))}\n            filtered_groups = set()\n            for dupe in self.__filtered_dupes:\n                filtered_groups.add(self.get_group_of_duplicate(dupe))\n            self.__filtered_groups = list(filtered_groups)\n        self.__recalculate_stats()\n        sd = self.__groups_sort_descriptor\n        if sd:\n            self.sort_groups(sd[0], sd[1])\n        self.__dupes = None\n\n    def get_group_of_duplicate(self, dupe):\n        \"\"\"Returns :class:`~core.engine.Group` in which ``dupe`` belongs.\"\"\"\n        try:\n            return self.__group_of_duplicate[dupe]\n        except (TypeError, KeyError):\n            return None\n\n    is_markable = _is_markable\n\n    def load_from_xml(self, infile, get_file, j=nulljob):\n        \"\"\"Load results from ``infile``.\n\n        :param infile: a file or path pointing to an XML file created with :meth:`save_to_xml`.\n        :param get_file: a function f(path) returning a :class:`~core.fs.File` wrapping the path.\n        :param j: A :ref:`job progress instance <jobs>`.\n        \"\"\"\n\n        def do_match(ref_file, other_files, group):\n            if not other_files:\n                return\n            for other_file in other_files:\n                group.add_match(engine.get_match(ref_file, other_file))\n            do_match(other_files[0], other_files[1:], group)\n\n        self.apply_filter(None)\n        root = ET.parse(infile).getroot()\n        group_elems = list(root.iter(\"group\"))\n        groups = []\n        marked = set()\n        for group_elem in j.iter_with_progress(group_elems, every=100):\n            group = engine.Group()\n            dupes = []\n            for file_elem in group_elem.iter(\"file\"):\n                path = file_elem.get(\"path\")\n                words = file_elem.get(\"words\", \"\")\n                if not path:\n                    continue\n                file = get_file(path)\n                if file is None:\n                    continue\n                file.words = words.split(\",\")\n                file.is_ref = file_elem.get(\"is_ref\") == \"y\"\n                dupes.append(file)\n                if file_elem.get(\"marked\") == \"y\":\n                    marked.add(file)\n            for match_elem in group_elem.iter(\"match\"):\n                try:\n                    attrs = match_elem.attrib\n                    first_file = dupes[int(attrs[\"first\"])]\n                    second_file = dupes[int(attrs[\"second\"])]\n                    percentage = int(attrs[\"percentage\"])\n                    group.add_match(engine.Match(first_file, second_file, percentage))\n                except (IndexError, KeyError, ValueError):\n                    # Covers missing attr, non-int values and indexes out of bounds\n                    pass\n            if (not group.matches) and (len(dupes) >= 2):\n                do_match(dupes[0], dupes[1:], group)\n            group.prioritize(lambda x: dupes.index(x))\n            if len(group):\n                groups.append(group)\n            j.add_progress()\n        self.groups = groups\n        for dupe_file in marked:\n            self.mark(dupe_file)\n        self.is_modified = False\n\n    def make_ref(self, dupe):\n        \"\"\"Make ``dupe`` take the :attr:`~core.engine.Group.ref` position of its group.\"\"\"\n        g = self.get_group_of_duplicate(dupe)\n        r = g.ref\n        if not g.switch_ref(dupe):\n            return False\n        self._remove_mark_flag(dupe)\n        if not r.is_ref:\n            self.__total_count += 1\n            self.__total_size += r.size\n        if not dupe.is_ref:\n            self.__total_count -= 1\n            self.__total_size -= dupe.size\n        self.__dupes = None\n        self.is_modified = True\n        return True\n\n    def perform_on_marked(self, func, remove_from_results):\n        \"\"\"Performs ``func`` on all marked dupes.\n\n        If an ``EnvironmentError`` is raised during the call, the problematic dupe is added to\n        self.problems.\n\n        :param bool remove_from_results: If true, dupes which had ``func`` applied and didn't cause\n                                         any problem.\n        \"\"\"\n        self.problems = []\n        to_remove = []\n        marked = (dupe for dupe in self.dupes if self.is_marked(dupe))\n        for dupe in marked:\n            try:\n                func(dupe)\n                to_remove.append(dupe)\n            except (OSError, UnicodeEncodeError) as e:\n                self.problems.append((dupe, str(e)))\n        if remove_from_results:\n            self.remove_duplicates(to_remove)\n            self.mark_none()\n            for dupe, _ in self.problems:\n                self.mark(dupe)\n\n    def remove_duplicates(self, dupes):\n        \"\"\"Remove ``dupes`` from their respective :class:`~core.engine.Group`.\n\n        Also, remove the group from :attr:`groups` if it ends up empty.\n        \"\"\"\n        affected_groups = set()\n        for dupe in dupes:\n            group = self.get_group_of_duplicate(dupe)\n            if dupe not in group.dupes:\n                return\n            ref = group.ref\n            group.remove_dupe(dupe, False)\n            del self.__group_of_duplicate[dupe]\n            self._remove_mark_flag(dupe)\n            self.__total_count -= 1\n            self.__total_size = max(0, self.__total_size - dupe.size)\n            if not group:\n                del self.__group_of_duplicate[ref]\n                self.__groups.remove(group)\n                if self.__filtered_groups:\n                    self.__filtered_groups.remove(group)\n            else:\n                affected_groups.add(group)\n        for group in affected_groups:\n            group.discard_matches()\n        self.__dupes = None\n        self.is_modified = bool(self.__groups)\n\n    def save_to_xml(self, outfile):\n        \"\"\"Save results to ``outfile`` in XML.\n\n        :param outfile: file object or path.\n        \"\"\"\n        self.apply_filter(None)\n        root = ET.Element(\"results\")\n        for g in self.groups:\n            group_elem = ET.SubElement(root, \"group\")\n            dupe2index = {}\n            for index, d in enumerate(g):\n                dupe2index[d] = index\n                try:\n                    words = engine.unpack_fields(d.words)\n                except AttributeError:\n                    words = ()\n                file_elem = ET.SubElement(group_elem, \"file\")\n                try:\n                    file_elem.set(\"path\", str(d.path))\n                    file_elem.set(\"words\", \",\".join(words))\n                except ValueError:  # If there's an invalid character, just skip the file\n                    file_elem.set(\"path\", \"\")\n                file_elem.set(\"is_ref\", (\"y\" if d.is_ref else \"n\"))\n                file_elem.set(\"marked\", (\"y\" if self.is_marked(d) else \"n\"))\n            for match in g.matches:\n                match_elem = ET.SubElement(group_elem, \"match\")\n                match_elem.set(\"first\", str(dupe2index[match.first]))\n                match_elem.set(\"second\", str(dupe2index[match.second]))\n                match_elem.set(\"percentage\", str(int(match.percentage)))\n        tree = ET.ElementTree(root)\n\n        def do_write(outfile):\n            with FileOrPath(outfile, \"wb\") as fp:\n                tree.write(fp, encoding=\"utf-8\")\n\n        try:\n            do_write(outfile)\n        except OSError as e:\n            # If our OSError is because dest is already a directory, we want to handle that. 21 is\n            # the code we get on OS X and Linux (EISDIR), 13 is what we get on Windows (EACCES).\n            if e.errno in (EISDIR, EACCES):\n                p = str(outfile)\n                dirname, basename = op.split(p)\n                otherfiles = os.listdir(dirname)\n                newname = get_conflicted_name(otherfiles, basename)\n                do_write(op.join(dirname, newname))\n            else:\n                raise\n        self.is_modified = False\n\n    def sort_dupes(self, key, asc=True, delta=False):\n        \"\"\"Sort :attr:`dupes` according to ``key``.\n\n        :param str key: key attribute name to sort with.\n        :param bool asc: If false, sorting is reversed.\n        :param bool delta: If true, sorting occurs using :ref:`delta values <deltavalues>`.\n        \"\"\"\n        if not self.__dupes:\n            self.__get_dupe_list()\n        self.__dupes.sort(\n            key=lambda d: self.app._get_dupe_sort_key(d, lambda: self.get_group_of_duplicate(d), key, delta),\n            reverse=not asc,\n        )\n        self.__dupes_sort_descriptor = (key, asc, delta)\n\n    def sort_groups(self, key, asc=True):\n        \"\"\"Sort :attr:`groups` according to ``key``.\n\n        The :attr:`~core.engine.Group.ref` of each group is used to extract values for sorting.\n\n        :param str key: key attribute name to sort with.\n        :param bool asc: If false, sorting is reversed.\n        \"\"\"\n        self.groups.sort(key=lambda g: self.app._get_group_sort_key(g, key), reverse=not asc)\n        self.__groups_sort_descriptor = (key, asc)\n\n    # ---Properties\n    dupes = property(__get_dupe_list)\n    groups = property(__get_groups, __set_groups)\n    stat_line = property(__get_stat_line)\n"
  },
  {
    "path": "core/scanner.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\nimport re\nimport os.path as op\nfrom collections import namedtuple\n\nfrom hscommon.jobprogress import job\nfrom hscommon.util import dedupe, rem_file_ext, get_file_ext\nfrom hscommon.trans import tr\n\nfrom core import engine\n\n# It's quite ugly to have scan types from all editions all put in the same class, but because there's\n# there will be some nasty bugs popping up (ScanType is used in core when in should exclusively be\n# used in core_*). One day I'll clean this up.\n\n\nclass ScanType:\n    FILENAME = 0\n    FIELDS = 1\n    FIELDSNOORDER = 2\n    TAG = 3\n    FOLDERS = 4\n    CONTENTS = 5\n\n    # PE\n    FUZZYBLOCK = 10\n    EXIFTIMESTAMP = 11\n\n\nScanOption = namedtuple(\"ScanOption\", \"scan_type label\")\n\nSCANNABLE_TAGS = [\"track\", \"artist\", \"album\", \"title\", \"genre\", \"year\"]\n\nRE_DIGIT_ENDING = re.compile(r\"\\d+|\\(\\d+\\)|\\[\\d+\\]|{\\d+}\")\n\n\ndef is_same_with_digit(name, refname):\n    # Returns True if name is the same as refname, but with digits (with brackets or not) at the end\n    if not name.startswith(refname):\n        return False\n    end = name[len(refname) :].strip()\n    return RE_DIGIT_ENDING.match(end) is not None\n\n\ndef remove_dupe_paths(files):\n    # Returns files with duplicates-by-path removed. Files with the exact same path are considered\n    # duplicates and only the first file to have a path is kept. In certain cases, we have files\n    # that have the same path, but not with the same case, that's why we normalize. However, we also\n    # have case-sensitive filesystems, and in those, we don't want to falsely remove duplicates,\n    # that's why we have a `samefile` mechanism.\n    result = []\n    path2file = {}\n    for f in files:\n        normalized = str(f.path).lower()\n        if normalized in path2file:\n            try:\n                if op.samefile(normalized, str(path2file[normalized].path)):\n                    continue  # same file, it's a dupe\n                else:\n                    pass  # We don't treat them as dupes\n            except OSError:\n                continue  # File doesn't exist? Well, treat them as dupes\n        else:\n            path2file[normalized] = f\n        result.append(f)\n    return result\n\n\nclass Scanner:\n    def __init__(self):\n        self.discarded_file_count = 0\n\n    def _getmatches(self, files, j):\n        if (\n            self.size_threshold\n            or self.large_size_threshold\n            or self.scan_type\n            in {\n                ScanType.CONTENTS,\n                ScanType.FOLDERS,\n            }\n        ):\n            j = j.start_subjob([2, 8])\n            if self.size_threshold:\n                files = [f for f in files if f.size >= self.size_threshold]\n            if self.large_size_threshold:\n                files = [f for f in files if f.size <= self.large_size_threshold]\n        if self.scan_type in {ScanType.CONTENTS, ScanType.FOLDERS}:\n            return engine.getmatches_by_contents(files, bigsize=self.big_file_size_threshold, j=j)\n        else:\n            j = j.start_subjob([2, 8])\n            kw = {}\n            kw[\"match_similar_words\"] = self.match_similar_words\n            kw[\"weight_words\"] = self.word_weighting\n            kw[\"min_match_percentage\"] = self.min_match_percentage\n            if self.scan_type == ScanType.FIELDSNOORDER:\n                self.scan_type = ScanType.FIELDS\n                kw[\"no_field_order\"] = True\n            func = {\n                ScanType.FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),\n                ScanType.FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),\n                ScanType.TAG: lambda f: [\n                    engine.getwords(str(getattr(f, attrname)))\n                    for attrname in SCANNABLE_TAGS\n                    if attrname in self.scanned_tags\n                ],\n            }[self.scan_type]\n            for f in j.iter_with_progress(files, tr(\"Read metadata of %d/%d files\")):\n                logging.debug(\"Reading metadata of %s\", f.path)\n                f.words = func(f)\n            return engine.getmatches(files, j=j, **kw)\n\n    @staticmethod\n    def _key_func(dupe):\n        return -dupe.size\n\n    @staticmethod\n    def _tie_breaker(ref, dupe):\n        refname = rem_file_ext(ref.name).lower()\n        dupename = rem_file_ext(dupe.name).lower()\n        if \"copy\" in dupename:\n            return False\n        if \"copy\" in refname:\n            return True\n        if is_same_with_digit(dupename, refname):\n            return False\n        if is_same_with_digit(refname, dupename):\n            return True\n        return len(dupe.path.parts) > len(ref.path.parts)\n\n    @staticmethod\n    def get_scan_options():\n        \"\"\"Returns a list of scanning options for this scanner.\n\n        Returns a list of ``ScanOption``.\n        \"\"\"\n        raise NotImplementedError()\n\n    def get_dupe_groups(self, files, ignore_list=None, j=job.nulljob):\n        for f in (f for f in files if not hasattr(f, \"is_ref\")):\n            f.is_ref = False\n        files = remove_dupe_paths(files)\n        logging.info(\"Getting matches. Scan type: %d\", self.scan_type)\n        matches = self._getmatches(files, j)\n        logging.info(\"Found %d matches\" % len(matches))\n        j.set_progress(100, tr(\"Almost done! Fiddling with results...\"))\n        # In removing what we call here \"false matches\", we first want to remove, if we scan by\n        # folders, we want to remove folder matches for which the parent is also in a match (they're\n        # \"duplicated duplicates if you will). Then, we also don't want mixed file kinds if the\n        # option isn't enabled, we want matches for which both files exist and, lastly, we don't\n        # want matches with both files as ref.\n        if self.scan_type == ScanType.FOLDERS and matches:\n            allpath = {m.first.path for m in matches}\n            allpath |= {m.second.path for m in matches}\n            sortedpaths = sorted(allpath)\n            toremove = set()\n            last_parent_path = sortedpaths[0]\n            for p in sortedpaths[1:]:\n                if last_parent_path in p.parents:\n                    toremove.add(p)\n                else:\n                    last_parent_path = p\n            matches = [m for m in matches if m.first.path not in toremove or m.second.path not in toremove]\n        if not self.mix_file_kind:\n            matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]\n        if self.include_exists_check:\n            matches = [m for m in matches if m.first.exists() and m.second.exists()]\n        # Contents already handles ref checks, other scan types might not catch during scan\n        if self.scan_type != ScanType.CONTENTS:\n            matches = [m for m in matches if not (m.first.is_ref and m.second.is_ref)]\n        if ignore_list:\n            matches = [m for m in matches if not ignore_list.are_ignored(str(m.first.path), str(m.second.path))]\n        logging.info(\"Grouping matches\")\n        groups = engine.get_groups(matches)\n        if self.scan_type in {\n            ScanType.FILENAME,\n            ScanType.FIELDS,\n            ScanType.FIELDSNOORDER,\n            ScanType.TAG,\n        }:\n            matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])\n            self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)\n        else:\n            # Ticket #195\n            # To speed up the scan, we don't bother comparing contents of files that are both ref\n            # files. However, this messes up \"discarded\" counting because there's a missing match\n            # in cases where we end up with a dupe group anyway (with a non-ref file). Because it's\n            # impossible to have discarded matches in exact dupe scans, we simply set it at 0, thus\n            # bypassing our tricky problem.\n            # Also, although ScanType.FuzzyBlock is not always doing exact comparisons, we also\n            # bypass ref comparison, thus messing up with our \"discarded\" count. So we're\n            # effectively disabling the \"discarded\" feature in PE, but it's better than falsely\n            # reporting discarded matches.\n            self.discarded_file_count = 0\n        groups = [g for g in groups if any(not f.is_ref for f in g)]\n        logging.info(\"Created %d groups\" % len(groups))\n        for g in groups:\n            g.prioritize(self._key_func, self._tie_breaker)\n        return groups\n\n    match_similar_words = False\n    min_match_percentage = 80\n    mix_file_kind = True\n    scan_type = ScanType.FILENAME\n    scanned_tags = {\"artist\", \"title\"}\n    size_threshold = 0\n    large_size_threshold = 0\n    big_file_size_threshold = 0\n    word_weighting = False\n    include_exists_check = True\n"
  },
  {
    "path": "core/se/__init__.py",
    "content": "from core.se import fs, result_table, scanner  # noqa\n"
  },
  {
    "path": "core/se/fs.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2013-07-14\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.util import format_size\n\nfrom core import fs\nfrom core.util import format_timestamp, format_perc, format_words, format_dupe_count\n\n\ndef get_display_info(dupe, group, delta):\n    size = dupe.size\n    mtime = dupe.mtime\n    m = group.get_match_of(dupe)\n    if m:\n        percentage = m.percentage\n        dupe_count = 0\n        if delta:\n            r = group.ref\n            size -= r.size\n            mtime -= r.mtime\n    else:\n        percentage = group.percentage\n        dupe_count = len(group.dupes)\n    return {\n        \"name\": dupe.name,\n        \"folder_path\": str(dupe.folder_path),\n        \"size\": format_size(size, 0, 1, False),\n        \"extension\": dupe.extension,\n        \"mtime\": format_timestamp(mtime, delta and m),\n        \"percentage\": format_perc(percentage),\n        \"words\": format_words(dupe.words) if hasattr(dupe, \"words\") else \"\",\n        \"dupe_count\": format_dupe_count(dupe_count),\n    }\n\n\nclass File(fs.File):\n    def get_display_info(self, group, delta):\n        return get_display_info(self, group, delta)\n\n\nclass Folder(fs.Folder):\n    def get_display_info(self, group, delta):\n        return get_display_info(self, group, delta)\n"
  },
  {
    "path": "core/se/result_table.py",
    "content": "# Created On: 2011-11-27\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.column import Column\nfrom hscommon.trans import trget\n\nfrom core.gui.result_table import ResultTable as ResultTableBase\n\ncoltr = trget(\"columns\")\n\n\nclass ResultTable(ResultTableBase):\n    COLUMNS = [\n        Column(\"marked\", \"\"),\n        Column(\"name\", coltr(\"Filename\")),\n        Column(\"folder_path\", coltr(\"Folder\"), optional=True),\n        Column(\"size\", coltr(\"Size (KB)\"), optional=True),\n        Column(\"extension\", coltr(\"Kind\"), visible=False, optional=True),\n        Column(\"mtime\", coltr(\"Modification\"), visible=False, optional=True),\n        Column(\"percentage\", coltr(\"Match %\"), optional=True),\n        Column(\"words\", coltr(\"Words Used\"), visible=False, optional=True),\n        Column(\"dupe_count\", coltr(\"Dupe Count\"), visible=False, optional=True),\n    ]\n    DELTA_COLUMNS = {\"size\", \"mtime\"}\n"
  },
  {
    "path": "core/se/scanner.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.trans import tr\n\nfrom core.scanner import Scanner as ScannerBase, ScanOption, ScanType\n\n\nclass ScannerSE(ScannerBase):\n    @staticmethod\n    def get_scan_options():\n        return [\n            ScanOption(ScanType.FILENAME, tr(\"Filename\")),\n            ScanOption(ScanType.CONTENTS, tr(\"Contents\")),\n            ScanOption(ScanType.FOLDERS, tr(\"Folders\")),\n        ]\n"
  },
  {
    "path": "core/tests/__init__.py",
    "content": ""
  },
  {
    "path": "core/tests/app_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os\nimport os.path as op\nimport logging\nimport tempfile\n\nimport pytest\nfrom pathlib import Path\nimport hscommon.conflict\nimport hscommon.util\nfrom hscommon.testutil import eq_, log_calls\nfrom hscommon.jobprogress.job import Job\n\nfrom core.tests.base import TestApp\nfrom core.tests.results_test import GetTestGroups\nfrom core import app, fs, engine\nfrom core.scanner import ScanType\n\n\ndef add_fake_files_to_directories(directories, files):\n    directories.get_files = lambda j=None: iter(files)\n    directories._dirs.append(\"this is just so Scan() doesn't return 3\")\n\n\nclass TestCaseDupeGuru:\n    def test_apply_filter_calls_results_apply_filter(self, monkeypatch):\n        dgapp = TestApp().app\n        monkeypatch.setattr(dgapp.results, \"apply_filter\", log_calls(dgapp.results.apply_filter))\n        dgapp.apply_filter(\"foo\")\n        eq_(2, len(dgapp.results.apply_filter.calls))\n        call = dgapp.results.apply_filter.calls[0]\n        assert call[\"filter_str\"] is None\n        call = dgapp.results.apply_filter.calls[1]\n        eq_(\"foo\", call[\"filter_str\"])\n\n    def test_apply_filter_escapes_regexp(self, monkeypatch):\n        dgapp = TestApp().app\n        monkeypatch.setattr(dgapp.results, \"apply_filter\", log_calls(dgapp.results.apply_filter))\n        dgapp.apply_filter(\"()[]\\\\.|+?^abc\")\n        call = dgapp.results.apply_filter.calls[1]\n        eq_(\"\\\\(\\\\)\\\\[\\\\]\\\\\\\\\\\\.\\\\|\\\\+\\\\?\\\\^abc\", call[\"filter_str\"])\n        dgapp.apply_filter(\"(*)\")  # In \"simple mode\", we want the * to behave as a wildcard\n        call = dgapp.results.apply_filter.calls[3]\n        eq_(r\"\\(.*\\)\", call[\"filter_str\"])\n        dgapp.options[\"escape_filter_regexp\"] = False\n        dgapp.apply_filter(\"(abc)\")\n        call = dgapp.results.apply_filter.calls[5]\n        eq_(\"(abc)\", call[\"filter_str\"])\n\n    def test_copy_or_move(self, tmpdir, monkeypatch):\n        # The goal here is just to have a test for a previous blowup I had. I know my test coverage\n        # for this unit is pathetic. What's done is done. My approach now is to add tests for\n        # every change I want to make. The blowup was caused by a missing import.\n        p = Path(str(tmpdir))\n        p.joinpath(\"foo\").touch()\n        monkeypatch.setattr(\n            hscommon.conflict,\n            \"smart_copy\",\n            log_calls(lambda source_path, dest_path: None),\n        )\n        # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.\n        monkeypatch.setattr(app, \"smart_copy\", hscommon.conflict.smart_copy)\n        monkeypatch.setattr(os, \"makedirs\", lambda path: None)  # We don't want the test to create that fake directory\n        dgapp = TestApp().app\n        dgapp.directories.add_path(p)\n        [f] = dgapp.directories.get_files()\n        with tempfile.TemporaryDirectory() as tmp_dir:\n            dgapp.copy_or_move(f, True, tmp_dir, 0)\n            eq_(1, len(hscommon.conflict.smart_copy.calls))\n            call = hscommon.conflict.smart_copy.calls[0]\n            eq_(call[\"dest_path\"], Path(tmp_dir, \"foo\"))\n            eq_(call[\"source_path\"], f.path)\n\n    def test_copy_or_move_clean_empty_dirs(self, tmpdir, monkeypatch):\n        tmppath = Path(str(tmpdir))\n        sourcepath = tmppath.joinpath(\"source\")\n        sourcepath.mkdir()\n        sourcepath.joinpath(\"myfile\").touch()\n        app = TestApp().app\n        app.directories.add_path(tmppath)\n        [myfile] = app.directories.get_files()\n        monkeypatch.setattr(app, \"clean_empty_dirs\", log_calls(lambda path: None))\n        app.copy_or_move(myfile, False, tmppath.joinpath(\"dest\"), 0)\n        calls = app.clean_empty_dirs.calls\n        eq_(1, len(calls))\n        eq_(sourcepath, calls[0][\"path\"])\n\n    def test_scan_with_objects_evaluating_to_false(self):\n        class FakeFile(fs.File):\n            def __bool__(self):\n                return False\n\n        # At some point, any() was used in a wrong way that made Scan() wrongly return 1\n        app = TestApp().app\n        f1, f2 = (FakeFile(\"foo\") for _ in range(2))\n        f1.is_ref, f2.is_ref = (False, False)\n        assert not (bool(f1) and bool(f2))\n        add_fake_files_to_directories(app.directories, [f1, f2])\n        app.start_scanning()  # no exception\n\n    @pytest.mark.skipif(\"not hasattr(os, 'link')\")\n    def test_ignore_hardlink_matches(self, tmpdir):\n        # If the ignore_hardlink_matches option is set, don't match files hardlinking to the same\n        # inode.\n        tmppath = Path(str(tmpdir))\n        tmppath.joinpath(\"myfile\").open(\"wt\").write(\"foo\")\n        os.link(str(tmppath.joinpath(\"myfile\")), str(tmppath.joinpath(\"hardlink\")))\n        app = TestApp().app\n        app.directories.add_path(tmppath)\n        app.options[\"scan_type\"] = ScanType.CONTENTS\n        app.options[\"ignore_hardlink_matches\"] = True\n        app.start_scanning()\n        eq_(len(app.results.groups), 0)\n\n    def test_rename_when_nothing_is_selected(self):\n        # Issue #140\n        # It's possible that rename operation has its selected row swept off from under it, thus\n        # making the selected row None. Don't crash when it happens.\n        dgapp = TestApp().app\n        # selected_row is None because there's no result.\n        assert not dgapp.result_table.rename_selected(\"foo\")  # no crash\n\n\nclass TestCaseDupeGuruCleanEmptyDirs:\n    @pytest.fixture\n    def do_setup(self, request):\n        monkeypatch = request.getfixturevalue(\"monkeypatch\")\n        monkeypatch.setattr(\n            hscommon.util,\n            \"delete_if_empty\",\n            log_calls(lambda path, files_to_delete=[]: None),\n        )\n        # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.\n        monkeypatch.setattr(app, \"delete_if_empty\", hscommon.util.delete_if_empty)\n        self.app = TestApp().app\n\n    def test_option_off(self, do_setup):\n        self.app.clean_empty_dirs(Path(\"/foo/bar\"))\n        eq_(0, len(hscommon.util.delete_if_empty.calls))\n\n    def test_option_on(self, do_setup):\n        self.app.options[\"clean_empty_dirs\"] = True\n        self.app.clean_empty_dirs(Path(\"/foo/bar\"))\n        calls = hscommon.util.delete_if_empty.calls\n        eq_(1, len(calls))\n        eq_(Path(\"/foo/bar\"), calls[0][\"path\"])\n        eq_([\".DS_Store\"], calls[0][\"files_to_delete\"])\n\n    def test_recurse_up(self, do_setup, monkeypatch):\n        # delete_if_empty must be recursively called up in the path until it returns False\n        @log_calls\n        def mock_delete_if_empty(path, files_to_delete=[]):\n            return len(path.parts) > 1\n\n        monkeypatch.setattr(hscommon.util, \"delete_if_empty\", mock_delete_if_empty)\n        # XXX This monkeypatch is temporary. will be fixed in a better monkeypatcher.\n        monkeypatch.setattr(app, \"delete_if_empty\", mock_delete_if_empty)\n        self.app.options[\"clean_empty_dirs\"] = True\n        self.app.clean_empty_dirs(Path(\"not-empty/empty/empty\"))\n        calls = hscommon.util.delete_if_empty.calls\n        eq_(3, len(calls))\n        eq_(Path(\"not-empty/empty/empty\"), calls[0][\"path\"])\n        eq_(Path(\"not-empty/empty\"), calls[1][\"path\"])\n        eq_(Path(\"not-empty\"), calls[2][\"path\"])\n\n\nclass TestCaseDupeGuruWithResults:\n    @pytest.fixture\n    def do_setup(self, request):\n        app = TestApp()\n        self.app = app.app\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.app.results.groups = self.groups\n        self.dpanel = app.dpanel\n        self.dtree = app.dtree\n        self.rtable = app.rtable\n        self.rtable.refresh()\n        tmpdir = request.getfixturevalue(\"tmpdir\")\n        tmppath = Path(str(tmpdir))\n        tmppath.joinpath(\"foo\").mkdir()\n        tmppath.joinpath(\"bar\").mkdir()\n        self.app.directories.add_path(tmppath)\n\n    def test_get_objects(self, do_setup):\n        objects = self.objects\n        groups = self.groups\n        r = self.rtable[0]\n        assert r._group is groups[0]\n        assert r._dupe is objects[0]\n        r = self.rtable[1]\n        assert r._group is groups[0]\n        assert r._dupe is objects[1]\n        r = self.rtable[4]\n        assert r._group is groups[1]\n        assert r._dupe is objects[4]\n\n    def test_get_objects_after_sort(self, do_setup):\n        objects = self.objects\n        groups = self.groups[:]  # we need an un-sorted reference\n        self.rtable.sort(\"name\", False)\n        r = self.rtable[1]\n        assert r._group is groups[1]\n        assert r._dupe is objects[4]\n\n    def test_selected_result_node_paths_after_deletion(self, do_setup):\n        # cases where the selected dupes aren't there are correctly handled\n        self.rtable.select([1, 2, 3])\n        self.app.remove_selected()\n        # The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.\n        eq_(self.rtable.selected_indexes, [1])  # no exception\n\n    def test_select_result_node_paths(self, do_setup):\n        app = self.app\n        objects = self.objects\n        self.rtable.select([1, 2])\n        eq_(len(app.selected_dupes), 2)\n        assert app.selected_dupes[0] is objects[1]\n        assert app.selected_dupes[1] is objects[2]\n\n    def test_select_result_node_paths_with_ref(self, do_setup):\n        app = self.app\n        objects = self.objects\n        self.rtable.select([1, 2, 3])\n        eq_(len(app.selected_dupes), 3)\n        assert app.selected_dupes[0] is objects[1]\n        assert app.selected_dupes[1] is objects[2]\n        assert app.selected_dupes[2] is self.groups[1].ref\n\n    def test_select_result_node_paths_after_sort(self, do_setup):\n        app = self.app\n        objects = self.objects\n        groups = self.groups[:]  # To keep the old order in memory\n        self.rtable.sort(\"name\", False)  # 0\n        # Now, the group order is supposed to be reversed\n        self.rtable.select([1, 2, 3])\n        eq_(len(app.selected_dupes), 3)\n        assert app.selected_dupes[0] is objects[4]\n        assert app.selected_dupes[1] is groups[0].ref\n        assert app.selected_dupes[2] is objects[1]\n\n    def test_selected_powermarker_node_paths(self, do_setup):\n        # app.selected_dupes is correctly converted into paths\n        self.rtable.power_marker = True\n        self.rtable.select([0, 1, 2])\n        self.rtable.power_marker = False\n        eq_(self.rtable.selected_indexes, [1, 2, 4])\n\n    def test_selected_powermarker_node_paths_after_deletion(self, do_setup):\n        # cases where the selected dupes aren't there are correctly handled\n        app = self.app\n        self.rtable.power_marker = True\n        self.rtable.select([0, 1, 2])\n        app.remove_selected()\n        eq_(self.rtable.selected_indexes, [])  # no exception\n\n    def test_select_powermarker_rows_after_sort(self, do_setup):\n        app = self.app\n        objects = self.objects\n        self.rtable.power_marker = True\n        self.rtable.sort(\"name\", False)\n        self.rtable.select([0, 1, 2])\n        eq_(len(app.selected_dupes), 3)\n        assert app.selected_dupes[0] is objects[4]\n        assert app.selected_dupes[1] is objects[2]\n        assert app.selected_dupes[2] is objects[1]\n\n    def test_toggle_selected_mark_state(self, do_setup):\n        app = self.app\n        objects = self.objects\n        app.toggle_selected_mark_state()\n        eq_(app.results.mark_count, 0)\n        self.rtable.select([1, 4])\n        app.toggle_selected_mark_state()\n        eq_(app.results.mark_count, 2)\n        assert not app.results.is_marked(objects[0])\n        assert app.results.is_marked(objects[1])\n        assert not app.results.is_marked(objects[2])\n        assert not app.results.is_marked(objects[3])\n        assert app.results.is_marked(objects[4])\n\n    def test_toggle_selected_mark_state_with_different_selected_state(self, do_setup):\n        # When marking selected dupes with a heterogenous selection, mark all selected dupes. When\n        # it's homogenous, simply toggle.\n        app = self.app\n        self.rtable.select([1])\n        app.toggle_selected_mark_state()\n        # index 0 is unmarkable, but we throw it in the bunch to be sure that it doesn't make the\n        # selection heterogenoug when it shouldn't.\n        self.rtable.select([0, 1, 4])\n        app.toggle_selected_mark_state()\n        eq_(app.results.mark_count, 2)\n        app.toggle_selected_mark_state()\n        eq_(app.results.mark_count, 0)\n\n    def test_refresh_details_with_selected(self, do_setup):\n        self.rtable.select([1, 4])\n        eq_(self.dpanel.row(0), (\"Filename\", \"bar bleh\", \"foo bar\"))\n        self.dpanel.view.check_gui_calls([\"refresh\"])\n        self.rtable.select([])\n        eq_(self.dpanel.row(0), (\"Filename\", \"---\", \"---\"))\n        self.dpanel.view.check_gui_calls([\"refresh\"])\n\n    def test_make_selected_reference(self, do_setup):\n        app = self.app\n        objects = self.objects\n        groups = self.groups\n        self.rtable.select([1, 4])\n        app.make_selected_reference()\n        assert groups[0].ref is objects[1]\n        assert groups[1].ref is objects[4]\n\n    def test_make_selected_reference_by_selecting_two_dupes_in_the_same_group(self, do_setup):\n        app = self.app\n        objects = self.objects\n        groups = self.groups\n        self.rtable.select([1, 2, 4])\n        # Only [0, 0] and [1, 0] must go ref, not [0, 1] because it is a part of the same group\n        app.make_selected_reference()\n        assert groups[0].ref is objects[1]\n        assert groups[1].ref is objects[4]\n\n    def test_remove_selected(self, do_setup):\n        app = self.app\n        self.rtable.select([1, 4])\n        app.remove_selected()\n        eq_(len(app.results.dupes), 1)  # the first path is now selected\n        app.remove_selected()\n        eq_(len(app.results.dupes), 0)\n\n    def test_add_directory_simple(self, do_setup):\n        # There's already a directory in self.app, so adding another once makes 2 of em\n        app = self.app\n        # any other path that isn't a parent or child of the already added path\n        otherpath = Path(op.dirname(__file__))\n        app.add_directory(otherpath)\n        eq_(len(app.directories), 2)\n\n    def test_add_directory_already_there(self, do_setup):\n        app = self.app\n        otherpath = Path(op.dirname(__file__))\n        app.add_directory(otherpath)\n        app.add_directory(otherpath)\n        eq_(len(app.view.messages), 1)\n        assert \"already\" in app.view.messages[0]\n\n    def test_add_directory_does_not_exist(self, do_setup):\n        app = self.app\n        app.add_directory(\"/does_not_exist\")\n        eq_(len(app.view.messages), 1)\n        assert \"exist\" in app.view.messages[0]\n\n    def test_ignore(self, do_setup):\n        app = self.app\n        self.rtable.select([4])  # The dupe of the second, 2 sized group\n        app.add_selected_to_ignore_list()\n        eq_(len(app.ignore_list), 1)\n        self.rtable.select([1])  # first dupe of the 3 dupes group\n        app.add_selected_to_ignore_list()\n        # BOTH the ref and the other dupe should have been added\n        eq_(len(app.ignore_list), 3)\n\n    def test_purge_ignorelist(self, do_setup, tmpdir):\n        app = self.app\n        p1 = str(tmpdir.join(\"file1\"))\n        p2 = str(tmpdir.join(\"file2\"))\n        open(p1, \"w\").close()\n        open(p2, \"w\").close()\n        dne = \"/does_not_exist\"\n        app.ignore_list.ignore(dne, p1)\n        app.ignore_list.ignore(p2, dne)\n        app.ignore_list.ignore(p1, p2)\n        app.purge_ignore_list()\n        eq_(1, len(app.ignore_list))\n        assert app.ignore_list.are_ignored(p1, p2)\n        assert not app.ignore_list.are_ignored(dne, p1)\n\n    def test_only_unicode_is_added_to_ignore_list(self, do_setup):\n        def fake_ignore(first, second):\n            if not isinstance(first, str):\n                self.fail()\n            if not isinstance(second, str):\n                self.fail()\n\n        app = self.app\n        app.ignore_list.ignore = fake_ignore\n        self.rtable.select([4])\n        app.add_selected_to_ignore_list()\n\n    def test_cancel_scan_with_previous_results(self, do_setup):\n        # When doing a scan with results being present prior to the scan, correctly invalidate the\n        # results table.\n        app = self.app\n        app.JOB = Job(1, lambda *args, **kw: False)  # Cancels the task\n        add_fake_files_to_directories(app.directories, self.objects)  # We want the scan to at least start\n        app.start_scanning()  # will be cancelled immediately\n        eq_(len(app.result_table), 0)\n\n    def test_selected_dupes_after_removal(self, do_setup):\n        # Purge the app's `selected_dupes` attribute when removing dupes, or else it might cause a\n        # crash later with None refs.\n        app = self.app\n        app.results.mark_all()\n        self.rtable.select([0, 1, 2, 3, 4])\n        app.remove_marked()\n        eq_(len(self.rtable), 0)\n        eq_(app.selected_dupes, [])\n\n    def test_dont_crash_on_delta_powermarker_dupecount_sort(self, do_setup):\n        # Don't crash when sorting by dupe count or percentage while delta+powermarker are enabled.\n        # Ref #238\n        self.rtable.delta_values = True\n        self.rtable.power_marker = True\n        self.rtable.sort(\"dupe_count\", False)\n        # don't crash\n        self.rtable.sort(\"percentage\", False)\n        # don't crash\n\n\nclass TestCaseDupeGuruRenameSelected:\n    @pytest.fixture\n    def do_setup(self, request):\n        tmpdir = request.getfixturevalue(\"tmpdir\")\n        p = Path(str(tmpdir))\n        p.joinpath(\"foo bar 1\").touch()\n        p.joinpath(\"foo bar 2\").touch()\n        p.joinpath(\"foo bar 3\").touch()\n        files = fs.get_files(p)\n        for f in files:\n            f.is_ref = False\n        matches = engine.getmatches(files)\n        groups = engine.get_groups(matches)\n        g = groups[0]\n        g.prioritize(lambda x: x.name)\n        app = TestApp()\n        app.app.results.groups = groups\n        self.app = app.app\n        self.rtable = app.rtable\n        self.rtable.refresh()\n        self.groups = groups\n        self.p = p\n        self.files = files\n\n    def test_simple(self, do_setup):\n        app = self.app\n        g = self.groups[0]\n        self.rtable.select([1])\n        assert app.rename_selected(\"renamed\")\n        names = [p.name for p in self.p.glob(\"*\")]\n        assert \"renamed\" in names\n        assert \"foo bar 2\" not in names\n        eq_(g.dupes[0].name, \"renamed\")\n\n    def test_none_selected(self, do_setup, monkeypatch):\n        app = self.app\n        g = self.groups[0]\n        self.rtable.select([])\n        monkeypatch.setattr(logging, \"warning\", log_calls(lambda msg: None))\n        assert not app.rename_selected(\"renamed\")\n        msg = logging.warning.calls[0][\"msg\"]\n        eq_(\"dupeGuru Warning: list index out of range\", msg)\n        names = [p.name for p in self.p.glob(\"*\")]\n        assert \"renamed\" not in names\n        assert \"foo bar 2\" in names\n        eq_(g.dupes[0].name, \"foo bar 2\")\n\n    def test_name_already_exists(self, do_setup, monkeypatch):\n        app = self.app\n        g = self.groups[0]\n        self.rtable.select([1])\n        monkeypatch.setattr(logging, \"warning\", log_calls(lambda msg: None))\n        assert not app.rename_selected(\"foo bar 1\")\n        msg = logging.warning.calls[0][\"msg\"]\n        assert msg.startswith(\"dupeGuru Warning: 'foo bar 1' already exists in\")\n        names = [p.name for p in self.p.glob(\"*\")]\n        assert \"foo bar 1\" in names\n        assert \"foo bar 2\" in names\n        eq_(g.dupes[0].name, \"foo bar 2\")\n\n\nclass TestAppWithDirectoriesInTree:\n    @pytest.fixture\n    def do_setup(self, request):\n        tmpdir = request.getfixturevalue(\"tmpdir\")\n        p = Path(str(tmpdir))\n        p.joinpath(\"sub1\").mkdir()\n        p.joinpath(\"sub2\").mkdir()\n        p.joinpath(\"sub3\").mkdir()\n        app = TestApp()\n        self.app = app.app\n        self.dtree = app.dtree\n        self.dtree.add_directory(p)\n        self.dtree.view.clear_calls()\n\n    def test_set_root_as_ref_makes_subfolders_ref_as_well(self, do_setup):\n        # Setting a node state to something also affect subnodes. These subnodes must be correctly\n        # refreshed.\n        node = self.dtree[0]\n        eq_(len(node), 3)  # a len() call is required for subnodes to be loaded\n        node.state = 1  # the state property is a state index\n        node = self.dtree[0]\n        eq_(len(node), 3)\n        subnode = node[0]\n        eq_(subnode.state, 1)\n        self.dtree.view.check_gui_calls([\"refresh_states\"])\n"
  },
  {
    "path": "core/tests/base.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import TestApp as TestAppBase, CallLogger, eq_, with_app  # noqa\nfrom pathlib import Path\nfrom hscommon.util import get_file_ext, format_size\nfrom hscommon.gui.column import Column\nfrom hscommon.jobprogress.job import nulljob, JobCancelled\n\nfrom core import engine, prioritize\nfrom core.engine import getwords\nfrom core.app import DupeGuru as DupeGuruBase\nfrom core.gui.result_table import ResultTable as ResultTableBase\nfrom core.gui.prioritize_dialog import PrioritizeDialog\n\n\nclass DupeGuruView:\n    JOB = nulljob\n\n    def __init__(self):\n        self.messages = []\n\n    def start_job(self, jobid, func, args=()):\n        try:\n            func(self.JOB, *args)\n        except JobCancelled:\n            return\n\n    def get_default(self, key_name):\n        return None\n\n    def set_default(self, key_name, value):\n        pass\n\n    def show_message(self, msg):\n        self.messages.append(msg)\n\n    def ask_yes_no(self, prompt):\n        return True  # always answer yes\n\n    def create_results_window(self):\n        pass\n\n\nclass ResultTable(ResultTableBase):\n    COLUMNS = [\n        Column(\"marked\", \"\"),\n        Column(\"name\", \"Filename\"),\n        Column(\"folder_path\", \"Directory\"),\n        Column(\"size\", \"Size (KB)\"),\n        Column(\"extension\", \"Kind\"),\n    ]\n    DELTA_COLUMNS = {\n        \"size\",\n    }\n\n\nclass DupeGuru(DupeGuruBase):\n    NAME = \"dupeGuru\"\n    METADATA_TO_READ = [\"size\"]\n\n    def __init__(self):\n        DupeGuruBase.__init__(self, DupeGuruView())\n        self.appdata = \"/tmp\"\n        self._recreate_result_table()\n\n    def _prioritization_categories(self):\n        return prioritize.all_categories()\n\n    def _recreate_result_table(self):\n        if self.result_table is not None:\n            self.result_table.disconnect()\n        self.result_table = ResultTable(self)\n        self.result_table.view = CallLogger()\n        self.result_table.connect()\n\n\nclass NamedObject:\n    def __init__(self, name=\"foobar\", with_words=False, size=1, folder=None):\n        self.name = name\n        if folder is None:\n            folder = \"basepath\"\n        self._folder = Path(folder)\n        self.size = size\n        self.digest_partial = name\n        self.digest = name\n        self.digest_samples = name\n        if with_words:\n            self.words = getwords(name)\n        self.is_ref = False\n\n    def __bool__(self):\n        return False  # Make sure that operations are made correctly when the bool value of files is false.\n\n    def get_display_info(self, group, delta):\n        size = self.size\n        m = group.get_match_of(self)\n        if m and delta:\n            r = group.ref\n            size -= r.size\n        return {\n            \"name\": self.name,\n            \"folder_path\": str(self.folder_path),\n            \"size\": format_size(size, 0, 1, False),\n            \"extension\": self.extension if hasattr(self, \"extension\") else \"---\",\n        }\n\n    @property\n    def path(self):\n        return self._folder.joinpath(self.name)\n\n    @property\n    def folder_path(self):\n        return self.path.parent\n\n    @property\n    def extension(self):\n        return get_file_ext(self.name)\n\n\n# Returns a group set that looks like that:\n# \"foo bar\" (1)\n#   \"bar bleh\" (1024)\n#   \"foo bleh\" (1)\n# \"ibabtu\" (1)\n#   \"ibabtu\" (1)\ndef GetTestGroups():\n    objects = [\n        NamedObject(\"foo bar\"),\n        NamedObject(\"bar bleh\"),\n        NamedObject(\"foo bleh\"),\n        NamedObject(\"ibabtu\"),\n        NamedObject(\"ibabtu\"),\n    ]\n    objects[1].size = 1024\n    matches = engine.getmatches(objects)  # we should have 5 matches\n    groups = engine.get_groups(matches)  # We should have 2 groups\n    for g in groups:\n        g.prioritize(lambda x: objects.index(x))  # We want the dupes to be in the same order as the list is\n    groups.sort(key=len, reverse=True)  # We want the group with 3 members to be first.\n    return (objects, matches, groups)\n\n\nclass TestApp(TestAppBase):\n    __test__ = False\n\n    def __init__(self):\n        def link_gui(gui):\n            gui.view = self.make_logger()\n            if hasattr(gui, \"_columns\"):  # tables\n                gui._columns.view = self.make_logger()\n            return gui\n\n        TestAppBase.__init__(self)\n        self.app = DupeGuru()\n        self.default_parent = self.app\n        self.dtree = link_gui(self.app.directory_tree)\n        self.dpanel = link_gui(self.app.details_panel)\n        self.slabel = link_gui(self.app.stats_label)\n        self.pdialog = PrioritizeDialog(self.app)\n        link_gui(self.pdialog.category_list)\n        link_gui(self.pdialog.criteria_list)\n        link_gui(self.pdialog.prioritization_list)\n        link_gui(self.app.ignore_list_dialog)\n        link_gui(self.app.ignore_list_dialog.ignore_list_table)\n        link_gui(self.app.progress_window)\n        link_gui(self.app.progress_window.jobdesc_textfield)\n        link_gui(self.app.progress_window.progressdesc_textfield)\n\n    @property\n    def rtable(self):\n        # rtable is a property because its instance can be replaced during execution\n        return self.app.result_table\n\n    # --- Helpers\n    def select_pri_criterion(self, name):\n        # Select a main prioritize criterion by name instead of by index. Makes tests more\n        # maintainable.\n        index = self.pdialog.category_list.index(name)\n        self.pdialog.category_list.select(index)\n\n    def add_pri_criterion(self, name, index):\n        self.select_pri_criterion(name)\n        self.pdialog.criteria_list.select([index])\n        self.pdialog.add_selected()\n"
  },
  {
    "path": "core/tests/block_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n# The commented out tests are tests for function that have been converted to pure C for speed\n\nfrom pytest import raises, skip\nfrom hscommon.testutil import eq_\n\ntry:\n    from core.pe.block import avgdiff, getblocks2, NoBlocksError, DifferentBlockCountError\nexcept ImportError:\n    skip(\"Can't import the block module, probably hasn't been compiled.\")\n\n\ndef my_avgdiff(first, second, limit=768, min_iter=3):  # this is so I don't have to re-write every call\n    return avgdiff(first, second, limit, min_iter)\n\n\nBLACK = (0, 0, 0)\nRED = (0xFF, 0, 0)\nGREEN = (0, 0xFF, 0)\nBLUE = (0, 0, 0xFF)\n\n\nclass FakeImage:\n    def __init__(self, size, data):\n        self.size = size\n        self.data = data\n\n    def getdata(self):\n        return self.data\n\n    def crop(self, box):\n        pixels = []\n        for i in range(box[1], box[3]):\n            for j in range(box[0], box[2]):\n                pixel = self.data[i * self.size[0] + j]\n                pixels.append(pixel)\n        return FakeImage((box[2] - box[0], box[3] - box[1]), pixels)\n\n\ndef empty():\n    return FakeImage((0, 0), [])\n\n\ndef single_pixel():  # one red pixel\n    return FakeImage((1, 1), [(0xFF, 0, 0)])\n\n\ndef four_pixels():\n    pixels = [RED, (0, 0x80, 0xFF), (0x80, 0, 0), (0, 0x40, 0x80)]\n    return FakeImage((2, 2), pixels)\n\n\nclass TestCasegetblock:\n    def test_single_pixel(self):\n        im = single_pixel()\n        [b] = getblocks2(im, 1)\n        eq_(RED, b)\n\n    def test_no_pixel(self):\n        im = empty()\n        eq_([], getblocks2(im, 1))\n\n    def test_four_pixels(self):\n        im = four_pixels()\n        [b] = getblocks2(im, 1)\n        meanred = (0xFF + 0x80) // 4\n        meangreen = (0x80 + 0x40) // 4\n        meanblue = (0xFF + 0x80) // 4\n        eq_((meanred, meangreen, meanblue), b)\n\n\nclass TestCasegetblocks2:\n    def test_empty_image(self):\n        im = empty()\n        blocks = getblocks2(im, 1)\n        eq_(0, len(blocks))\n\n    def test_one_block_image(self):\n        im = four_pixels()\n        blocks = getblocks2(im, 1)\n        eq_(1, len(blocks))\n        block = blocks[0]\n        meanred = (0xFF + 0x80) // 4\n        meangreen = (0x80 + 0x40) // 4\n        meanblue = (0xFF + 0x80) // 4\n        eq_((meanred, meangreen, meanblue), block)\n\n    def test_four_blocks_all_black(self):\n        im = FakeImage((2, 2), [BLACK, BLACK, BLACK, BLACK])\n        blocks = getblocks2(im, 2)\n        eq_(4, len(blocks))\n        for block in blocks:\n            eq_(BLACK, block)\n\n    def test_two_pixels_image_horizontal(self):\n        pixels = [RED, BLUE]\n        im = FakeImage((2, 1), pixels)\n        blocks = getblocks2(im, 2)\n        eq_(4, len(blocks))\n        eq_(RED, blocks[0])\n        eq_(BLUE, blocks[1])\n        eq_(RED, blocks[2])\n        eq_(BLUE, blocks[3])\n\n    def test_two_pixels_image_vertical(self):\n        pixels = [RED, BLUE]\n        im = FakeImage((1, 2), pixels)\n        blocks = getblocks2(im, 2)\n        eq_(4, len(blocks))\n        eq_(RED, blocks[0])\n        eq_(RED, blocks[1])\n        eq_(BLUE, blocks[2])\n        eq_(BLUE, blocks[3])\n\n\nclass TestCaseavgdiff:\n    def test_empty(self):\n        with raises(NoBlocksError):\n            my_avgdiff([], [])\n\n    def test_two_blocks(self):\n        b1 = (5, 10, 15)\n        b2 = (255, 250, 245)\n        b3 = (0, 0, 0)\n        b4 = (255, 0, 255)\n        blocks1 = [b1, b2]\n        blocks2 = [b3, b4]\n        expected1 = 5 + 10 + 15\n        expected2 = 0 + 250 + 10\n        expected = (expected1 + expected2) // 2\n        eq_(expected, my_avgdiff(blocks1, blocks2))\n\n    def test_blocks_not_the_same_size(self):\n        b = (0, 0, 0)\n        with raises(DifferentBlockCountError):\n            my_avgdiff([b, b], [b])\n\n    def test_first_arg_is_empty_but_not_second(self):\n        # Don't return 0 (as when the 2 lists are empty), raise!\n        b = (0, 0, 0)\n        with raises(DifferentBlockCountError):\n            my_avgdiff([], [b])\n\n    def test_limit(self):\n        ref = (0, 0, 0)\n        b1 = (10, 10, 10)  # avg 30\n        b2 = (20, 20, 20)  # avg 45\n        b3 = (30, 30, 30)  # avg 60\n        blocks1 = [ref, ref, ref]\n        blocks2 = [b1, b2, b3]\n        eq_(45, my_avgdiff(blocks1, blocks2, 44))\n\n    def test_min_iterations(self):\n        ref = (0, 0, 0)\n        b1 = (10, 10, 10)  # avg 30\n        b2 = (20, 20, 20)  # avg 45\n        b3 = (10, 10, 10)  # avg 40\n        blocks1 = [ref, ref, ref]\n        blocks2 = [b1, b2, b3]\n        eq_(40, my_avgdiff(blocks1, blocks2, 45 - 1, 3))\n\n    # Bah, I don't know why this test fails, but I don't think it matters very much\n    # def test_just_over_the_limit(self):\n    #     #A score just over the limit might return exactly the limit due to truncating. We should\n    #     #ceil() the result in this case.\n    #     ref = (0, 0, 0)\n    #     b1 = (10, 0, 0)\n    #     b2 = (11, 0, 0)\n    #     blocks1 = [ref, ref]\n    #     blocks2 = [b1, b2]\n    #     eq_(11, my_avgdiff(blocks1, blocks2, 10))\n    #\n    def test_return_at_least_1_at_the_slightest_difference(self):\n        ref = (0, 0, 0)\n        b1 = (1, 0, 0)\n        blocks1 = [ref for _ in range(250)]\n        blocks2 = [ref for _ in range(250)]\n        blocks2[0] = b1\n        eq_(1, my_avgdiff(blocks1, blocks2))\n\n    def test_return_0_if_there_is_no_difference(self):\n        ref = (0, 0, 0)\n        blocks1 = [ref, ref]\n        blocks2 = [ref, ref]\n        eq_(0, my_avgdiff(blocks1, blocks2))\n"
  },
  {
    "path": "core/tests/cache_test.py",
    "content": "# Copyright 2016 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\n\nfrom pytest import raises, skip\nfrom hscommon.testutil import eq_\n\ntry:\n    from core.pe.cache import colors_to_bytes, bytes_to_colors\n    from core.pe.cache_sqlite import SqliteCache\nexcept ImportError:\n    skip(\"Can't import the cache module, probably hasn't been compiled.\")\n\n\nclass TestCaseColorsToString:\n    def test_no_color(self):\n        eq_(b\"\", colors_to_bytes([]))\n\n    def test_single_color(self):\n        eq_(b\"\\x00\\x00\\x00\", colors_to_bytes([(0, 0, 0)]))\n        eq_(b\"\\x01\\x01\\x01\", colors_to_bytes([(1, 1, 1)]))\n        eq_(b\"\\x0a\\x14\\x1e\", colors_to_bytes([(10, 20, 30)]))\n\n    def test_two_colors(self):\n        eq_(b\"\\x00\\x01\\x02\\x03\\x04\\x05\", colors_to_bytes([(0, 1, 2), (3, 4, 5)]))\n\n\nclass TestCaseStringToColors:\n    def test_empty(self):\n        eq_([], bytes_to_colors(b\"\"))\n\n    def test_single_color(self):\n        eq_([(0, 0, 0)], bytes_to_colors(b\"\\x00\\x00\\x00\"))\n        eq_([(2, 3, 4)], bytes_to_colors(b\"\\x02\\x03\\x04\"))\n        eq_([(10, 20, 30)], bytes_to_colors(b\"\\x0a\\x14\\x1e\"))\n\n    def test_two_colors(self):\n        eq_([(10, 20, 30), (40, 50, 60)], bytes_to_colors(b\"\\x0a\\x14\\x1e\\x28\\x32\\x3c\"))\n\n    def test_incomplete_color(self):\n        # don't return anything if it's not a complete color\n        eq_([], bytes_to_colors(b\"\\x01\"))\n        eq_([(1, 2, 3)], bytes_to_colors(b\"\\x01\\x02\\x03\\x04\"))\n\n\nclass BaseTestCaseCache:\n    def get_cache(self, dbname=None):\n        raise NotImplementedError()\n\n    def test_empty(self):\n        c = self.get_cache()\n        eq_(0, len(c))\n        with raises(KeyError):\n            c[\"foo\"]\n\n    def test_set_then_retrieve_blocks(self):\n        c = self.get_cache()\n        b = [[(0, 0, 0), (1, 2, 3)]] * 8\n        c[\"foo\"] = b\n        eq_(b, c[\"foo\"])\n\n    def test_delitem(self):\n        c = self.get_cache()\n        c[\"foo\"] = [[]] * 8\n        del c[\"foo\"]\n        assert \"foo\" not in c\n        with raises(KeyError):\n            del c[\"foo\"]\n\n    def test_persistance(self, tmpdir):\n        DBNAME = tmpdir.join(\"hstest.db\")\n        c = self.get_cache(str(DBNAME))\n        c[\"foo\"] = [[(1, 2, 3)]] * 8\n        del c\n        c = self.get_cache(str(DBNAME))\n        eq_([[(1, 2, 3)]] * 8, c[\"foo\"])\n\n    def test_filter(self):\n        c = self.get_cache()\n        c[\"foo\"] = [[]] * 8\n        c[\"bar\"] = [[]] * 8\n        c[\"baz\"] = [[]] * 8\n        c.filter(lambda p: p != \"bar\")  # only 'bar' is removed\n        eq_(2, len(c))\n        assert \"foo\" in c\n        assert \"baz\" in c\n        assert \"bar\" not in c\n\n    def test_clear(self):\n        c = self.get_cache()\n        c[\"foo\"] = [[]] * 8\n        c[\"bar\"] = [[]] * 8\n        c[\"baz\"] = [[]] * 8\n        c.clear()\n        eq_(0, len(c))\n        assert \"foo\" not in c\n        assert \"baz\" not in c\n        assert \"bar\" not in c\n\n    def test_by_id(self):\n        # it's possible to use the cache by referring to the files by their row_id\n        c = self.get_cache()\n        b = [[(0, 0, 0), (1, 2, 3)]] * 8\n        c[\"foo\"] = b\n        foo_id = c.get_id(\"foo\")\n        eq_(c[foo_id], b)\n\n\nclass TestCaseSqliteCache(BaseTestCaseCache):\n    def get_cache(self, dbname=None):\n        if dbname:\n            return SqliteCache(dbname)\n        else:\n            return SqliteCache()\n\n    def test_corrupted_db(self, tmpdir, monkeypatch):\n        # If we don't do this monkeypatching, we get a weird exception about trying to flush a\n        # closed file. I've tried setting logging level and stuff, but nothing worked. So, there we\n        # go, a dirty monkeypatch.\n        monkeypatch.setattr(logging, \"warning\", lambda *args, **kw: None)\n        dbname = str(tmpdir.join(\"foo.db\"))\n        fp = open(dbname, \"w\")\n        fp.write(\"invalid sqlite content\")\n        fp.close()\n        c = self.get_cache(dbname)  # should not raise a DatabaseError\n        c[\"foo\"] = [[(1, 2, 3)]] * 8\n        del c\n        c = self.get_cache(dbname)\n        eq_(c[\"foo\"], [[(1, 2, 3)]] * 8)\n\n\nclass TestCaseCacheSQLEscape:\n    def get_cache(self):\n        return SqliteCache()\n\n    def test_contains(self):\n        c = self.get_cache()\n        assert \"foo'bar\" not in c\n\n    def test_getitem(self):\n        c = self.get_cache()\n        with raises(KeyError):\n            c[\"foo'bar\"]\n\n    def test_setitem(self):\n        c = self.get_cache()\n        c[\"foo'bar\"] = []\n\n    def test_delitem(self):\n        c = self.get_cache()\n        c[\"foo'bar\"] = [[]] * 8\n        try:\n            del c[\"foo'bar\"]\n        except KeyError:\n            assert False\n"
  },
  {
    "path": "core/tests/conftest.py",
    "content": "from hscommon.testutil import app  # noqa\n"
  },
  {
    "path": "core/tests/directories_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os\nimport time\nimport tempfile\nimport shutil\n\nfrom pytest import raises\nfrom pathlib import Path\nfrom hscommon.testutil import eq_\nfrom hscommon.plat import ISWINDOWS\n\nfrom core.fs import File\nfrom core.directories import (\n    Directories,\n    DirectoryState,\n    AlreadyThereError,\n    InvalidPathError,\n)\nfrom core.exclude import ExcludeList, ExcludeDict\n\n\ndef create_fake_fs(rootpath):\n    # We have it as a separate function because other units are using it.\n    rootpath = rootpath.joinpath(\"fs\")\n    rootpath.mkdir()\n    rootpath.joinpath(\"dir1\").mkdir()\n    rootpath.joinpath(\"dir2\").mkdir()\n    rootpath.joinpath(\"dir3\").mkdir()\n    with rootpath.joinpath(\"file1.test\").open(\"wt\") as fp:\n        fp.write(\"1\")\n    with rootpath.joinpath(\"file2.test\").open(\"wt\") as fp:\n        fp.write(\"12\")\n    with rootpath.joinpath(\"file3.test\").open(\"wt\") as fp:\n        fp.write(\"123\")\n    with rootpath.joinpath(\"dir1\", \"file1.test\").open(\"wt\") as fp:\n        fp.write(\"1\")\n    with rootpath.joinpath(\"dir2\", \"file2.test\").open(\"wt\") as fp:\n        fp.write(\"12\")\n    with rootpath.joinpath(\"dir3\", \"file3.test\").open(\"wt\") as fp:\n        fp.write(\"123\")\n    return rootpath\n\n\ntestpath = None\n\n\ndef setup_module(module):\n    # In this unit, we have tests depending on two directory structure. One with only one file in it\n    # and another with a more complex structure.\n    testpath = Path(tempfile.mkdtemp())\n    module.testpath = testpath\n    rootpath = testpath.joinpath(\"onefile\")\n    rootpath.mkdir()\n    with rootpath.joinpath(\"test.txt\").open(\"wt\") as fp:\n        fp.write(\"test_data\")\n    create_fake_fs(testpath)\n\n\ndef teardown_module(module):\n    shutil.rmtree(str(module.testpath))\n\n\ndef test_empty():\n    d = Directories()\n    eq_(len(d), 0)\n    assert \"foobar\" not in d\n\n\ndef test_add_path():\n    d = Directories()\n    p = testpath.joinpath(\"onefile\")\n    d.add_path(p)\n    eq_(1, len(d))\n    assert p in d\n    assert (p.joinpath(\"foobar\")) in d\n    assert p.parent not in d\n    p = testpath.joinpath(\"fs\")\n    d.add_path(p)\n    eq_(2, len(d))\n    assert p in d\n\n\ndef test_add_path_when_path_is_already_there():\n    d = Directories()\n    p = testpath.joinpath(\"onefile\")\n    d.add_path(p)\n    with raises(AlreadyThereError):\n        d.add_path(p)\n    with raises(AlreadyThereError):\n        d.add_path(p.joinpath(\"foobar\"))\n    eq_(1, len(d))\n\n\ndef test_add_path_containing_paths_already_there():\n    d = Directories()\n    d.add_path(testpath.joinpath(\"onefile\"))\n    eq_(1, len(d))\n    d.add_path(testpath)\n    eq_(len(d), 1)\n    eq_(d[0], testpath)\n\n\ndef test_add_path_non_latin(tmpdir):\n    p = Path(str(tmpdir))\n    to_add = p.joinpath(\"unicode\\u201a\")\n    os.mkdir(str(to_add))\n    d = Directories()\n    try:\n        d.add_path(to_add)\n    except UnicodeDecodeError:\n        assert False\n\n\ndef test_del():\n    d = Directories()\n    d.add_path(testpath.joinpath(\"onefile\"))\n    try:\n        del d[1]\n        assert False\n    except IndexError:\n        pass\n    d.add_path(testpath.joinpath(\"fs\"))\n    del d[1]\n    eq_(1, len(d))\n\n\ndef test_states():\n    d = Directories()\n    p = testpath.joinpath(\"onefile\")\n    d.add_path(p)\n    eq_(DirectoryState.NORMAL, d.get_state(p))\n    d.set_state(p, DirectoryState.REFERENCE)\n    eq_(DirectoryState.REFERENCE, d.get_state(p))\n    eq_(DirectoryState.REFERENCE, d.get_state(p.joinpath(\"dir1\")))\n    eq_(1, len(d.states))\n    eq_(p, list(d.states.keys())[0])\n    eq_(DirectoryState.REFERENCE, d.states[p])\n\n\ndef test_get_state_with_path_not_there():\n    # When the path's not there, just return DirectoryState.Normal\n    d = Directories()\n    d.add_path(testpath.joinpath(\"onefile\"))\n    eq_(d.get_state(testpath), DirectoryState.NORMAL)\n\n\ndef test_states_overwritten_when_larger_directory_eat_smaller_ones():\n    # ref #248\n    # When setting the state of a folder, we overwrite previously set states for subfolders.\n    d = Directories()\n    p = testpath.joinpath(\"onefile\")\n    d.add_path(p)\n    d.set_state(p, DirectoryState.EXCLUDED)\n    d.add_path(testpath)\n    d.set_state(testpath, DirectoryState.REFERENCE)\n    eq_(d.get_state(p), DirectoryState.REFERENCE)\n    eq_(d.get_state(p.joinpath(\"dir1\")), DirectoryState.REFERENCE)\n    eq_(d.get_state(testpath), DirectoryState.REFERENCE)\n\n\ndef test_get_files():\n    d = Directories()\n    p = testpath.joinpath(\"fs\")\n    d.add_path(p)\n    d.set_state(p.joinpath(\"dir1\"), DirectoryState.REFERENCE)\n    d.set_state(p.joinpath(\"dir2\"), DirectoryState.EXCLUDED)\n    files = list(d.get_files())\n    eq_(5, len(files))\n    for f in files:\n        if f.path.parent == p.joinpath(\"dir1\"):\n            assert f.is_ref\n        else:\n            assert not f.is_ref\n\n\ndef test_get_files_with_folders():\n    # When fileclasses handle folders, return them and stop recursing!\n    class FakeFile(File):\n        @classmethod\n        def can_handle(cls, path):\n            return True\n\n    d = Directories()\n    p = testpath.joinpath(\"fs\")\n    d.add_path(p)\n    files = list(d.get_files(fileclasses=[FakeFile]))\n    # We have the 3 root files and the 3 root dirs\n    eq_(6, len(files))\n\n\ndef test_get_folders():\n    d = Directories()\n    p = testpath.joinpath(\"fs\")\n    d.add_path(p)\n    d.set_state(p.joinpath(\"dir1\"), DirectoryState.REFERENCE)\n    d.set_state(p.joinpath(\"dir2\"), DirectoryState.EXCLUDED)\n    folders = list(d.get_folders())\n    eq_(len(folders), 3)\n    ref = [f for f in folders if f.is_ref]\n    not_ref = [f for f in folders if not f.is_ref]\n    eq_(len(ref), 1)\n    eq_(ref[0].path, p.joinpath(\"dir1\"))\n    eq_(len(not_ref), 2)\n    eq_(ref[0].size, 1)\n\n\ndef test_get_files_with_inherited_exclusion():\n    d = Directories()\n    p = testpath.joinpath(\"onefile\")\n    d.add_path(p)\n    d.set_state(p, DirectoryState.EXCLUDED)\n    eq_([], list(d.get_files()))\n\n\ndef test_save_and_load(tmpdir):\n    d1 = Directories()\n    d2 = Directories()\n    p1 = Path(str(tmpdir.join(\"p1\")))\n    p1.mkdir()\n    p2 = Path(str(tmpdir.join(\"p2\")))\n    p2.mkdir()\n    d1.add_path(p1)\n    d1.add_path(p2)\n    d1.set_state(p1, DirectoryState.REFERENCE)\n    d1.set_state(p1.joinpath(\"dir1\"), DirectoryState.EXCLUDED)\n    tmpxml = str(tmpdir.join(\"directories_testunit.xml\"))\n    d1.save_to_file(tmpxml)\n    d2.load_from_file(tmpxml)\n    eq_(2, len(d2))\n    eq_(DirectoryState.REFERENCE, d2.get_state(p1))\n    eq_(DirectoryState.EXCLUDED, d2.get_state(p1.joinpath(\"dir1\")))\n\n\ndef test_invalid_path():\n    d = Directories()\n    p = Path(\"does_not_exist\")\n    with raises(InvalidPathError):\n        d.add_path(p)\n    eq_(0, len(d))\n\n\ndef test_set_state_on_invalid_path():\n    d = Directories()\n    try:\n        d.set_state(\n            Path(\n                \"foobar\",\n            ),\n            DirectoryState.NORMAL,\n        )\n    except LookupError:\n        assert False\n\n\ndef test_load_from_file_with_invalid_path(tmpdir):\n    # This test simulates a load from file resulting in a\n    # InvalidPath raise. Other directories must be loaded.\n    d1 = Directories()\n    d1.add_path(testpath.joinpath(\"onefile\"))\n    # Will raise InvalidPath upon loading\n    p = Path(str(tmpdir.join(\"toremove\")))\n    p.mkdir()\n    d1.add_path(p)\n    p.rmdir()\n    tmpxml = str(tmpdir.join(\"directories_testunit.xml\"))\n    d1.save_to_file(tmpxml)\n    d2 = Directories()\n    d2.load_from_file(tmpxml)\n    eq_(1, len(d2))\n\n\ndef test_unicode_save(tmpdir):\n    d = Directories()\n    p1 = Path(str(tmpdir), \"hello\\xe9\")\n    p1.mkdir()\n    p1.joinpath(\"foo\\xe9\").mkdir()\n    d.add_path(p1)\n    d.set_state(p1.joinpath(\"foo\\xe9\"), DirectoryState.EXCLUDED)\n    tmpxml = str(tmpdir.join(\"directories_testunit.xml\"))\n    try:\n        d.save_to_file(tmpxml)\n    except UnicodeDecodeError:\n        assert False\n\n\ndef test_get_files_refreshes_its_directories():\n    d = Directories()\n    p = testpath.joinpath(\"fs\")\n    d.add_path(p)\n    files = d.get_files()\n    eq_(6, len(list(files)))\n    time.sleep(1)\n    os.remove(str(p.joinpath(\"dir1\", \"file1.test\")))\n    files = d.get_files()\n    eq_(5, len(list(files)))\n\n\ndef test_get_files_does_not_choke_on_non_existing_directories(tmpdir):\n    d = Directories()\n    p = Path(str(tmpdir))\n    d.add_path(p)\n    shutil.rmtree(str(p))\n    eq_([], list(d.get_files()))\n\n\ndef test_get_state_returns_excluded_by_default_for_hidden_directories(tmpdir):\n    d = Directories()\n    p = Path(str(tmpdir))\n    hidden_dir_path = p.joinpath(\".foo\")\n    p.joinpath(\".foo\").mkdir()\n    d.add_path(p)\n    eq_(d.get_state(hidden_dir_path), DirectoryState.EXCLUDED)\n    # But it can be overriden\n    d.set_state(hidden_dir_path, DirectoryState.NORMAL)\n    eq_(d.get_state(hidden_dir_path), DirectoryState.NORMAL)\n\n\ndef test_default_path_state_override(tmpdir):\n    # It's possible for a subclass to override the default state of a path\n    class MyDirectories(Directories):\n        def _default_state_for_path(self, path):\n            if \"foobar\" in path.parts:\n                return DirectoryState.EXCLUDED\n            return DirectoryState.NORMAL\n\n    d = MyDirectories()\n    p1 = Path(str(tmpdir))\n    p1.joinpath(\"foobar\").mkdir()\n    p1.joinpath(\"foobar/somefile\").touch()\n    p1.joinpath(\"foobaz\").mkdir()\n    p1.joinpath(\"foobaz/somefile\").touch()\n    d.add_path(p1)\n    eq_(d.get_state(p1.joinpath(\"foobaz\")), DirectoryState.NORMAL)\n    eq_(d.get_state(p1.joinpath(\"foobar\")), DirectoryState.EXCLUDED)\n    eq_(len(list(d.get_files())), 1)  # only the 'foobaz' file is there\n    # However, the default state can be changed\n    d.set_state(p1.joinpath(\"foobar\"), DirectoryState.NORMAL)\n    eq_(d.get_state(p1.joinpath(\"foobar\")), DirectoryState.NORMAL)\n    eq_(len(list(d.get_files())), 2)\n\n\nclass TestExcludeList:\n    def setup_method(self, method):\n        self.d = Directories(exclude_list=ExcludeList(union_regex=False))\n\n    def get_files_and_expect_num_result(self, num_result):\n        \"\"\"Calls get_files(), get the filenames only, print for debugging.\n        num_result is how many files are expected as a result.\"\"\"\n        print(\n            f\"EXCLUDED REGEX: paths {self.d._exclude_list.compiled_paths} \\\nfiles: {self.d._exclude_list.compiled_files} all: {self.d._exclude_list.compiled}\"\n        )\n        files = list(self.d.get_files())\n        files = [file.name for file in files]\n        print(f\"FINAL FILES {files}\")\n        eq_(len(files), num_result)\n        return files\n\n    def test_exclude_recycle_bin_by_default(self, tmpdir):\n        regex = r\"^.*Recycle\\.Bin$\"\n        self.d._exclude_list.add(regex)\n        self.d._exclude_list.mark(regex)\n        p1 = Path(str(tmpdir))\n        p1.joinpath(\"$Recycle.Bin\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\").mkdir()\n        self.d.add_path(p1)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\")), DirectoryState.EXCLUDED)\n        # By default, subdirs should be excluded too, but this can be overridden separately\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.EXCLUDED)\n        self.d.set_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\"), DirectoryState.NORMAL)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n\n    def test_exclude_refined(self, tmpdir):\n        regex1 = r\"^\\$Recycle\\.Bin$\"\n        self.d._exclude_list.add(regex1)\n        self.d._exclude_list.mark(regex1)\n        p1 = Path(str(tmpdir))\n        p1.joinpath(\"$Recycle.Bin\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"somefile.png\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"some_unwanted_file.jpg\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\", \"somesubdirfile.png\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\", \"unwanted_subdirfile.gif\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"subdar\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"subdar\", \"somesubdarfile.jpeg\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"subdar\", \"unwanted_subdarfile.png\").touch()\n        self.d.add_path(p1.joinpath(\"$Recycle.Bin\"))\n\n        # Filter should set the default state to Excluded\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\")), DirectoryState.EXCLUDED)\n        # The subdir should inherit its parent state\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.EXCLUDED)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdar\")), DirectoryState.EXCLUDED)\n        # Override a child path's state\n        self.d.set_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\"), DirectoryState.NORMAL)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n        # Parent should keep its default state, and the other child too\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\")), DirectoryState.EXCLUDED)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdar\")), DirectoryState.EXCLUDED)\n        # print(f\"get_folders(): {[x for x in self.d.get_folders()]}\")\n\n        # only the 2 files directly under the Normal directory\n        files = self.get_files_and_expect_num_result(2)\n        assert \"somefile.png\" not in files\n        assert \"some_unwanted_file.jpg\" not in files\n        assert \"somesubdarfile.jpeg\" not in files\n        assert \"unwanted_subdarfile.png\" not in files\n        assert \"somesubdirfile.png\" in files\n        assert \"unwanted_subdirfile.gif\" in files\n        # Overriding the parent should enable all children\n        self.d.set_state(p1.joinpath(\"$Recycle.Bin\"), DirectoryState.NORMAL)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdar\")), DirectoryState.NORMAL)\n        # all files there\n        files = self.get_files_and_expect_num_result(6)\n        assert \"somefile.png\" in files\n        assert \"some_unwanted_file.jpg\" in files\n\n        # This should still filter out files under directory, despite the Normal state\n        regex2 = r\".*unwanted.*\"\n        self.d._exclude_list.add(regex2)\n        self.d._exclude_list.mark(regex2)\n        files = self.get_files_and_expect_num_result(3)\n        assert \"somefile.png\" in files\n        assert \"some_unwanted_file.jpg\" not in files\n        assert \"unwanted_subdirfile.gif\" not in files\n        assert \"unwanted_subdarfile.png\" not in files\n\n        if ISWINDOWS:\n            regex3 = r\".*Recycle\\.Bin\\\\.*unwanted.*subdirfile.*\"\n        else:\n            regex3 = r\".*Recycle\\.Bin\\/.*unwanted.*subdirfile.*\"\n        self.d._exclude_list.rename(regex2, regex3)\n        assert self.d._exclude_list.error(regex3) is None\n        # print(f\"get_folders(): {[x for x in self.d.get_folders()]}\")\n        # Directory shouldn't change its state here, unless explicitely done by user\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n        files = self.get_files_and_expect_num_result(5)\n        assert \"unwanted_subdirfile.gif\" not in files\n        assert \"unwanted_subdarfile.png\" in files\n\n        # using end of line character should only filter the directory, or file ending with subdir\n        regex4 = r\".*subdir$\"\n        self.d._exclude_list.rename(regex3, regex4)\n        assert self.d._exclude_list.error(regex4) is None\n        p1.joinpath(\"$Recycle.Bin\", \"subdar\", \"file_ending_with_subdir\").touch()\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.EXCLUDED)\n        files = self.get_files_and_expect_num_result(4)\n        assert \"file_ending_with_subdir\" not in files\n        assert \"somesubdarfile.jpeg\" in files\n        assert \"somesubdirfile.png\" not in files\n        assert \"unwanted_subdirfile.gif\" not in files\n        self.d.set_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\"), DirectoryState.NORMAL)\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n        # print(f\"get_folders(): {[x for x in self.d.get_folders()]}\")\n        files = self.get_files_and_expect_num_result(6)\n        assert \"file_ending_with_subdir\" not in files\n        assert \"somesubdirfile.png\" in files\n        assert \"unwanted_subdirfile.gif\" in files\n\n        regex5 = r\".*subdir.*\"\n        self.d._exclude_list.rename(regex4, regex5)\n        # Files containing substring should be filtered\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n        # The path should not match, only the filename, the \"subdir\" in the directory name shouldn't matter\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\", \"file_which_shouldnt_match\").touch()\n        files = self.get_files_and_expect_num_result(5)\n        assert \"somesubdirfile.png\" not in files\n        assert \"unwanted_subdirfile.gif\" not in files\n        assert \"file_ending_with_subdir\" not in files\n        assert \"file_which_shouldnt_match\" in files\n\n        # This should match the directory only\n        regex6 = r\".*/.*subdir.*/.*\"\n        if ISWINDOWS:\n            regex6 = r\".*\\\\.*subdir.*\\\\.*\"\n        assert os.sep in regex6\n        self.d._exclude_list.rename(regex5, regex6)\n        self.d._exclude_list.remove(regex1)\n        eq_(len(self.d._exclude_list.compiled), 1)\n        assert regex1 not in self.d._exclude_list\n        assert regex5 not in self.d._exclude_list\n        assert self.d._exclude_list.error(regex6) is None\n        assert regex6 in self.d._exclude_list\n        # This still should not be affected\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"subdir\")), DirectoryState.NORMAL)\n        files = self.get_files_and_expect_num_result(5)\n        # These files are under the \"/subdir\" directory\n        assert \"somesubdirfile.png\" not in files\n        assert \"unwanted_subdirfile.gif\" not in files\n        # This file under \"subdar\" directory should not be filtered out\n        assert \"file_ending_with_subdir\" in files\n        # This file is in a directory that should be filtered out\n        assert \"file_which_shouldnt_match\" not in files\n\n    def test_japanese_unicode(self, tmpdir):\n        p1 = Path(str(tmpdir))\n        p1.joinpath(\"$Recycle.Bin\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"somerecycledfile.png\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"some_unwanted_file.jpg\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"subdir\", \"過去白濁物語～]_カラー.jpg\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"思叫物語\").mkdir()\n        p1.joinpath(\"$Recycle.Bin\", \"思叫物語\", \"なししろ会う前\").touch()\n        p1.joinpath(\"$Recycle.Bin\", \"思叫物語\", \"堂～ロ\").touch()\n        self.d.add_path(p1.joinpath(\"$Recycle.Bin\"))\n        regex3 = r\".*物語.*\"\n        self.d._exclude_list.add(regex3)\n        self.d._exclude_list.mark(regex3)\n        # print(f\"get_folders(): {[x for x in self.d.get_folders()]}\")\n        eq_(self.d.get_state(p1.joinpath(\"$Recycle.Bin\", \"思叫物語\")), DirectoryState.EXCLUDED)\n        files = self.get_files_and_expect_num_result(2)\n        assert \"過去白濁物語～]_カラー.jpg\" not in files\n        assert \"なししろ会う前\" not in files\n        assert \"堂～ロ\" not in files\n        # using end of line character should only filter that directory, not affecting its files\n        regex4 = r\".*物語$\"\n        self.d._exclude_list.rename(regex3, regex4)\n        assert self.d._exclude_list.error(regex4) is None\n        self.d.set_state(p1.joinpath(\"$Recycle.Bin\", \"思叫物語\"), DirectoryState.NORMAL)\n        files = self.get_files_and_expect_num_result(5)\n        assert \"過去白濁物語～]_カラー.jpg\" in files\n        assert \"なししろ会う前\" in files\n        assert \"堂～ロ\" in files\n\n    def test_get_state_returns_excluded_for_hidden_directories_and_files(self, tmpdir):\n        # This regex only work for files, not paths\n        regex = r\"^\\..*$\"\n        self.d._exclude_list.add(regex)\n        self.d._exclude_list.mark(regex)\n        p1 = Path(str(tmpdir))\n        p1.joinpath(\"foobar\").mkdir()\n        p1.joinpath(\"foobar\", \".hidden_file.txt\").touch()\n        p1.joinpath(\"foobar\", \".hidden_dir\").mkdir()\n        p1.joinpath(\"foobar\", \".hidden_dir\", \"foobar.jpg\").touch()\n        p1.joinpath(\"foobar\", \".hidden_dir\", \".hidden_subfile.png\").touch()\n        self.d.add_path(p1.joinpath(\"foobar\"))\n        # It should not inherit its parent's state originally\n        eq_(self.d.get_state(p1.joinpath(\"foobar\", \".hidden_dir\")), DirectoryState.EXCLUDED)\n        self.d.set_state(p1.joinpath(\"foobar\", \".hidden_dir\"), DirectoryState.NORMAL)\n        # The files should still be filtered\n        files = self.get_files_and_expect_num_result(1)\n        eq_(len(self.d._exclude_list.compiled_paths), 0)\n        eq_(len(self.d._exclude_list.compiled_files), 1)\n        assert \".hidden_file.txt\" not in files\n        assert \".hidden_subfile.png\" not in files\n        assert \"foobar.jpg\" in files\n\n\nclass TestExcludeDict(TestExcludeList):\n    def setup_method(self, method):\n        self.d = Directories(exclude_list=ExcludeDict(union_regex=False))\n\n\nclass TestExcludeListunion(TestExcludeList):\n    def setup_method(self, method):\n        self.d = Directories(exclude_list=ExcludeList(union_regex=True))\n\n\nclass TestExcludeDictunion(TestExcludeList):\n    def setup_method(self, method):\n        self.d = Directories(exclude_list=ExcludeDict(union_regex=True))\n"
  },
  {
    "path": "core/tests/engine_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport sys\n\nfrom hscommon.jobprogress import job\nfrom hscommon.util import first\nfrom hscommon.testutil import eq_, log_calls\n\nfrom core.tests.base import NamedObject\nfrom core import engine\nfrom core.engine import (\n    get_match,\n    getwords,\n    Group,\n    getfields,\n    unpack_fields,\n    compare_fields,\n    compare,\n    WEIGHT_WORDS,\n    MATCH_SIMILAR_WORDS,\n    NO_FIELD_ORDER,\n    build_word_dict,\n    get_groups,\n    getmatches,\n    Match,\n    getmatches_by_contents,\n    merge_similar_words,\n    reduce_common_words,\n)\n\nno = NamedObject\n\n\ndef get_match_triangle():\n    o1 = NamedObject(with_words=True)\n    o2 = NamedObject(with_words=True)\n    o3 = NamedObject(with_words=True)\n    m1 = get_match(o1, o2)\n    m2 = get_match(o1, o3)\n    m3 = get_match(o2, o3)\n    return [m1, m2, m3]\n\n\ndef get_test_group():\n    m1, m2, m3 = get_match_triangle()\n    result = Group()\n    result.add_match(m1)\n    result.add_match(m2)\n    result.add_match(m3)\n    return result\n\n\ndef assert_match(m, name1, name2):\n    # When testing matches, whether objects are in first or second position very often doesn't\n    # matter. This function makes this test more convenient.\n    if m.first.name == name1:\n        eq_(m.second.name, name2)\n    else:\n        eq_(m.first.name, name2)\n        eq_(m.second.name, name1)\n\n\nclass TestCasegetwords:\n    def test_spaces(self):\n        eq_([\"a\", \"b\", \"c\", \"d\"], getwords(\"a b c d\"))\n        eq_([\"a\", \"b\", \"c\", \"d\"], getwords(\" a  b  c d \"))\n\n    def test_unicode(self):\n        eq_([\"e\", \"c\", \"0\", \"a\", \"o\", \"u\", \"e\", \"u\"], getwords(\"é ç 0 à ö û è ¤ ù\"))\n        eq_(\n            [\"02\", \"君のこころは輝いてるかい？\", \"国木田花丸\", \"solo\", \"ver\"],\n            getwords(\"02 君のこころは輝いてるかい？ 国木田花丸 Solo Ver\"),\n        )\n\n    def test_splitter_chars(self):\n        eq_(\n            [chr(i) for i in range(ord(\"a\"), ord(\"z\") + 1)],\n            getwords(\"a-b_c&d+e(f)g;h\\\\i[j]k{l}m:n.o,p<q>r/s?t~u!v@w#x$y*z\"),\n        )\n\n    def test_joiner_chars(self):\n        eq_([\"aec\"], getwords(\"a'e\\u0301c\"))\n\n    def test_empty(self):\n        eq_([], getwords(\"\"))\n\n    def test_returns_lowercase(self):\n        eq_([\"foo\", \"bar\"], getwords(\"FOO BAR\"))\n\n    def test_decompose_unicode(self):\n        eq_([\"fooebar\"], getwords(\"foo\\xe9bar\"))\n\n\nclass TestCasegetfields:\n    def test_simple(self):\n        eq_([[\"a\", \"b\"], [\"c\", \"d\", \"e\"]], getfields(\"a b - c d e\"))\n\n    def test_empty(self):\n        eq_([], getfields(\"\"))\n\n    def test_cleans_empty_fields(self):\n        expected = [[\"a\", \"bc\", \"def\"]]\n        actual = getfields(\" - a bc def\")\n        eq_(expected, actual)\n\n\nclass TestCaseUnpackFields:\n    def test_with_fields(self):\n        expected = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"]\n        actual = unpack_fields([[\"a\"], [\"b\", \"c\"], [\"d\", \"e\", \"f\"]])\n        eq_(expected, actual)\n\n    def test_without_fields(self):\n        expected = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"]\n        actual = unpack_fields([\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"])\n        eq_(expected, actual)\n\n    def test_empty(self):\n        eq_([], unpack_fields([]))\n\n\nclass TestCaseWordCompare:\n    def test_list(self):\n        eq_(100, compare([\"a\", \"b\", \"c\", \"d\"], [\"a\", \"b\", \"c\", \"d\"]))\n        eq_(86, compare([\"a\", \"b\", \"c\", \"d\"], [\"a\", \"b\", \"c\"]))\n\n    def test_unordered(self):\n        # Sometimes, users don't want fuzzy matching too much When they set the slider\n        # to 100, they don't expect a filename with the same words, but not the same order, to match.\n        # Thus, we want to return 99 in that case.\n        eq_(99, compare([\"a\", \"b\", \"c\", \"d\"], [\"d\", \"b\", \"c\", \"a\"]))\n\n    def test_word_occurs_twice(self):\n        # if a word occurs twice in first, but once in second, we want the word to be only counted once\n        eq_(89, compare([\"a\", \"b\", \"c\", \"d\", \"a\"], [\"d\", \"b\", \"c\", \"a\"]))\n\n    def test_uses_copy_of_lists(self):\n        first = [\"foo\", \"bar\"]\n        second = [\"bar\", \"bleh\"]\n        compare(first, second)\n        eq_([\"foo\", \"bar\"], first)\n        eq_([\"bar\", \"bleh\"], second)\n\n    def test_word_weight(self):\n        eq_(\n            int((6.0 / 13.0) * 100),\n            compare([\"foo\", \"bar\"], [\"bar\", \"bleh\"], (WEIGHT_WORDS,)),\n        )\n\n    def test_similar_words(self):\n        eq_(\n            100,\n            compare(\n                [\"the\", \"white\", \"stripes\"],\n                [\"the\", \"whites\", \"stripe\"],\n                (MATCH_SIMILAR_WORDS,),\n            ),\n        )\n\n    def test_empty(self):\n        eq_(0, compare([], []))\n\n    def test_with_fields(self):\n        eq_(67, compare([[\"a\", \"b\"], [\"c\", \"d\", \"e\"]], [[\"a\", \"b\"], [\"c\", \"d\", \"f\"]]))\n\n    def test_propagate_flags_with_fields(self, monkeypatch):\n        def mock_compare(first, second, flags):\n            eq_((0, 1, 2, 3, 5), flags)\n\n        monkeypatch.setattr(engine, \"compare_fields\", mock_compare)\n        compare([[\"a\"]], [[\"a\"]], (0, 1, 2, 3, 5))\n\n\nclass TestCaseWordCompareWithFields:\n    def test_simple(self):\n        eq_(\n            67,\n            compare_fields([[\"a\", \"b\"], [\"c\", \"d\", \"e\"]], [[\"a\", \"b\"], [\"c\", \"d\", \"f\"]]),\n        )\n\n    def test_empty(self):\n        eq_(0, compare_fields([], []))\n\n    def test_different_length(self):\n        eq_(0, compare_fields([[\"a\"], [\"b\"]], [[\"a\"], [\"b\"], [\"c\"]]))\n\n    def test_propagates_flags(self, monkeypatch):\n        def mock_compare(first, second, flags):\n            eq_((0, 1, 2, 3, 5), flags)\n\n        monkeypatch.setattr(engine, \"compare_fields\", mock_compare)\n        compare_fields([[\"a\"]], [[\"a\"]], (0, 1, 2, 3, 5))\n\n    def test_order(self):\n        first = [[\"a\", \"b\"], [\"c\", \"d\", \"e\"]]\n        second = [[\"c\", \"d\", \"f\"], [\"a\", \"b\"]]\n        eq_(0, compare_fields(first, second))\n\n    def test_no_order(self):\n        first = [[\"a\", \"b\"], [\"c\", \"d\", \"e\"]]\n        second = [[\"c\", \"d\", \"f\"], [\"a\", \"b\"]]\n        eq_(67, compare_fields(first, second, (NO_FIELD_ORDER,)))\n        first = [[\"a\", \"b\"], [\"a\", \"b\"]]  # a field can only be matched once.\n        second = [[\"c\", \"d\", \"f\"], [\"a\", \"b\"]]\n        eq_(0, compare_fields(first, second, (NO_FIELD_ORDER,)))\n        first = [[\"a\", \"b\"], [\"a\", \"b\", \"c\"]]\n        second = [[\"c\", \"d\", \"f\"], [\"a\", \"b\"]]\n        eq_(33, compare_fields(first, second, (NO_FIELD_ORDER,)))\n\n    def test_compare_fields_without_order_doesnt_alter_fields(self):\n        # The NO_ORDER comp type altered the fields!\n        first = [[\"a\", \"b\"], [\"c\", \"d\", \"e\"]]\n        second = [[\"c\", \"d\", \"f\"], [\"a\", \"b\"]]\n        eq_(67, compare_fields(first, second, (NO_FIELD_ORDER,)))\n        eq_([[\"a\", \"b\"], [\"c\", \"d\", \"e\"]], first)\n        eq_([[\"c\", \"d\", \"f\"], [\"a\", \"b\"]], second)\n\n\nclass TestCaseBuildWordDict:\n    def test_with_standard_words(self):\n        item_list = [NamedObject(\"foo bar\", True)]\n        item_list.append(NamedObject(\"bar baz\", True))\n        item_list.append(NamedObject(\"baz bleh foo\", True))\n        d = build_word_dict(item_list)\n        eq_(4, len(d))\n        eq_(2, len(d[\"foo\"]))\n        assert item_list[0] in d[\"foo\"]\n        assert item_list[2] in d[\"foo\"]\n        eq_(2, len(d[\"bar\"]))\n        assert item_list[0] in d[\"bar\"]\n        assert item_list[1] in d[\"bar\"]\n        eq_(2, len(d[\"baz\"]))\n        assert item_list[1] in d[\"baz\"]\n        assert item_list[2] in d[\"baz\"]\n        eq_(1, len(d[\"bleh\"]))\n        assert item_list[2] in d[\"bleh\"]\n\n    def test_unpack_fields(self):\n        o = NamedObject(\"\")\n        o.words = [[\"foo\", \"bar\"], [\"baz\"]]\n        d = build_word_dict([o])\n        eq_(3, len(d))\n        eq_(1, len(d[\"foo\"]))\n\n    def test_words_are_unaltered(self):\n        o = NamedObject(\"\")\n        o.words = [[\"foo\", \"bar\"], [\"baz\"]]\n        build_word_dict([o])\n        eq_([[\"foo\", \"bar\"], [\"baz\"]], o.words)\n\n    def test_object_instances_can_only_be_once_in_words_object_list(self):\n        o = NamedObject(\"foo foo\", True)\n        d = build_word_dict([o])\n        eq_(1, len(d[\"foo\"]))\n\n    def test_job(self):\n        def do_progress(p, d=\"\"):\n            self.log.append(p)\n            return True\n\n        j = job.Job(1, do_progress)\n        self.log = []\n        s = \"foo bar\"\n        build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j)\n        # We don't have intermediate log because iter_with_progress is called with every > 1\n        eq_(0, self.log[0])\n        eq_(100, self.log[1])\n\n\nclass TestCaseMergeSimilarWords:\n    def test_some_similar_words(self):\n        d = {\n            \"foobar\": {1},\n            \"foobar1\": {2},\n            \"foobar2\": {3},\n        }\n        merge_similar_words(d)\n        eq_(1, len(d))\n        eq_(3, len(d[\"foobar\"]))\n\n\nclass TestCaseReduceCommonWords:\n    def test_typical(self):\n        d = {\n            \"foo\": {NamedObject(\"foo bar\", True) for _ in range(50)},\n            \"bar\": {NamedObject(\"foo bar\", True) for _ in range(49)},\n        }\n        reduce_common_words(d, 50)\n        assert \"foo\" not in d\n        eq_(49, len(d[\"bar\"]))\n\n    def test_dont_remove_objects_with_only_common_words(self):\n        d = {\n            \"common\": set([NamedObject(\"common uncommon\", True) for _ in range(50)] + [NamedObject(\"common\", True)]),\n            \"uncommon\": {NamedObject(\"common uncommon\", True)},\n        }\n        reduce_common_words(d, 50)\n        eq_(1, len(d[\"common\"]))\n        eq_(1, len(d[\"uncommon\"]))\n\n    def test_values_still_are_set_instances(self):\n        d = {\n            \"common\": set([NamedObject(\"common uncommon\", True) for _ in range(50)] + [NamedObject(\"common\", True)]),\n            \"uncommon\": {NamedObject(\"common uncommon\", True)},\n        }\n        reduce_common_words(d, 50)\n        assert isinstance(d[\"common\"], set)\n        assert isinstance(d[\"uncommon\"], set)\n\n    def test_dont_raise_keyerror_when_a_word_has_been_removed(self):\n        # If a word has been removed by the reduce, an object in a subsequent common word that\n        # contains the word that has been removed would cause a KeyError.\n        d = {\n            \"foo\": {NamedObject(\"foo bar baz\", True) for _ in range(50)},\n            \"bar\": {NamedObject(\"foo bar baz\", True) for _ in range(50)},\n            \"baz\": {NamedObject(\"foo bar baz\", True) for _ in range(49)},\n        }\n        try:\n            reduce_common_words(d, 50)\n        except KeyError:\n            self.fail()\n\n    def test_unpack_fields(self):\n        # object.words may be fields.\n        def create_it():\n            o = NamedObject(\"\")\n            o.words = [[\"foo\", \"bar\"], [\"baz\"]]\n            return o\n\n        d = {\"foo\": {create_it() for _ in range(50)}}\n        try:\n            reduce_common_words(d, 50)\n        except TypeError:\n            self.fail(\"must support fields.\")\n\n    def test_consider_a_reduced_common_word_common_even_after_reduction(self):\n        # There was a bug in the code that causeda word that has already been reduced not to\n        # be counted as a common word for subsequent words. For example, if 'foo' is processed\n        # as a common word, keeping a \"foo bar\" file in it, and the 'bar' is processed, \"foo bar\"\n        # would not stay in 'bar' because 'foo' is not a common word anymore.\n        only_common = NamedObject(\"foo bar\", True)\n        d = {\n            \"foo\": set([NamedObject(\"foo bar baz\", True) for _ in range(49)] + [only_common]),\n            \"bar\": set([NamedObject(\"foo bar baz\", True) for _ in range(49)] + [only_common]),\n            \"baz\": {NamedObject(\"foo bar baz\", True) for _ in range(49)},\n        }\n        reduce_common_words(d, 50)\n        eq_(1, len(d[\"foo\"]))\n        eq_(1, len(d[\"bar\"]))\n        eq_(49, len(d[\"baz\"]))\n\n\nclass TestCaseGetMatch:\n    def test_simple(self):\n        o1 = NamedObject(\"foo bar\", True)\n        o2 = NamedObject(\"bar bleh\", True)\n        m = get_match(o1, o2)\n        eq_(50, m.percentage)\n        eq_([\"foo\", \"bar\"], m.first.words)\n        eq_([\"bar\", \"bleh\"], m.second.words)\n        assert m.first is o1\n        assert m.second is o2\n\n    def test_in(self):\n        o1 = NamedObject(\"foo\", True)\n        o2 = NamedObject(\"bar\", True)\n        m = get_match(o1, o2)\n        assert o1 in m\n        assert o2 in m\n        assert object() not in m\n\n    def test_word_weight(self):\n        m = get_match(NamedObject(\"foo bar\", True), NamedObject(\"bar bleh\", True), (WEIGHT_WORDS,))\n        eq_(m.percentage, int((6.0 / 13.0) * 100))\n\n\nclass TestCaseGetMatches:\n    def test_empty(self):\n        eq_(getmatches([]), [])\n\n    def test_simple(self):\n        item_list = [\n            NamedObject(\"foo bar\"),\n            NamedObject(\"bar bleh\"),\n            NamedObject(\"a b c foo\"),\n        ]\n        r = getmatches(item_list)\n        eq_(2, len(r))\n        m = first(m for m in r if m.percentage == 50)  # \"foo bar\" and \"bar bleh\"\n        assert_match(m, \"foo bar\", \"bar bleh\")\n        m = first(m for m in r if m.percentage == 33)  # \"foo bar\" and \"a b c foo\"\n        assert_match(m, \"foo bar\", \"a b c foo\")\n\n    def test_null_and_unrelated_objects(self):\n        item_list = [\n            NamedObject(\"foo bar\"),\n            NamedObject(\"bar bleh\"),\n            NamedObject(\"\"),\n            NamedObject(\"unrelated object\"),\n        ]\n        r = getmatches(item_list)\n        eq_(len(r), 1)\n        m = r[0]\n        eq_(m.percentage, 50)\n        assert_match(m, \"foo bar\", \"bar bleh\")\n\n    def test_twice_the_same_word(self):\n        item_list = [NamedObject(\"foo foo bar\"), NamedObject(\"bar bleh\")]\n        r = getmatches(item_list)\n        eq_(1, len(r))\n\n    def test_twice_the_same_word_when_preworded(self):\n        item_list = [NamedObject(\"foo foo bar\", True), NamedObject(\"bar bleh\", True)]\n        r = getmatches(item_list)\n        eq_(1, len(r))\n\n    def test_two_words_match(self):\n        item_list = [NamedObject(\"foo bar\"), NamedObject(\"foo bar bleh\")]\n        r = getmatches(item_list)\n        eq_(1, len(r))\n\n    def test_match_files_with_only_common_words(self):\n        # If a word occurs more than 50 times, it is excluded from the matching process\n        # The problem with the common_word_threshold is that the files containing only common\n        # words will never be matched together. We *should* match them.\n        # This test assumes that the common word threshold const is 50\n        item_list = [NamedObject(\"foo\") for _ in range(50)]\n        r = getmatches(item_list)\n        eq_(1225, len(r))\n\n    def test_use_words_already_there_if_there(self):\n        o1 = NamedObject(\"foo\")\n        o2 = NamedObject(\"bar\")\n        o2.words = [\"foo\"]\n        eq_(1, len(getmatches([o1, o2])))\n\n    def test_job(self):\n        def do_progress(p, d=\"\"):\n            self.log.append(p)\n            return True\n\n        j = job.Job(1, do_progress)\n        self.log = []\n        s = \"foo bar\"\n        getmatches([NamedObject(s), NamedObject(s), NamedObject(s)], j=j)\n        assert len(self.log) > 2\n        eq_(0, self.log[0])\n        eq_(100, self.log[-1])\n\n    def test_weight_words(self):\n        item_list = [NamedObject(\"foo bar\"), NamedObject(\"bar bleh\")]\n        m = getmatches(item_list, weight_words=True)[0]\n        eq_(int((6.0 / 13.0) * 100), m.percentage)\n\n    def test_similar_word(self):\n        item_list = [NamedObject(\"foobar\"), NamedObject(\"foobars\")]\n        eq_(len(getmatches(item_list, match_similar_words=True)), 1)\n        eq_(getmatches(item_list, match_similar_words=True)[0].percentage, 100)\n        item_list = [NamedObject(\"foobar\"), NamedObject(\"foo\")]\n        eq_(len(getmatches(item_list, match_similar_words=True)), 0)  # too far\n        item_list = [NamedObject(\"bizkit\"), NamedObject(\"bizket\")]\n        eq_(len(getmatches(item_list, match_similar_words=True)), 1)\n        item_list = [NamedObject(\"foobar\"), NamedObject(\"foosbar\")]\n        eq_(len(getmatches(item_list, match_similar_words=True)), 1)\n\n    def test_single_object_with_similar_words(self):\n        item_list = [NamedObject(\"foo foos\")]\n        eq_(len(getmatches(item_list, match_similar_words=True)), 0)\n\n    def test_double_words_get_counted_only_once(self):\n        item_list = [NamedObject(\"foo bar foo bleh\"), NamedObject(\"foo bar bleh bar\")]\n        m = getmatches(item_list)[0]\n        eq_(75, m.percentage)\n\n    def test_with_fields(self):\n        o1 = NamedObject(\"foo bar - foo bleh\")\n        o2 = NamedObject(\"foo bar - bleh bar\")\n        o1.words = getfields(o1.name)\n        o2.words = getfields(o2.name)\n        m = getmatches([o1, o2])[0]\n        eq_(50, m.percentage)\n\n    def test_with_fields_no_order(self):\n        o1 = NamedObject(\"foo bar - foo bleh\")\n        o2 = NamedObject(\"bleh bang - foo bar\")\n        o1.words = getfields(o1.name)\n        o2.words = getfields(o2.name)\n        m = getmatches([o1, o2], no_field_order=True)[0]\n        eq_(m.percentage, 50)\n\n    def test_only_match_similar_when_the_option_is_set(self):\n        item_list = [NamedObject(\"foobar\"), NamedObject(\"foobars\")]\n        eq_(len(getmatches(item_list, match_similar_words=False)), 0)\n\n    def test_dont_recurse_do_match(self):\n        # with nosetests, the stack is increased. The number has to be high enough not to be failing falsely\n        sys.setrecursionlimit(200)\n        files = [NamedObject(\"foo bar\") for _ in range(201)]\n        try:\n            getmatches(files)\n        except RuntimeError:\n            self.fail()\n        finally:\n            sys.setrecursionlimit(1000)\n\n    def test_min_match_percentage(self):\n        item_list = [\n            NamedObject(\"foo bar\"),\n            NamedObject(\"bar bleh\"),\n            NamedObject(\"a b c foo\"),\n        ]\n        r = getmatches(item_list, min_match_percentage=50)\n        eq_(1, len(r))  # Only \"foo bar\" / \"bar bleh\" should match\n\n    def test_memory_error(self, monkeypatch):\n        @log_calls\n        def mocked_match(first, second, flags):\n            if len(mocked_match.calls) > 42:\n                raise MemoryError()\n            return Match(first, second, 0)\n\n        objects = [NamedObject() for _ in range(10)]  # results in 45 matches\n        monkeypatch.setattr(engine, \"get_match\", mocked_match)\n        try:\n            r = getmatches(objects)\n        except MemoryError:\n            self.fail(\"MemoryError must be handled\")\n        eq_(42, len(r))\n\n\nclass TestCaseGetMatchesByContents:\n    def test_big_file_partial_hashing(self):\n        smallsize = 1\n        bigsize = 100 * 1024 * 1024  # 100MB\n        f = [\n            no(\"bigfoo\", size=bigsize),\n            no(\"bigbar\", size=bigsize),\n            no(\"smallfoo\", size=smallsize),\n            no(\"smallbar\", size=smallsize),\n        ]\n        f[0].digest = f[0].digest_partial = f[0].digest_samples = \"foobar\"\n        f[1].digest = f[1].digest_partial = f[1].digest_samples = \"foobar\"\n        f[2].digest = f[2].digest_partial = \"bleh\"\n        f[3].digest = f[3].digest_partial = \"bleh\"\n        r = getmatches_by_contents(f, bigsize=bigsize)\n        eq_(len(r), 2)\n        # User disabled optimization for big files, compute digests as usual\n        r = getmatches_by_contents(f, bigsize=0)\n        eq_(len(r), 2)\n        # Other file is now slightly different, digest_partial is still the same\n        f[1].digest = f[1].digest_samples = \"foobardiff\"\n        r = getmatches_by_contents(f, bigsize=bigsize)\n        # Successfully filter it out\n        eq_(len(r), 1)\n        r = getmatches_by_contents(f, bigsize=0)\n        eq_(len(r), 1)\n\n\nclass TestCaseGroup:\n    def test_empty(self):\n        g = Group()\n        eq_(None, g.ref)\n        eq_([], g.dupes)\n        eq_(0, len(g.matches))\n\n    def test_add_match(self):\n        g = Group()\n        m = get_match(NamedObject(\"foo\", True), NamedObject(\"bar\", True))\n        g.add_match(m)\n        assert g.ref is m.first\n        eq_([m.second], g.dupes)\n        eq_(1, len(g.matches))\n        assert m in g.matches\n\n    def test_multiple_add_match(self):\n        g = Group()\n        o1 = NamedObject(\"a\", True)\n        o2 = NamedObject(\"b\", True)\n        o3 = NamedObject(\"c\", True)\n        o4 = NamedObject(\"d\", True)\n        g.add_match(get_match(o1, o2))\n        assert g.ref is o1\n        eq_([o2], g.dupes)\n        eq_(1, len(g.matches))\n        g.add_match(get_match(o1, o3))\n        eq_([o2], g.dupes)\n        eq_(2, len(g.matches))\n        g.add_match(get_match(o2, o3))\n        eq_([o2, o3], g.dupes)\n        eq_(3, len(g.matches))\n        g.add_match(get_match(o1, o4))\n        eq_([o2, o3], g.dupes)\n        eq_(4, len(g.matches))\n        g.add_match(get_match(o2, o4))\n        eq_([o2, o3], g.dupes)\n        eq_(5, len(g.matches))\n        g.add_match(get_match(o3, o4))\n        eq_([o2, o3, o4], g.dupes)\n        eq_(6, len(g.matches))\n\n    def test_len(self):\n        g = Group()\n        eq_(0, len(g))\n        g.add_match(get_match(NamedObject(\"foo\", True), NamedObject(\"bar\", True)))\n        eq_(2, len(g))\n\n    def test_add_same_match_twice(self):\n        g = Group()\n        m = get_match(NamedObject(\"foo\", True), NamedObject(\"foo\", True))\n        g.add_match(m)\n        eq_(2, len(g))\n        eq_(1, len(g.matches))\n        g.add_match(m)\n        eq_(2, len(g))\n        eq_(1, len(g.matches))\n\n    def test_in(self):\n        g = Group()\n        o1 = NamedObject(\"foo\", True)\n        o2 = NamedObject(\"bar\", True)\n        assert o1 not in g\n        g.add_match(get_match(o1, o2))\n        assert o1 in g\n        assert o2 in g\n\n    def test_remove(self):\n        g = Group()\n        o1 = NamedObject(\"foo\", True)\n        o2 = NamedObject(\"bar\", True)\n        o3 = NamedObject(\"bleh\", True)\n        g.add_match(get_match(o1, o2))\n        g.add_match(get_match(o1, o3))\n        g.add_match(get_match(o2, o3))\n        eq_(3, len(g.matches))\n        eq_(3, len(g))\n        g.remove_dupe(o3)\n        eq_(1, len(g.matches))\n        eq_(2, len(g))\n        g.remove_dupe(o1)\n        eq_(0, len(g.matches))\n        eq_(0, len(g))\n\n    def test_remove_with_ref_dupes(self):\n        g = Group()\n        o1 = NamedObject(\"foo\", True)\n        o2 = NamedObject(\"bar\", True)\n        o3 = NamedObject(\"bleh\", True)\n        g.add_match(get_match(o1, o2))\n        g.add_match(get_match(o1, o3))\n        g.add_match(get_match(o2, o3))\n        o1.is_ref = True\n        o2.is_ref = True\n        g.remove_dupe(o3)\n        eq_(0, len(g))\n\n    def test_switch_ref(self):\n        o1 = NamedObject(with_words=True)\n        o2 = NamedObject(with_words=True)\n        g = Group()\n        g.add_match(get_match(o1, o2))\n        assert o1 is g.ref\n        g.switch_ref(o2)\n        assert o2 is g.ref\n        eq_([o1], g.dupes)\n        g.switch_ref(o2)\n        assert o2 is g.ref\n        g.switch_ref(NamedObject(\"\", True))\n        assert o2 is g.ref\n\n    def test_switch_ref_from_ref_dir(self):\n        # When the ref dupe is from a ref dir, switch_ref() does nothing\n        o1 = no(with_words=True)\n        o2 = no(with_words=True)\n        o1.is_ref = True\n        g = Group()\n        g.add_match(get_match(o1, o2))\n        g.switch_ref(o2)\n        assert o1 is g.ref\n\n    def test_get_match_of(self):\n        g = Group()\n        for m in get_match_triangle():\n            g.add_match(m)\n        o = g.dupes[0]\n        m = g.get_match_of(o)\n        assert g.ref in m\n        assert o in m\n        assert g.get_match_of(NamedObject(\"\", True)) is None\n        assert g.get_match_of(g.ref) is None\n\n    def test_percentage(self):\n        # percentage should return the avg percentage in relation to the ref\n        m1, m2, m3 = get_match_triangle()\n        m1 = Match(m1[0], m1[1], 100)\n        m2 = Match(m2[0], m2[1], 50)\n        m3 = Match(m3[0], m3[1], 33)\n        g = Group()\n        g.add_match(m1)\n        g.add_match(m2)\n        g.add_match(m3)\n        eq_(75, g.percentage)\n        g.switch_ref(g.dupes[0])\n        eq_(66, g.percentage)\n        g.remove_dupe(g.dupes[0])\n        eq_(33, g.percentage)\n        g.add_match(m1)\n        g.add_match(m2)\n        eq_(66, g.percentage)\n\n    def test_percentage_on_empty_group(self):\n        g = Group()\n        eq_(0, g.percentage)\n\n    def test_prioritize(self):\n        m1, m2, m3 = get_match_triangle()\n        o1 = m1.first\n        o2 = m1.second\n        o3 = m2.second\n        o1.name = \"c\"\n        o2.name = \"b\"\n        o3.name = \"a\"\n        g = Group()\n        g.add_match(m1)\n        g.add_match(m2)\n        g.add_match(m3)\n        assert o1 is g.ref\n        assert g.prioritize(lambda x: x.name)\n        assert o3 is g.ref\n\n    def test_prioritize_with_tie_breaker(self):\n        # if the ref has the same key as one or more of the dupe, run the tie_breaker func among them\n        g = get_test_group()\n        o1, o2, o3 = g.ordered\n        g.prioritize(lambda x: 0, lambda ref, dupe: dupe is o3)\n        assert g.ref is o3\n\n    def test_prioritize_with_tie_breaker_runs_on_all_dupes(self):\n        # Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker\n        # with other dupes and the newly chosen ref\n        g = get_test_group()\n        o1, o2, o3 = g.ordered\n        o1.foo = 1\n        o2.foo = 2\n        o3.foo = 3\n        g.prioritize(lambda x: 0, lambda ref, dupe: dupe.foo > ref.foo)\n        assert g.ref is o3\n\n    def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self):\n        # The tie breaker only runs on dupes that had the same value for the key_func\n        g = get_test_group()\n        o1, o2, o3 = g.ordered\n        o1.foo = 2\n        o2.foo = 2\n        o3.foo = 1\n        o1.bar = 1\n        o2.bar = 2\n        o3.bar = 3\n        g.prioritize(lambda x: -x.foo, lambda ref, dupe: dupe.bar > ref.bar)\n        assert g.ref is o2\n\n    def test_prioritize_with_ref_dupe(self):\n        # when the ref dupe of a group is from a ref dir, make it stay on top.\n        g = get_test_group()\n        o1, o2, o3 = g\n        o1.is_ref = True\n        o2.size = 2\n        g.prioritize(lambda x: -x.size)\n        assert g.ref is o1\n\n    def test_prioritize_nothing_changes(self):\n        # prioritize() returns False when nothing changes in the group.\n        g = get_test_group()\n        g[0].name = \"a\"\n        g[1].name = \"b\"\n        g[2].name = \"c\"\n        assert not g.prioritize(lambda x: x.name)\n\n    def test_list_like(self):\n        g = Group()\n        o1, o2 = (NamedObject(\"foo\", True), NamedObject(\"bar\", True))\n        g.add_match(get_match(o1, o2))\n        assert g[0] is o1\n        assert g[1] is o2\n\n    def test_discard_matches(self):\n        g = Group()\n        o1, o2, o3 = (\n            NamedObject(\"foo\", True),\n            NamedObject(\"bar\", True),\n            NamedObject(\"baz\", True),\n        )\n        g.add_match(get_match(o1, o2))\n        g.add_match(get_match(o1, o3))\n        g.discard_matches()\n        eq_(1, len(g.matches))\n        eq_(0, len(g.candidates))\n\n\nclass TestCaseGetGroups:\n    def test_empty(self):\n        r = get_groups([])\n        eq_([], r)\n\n    def test_simple(self):\n        item_list = [NamedObject(\"foo bar\"), NamedObject(\"bar bleh\")]\n        matches = getmatches(item_list)\n        m = matches[0]\n        r = get_groups(matches)\n        eq_(1, len(r))\n        g = r[0]\n        assert g.ref is m.first\n        eq_([m.second], g.dupes)\n\n    def test_group_with_multiple_matches(self):\n        # This results in 3 matches\n        item_list = [NamedObject(\"foo\"), NamedObject(\"foo\"), NamedObject(\"foo\")]\n        matches = getmatches(item_list)\n        r = get_groups(matches)\n        eq_(1, len(r))\n        g = r[0]\n        eq_(3, len(g))\n\n    def test_must_choose_a_group(self):\n        item_list = [\n            NamedObject(\"a b\"),\n            NamedObject(\"a b\"),\n            NamedObject(\"b c\"),\n            NamedObject(\"c d\"),\n            NamedObject(\"c d\"),\n        ]\n        # There will be 2 groups here: group \"a b\" and group \"c d\"\n        # \"b c\" can go either of them, but not both.\n        matches = getmatches(item_list)\n        r = get_groups(matches)\n        eq_(2, len(r))\n        eq_(5, len(r[0]) + len(r[1]))\n\n    def test_should_all_go_in_the_same_group(self):\n        item_list = [\n            NamedObject(\"a b\"),\n            NamedObject(\"a b\"),\n            NamedObject(\"a b\"),\n            NamedObject(\"a b\"),\n        ]\n        # There will be 2 groups here: group \"a b\" and group \"c d\"\n        # \"b c\" can fit in both, but it must be in only one of them\n        matches = getmatches(item_list)\n        r = get_groups(matches)\n        eq_(1, len(r))\n\n    def test_give_priority_to_matches_with_higher_percentage(self):\n        o1 = NamedObject(with_words=True)\n        o2 = NamedObject(with_words=True)\n        o3 = NamedObject(with_words=True)\n        m1 = Match(o1, o2, 1)\n        m2 = Match(o2, o3, 2)\n        r = get_groups([m1, m2])\n        eq_(1, len(r))\n        g = r[0]\n        eq_(2, len(g))\n        assert o1 not in g\n        assert o2 in g\n        assert o3 in g\n\n    def test_four_sized_group(self):\n        item_list = [NamedObject(\"foobar\") for _ in range(4)]\n        m = getmatches(item_list)\n        r = get_groups(m)\n        eq_(1, len(r))\n        eq_(4, len(r[0]))\n\n    def test_referenced_by_ref2(self):\n        o1 = NamedObject(with_words=True)\n        o2 = NamedObject(with_words=True)\n        o3 = NamedObject(with_words=True)\n        m1 = get_match(o1, o2)\n        m2 = get_match(o3, o1)\n        m3 = get_match(o3, o2)\n        r = get_groups([m1, m2, m3])\n        eq_(3, len(r[0]))\n\n    def test_group_admissible_discarded_dupes(self):\n        # If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the\n        # (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D\n        # in a separate group instead of discarding them.\n        A, B, C, D = (NamedObject() for _ in range(4))\n        m1 = Match(A, B, 90)  # This is the strongest \"A\" match\n        m2 = Match(A, C, 80)  # Because C doesn't match with B, it won't be in the group\n        m3 = Match(A, D, 80)  # Same thing for D\n        m4 = Match(C, D, 70)  # However, because C and D match, they should have their own group.\n        groups = get_groups([m1, m2, m3, m4])\n        eq_(len(groups), 2)\n        g1, g2 = groups\n        assert A in g1\n        assert B in g1\n        assert C in g2\n        assert D in g2\n"
  },
  {
    "path": "core/tests/exclude_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport io\nfrom xml.etree import ElementTree as ET\n\nfrom hscommon.testutil import eq_\nfrom hscommon.plat import ISWINDOWS\n\nfrom core.tests.base import DupeGuru\nfrom core.exclude import ExcludeList, ExcludeDict, default_regexes, AlreadyThereException\n\nfrom re import error\n\n\n# Two slightly different implementations here, one around a list of lists,\n# and another around a dictionary.\n\n\nclass TestCaseListXMLLoading:\n    def setup_method(self, method):\n        self.exclude_list = ExcludeList()\n\n    def test_load_non_existant_file(self):\n        # Loads the pre-defined regexes\n        self.exclude_list.load_from_xml(\"non_existant.xml\")\n        eq_(len(default_regexes), len(self.exclude_list))\n        # they should also be marked by default\n        eq_(len(default_regexes), self.exclude_list.marked_count)\n\n    def test_save_to_xml(self):\n        f = io.BytesIO()\n        self.exclude_list.save_to_xml(f)\n        f.seek(0)\n        doc = ET.parse(f)\n        root = doc.getroot()\n        eq_(\"exclude_list\", root.tag)\n\n    def test_save_and_load(self, tmpdir):\n        e1 = ExcludeList()\n        e2 = ExcludeList()\n        eq_(len(e1), 0)\n        e1.add(r\"one\")\n        e1.mark(r\"one\")\n        e1.add(r\"two\")\n        tmpxml = str(tmpdir.join(\"exclude_testunit.xml\"))\n        e1.save_to_xml(tmpxml)\n        e2.load_from_xml(tmpxml)\n        # We should have the default regexes\n        assert r\"one\" in e2\n        assert r\"two\" in e2\n        eq_(len(e2), 2)\n        eq_(e2.marked_count, 1)\n\n    def test_load_xml_with_garbage_and_missing_elements(self):\n        root = ET.Element(\"foobar\")  # The root element shouldn't matter\n        exclude_node = ET.SubElement(root, \"bogus\")\n        exclude_node.set(\"regex\", \"None\")\n        exclude_node.set(\"marked\", \"y\")\n\n        exclude_node = ET.SubElement(root, \"exclude\")\n        exclude_node.set(\"regex\", \"one\")\n        # marked field invalid\n        exclude_node.set(\"markedddd\", \"y\")\n\n        exclude_node = ET.SubElement(root, \"exclude\")\n        exclude_node.set(\"regex\", \"two\")\n        # missing marked field\n\n        exclude_node = ET.SubElement(root, \"exclude\")\n        exclude_node.set(\"regex\", \"three\")\n        exclude_node.set(\"markedddd\", \"pazjbjepo\")\n\n        f = io.BytesIO()\n        tree = ET.ElementTree(root)\n        tree.write(f, encoding=\"utf-8\")\n        f.seek(0)\n        self.exclude_list.load_from_xml(f)\n        print(f\"{[x for x in self.exclude_list]}\")\n        # only the two \"exclude\" nodes should be added,\n        eq_(3, len(self.exclude_list))\n        # None should be marked\n        eq_(0, self.exclude_list.marked_count)\n\n\nclass TestCaseDictXMLLoading(TestCaseListXMLLoading):\n    def setup_method(self, method):\n        self.exclude_list = ExcludeDict()\n\n\nclass TestCaseListEmpty:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.app.exclude_list = ExcludeList(union_regex=False)\n        self.exclude_list = self.app.exclude_list\n\n    def test_add_mark_and_remove_regex(self):\n        regex1 = r\"one\"\n        regex2 = r\"two\"\n        self.exclude_list.add(regex1)\n        assert regex1 in self.exclude_list\n        self.exclude_list.add(regex2)\n        self.exclude_list.mark(regex1)\n        self.exclude_list.mark(regex2)\n        eq_(len(self.exclude_list), 2)\n        eq_(len(self.exclude_list.compiled), 2)\n        compiled_files = [x for x in self.exclude_list.compiled_files]\n        eq_(len(compiled_files), 2)\n        self.exclude_list.remove(regex2)\n        assert regex2 not in self.exclude_list\n        eq_(len(self.exclude_list), 1)\n\n    def test_add_duplicate(self):\n        self.exclude_list.add(r\"one\")\n        eq_(1, len(self.exclude_list))\n        try:\n            self.exclude_list.add(r\"one\")\n        except Exception:\n            pass\n        eq_(1, len(self.exclude_list))\n\n    def test_add_not_compilable(self):\n        # Trying to add a non-valid regex should not work and raise exception\n        regex = r\"one))\"\n        try:\n            self.exclude_list.add(regex)\n        except Exception as e:\n            # Make sure we raise a re.error so that the interface can process it\n            eq_(type(e), error)\n        added = self.exclude_list.mark(regex)\n        eq_(added, False)\n        eq_(len(self.exclude_list), 0)\n        eq_(len(self.exclude_list.compiled), 0)\n        compiled_files = [x for x in self.exclude_list.compiled_files]\n        eq_(len(compiled_files), 0)\n\n    def test_force_add_not_compilable(self):\n        \"\"\"Used when loading from XML for example\"\"\"\n        regex = r\"one))\"\n        self.exclude_list.add(regex, forced=True)\n        marked = self.exclude_list.mark(regex)\n        eq_(marked, False)  # can't be marked since not compilable\n        eq_(len(self.exclude_list), 1)\n        eq_(len(self.exclude_list.compiled), 0)\n        compiled_files = [x for x in self.exclude_list.compiled_files]\n        eq_(len(compiled_files), 0)\n        # adding a duplicate\n        regex = r\"one))\"\n        try:\n            self.exclude_list.add(regex, forced=True)\n        except Exception as e:\n            # we should have this exception, and it shouldn't be added\n            assert type(e) is AlreadyThereException\n        eq_(len(self.exclude_list), 1)\n        eq_(len(self.exclude_list.compiled), 0)\n\n    def test_rename_regex(self):\n        regex = r\"one\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        regex_renamed = r\"one))\"\n        # Not compilable, can't be marked\n        self.exclude_list.rename(regex, regex_renamed)\n        assert regex not in self.exclude_list\n        assert regex_renamed in self.exclude_list\n        eq_(self.exclude_list.is_marked(regex_renamed), False)\n        self.exclude_list.mark(regex_renamed)\n        eq_(self.exclude_list.is_marked(regex_renamed), False)\n        regex_renamed_compilable = r\"two\"\n        self.exclude_list.rename(regex_renamed, regex_renamed_compilable)\n        assert regex_renamed_compilable in self.exclude_list\n        eq_(self.exclude_list.is_marked(regex_renamed), False)\n        self.exclude_list.mark(regex_renamed_compilable)\n        eq_(self.exclude_list.is_marked(regex_renamed_compilable), True)\n        eq_(len(self.exclude_list), 1)\n        # Should still be marked after rename\n        regex_compilable = r\"three\"\n        self.exclude_list.rename(regex_renamed_compilable, regex_compilable)\n        eq_(self.exclude_list.is_marked(regex_compilable), True)\n\n    def test_rename_regex_file_to_path(self):\n        regex = r\".*/one.*\"\n        if ISWINDOWS:\n            regex = r\".*\\\\one.*\"\n        regex2 = r\".*one.*\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex in compiled_re\n        assert regex not in files_re\n        assert regex in paths_re\n        self.exclude_list.rename(regex, regex2)\n        compiled_re = [x.pattern for x in self.exclude_list._excluded_compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex not in compiled_re\n        assert regex2 in compiled_re\n        assert regex2 in files_re\n        assert regex2 not in paths_re\n\n    def test_restore_default(self):\n        \"\"\"Only unmark previously added regexes and mark the pre-defined ones\"\"\"\n        regex = r\"one\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        self.exclude_list.restore_defaults()\n        eq_(len(default_regexes), self.exclude_list.marked_count)\n        # added regex shouldn't be marked\n        eq_(self.exclude_list.is_marked(regex), False)\n        # added regex shouldn't be in compiled list either\n        compiled = [x for x in self.exclude_list.compiled]\n        assert regex not in compiled\n        # Only default regexes marked and in compiled list\n        for re in default_regexes:\n            assert self.exclude_list.is_marked(re)\n            found = False\n            for compiled_re in compiled:\n                if compiled_re.pattern == re:\n                    found = True\n            if not found:\n                raise (Exception(f\"Default RE {re} not found in compiled list.\"))\n        eq_(len(default_regexes), len(self.exclude_list.compiled))\n\n\nclass TestCaseListEmptyUnion(TestCaseListEmpty):\n    \"\"\"Same but with union regex\"\"\"\n\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.app.exclude_list = ExcludeList(union_regex=True)\n        self.exclude_list = self.app.exclude_list\n\n    def test_add_mark_and_remove_regex(self):\n        regex1 = r\"one\"\n        regex2 = r\"two\"\n        self.exclude_list.add(regex1)\n        assert regex1 in self.exclude_list\n        self.exclude_list.add(regex2)\n        self.exclude_list.mark(regex1)\n        self.exclude_list.mark(regex2)\n        eq_(len(self.exclude_list), 2)\n        eq_(len(self.exclude_list.compiled), 1)\n        compiled_files = [x for x in self.exclude_list.compiled_files]\n        eq_(len(compiled_files), 1)  # Two patterns joined together into one\n        assert \"|\" in compiled_files[0].pattern\n        self.exclude_list.remove(regex2)\n        assert regex2 not in self.exclude_list\n        eq_(len(self.exclude_list), 1)\n\n    def test_rename_regex_file_to_path(self):\n        regex = r\".*/one.*\"\n        if ISWINDOWS:\n            regex = r\".*\\\\one.*\"\n        regex2 = r\".*one.*\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        eq_(len([x for x in self.exclude_list]), 1)\n        compiled_re = [x.pattern for x in self.exclude_list.compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex in compiled_re\n        assert regex not in files_re\n        assert regex in paths_re\n        self.exclude_list.rename(regex, regex2)\n        eq_(len([x for x in self.exclude_list]), 1)\n        compiled_re = [x.pattern for x in self.exclude_list.compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex not in compiled_re\n        assert regex2 in compiled_re\n        assert regex2 in files_re\n        assert regex2 not in paths_re\n\n    def test_restore_default(self):\n        \"\"\"Only unmark previously added regexes and mark the pre-defined ones\"\"\"\n        regex = r\"one\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        self.exclude_list.restore_defaults()\n        eq_(len(default_regexes), self.exclude_list.marked_count)\n        # added regex shouldn't be marked\n        eq_(self.exclude_list.is_marked(regex), False)\n        # added regex shouldn't be in compiled list either\n        compiled = [x for x in self.exclude_list.compiled]\n        assert regex not in compiled\n        # Need to escape both to get the same strings after compilation\n        compiled_escaped = {x.encode(\"unicode-escape\").decode() for x in compiled[0].pattern.split(\"|\")}\n        default_escaped = {x.encode(\"unicode-escape\").decode() for x in default_regexes}\n        assert compiled_escaped == default_escaped\n        eq_(len(default_regexes), len(compiled[0].pattern.split(\"|\")))\n\n\nclass TestCaseDictEmpty(TestCaseListEmpty):\n    \"\"\"Same, but with dictionary implementation\"\"\"\n\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.app.exclude_list = ExcludeDict(union_regex=False)\n        self.exclude_list = self.app.exclude_list\n\n\nclass TestCaseDictEmptyUnion(TestCaseDictEmpty):\n    \"\"\"Same, but with union regex\"\"\"\n\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.app.exclude_list = ExcludeDict(union_regex=True)\n        self.exclude_list = self.app.exclude_list\n\n    def test_add_mark_and_remove_regex(self):\n        regex1 = r\"one\"\n        regex2 = r\"two\"\n        self.exclude_list.add(regex1)\n        assert regex1 in self.exclude_list\n        self.exclude_list.add(regex2)\n        self.exclude_list.mark(regex1)\n        self.exclude_list.mark(regex2)\n        eq_(len(self.exclude_list), 2)\n        eq_(len(self.exclude_list.compiled), 1)\n        compiled_files = [x for x in self.exclude_list.compiled_files]\n        # two patterns joined into one\n        eq_(len(compiled_files), 1)\n        self.exclude_list.remove(regex2)\n        assert regex2 not in self.exclude_list\n        eq_(len(self.exclude_list), 1)\n\n    def test_rename_regex_file_to_path(self):\n        regex = r\".*/one.*\"\n        if ISWINDOWS:\n            regex = r\".*\\\\one.*\"\n        regex2 = r\".*one.*\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        marked_re = [x for marked, x in self.exclude_list if marked]\n        eq_(len(marked_re), 1)\n        compiled_re = [x.pattern for x in self.exclude_list.compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex in compiled_re\n        assert regex not in files_re\n        assert regex in paths_re\n        self.exclude_list.rename(regex, regex2)\n        compiled_re = [x.pattern for x in self.exclude_list.compiled]\n        files_re = [x.pattern for x in self.exclude_list.compiled_files]\n        paths_re = [x.pattern for x in self.exclude_list.compiled_paths]\n        assert regex not in compiled_re\n        assert regex2 in compiled_re\n        assert regex2 in files_re\n        assert regex2 not in paths_re\n\n    def test_restore_default(self):\n        \"\"\"Only unmark previously added regexes and mark the pre-defined ones\"\"\"\n        regex = r\"one\"\n        self.exclude_list.add(regex)\n        self.exclude_list.mark(regex)\n        self.exclude_list.restore_defaults()\n        eq_(len(default_regexes), self.exclude_list.marked_count)\n        # added regex shouldn't be marked\n        eq_(self.exclude_list.is_marked(regex), False)\n        # added regex shouldn't be in compiled list either\n        compiled = [x for x in self.exclude_list.compiled]\n        assert regex not in compiled\n        # Need to escape both to get the same strings after compilation\n        compiled_escaped = {x.encode(\"unicode-escape\").decode() for x in compiled[0].pattern.split(\"|\")}\n        default_escaped = {x.encode(\"unicode-escape\").decode() for x in default_regexes}\n        assert compiled_escaped == default_escaped\n        eq_(len(default_regexes), len(compiled[0].pattern.split(\"|\")))\n\n\ndef split_union(pattern_object):\n    \"\"\"Returns list of strings for each union pattern\"\"\"\n    return [x for x in pattern_object.pattern.split(\"|\")]\n\n\nclass TestCaseCompiledList:\n    \"\"\"Test consistency between union or and separate versions.\"\"\"\n\n    def setup_method(self, method):\n        self.e_separate = ExcludeList(union_regex=False)\n        self.e_separate.restore_defaults()\n        self.e_union = ExcludeList(union_regex=True)\n        self.e_union.restore_defaults()\n\n    def test_same_number_of_expressions(self):\n        # We only get one union Pattern item in a tuple, which is made of however many parts\n        eq_(len(split_union(self.e_union.compiled[0])), len(default_regexes))\n        # We get as many as there are marked items\n        eq_(len(self.e_separate.compiled), len(default_regexes))\n        exprs = split_union(self.e_union.compiled[0])\n        # We should have the same number and the same expressions\n        eq_(len(exprs), len(self.e_separate.compiled))\n        for expr in self.e_separate.compiled:\n            assert expr.pattern in exprs\n\n    def test_compiled_files(self):\n        # is path separator checked properly to yield the output\n        if ISWINDOWS:\n            regex1 = r\"test\\\\one\\\\sub\"\n        else:\n            regex1 = r\"test/one/sub\"\n        self.e_separate.add(regex1)\n        self.e_separate.mark(regex1)\n        self.e_union.add(regex1)\n        self.e_union.mark(regex1)\n        separate_compiled_dirs = self.e_separate.compiled\n        separate_compiled_files = [x for x in self.e_separate.compiled_files]\n        # HACK we need to call compiled property FIRST to generate the cache\n        union_compiled_dirs = self.e_union.compiled\n        # print(f\"type: {type(self.e_union.compiled_files[0])}\")\n        # A generator returning only one item... ugh\n        union_compiled_files = [x for x in self.e_union.compiled_files][0]\n        print(f\"compiled files: {union_compiled_files}\")\n        # Separate should give several plus the one added\n        eq_(len(separate_compiled_dirs), len(default_regexes) + 1)\n        # regex1 shouldn't be in the \"files\" version\n        eq_(len(separate_compiled_files), len(default_regexes))\n        # Only one Pattern returned, which when split should be however many + 1\n        eq_(len(split_union(union_compiled_dirs[0])), len(default_regexes) + 1)\n        # regex1 shouldn't be here either\n        eq_(len(split_union(union_compiled_files)), len(default_regexes))\n\n\nclass TestCaseCompiledDict(TestCaseCompiledList):\n    \"\"\"Test the dictionary version\"\"\"\n\n    def setup_method(self, method):\n        self.e_separate = ExcludeDict(union_regex=False)\n        self.e_separate.restore_defaults()\n        self.e_union = ExcludeDict(union_regex=True)\n        self.e_union.restore_defaults()\n"
  },
  {
    "path": "core/tests/fs_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-10-23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport typing\nfrom os import urandom\n\nfrom pathlib import Path\nfrom hscommon.testutil import eq_\nfrom core.tests.directories_test import create_fake_fs\n\nfrom core import fs\n\nhasher: typing.Callable\ntry:\n    import xxhash\n\n    hasher = xxhash.xxh128\nexcept ImportError:\n    import hashlib\n\n    hasher = hashlib.md5\n\n\ndef create_fake_fs_with_random_data(rootpath):\n    rootpath = rootpath.joinpath(\"fs\")\n    rootpath.mkdir()\n    rootpath.joinpath(\"dir1\").mkdir()\n    rootpath.joinpath(\"dir2\").mkdir()\n    rootpath.joinpath(\"dir3\").mkdir()\n    data1 = urandom(200 * 1024)  # 200KiB\n    data2 = urandom(1024 * 1024)  # 1MiB\n    data3 = urandom(10 * 1024 * 1024)  # 10MiB\n    with rootpath.joinpath(\"file1.test\").open(\"wb\") as fp:\n        fp.write(data1)\n    with rootpath.joinpath(\"file2.test\").open(\"wb\") as fp:\n        fp.write(data2)\n    with rootpath.joinpath(\"file3.test\").open(\"wb\") as fp:\n        fp.write(data3)\n    with rootpath.joinpath(\"dir1\", \"file1.test\").open(\"wb\") as fp:\n        fp.write(data1)\n    with rootpath.joinpath(\"dir2\", \"file2.test\").open(\"wb\") as fp:\n        fp.write(data2)\n    with rootpath.joinpath(\"dir3\", \"file3.test\").open(\"wb\") as fp:\n        fp.write(data3)\n    return rootpath\n\n\ndef test_size_aggregates_subfiles(tmpdir):\n    p = create_fake_fs(Path(str(tmpdir)))\n    b = fs.Folder(p)\n    eq_(b.size, 12)\n\n\ndef test_digest_aggregate_subfiles_sorted(tmpdir):\n    # dir.allfiles can return child in any order. Thus, bundle.digest must aggregate\n    # all files' digests it contains, but it must make sure that it does so in the\n    # same order everytime.\n    p = create_fake_fs_with_random_data(Path(str(tmpdir)))\n    b = fs.Folder(p)\n    digest1 = fs.File(p.joinpath(\"dir1\", \"file1.test\")).digest\n    digest2 = fs.File(p.joinpath(\"dir2\", \"file2.test\")).digest\n    digest3 = fs.File(p.joinpath(\"dir3\", \"file3.test\")).digest\n    digest4 = fs.File(p.joinpath(\"file1.test\")).digest\n    digest5 = fs.File(p.joinpath(\"file2.test\")).digest\n    digest6 = fs.File(p.joinpath(\"file3.test\")).digest\n    # The expected digest is the hash of digests for folders and the direct digest for files\n    folder_digest1 = hasher(digest1).digest()\n    folder_digest2 = hasher(digest2).digest()\n    folder_digest3 = hasher(digest3).digest()\n    digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()\n    eq_(b.digest, digest)\n\n\ndef test_partial_digest_aggregate_subfile_sorted(tmpdir):\n    p = create_fake_fs_with_random_data(Path(str(tmpdir)))\n    b = fs.Folder(p)\n    digest1 = fs.File(p.joinpath(\"dir1\", \"file1.test\")).digest_partial\n    digest2 = fs.File(p.joinpath(\"dir2\", \"file2.test\")).digest_partial\n    digest3 = fs.File(p.joinpath(\"dir3\", \"file3.test\")).digest_partial\n    digest4 = fs.File(p.joinpath(\"file1.test\")).digest_partial\n    digest5 = fs.File(p.joinpath(\"file2.test\")).digest_partial\n    digest6 = fs.File(p.joinpath(\"file3.test\")).digest_partial\n    # The expected digest is the hash of digests for folders and the direct digest for files\n    folder_digest1 = hasher(digest1).digest()\n    folder_digest2 = hasher(digest2).digest()\n    folder_digest3 = hasher(digest3).digest()\n    digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()\n    eq_(b.digest_partial, digest)\n\n    digest1 = fs.File(p.joinpath(\"dir1\", \"file1.test\")).digest_samples\n    digest2 = fs.File(p.joinpath(\"dir2\", \"file2.test\")).digest_samples\n    digest3 = fs.File(p.joinpath(\"dir3\", \"file3.test\")).digest_samples\n    digest4 = fs.File(p.joinpath(\"file1.test\")).digest_samples\n    digest5 = fs.File(p.joinpath(\"file2.test\")).digest_samples\n    digest6 = fs.File(p.joinpath(\"file3.test\")).digest_samples\n    # The expected digest is the digest of digests for folders and the direct digest for files\n    folder_digest1 = hasher(digest1).digest()\n    folder_digest2 = hasher(digest2).digest()\n    folder_digest3 = hasher(digest3).digest()\n    digest = hasher(folder_digest1 + folder_digest2 + folder_digest3 + digest4 + digest5 + digest6).digest()\n    eq_(b.digest_samples, digest)\n\n\ndef test_has_file_attrs(tmpdir):\n    # a Folder must behave like a file, so it must have mtime attributes\n    b = fs.Folder(Path(str(tmpdir)))\n    assert b.mtime > 0\n    eq_(b.extension, \"\")\n"
  },
  {
    "path": "core/tests/ignore_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport io\nfrom xml.etree import ElementTree as ET\n\nfrom pytest import raises\nfrom hscommon.testutil import eq_\n\nfrom core.ignore import IgnoreList\n\n\ndef test_empty():\n    il = IgnoreList()\n    eq_(0, len(il))\n    assert not il.are_ignored(\"foo\", \"bar\")\n\n\ndef test_simple():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    assert il.are_ignored(\"foo\", \"bar\")\n    assert il.are_ignored(\"bar\", \"foo\")\n    assert not il.are_ignored(\"foo\", \"bleh\")\n    assert not il.are_ignored(\"bleh\", \"bar\")\n    eq_(1, len(il))\n\n\ndef test_multiple():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"foo\", \"bleh\")\n    il.ignore(\"bleh\", \"bar\")\n    il.ignore(\"aybabtu\", \"bleh\")\n    assert il.are_ignored(\"foo\", \"bar\")\n    assert il.are_ignored(\"bar\", \"foo\")\n    assert il.are_ignored(\"foo\", \"bleh\")\n    assert il.are_ignored(\"bleh\", \"bar\")\n    assert not il.are_ignored(\"aybabtu\", \"bar\")\n    eq_(4, len(il))\n\n\ndef test_clear():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.clear()\n    assert not il.are_ignored(\"foo\", \"bar\")\n    assert not il.are_ignored(\"bar\", \"foo\")\n    eq_(0, len(il))\n\n\ndef test_add_same_twice():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"bar\", \"foo\")\n    eq_(1, len(il))\n\n\ndef test_save_to_xml():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"foo\", \"bleh\")\n    il.ignore(\"bleh\", \"bar\")\n    f = io.BytesIO()\n    il.save_to_xml(f)\n    f.seek(0)\n    doc = ET.parse(f)\n    root = doc.getroot()\n    eq_(root.tag, \"ignore_list\")\n    eq_(len(root), 2)\n    eq_(len([c for c in root if c.tag == \"file\"]), 2)\n    f1, f2 = root[:]\n    subchildren = [c for c in f1 if c.tag == \"file\"] + [c for c in f2 if c.tag == \"file\"]\n    eq_(len(subchildren), 3)\n\n\ndef test_save_then_load():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"foo\", \"bleh\")\n    il.ignore(\"bleh\", \"bar\")\n    il.ignore(\"\\u00e9\", \"bar\")\n    f = io.BytesIO()\n    il.save_to_xml(f)\n    f.seek(0)\n    il = IgnoreList()\n    il.load_from_xml(f)\n    eq_(4, len(il))\n    assert il.are_ignored(\"\\u00e9\", \"bar\")\n\n\ndef test_load_xml_with_empty_file_tags():\n    f = io.BytesIO()\n    f.write(b'<?xml version=\"1.0\" encoding=\"utf-8\"?><ignore_list><file><file/></file></ignore_list>')\n    f.seek(0)\n    il = IgnoreList()\n    il.load_from_xml(f)\n    eq_(0, len(il))\n\n\ndef test_are_ignore_works_when_a_child_is_a_key_somewhere_else():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"bar\", \"baz\")\n    assert il.are_ignored(\"bar\", \"foo\")\n\n\ndef test_no_dupes_when_a_child_is_a_key_somewhere_else():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"bar\", \"baz\")\n    il.ignore(\"bar\", \"foo\")\n    eq_(2, len(il))\n\n\ndef test_iterate():\n    # It must be possible to iterate through ignore list\n    il = IgnoreList()\n    expected = [(\"foo\", \"bar\"), (\"bar\", \"baz\"), (\"foo\", \"baz\")]\n    for i in expected:\n        il.ignore(i[0], i[1])\n    for i in il:\n        expected.remove(i)  # No exception should be raised\n    assert not expected  # expected should be empty\n\n\ndef test_filter():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"bar\", \"baz\")\n    il.ignore(\"foo\", \"baz\")\n    il.filter(lambda f, s: f == \"bar\")\n    eq_(1, len(il))\n    assert not il.are_ignored(\"foo\", \"bar\")\n    assert il.are_ignored(\"bar\", \"baz\")\n\n\ndef test_save_with_non_ascii_items():\n    il = IgnoreList()\n    il.ignore(\"\\xac\", \"\\xbf\")\n    f = io.BytesIO()\n    try:\n        il.save_to_xml(f)\n    except Exception as e:\n        raise AssertionError(str(e))\n\n\ndef test_len():\n    il = IgnoreList()\n    eq_(0, len(il))\n    il.ignore(\"foo\", \"bar\")\n    eq_(1, len(il))\n\n\ndef test_nonzero():\n    il = IgnoreList()\n    assert not il\n    il.ignore(\"foo\", \"bar\")\n    assert il\n\n\ndef test_remove():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"foo\", \"baz\")\n    il.remove(\"bar\", \"foo\")\n    eq_(len(il), 1)\n    assert not il.are_ignored(\"foo\", \"bar\")\n\n\ndef test_remove_non_existant():\n    il = IgnoreList()\n    il.ignore(\"foo\", \"bar\")\n    il.ignore(\"foo\", \"baz\")\n    with raises(ValueError):\n        il.remove(\"foo\", \"bleh\")\n"
  },
  {
    "path": "core/tests/markable_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import eq_\n\nfrom core.markable import MarkableList, Markable\n\n\ndef gen():\n    ml = MarkableList()\n    ml.extend(list(range(10)))\n    return ml\n\n\ndef test_unmarked():\n    ml = gen()\n    for i in ml:\n        assert not ml.is_marked(i)\n\n\ndef test_mark():\n    ml = gen()\n    assert ml.mark(3)\n    assert ml.is_marked(3)\n    assert not ml.is_marked(2)\n\n\ndef test_unmark():\n    ml = gen()\n    ml.mark(4)\n    assert ml.unmark(4)\n    assert not ml.is_marked(4)\n\n\ndef test_unmark_unmarked():\n    ml = gen()\n    assert not ml.unmark(4)\n    assert not ml.is_marked(4)\n\n\ndef test_mark_twice_and_unmark():\n    ml = gen()\n    assert ml.mark(5)\n    assert not ml.mark(5)\n    ml.unmark(5)\n    assert not ml.is_marked(5)\n\n\ndef test_mark_toggle():\n    ml = gen()\n    ml.mark_toggle(6)\n    assert ml.is_marked(6)\n    ml.mark_toggle(6)\n    assert not ml.is_marked(6)\n    ml.mark_toggle(6)\n    assert ml.is_marked(6)\n\n\ndef test_is_markable():\n    class Foobar(Markable):\n        def _is_markable(self, o):\n            return o == \"foobar\"\n\n    f = Foobar()\n    assert not f.is_marked(\"foobar\")\n    assert not f.mark(\"foo\")\n    assert not f.is_marked(\"foo\")\n    f.mark_toggle(\"foo\")\n    assert not f.is_marked(\"foo\")\n    f.mark(\"foobar\")\n    assert f.is_marked(\"foobar\")\n    ml = gen()\n    ml.mark(11)\n    assert not ml.is_marked(11)\n\n\ndef test_change_notifications():\n    class Foobar(Markable):\n        def _did_mark(self, o):\n            self.log.append((True, o))\n\n        def _did_unmark(self, o):\n            self.log.append((False, o))\n\n    f = Foobar()\n    f.log = []\n    f.mark(\"foo\")\n    f.mark(\"foo\")\n    f.mark_toggle(\"bar\")\n    f.unmark(\"foo\")\n    f.unmark(\"foo\")\n    f.mark_toggle(\"bar\")\n    eq_([(True, \"foo\"), (True, \"bar\"), (False, \"foo\"), (False, \"bar\")], f.log)\n\n\ndef test_mark_count():\n    ml = gen()\n    eq_(0, ml.mark_count)\n    ml.mark(7)\n    eq_(1, ml.mark_count)\n    ml.mark(11)\n    eq_(1, ml.mark_count)\n\n\ndef test_mark_none():\n    log = []\n    ml = gen()\n    ml._did_unmark = lambda o: log.append(o)\n    ml.mark(1)\n    ml.mark(2)\n    eq_(2, ml.mark_count)\n    ml.mark_none()\n    eq_(0, ml.mark_count)\n    eq_([1, 2], log)\n\n\ndef test_mark_all():\n    ml = gen()\n    eq_(0, ml.mark_count)\n    ml.mark_all()\n    eq_(10, ml.mark_count)\n    assert ml.is_marked(1)\n\n\ndef test_mark_invert():\n    ml = gen()\n    ml.mark(1)\n    ml.mark_invert()\n    assert not ml.is_marked(1)\n    assert ml.is_marked(2)\n\n\ndef test_mark_while_inverted():\n    log = []\n    ml = gen()\n    ml._did_unmark = lambda o: log.append((False, o))\n    ml._did_mark = lambda o: log.append((True, o))\n    ml.mark(1)\n    ml.mark_invert()\n    assert ml.mark_inverted\n    assert ml.mark(1)\n    assert ml.unmark(2)\n    assert ml.unmark(1)\n    ml.mark_toggle(3)\n    assert not ml.is_marked(3)\n    eq_(7, ml.mark_count)\n    eq_([(True, 1), (False, 1), (True, 2), (True, 1), (True, 3)], log)\n\n\ndef test_remove_mark_flag():\n    ml = gen()\n    ml.mark(1)\n    ml._remove_mark_flag(1)\n    assert not ml.is_marked(1)\n    ml.mark(1)\n    ml.mark_invert()\n    assert not ml.is_marked(1)\n    ml._remove_mark_flag(1)\n    assert ml.is_marked(1)\n\n\ndef test_is_marked_returns_false_if_object_not_markable():\n    class MyMarkableList(MarkableList):\n        def _is_markable(self, o):\n            return o != 4\n\n    ml = MyMarkableList()\n    ml.extend(list(range(10)))\n    ml.mark_invert()\n    assert ml.is_marked(1)\n    assert not ml.is_marked(4)\n"
  },
  {
    "path": "core/tests/prioritize_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011/09/07\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os.path as op\nfrom itertools import combinations\n\nfrom core.tests.base import TestApp, NamedObject, with_app, eq_\nfrom core.engine import Group, Match\n\nno = NamedObject\n\n\ndef app_with_dupes(dupes):\n    # Creates an app with specified dupes. dupes is a list of lists, each list in the list being\n    # a dupe group. We cheat a little bit by creating dupe groups manually instead of running a\n    # dupe scan, but it simplifies the test code quite a bit\n    app = TestApp()\n    groups = []\n    for dupelist in dupes:\n        g = Group()\n        for dupe1, dupe2 in combinations(dupelist, 2):\n            g.add_match(Match(dupe1, dupe2, 100))\n        groups.append(g)\n    app.app.results.groups = groups\n    app.app._results_changed()\n    return app\n\n\n# ---\ndef app_normal_results():\n    # Just some results, with different extensions and size, for good measure.\n    dupes = [\n        [\n            no(\"foo1.ext1\", size=1, folder=\"folder1\"),\n            no(\"foo2.ext2\", size=2, folder=\"folder2\"),\n        ],\n    ]\n    return app_with_dupes(dupes)\n\n\n@with_app(app_normal_results)\ndef test_kind_subcrit(app):\n    # The subcriteria of the \"Kind\" criteria is a list of extensions contained in the dupes.\n    app.select_pri_criterion(\"Kind\")\n    eq_(app.pdialog.criteria_list[:], [\"ext1\", \"ext2\"])\n\n\n@with_app(app_normal_results)\ndef test_kind_reprioritization(app):\n    # Just a simple test of the system as a whole.\n    # select a criterion, and perform re-prioritization and see if it worked.\n    app.select_pri_criterion(\"Kind\")\n    app.pdialog.criteria_list.select([1])  # ext2\n    app.pdialog.add_selected()\n    app.pdialog.perform_reprioritization()\n    eq_(app.rtable[0].data[\"name\"], \"foo2.ext2\")\n\n\n@with_app(app_normal_results)\ndef test_folder_subcrit(app):\n    app.select_pri_criterion(\"Folder\")\n    eq_(app.pdialog.criteria_list[:], [\"folder1\", \"folder2\"])\n\n\n@with_app(app_normal_results)\ndef test_folder_reprioritization(app):\n    app.select_pri_criterion(\"Folder\")\n    app.pdialog.criteria_list.select([1])  # folder2\n    app.pdialog.add_selected()\n    app.pdialog.perform_reprioritization()\n    eq_(app.rtable[0].data[\"name\"], \"foo2.ext2\")\n\n\n@with_app(app_normal_results)\ndef test_prilist_display(app):\n    # The prioritization list displays selected criteria correctly.\n    app.select_pri_criterion(\"Kind\")\n    app.pdialog.criteria_list.select([1])  # ext2\n    app.pdialog.add_selected()\n    app.select_pri_criterion(\"Folder\")\n    app.pdialog.criteria_list.select([1])  # folder2\n    app.pdialog.add_selected()\n    app.select_pri_criterion(\"Size\")\n    app.pdialog.criteria_list.select([1])  # Lowest\n    app.pdialog.add_selected()\n    expected = [\n        \"Kind (ext2)\",\n        \"Folder (folder2)\",\n        \"Size (Lowest)\",\n    ]\n    eq_(app.pdialog.prioritization_list[:], expected)\n\n\n@with_app(app_normal_results)\ndef test_size_subcrit(app):\n    app.select_pri_criterion(\"Size\")\n    eq_(app.pdialog.criteria_list[:], [\"Highest\", \"Lowest\"])\n\n\n@with_app(app_normal_results)\ndef test_size_reprioritization(app):\n    app.select_pri_criterion(\"Size\")\n    app.pdialog.criteria_list.select([0])  # highest\n    app.pdialog.add_selected()\n    app.pdialog.perform_reprioritization()\n    eq_(app.rtable[0].data[\"name\"], \"foo2.ext2\")\n\n\n@with_app(app_normal_results)\ndef test_reorder_prioritizations(app):\n    app.add_pri_criterion(\"Kind\", 0)  # ext1\n    app.add_pri_criterion(\"Kind\", 1)  # ext2\n    app.pdialog.prioritization_list.move_indexes([1], 0)\n    expected = [\n        \"Kind (ext2)\",\n        \"Kind (ext1)\",\n    ]\n    eq_(app.pdialog.prioritization_list[:], expected)\n\n\n@with_app(app_normal_results)\ndef test_remove_crit_from_list(app):\n    app.add_pri_criterion(\"Kind\", 0)\n    app.add_pri_criterion(\"Kind\", 1)\n    app.pdialog.prioritization_list.select(0)\n    app.pdialog.remove_selected()\n    expected = [\n        \"Kind (ext2)\",\n    ]\n    eq_(app.pdialog.prioritization_list[:], expected)\n\n\n@with_app(app_normal_results)\ndef test_add_crit_without_selection(app):\n    # Adding a criterion without having made a selection doesn't cause a crash.\n    app.pdialog.add_selected()  # no crash\n\n\n# ---\ndef app_one_name_ends_with_number():\n    dupes = [\n        [no(\"foo.ext\"), no(\"foo1.ext\")],\n    ]\n    return app_with_dupes(dupes)\n\n\n@with_app(app_one_name_ends_with_number)\ndef test_filename_reprioritization(app):\n    app.add_pri_criterion(\"Filename\", 0)  # Ends with a number\n    app.pdialog.perform_reprioritization()\n    eq_(app.rtable[0].data[\"name\"], \"foo1.ext\")\n\n\n# ---\ndef app_with_subfolders():\n    dupes = [\n        [no(\"foo1\", folder=\"baz\"), no(\"foo2\", folder=\"foo/bar\")],\n        [no(\"foo3\", folder=\"baz\"), no(\"foo4\", folder=\"foo\")],\n    ]\n    return app_with_dupes(dupes)\n\n\n@with_app(app_with_subfolders)\ndef test_folder_crit_is_sorted(app):\n    # Folder subcriteria are sorted.\n    app.select_pri_criterion(\"Folder\")\n    eq_(app.pdialog.criteria_list[:], [\"baz\", \"foo\", op.join(\"foo\", \"bar\")])\n\n\n@with_app(app_with_subfolders)\ndef test_folder_crit_includes_subfolders(app):\n    # When selecting a folder crit, dupes in a subfolder are also considered as affected by that\n    # crit.\n    app.add_pri_criterion(\"Folder\", 1)  # foo\n    app.pdialog.perform_reprioritization()\n    # Both foo and foo/bar dupes will be prioritized\n    eq_(app.rtable[0].data[\"name\"], \"foo2\")\n    eq_(app.rtable[2].data[\"name\"], \"foo4\")\n\n\n@with_app(app_with_subfolders)\ndef test_display_something_on_empty_extensions(app):\n    # When there's no extension, display \"None\" instead of nothing at all.\n    app.select_pri_criterion(\"Kind\")\n    eq_(app.pdialog.criteria_list[:], [\"None\"])\n\n\n# ---\ndef app_one_name_longer_than_the_other():\n    dupes = [\n        [no(\"shortest.ext\"), no(\"loooongest.ext\")],\n    ]\n    return app_with_dupes(dupes)\n\n\n@with_app(app_one_name_longer_than_the_other)\ndef test_longest_filename_prioritization(app):\n    app.add_pri_criterion(\"Filename\", 2)  # Longest\n    app.pdialog.perform_reprioritization()\n    eq_(app.rtable[0].data[\"name\"], \"loooongest.ext\")\n"
  },
  {
    "path": "core/tests/result_table_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2013-07-28\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom core.tests.base import TestApp, GetTestGroups\n\n\ndef app_with_results():\n    app = TestApp()\n    objects, matches, groups = GetTestGroups()\n    app.app.results.groups = groups\n    app.rtable.refresh()\n    return app\n\n\ndef test_delta_flags_delta_mode_off():\n    app = app_with_results()\n    # When the delta mode is off, we never have delta values flags\n    app.rtable.delta_values = False\n    # Ref file, always false anyway\n    assert not app.rtable[0].is_cell_delta(\"size\")\n    # False because delta mode is off\n    assert not app.rtable[1].is_cell_delta(\"size\")\n\n\ndef test_delta_flags_delta_mode_on_delta_columns():\n    # When the delta mode is on, delta columns always have a delta flag, except for ref rows\n    app = app_with_results()\n    app.rtable.delta_values = True\n    # Ref file, always false anyway\n    assert not app.rtable[0].is_cell_delta(\"size\")\n    # But for a dupe, the flag is on\n    assert app.rtable[1].is_cell_delta(\"size\")\n\n\ndef test_delta_flags_delta_mode_on_non_delta_columns():\n    # When the delta mode is on, non-delta columns have a delta flag if their value differs from\n    # their ref.\n    app = app_with_results()\n    app.rtable.delta_values = True\n    # \"bar bleh\" != \"foo bar\", flag on\n    assert app.rtable[1].is_cell_delta(\"name\")\n    # \"ibabtu\" row, but it's a ref, flag off\n    assert not app.rtable[3].is_cell_delta(\"name\")\n    # \"ibabtu\" == \"ibabtu\", flag off\n    assert not app.rtable[4].is_cell_delta(\"name\")\n\n\ndef test_delta_flags_delta_mode_on_non_delta_columns_case_insensitive():\n    # Comparison that occurs for non-numeric columns to check whether they're delta is case\n    # insensitive\n    app = app_with_results()\n    app.app.results.groups[1].ref.name = \"ibAbtu\"\n    app.app.results.groups[1].dupes[0].name = \"IBaBTU\"\n    app.rtable.delta_values = True\n    # \"ibAbtu\" == \"IBaBTU\", flag off\n    assert not app.rtable[4].is_cell_delta(\"name\")\n"
  },
  {
    "path": "core/tests/results_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport io\nimport os.path as op\n\nfrom xml.etree import ElementTree as ET\n\nfrom pytest import raises\nfrom hscommon.testutil import eq_\nfrom hscommon.util import first\nfrom core import engine\nfrom core.tests.base import NamedObject, GetTestGroups, DupeGuru\nfrom core.results import Results\n\n\nclass TestCaseResultsEmpty:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n\n    def test_apply_invalid_filter(self):\n        # If the applied filter is an invalid regexp, just ignore the filter.\n        self.results.apply_filter(\"[\")  # invalid\n        self.test_stat_line()  # make sure that the stats line isn't saying we applied a '[' filter\n\n    def test_stat_line(self):\n        eq_(\"0 / 0 (0.00 B / 0.00 B) duplicates marked.\", self.results.stat_line)\n\n    def test_groups(self):\n        eq_(0, len(self.results.groups))\n\n    def test_get_group_of_duplicate(self):\n        assert self.results.get_group_of_duplicate(\"foo\") is None\n\n    def test_save_to_xml(self):\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        doc = ET.parse(f)\n        root = doc.getroot()\n        eq_(\"results\", root.tag)\n\n    def test_is_modified(self):\n        assert not self.results.is_modified\n\n    def test_is_modified_after_setting_empty_group(self):\n        # Don't consider results as modified if they're empty\n        self.results.groups = []\n        assert not self.results.is_modified\n\n    def test_save_to_same_name_as_folder(self, tmpdir):\n        # Issue #149\n        # When saving to a filename that already exists, the file is overwritten. However, when\n        # the name exists but that it's a folder, then there used to be a crash. The proper fix\n        # would have been some kind of feedback to the user, but the work involved for something\n        # that simply never happens (I never received a report of this crash, I experienced it\n        # while fooling around) is too much. Instead, use standard name conflict resolution.\n        folderpath = tmpdir.join(\"foo\")\n        folderpath.mkdir()\n        self.results.save_to_xml(str(folderpath))  # no crash\n        assert tmpdir.join(\"[000] foo\").check()\n\n\nclass TestCaseResultsWithSomeGroups:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n\n    def test_stat_line(self):\n        eq_(\"0 / 3 (0.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n\n    def test_groups(self):\n        eq_(2, len(self.results.groups))\n\n    def test_get_group_of_duplicate(self):\n        for o in self.objects:\n            g = self.results.get_group_of_duplicate(o)\n            assert isinstance(g, engine.Group)\n            assert o in g\n        assert self.results.get_group_of_duplicate(self.groups[0]) is None\n\n    def test_remove_duplicates(self):\n        g1, g2 = self.results.groups\n        self.results.remove_duplicates([g1.dupes[0]])\n        eq_(2, len(g1))\n        assert g1 in self.results.groups\n        self.results.remove_duplicates([g1.ref])\n        eq_(2, len(g1))\n        assert g1 in self.results.groups\n        self.results.remove_duplicates([g1.dupes[0]])\n        eq_(0, len(g1))\n        assert g1 not in self.results.groups\n        self.results.remove_duplicates([g2.dupes[0]])\n        eq_(0, len(g2))\n        assert g2 not in self.results.groups\n        eq_(0, len(self.results.groups))\n\n    def test_remove_duplicates_with_ref_files(self):\n        g1, g2 = self.results.groups\n        self.objects[0].is_ref = True\n        self.objects[1].is_ref = True\n        self.results.remove_duplicates([self.objects[2]])\n        eq_(0, len(g1))\n        assert g1 not in self.results.groups\n\n    def test_make_ref(self):\n        g = self.results.groups[0]\n        d = g.dupes[0]\n        self.results.make_ref(d)\n        assert d is g.ref\n\n    def test_sort_groups(self):\n        self.results.make_ref(self.objects[1])  # We want to make the 1024 sized object to go ref.\n        g1, g2 = self.groups\n        self.results.sort_groups(\"size\")\n        assert self.results.groups[0] is g2\n        assert self.results.groups[1] is g1\n        self.results.sort_groups(\"size\", False)\n        assert self.results.groups[0] is g1\n        assert self.results.groups[1] is g2\n\n    def test_set_groups_when_sorted(self):\n        self.results.make_ref(self.objects[1])  # We want to make the 1024 sized object to go ref.\n        self.results.sort_groups(\"size\")\n        objects, matches, groups = GetTestGroups()\n        g1, g2 = groups\n        g1.switch_ref(objects[1])\n        self.results.groups = groups\n        assert self.results.groups[0] is g2\n        assert self.results.groups[1] is g1\n\n    def test_get_dupe_list(self):\n        eq_([self.objects[1], self.objects[2], self.objects[4]], self.results.dupes)\n\n    def test_dupe_list_is_cached(self):\n        assert self.results.dupes is self.results.dupes\n\n    def test_dupe_list_cache_is_invalidated_when_needed(self):\n        o1, o2, o3, o4, o5 = self.objects\n        eq_([o2, o3, o5], self.results.dupes)\n        self.results.make_ref(o2)\n        eq_([o1, o3, o5], self.results.dupes)\n        objects, matches, groups = GetTestGroups()\n        o1, o2, o3, o4, o5 = objects\n        self.results.groups = groups\n        eq_([o2, o3, o5], self.results.dupes)\n\n    def test_dupe_list_sort(self):\n        o1, o2, o3, o4, o5 = self.objects\n        o1.size = 5\n        o2.size = 4\n        o3.size = 3\n        o4.size = 2\n        o5.size = 1\n        self.results.sort_dupes(\"size\")\n        eq_([o5, o3, o2], self.results.dupes)\n        self.results.sort_dupes(\"size\", False)\n        eq_([o2, o3, o5], self.results.dupes)\n\n    def test_dupe_list_remember_sort(self):\n        o1, o2, o3, o4, o5 = self.objects\n        o1.size = 5\n        o2.size = 4\n        o3.size = 3\n        o4.size = 2\n        o5.size = 1\n        self.results.sort_dupes(\"size\")\n        self.results.make_ref(o2)\n        eq_([o5, o3, o1], self.results.dupes)\n\n    def test_dupe_list_sort_delta_values(self):\n        o1, o2, o3, o4, o5 = self.objects\n        o1.size = 10\n        o2.size = 2  # -8\n        o3.size = 3  # -7\n        o4.size = 20\n        o5.size = 1  # -19\n        self.results.sort_dupes(\"size\", delta=True)\n        eq_([o5, o2, o3], self.results.dupes)\n\n    def test_sort_empty_list(self):\n        # There was an infinite loop when sorting an empty list.\n        app = DupeGuru()\n        r = app.results\n        r.sort_dupes(\"name\")\n        eq_([], r.dupes)\n\n    def test_dupe_list_update_on_remove_duplicates(self):\n        o1, o2, o3, o4, o5 = self.objects\n        eq_(3, len(self.results.dupes))\n        self.results.remove_duplicates([o2])\n        eq_(2, len(self.results.dupes))\n\n    def test_is_modified(self):\n        # Changing the groups sets the modified flag\n        assert self.results.is_modified\n\n    def test_is_modified_after_save_and_load(self):\n        # Saving/Loading a file sets the modified flag back to False\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        assert not self.results.is_modified\n        self.results.groups = self.groups  # sets the flag back\n        f.seek(0)\n        self.results.load_from_xml(f, get_file)\n        assert not self.results.is_modified\n\n    def test_is_modified_after_removing_all_results(self):\n        # Removing all results sets the is_modified flag to false.\n        self.results.mark_all()\n        self.results.perform_on_marked(lambda x: None, True)\n        assert not self.results.is_modified\n\n    def test_group_of_duplicate_after_removal(self):\n        # removing a duplicate also removes it from the dupe:group map.\n        dupe = self.results.groups[1].dupes[0]\n        ref = self.results.groups[1].ref\n        self.results.remove_duplicates([dupe])\n        assert self.results.get_group_of_duplicate(dupe) is None\n        # also remove group ref\n        assert self.results.get_group_of_duplicate(ref) is None\n\n    def test_dupe_list_sort_delta_values_nonnumeric(self):\n        # When sorting dupes in delta mode on a non-numeric column, our first sort criteria is if\n        # the string is the same as its ref.\n        g1r, g1d1, g1d2, g2r, g2d1 = self.objects\n        # \"aaa\" makes our dupe go first in alphabetical order, but since we have the same value as\n        # ref, we're going last.\n        g2r.name = g2d1.name = \"aaa\"\n        self.results.sort_dupes(\"name\", delta=True)\n        eq_(\"aaa\", self.results.dupes[2].name)\n\n    def test_dupe_list_sort_delta_values_nonnumeric_case_insensitive(self):\n        # Non-numeric delta sorting comparison is case insensitive\n        g1r, g1d1, g1d2, g2r, g2d1 = self.objects\n        g2r.name = \"AaA\"\n        g2d1.name = \"aAa\"\n        self.results.sort_dupes(\"name\", delta=True)\n        eq_(\"aAa\", self.results.dupes[2].name)\n\n\nclass TestCaseResultsWithSavedResults:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n        self.f = io.BytesIO()\n        self.results.save_to_xml(self.f)\n        self.f.seek(0)\n\n    def test_is_modified(self):\n        # Saving a file sets the modified flag back to False\n        assert not self.results.is_modified\n\n    def test_is_modified_after_load(self):\n        # Loading a file sets the modified flag back to False\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        self.results.groups = self.groups  # sets the flag back\n        self.results.load_from_xml(self.f, get_file)\n        assert not self.results.is_modified\n\n    def test_is_modified_after_remove(self):\n        # Removing dupes sets the modified flag\n        self.results.remove_duplicates([self.results.groups[0].dupes[0]])\n        assert self.results.is_modified\n\n    def test_is_modified_after_make_ref(self):\n        # Making a dupe ref sets the modified flag\n        self.results.make_ref(self.results.groups[0].dupes[0])\n        assert self.results.is_modified\n\n\nclass TestCaseResultsMarkings:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n\n    def test_stat_line(self):\n        eq_(\"0 / 3 (0.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.mark(self.objects[1])\n        eq_(\"1 / 3 (1.00 KB / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.mark_invert()\n        eq_(\"2 / 3 (2.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.mark_invert()\n        self.results.unmark(self.objects[1])\n        self.results.mark(self.objects[2])\n        self.results.mark(self.objects[4])\n        eq_(\"2 / 3 (2.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.mark(self.objects[0])  # this is a ref, it can't be counted\n        eq_(\"2 / 3 (2.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.groups = self.groups\n        eq_(\"0 / 3 (0.00 B / 1.01 KB) duplicates marked.\", self.results.stat_line)\n\n    def test_with_ref_duplicate(self):\n        self.objects[1].is_ref = True\n        self.results.groups = self.groups\n        assert not self.results.mark(self.objects[1])\n        self.results.mark(self.objects[2])\n        eq_(\"1 / 2 (1.00 B / 2.00 B) duplicates marked.\", self.results.stat_line)\n\n    def test_perform_on_marked(self):\n        def log_object(o):\n            log.append(o)\n            return True\n\n        log = []\n        self.results.mark_all()\n        self.results.perform_on_marked(log_object, False)\n        assert self.objects[1] in log\n        assert self.objects[2] in log\n        assert self.objects[4] in log\n        eq_(3, len(log))\n        log = []\n        self.results.mark_none()\n        self.results.mark(self.objects[4])\n        self.results.perform_on_marked(log_object, True)\n        eq_(1, len(log))\n        assert self.objects[4] in log\n        eq_(1, len(self.results.groups))\n\n    def test_perform_on_marked_with_problems(self):\n        def log_object(o):\n            log.append(o)\n            if o is self.objects[1]:\n                raise OSError(\"foobar\")\n\n        log = []\n        self.results.mark_all()\n        assert self.results.is_marked(self.objects[1])\n        self.results.perform_on_marked(log_object, True)\n        eq_(len(log), 3)\n        eq_(len(self.results.groups), 1)\n        eq_(len(self.results.groups[0]), 2)\n        assert self.objects[1] in self.results.groups[0]\n        assert not self.results.is_marked(self.objects[2])\n        assert self.results.is_marked(self.objects[1])\n        eq_(len(self.results.problems), 1)\n        dupe, msg = self.results.problems[0]\n        assert dupe is self.objects[1]\n        eq_(msg, \"foobar\")\n\n    def test_perform_on_marked_with_ref(self):\n        def log_object(o):\n            log.append(o)\n            return True\n\n        log = []\n        self.objects[0].is_ref = True\n        self.objects[1].is_ref = True\n        self.results.mark_all()\n        self.results.perform_on_marked(log_object, True)\n        assert self.objects[1] not in log\n        assert self.objects[2] in log\n        assert self.objects[4] in log\n        eq_(2, len(log))\n        eq_(0, len(self.results.groups))\n\n    def test_perform_on_marked_remove_objects_only_at_the_end(self):\n        def check_groups(o):\n            eq_(3, len(g1))\n            eq_(2, len(g2))\n            return True\n\n        g1, g2 = self.results.groups\n        self.results.mark_all()\n        self.results.perform_on_marked(check_groups, True)\n        eq_(0, len(g1))\n        eq_(0, len(g2))\n        eq_(0, len(self.results.groups))\n\n    def test_remove_duplicates(self):\n        g1 = self.results.groups[0]\n        self.results.mark(g1.dupes[0])\n        eq_(\"1 / 3 (1.00 KB / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.remove_duplicates([g1.dupes[1]])\n        eq_(\"1 / 2 (1.00 KB / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.remove_duplicates([g1.dupes[0]])\n        eq_(\"0 / 1 (0.00 B / 1.00 B) duplicates marked.\", self.results.stat_line)\n\n    def test_make_ref(self):\n        g = self.results.groups[0]\n        d = g.dupes[0]\n        self.results.mark(d)\n        eq_(\"1 / 3 (1.00 KB / 1.01 KB) duplicates marked.\", self.results.stat_line)\n        self.results.make_ref(d)\n        eq_(\"0 / 3 (0.00 B / 3.00 B) duplicates marked.\", self.results.stat_line)\n        self.results.make_ref(d)\n        eq_(\"0 / 3 (0.00 B / 3.00 B) duplicates marked.\", self.results.stat_line)\n\n    def test_save_xml(self):\n        self.results.mark(self.objects[1])\n        self.results.mark_invert()\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        doc = ET.parse(f)\n        root = doc.getroot()\n        g1, g2 = root.iter(\"group\")\n        d1, d2, d3 = g1.iter(\"file\")\n        eq_(\"n\", d1.get(\"marked\"))\n        eq_(\"n\", d2.get(\"marked\"))\n        eq_(\"y\", d3.get(\"marked\"))\n        d1, d2 = g2.iter(\"file\")\n        eq_(\"n\", d1.get(\"marked\"))\n        eq_(\"y\", d2.get(\"marked\"))\n\n    def test_load_xml(self):\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        self.objects[4].name = \"ibabtu 2\"  # we can't have 2 files with the same path\n        self.results.mark(self.objects[1])\n        self.results.mark_invert()\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(f, get_file)\n        assert not r.is_marked(self.objects[0])\n        assert not r.is_marked(self.objects[1])\n        assert r.is_marked(self.objects[2])\n        assert not r.is_marked(self.objects[3])\n        assert r.is_marked(self.objects[4])\n\n\nclass TestCaseResultsXML:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n\n    def get_file(self, path):  # use this as a callback for load_from_xml\n        return [o for o in self.objects if str(o.path) == path][0]\n\n    def test_save_to_xml(self):\n        self.objects[0].is_ref = True\n        self.objects[0].words = [[\"foo\", \"bar\"]]\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        doc = ET.parse(f)\n        root = doc.getroot()\n        eq_(\"results\", root.tag)\n        eq_(2, len(root))\n        eq_(2, len([c for c in root if c.tag == \"group\"]))\n        g1, g2 = root\n        eq_(6, len(g1))\n        eq_(3, len([c for c in g1 if c.tag == \"file\"]))\n        eq_(3, len([c for c in g1 if c.tag == \"match\"]))\n        d1, d2, d3 = (c for c in g1 if c.tag == \"file\")\n        eq_(op.join(\"basepath\", \"foo bar\"), d1.get(\"path\"))\n        eq_(op.join(\"basepath\", \"bar bleh\"), d2.get(\"path\"))\n        eq_(op.join(\"basepath\", \"foo bleh\"), d3.get(\"path\"))\n        eq_(\"y\", d1.get(\"is_ref\"))\n        eq_(\"n\", d2.get(\"is_ref\"))\n        eq_(\"n\", d3.get(\"is_ref\"))\n        eq_(\"foo,bar\", d1.get(\"words\"))\n        eq_(\"bar,bleh\", d2.get(\"words\"))\n        eq_(\"foo,bleh\", d3.get(\"words\"))\n        eq_(3, len(g2))\n        eq_(2, len([c for c in g2 if c.tag == \"file\"]))\n        eq_(1, len([c for c in g2 if c.tag == \"match\"]))\n        d1, d2 = (c for c in g2 if c.tag == \"file\")\n        eq_(op.join(\"basepath\", \"ibabtu\"), d1.get(\"path\"))\n        eq_(op.join(\"basepath\", \"ibabtu\"), d2.get(\"path\"))\n        eq_(\"n\", d1.get(\"is_ref\"))\n        eq_(\"n\", d2.get(\"is_ref\"))\n        eq_(\"ibabtu\", d1.get(\"words\"))\n        eq_(\"ibabtu\", d2.get(\"words\"))\n\n    def test_load_xml(self):\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        self.objects[0].is_ref = True\n        self.objects[4].name = \"ibabtu 2\"  # we can't have 2 files with the same path\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(f, get_file)\n        eq_(2, len(r.groups))\n        g1, g2 = r.groups\n        eq_(3, len(g1))\n        assert g1[0].is_ref\n        assert not g1[1].is_ref\n        assert not g1[2].is_ref\n        assert g1[0] is self.objects[0]\n        assert g1[1] is self.objects[1]\n        assert g1[2] is self.objects[2]\n        eq_([\"foo\", \"bar\"], g1[0].words)\n        eq_([\"bar\", \"bleh\"], g1[1].words)\n        eq_([\"foo\", \"bleh\"], g1[2].words)\n        eq_(2, len(g2))\n        assert not g2[0].is_ref\n        assert not g2[1].is_ref\n        assert g2[0] is self.objects[3]\n        assert g2[1] is self.objects[4]\n        eq_([\"ibabtu\"], g2[0].words)\n        eq_([\"ibabtu\"], g2[1].words)\n\n    def test_load_xml_with_filename(self, tmpdir):\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        filename = str(tmpdir.join(\"dupeguru_results.xml\"))\n        self.objects[4].name = \"ibabtu 2\"  # we can't have 2 files with the same path\n        self.results.save_to_xml(filename)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(filename, get_file)\n        eq_(2, len(r.groups))\n\n    def test_load_xml_with_some_files_that_dont_exist_anymore(self):\n        def get_file(path):\n            if path.endswith(\"ibabtu 2\"):\n                return None\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        self.objects[4].name = \"ibabtu 2\"  # we can't have 2 files with the same path\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(f, get_file)\n        eq_(1, len(r.groups))\n        eq_(3, len(r.groups[0]))\n\n    def test_load_xml_missing_attributes_and_bogus_elements(self):\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        root = ET.Element(\"foobar\")  # The root element shouldn't matter, really.\n        group_node = ET.SubElement(root, \"group\")\n        dupe_node = ET.SubElement(group_node, \"file\")  # Perfectly correct file\n        dupe_node.set(\"path\", op.join(\"basepath\", \"foo bar\"))\n        dupe_node.set(\"is_ref\", \"y\")\n        dupe_node.set(\"words\", \"foo, bar\")\n        dupe_node = ET.SubElement(group_node, \"file\")  # is_ref missing, default to 'n'\n        dupe_node.set(\"path\", op.join(\"basepath\", \"foo bleh\"))\n        dupe_node.set(\"words\", \"foo, bleh\")\n        dupe_node = ET.SubElement(group_node, \"file\")  # words are missing, valid.\n        dupe_node.set(\"path\", op.join(\"basepath\", \"bar bleh\"))\n        dupe_node = ET.SubElement(group_node, \"file\")  # path is missing, invalid.\n        dupe_node.set(\"words\", \"foo, bleh\")\n        dupe_node = ET.SubElement(group_node, \"foobar\")  # Invalid element name\n        dupe_node.set(\"path\", op.join(\"basepath\", \"bar bleh\"))\n        dupe_node.set(\"is_ref\", \"y\")\n        dupe_node.set(\"words\", \"bar, bleh\")\n        match_node = ET.SubElement(group_node, \"match\")  # match pointing to a bad index\n        match_node.set(\"first\", \"42\")\n        match_node.set(\"second\", \"45\")\n        match_node = ET.SubElement(group_node, \"match\")  # match with missing attrs\n        match_node = ET.SubElement(group_node, \"match\")  # match with non-int values\n        match_node.set(\"first\", \"foo\")\n        match_node.set(\"second\", \"bar\")\n        match_node.set(\"percentage\", \"baz\")\n        group_node = ET.SubElement(root, \"foobar\")  # invalid group\n        group_node = ET.SubElement(root, \"group\")  # empty group\n        f = io.BytesIO()\n        tree = ET.ElementTree(root)\n        tree.write(f, encoding=\"utf-8\")\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(f, get_file)\n        eq_(1, len(r.groups))\n        eq_(3, len(r.groups[0]))\n\n    def test_xml_non_ascii(self):\n        def get_file(path):\n            if path == op.join(\"basepath\", \"\\xe9foo bar\"):\n                return objects[0]\n            if path == op.join(\"basepath\", \"bar bleh\"):\n                return objects[1]\n\n        objects = [NamedObject(\"\\xe9foo bar\", True), NamedObject(\"bar bleh\", True)]\n        matches = engine.getmatches(objects)  # we should have 5 matches\n        groups = engine.get_groups(matches)  # We should have 2 groups\n        for g in groups:\n            g.prioritize(lambda x: objects.index(x))  # We want the dupes to be in the same order as the list is\n        app = DupeGuru()\n        results = Results(app)\n        results.groups = groups\n        f = io.BytesIO()\n        results.save_to_xml(f)\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        r.load_from_xml(f, get_file)\n        g = r.groups[0]\n        eq_(\"\\xe9foo bar\", g[0].name)\n        eq_([\"efoo\", \"bar\"], g[0].words)\n\n    def test_load_invalid_xml(self):\n        f = io.BytesIO()\n        f.write(b\"<this is invalid\")\n        f.seek(0)\n        app = DupeGuru()\n        r = Results(app)\n        with raises(ET.ParseError):\n            r.load_from_xml(f, None)\n        eq_(0, len(r.groups))\n\n    def test_load_non_existant_xml(self):\n        app = DupeGuru()\n        r = Results(app)\n        with raises(IOError):\n            r.load_from_xml(\"does_not_exist.xml\", None)\n        eq_(0, len(r.groups))\n\n    def test_remember_match_percentage(self):\n        group = self.groups[0]\n        d1, d2, d3 = group\n        fake_matches = set()\n        fake_matches.add(engine.Match(d1, d2, 42))\n        fake_matches.add(engine.Match(d1, d3, 43))\n        fake_matches.add(engine.Match(d2, d3, 46))\n        group.matches = fake_matches\n        f = io.BytesIO()\n        results = self.results\n        results.save_to_xml(f)\n        f.seek(0)\n        app = DupeGuru()\n        results = Results(app)\n        results.load_from_xml(f, self.get_file)\n        group = results.groups[0]\n        d1, d2, d3 = group\n        match = group.get_match_of(d2)  # d1 - d2\n        eq_(42, match[2])\n        match = group.get_match_of(d3)  # d1 - d3\n        eq_(43, match[2])\n        group.switch_ref(d2)\n        match = group.get_match_of(d3)  # d2 - d3\n        eq_(46, match[2])\n\n    def test_save_and_load(self):\n        # previously, when reloading matches, they wouldn't be reloaded as namedtuples\n        f = io.BytesIO()\n        self.results.save_to_xml(f)\n        f.seek(0)\n        self.results.load_from_xml(f, self.get_file)\n        first(self.results.groups[0].matches).percentage\n\n    def test_apply_filter_works_on_paths(self):\n        # apply_filter() searches on the whole path, not just on the filename.\n        self.results.apply_filter(\"basepath\")\n        eq_(len(self.results.groups), 2)\n\n    def test_save_xml_with_invalid_characters(self):\n        # Don't crash when saving files that have invalid xml characters in their path\n        self.objects[0].name = \"foo\\x19\"\n        self.results.save_to_xml(io.BytesIO())  # don't crash\n\n\nclass TestCaseResultsFilter:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n        self.results.apply_filter(r\"foo\")\n\n    def test_groups(self):\n        eq_(1, len(self.results.groups))\n        assert self.results.groups[0] is self.groups[0]\n\n    def test_dupes(self):\n        # There are 2 objects matching. The first one is ref. Only the 3rd one is supposed to be in dupes.\n        eq_(1, len(self.results.dupes))\n        assert self.results.dupes[0] is self.objects[2]\n\n    def test_cancel_filter(self):\n        self.results.apply_filter(None)\n        eq_(3, len(self.results.dupes))\n        eq_(2, len(self.results.groups))\n\n    def test_dupes_reconstructed_filtered(self):\n        # make_ref resets self.__dupes to None. When it's reconstructed, we want it filtered\n        dupe = self.results.dupes[0]  # 3rd object\n        self.results.make_ref(dupe)\n        eq_(1, len(self.results.dupes))\n        assert self.results.dupes[0] is self.objects[0]\n\n    def test_include_ref_dupes_in_filter(self):\n        # When only the ref of a group match the filter, include it in the group\n        self.results.apply_filter(None)\n        self.results.apply_filter(r\"foo bar\")\n        eq_(1, len(self.results.groups))\n        eq_(0, len(self.results.dupes))\n\n    def test_filters_build_on_one_another(self):\n        self.results.apply_filter(r\"bar\")\n        eq_(1, len(self.results.groups))\n        eq_(0, len(self.results.dupes))\n\n    def test_stat_line(self):\n        expected = \"0 / 1 (0.00 B / 1.00 B) duplicates marked. filter: foo\"\n        eq_(expected, self.results.stat_line)\n        self.results.apply_filter(r\"bar\")\n        expected = \"0 / 0 (0.00 B / 0.00 B) duplicates marked. filter: foo --> bar\"\n        eq_(expected, self.results.stat_line)\n        self.results.apply_filter(None)\n        expected = \"0 / 3 (0.00 B / 1.01 KB) duplicates marked.\"\n        eq_(expected, self.results.stat_line)\n\n    def test_mark_count_is_filtered_as_well(self):\n        self.results.apply_filter(None)\n        # We don't want to perform mark_all() because we want the mark list to contain objects\n        for dupe in self.results.dupes:\n            self.results.mark(dupe)\n        self.results.apply_filter(r\"foo\")\n        expected = \"1 / 1 (1.00 B / 1.00 B) duplicates marked. filter: foo\"\n        eq_(expected, self.results.stat_line)\n\n    def test_mark_all_only_affects_filtered_items(self):\n        # When performing actions like mark_all() and mark_none in a filtered environment, only mark\n        # items that are actually in the filter.\n        self.results.mark_all()\n        self.results.apply_filter(None)\n        eq_(self.results.mark_count, 1)\n\n    def test_sort_groups(self):\n        self.results.apply_filter(None)\n        self.results.make_ref(self.objects[1])  # to have the 1024 b obkect as ref\n        g1, g2 = self.groups\n        self.results.apply_filter(\"a\")  # Matches both group\n        self.results.sort_groups(\"size\")\n        assert self.results.groups[0] is g2\n        assert self.results.groups[1] is g1\n        self.results.apply_filter(None)\n        assert self.results.groups[0] is g2\n        assert self.results.groups[1] is g1\n        self.results.sort_groups(\"size\", False)\n        self.results.apply_filter(\"a\")\n        assert self.results.groups[1] is g2\n        assert self.results.groups[0] is g1\n\n    def test_set_group(self):\n        # We want the new group to be filtered\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.results.groups = self.groups\n        eq_(1, len(self.results.groups))\n        assert self.results.groups[0] is self.groups[0]\n\n    def test_load_cancels_filter(self, tmpdir):\n        def get_file(path):\n            return [f for f in self.objects if str(f.path) == path][0]\n\n        filename = str(tmpdir.join(\"dupeguru_results.xml\"))\n        self.objects[4].name = \"ibabtu 2\"  # we can't have 2 files with the same path\n        self.results.save_to_xml(filename)\n        app = DupeGuru()\n        r = Results(app)\n        r.apply_filter(\"foo\")\n        r.load_from_xml(filename, get_file)\n        eq_(2, len(r.groups))\n\n    def test_remove_dupe(self):\n        self.results.remove_duplicates([self.results.dupes[0]])\n        self.results.apply_filter(None)\n        eq_(2, len(self.results.groups))\n        eq_(2, len(self.results.dupes))\n        self.results.apply_filter(\"ibabtu\")\n        self.results.remove_duplicates([self.results.dupes[0]])\n        self.results.apply_filter(None)\n        eq_(1, len(self.results.groups))\n        eq_(1, len(self.results.dupes))\n\n    def test_filter_is_case_insensitive(self):\n        self.results.apply_filter(None)\n        self.results.apply_filter(\"FOO\")\n        eq_(1, len(self.results.dupes))\n\n    def test_make_ref_on_filtered_out_doesnt_mess_stats(self):\n        # When filtered, a group containing filtered out dupes will display them as being reference.\n        # When calling make_ref on such a dupe, the total size and dupecount stats gets messed up\n        # because they are *not* counted in the stats in the first place.\n        g1, g2 = self.groups\n        bar_bleh = g1[1]  # The \"bar bleh\" dupe is filtered out\n        self.results.make_ref(bar_bleh)\n        # Now the stats should display *2* markable dupes (instead of 1)\n        expected = \"0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo\"\n        eq_(expected, self.results.stat_line)\n        self.results.apply_filter(None)  # Now let's make sure our unfiltered results aren't fucked up\n        expected = \"0 / 3 (0.00 B / 3.00 B) duplicates marked.\"\n        eq_(expected, self.results.stat_line)\n\n\nclass TestCaseResultsRefFile:\n    def setup_method(self, method):\n        self.app = DupeGuru()\n        self.results = self.app.results\n        self.objects, self.matches, self.groups = GetTestGroups()\n        self.objects[0].is_ref = True\n        self.objects[1].is_ref = True\n        self.results.groups = self.groups\n\n    def test_stat_line(self):\n        expected = \"0 / 2 (0.00 B / 2.00 B) duplicates marked.\"\n        eq_(expected, self.results.stat_line)\n"
  },
  {
    "path": "core/tests/scanner_test.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport pytest\n\nfrom hscommon.jobprogress import job\nfrom pathlib import Path\nfrom hscommon.testutil import eq_\n\nfrom core import fs\nfrom core.engine import getwords, Match\nfrom core.ignore import IgnoreList\nfrom core.scanner import Scanner, ScanType\nfrom core.me.scanner import ScannerME\n\n\n# TODO update this to be able to inherit from fs.File\nclass NamedObject:\n    def __init__(self, name=\"foobar\", size=1, path=None):\n        if path is None:\n            path = Path(name)\n        else:\n            path = Path(path, name)\n        self.name = name\n        self.size = size\n        self.path = path\n        self.words = getwords(name)\n\n    def __repr__(self):\n        return \"<NamedObject {!r} {!r}>\".format(self.name, self.path)\n\n    def exists(self):\n        return self.path.exists()\n\n\nno = NamedObject\n\n\n@pytest.fixture\ndef fake_fileexists(request):\n    # This is a hack to avoid invalidating all previous tests since the scanner started to test\n    # for file existence before doing the match grouping.\n    monkeypatch = request.getfixturevalue(\"monkeypatch\")\n    monkeypatch.setattr(Path, \"exists\", lambda _: True)\n\n\ndef test_empty(fake_fileexists):\n    s = Scanner()\n    r = s.get_dupe_groups([])\n    eq_(r, [])\n\n\ndef test_default_settings(fake_fileexists):\n    s = Scanner()\n    eq_(s.min_match_percentage, 80)\n    eq_(s.scan_type, ScanType.FILENAME)\n    eq_(s.mix_file_kind, True)\n    eq_(s.word_weighting, False)\n    eq_(s.match_similar_words, False)\n    eq_(s.size_threshold, 0)\n    eq_(s.large_size_threshold, 0)\n    eq_(s.big_file_size_threshold, 0)\n\n\ndef test_simple_with_default_settings(fake_fileexists):\n    s = Scanner()\n    f = [no(\"foo bar\", path=\"p1\"), no(\"foo bar\", path=\"p2\"), no(\"foo bleh\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    g = r[0]\n    # 'foo bleh' cannot be in the group because the default min match % is 80\n    eq_(len(g), 2)\n    assert g.ref in f[:2]\n    assert g.dupes[0] in f[:2]\n\n\ndef test_simple_with_lower_min_match(fake_fileexists):\n    s = Scanner()\n    s.min_match_percentage = 50\n    f = [no(\"foo bar\", path=\"p1\"), no(\"foo bar\", path=\"p2\"), no(\"foo bleh\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    g = r[0]\n    eq_(len(g), 3)\n\n\ndef test_trim_all_ref_groups(fake_fileexists):\n    # When all files of a group are ref, don't include that group in the results, but also don't\n    # count the files from that group as discarded.\n    s = Scanner()\n    f = [\n        no(\"foo\", path=\"p1\"),\n        no(\"foo\", path=\"p2\"),\n        no(\"bar\", path=\"p1\"),\n        no(\"bar\", path=\"p2\"),\n    ]\n    f[2].is_ref = True\n    f[3].is_ref = True\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    eq_(s.discarded_file_count, 0)\n\n\ndef test_prioritize(fake_fileexists):\n    s = Scanner()\n    f = [\n        no(\"foo\", path=\"p1\"),\n        no(\"foo\", path=\"p2\"),\n        no(\"bar\", path=\"p1\"),\n        no(\"bar\", path=\"p2\"),\n    ]\n    f[1].size = 2\n    f[2].size = 3\n    f[3].is_ref = True\n    r = s.get_dupe_groups(f)\n    g1, g2 = r\n    assert f[1] in (g1.ref, g2.ref)\n    assert f[0] in (g1.dupes[0], g2.dupes[0])\n    assert f[3] in (g1.ref, g2.ref)\n    assert f[2] in (g1.dupes[0], g2.dupes[0])\n\n\ndef test_content_scan(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    f = [no(\"foo\"), no(\"bar\"), no(\"bleh\")]\n    f[0].digest = f[0].digest_partial = f[0].digest_samples = \"foobar\"\n    f[1].digest = f[1].digest_partial = f[1].digest_samples = \"foobar\"\n    f[2].digest = f[2].digest_partial = f[1].digest_samples = \"bleh\"\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    eq_(len(r[0]), 2)\n    eq_(s.discarded_file_count, 0)  # don't count the different digest as discarded!\n\n\ndef test_content_scan_compare_sizes_first(fake_fileexists):\n    class MyFile(no):\n        @property\n        def digest(self):\n            raise AssertionError()\n\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    f = [MyFile(\"foo\", 1), MyFile(\"bar\", 2)]\n    eq_(len(s.get_dupe_groups(f)), 0)\n\n\ndef test_ignore_file_size(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    small_size = 10  # 10KB\n    s.size_threshold = 0\n    large_size = 100 * 1024 * 1024  # 100MB\n    s.large_size_threshold = 0\n    f = [\n        no(\"smallignore1\", small_size - 1),\n        no(\"smallignore2\", small_size - 1),\n        no(\"small1\", small_size),\n        no(\"small2\", small_size),\n        no(\"large1\", large_size),\n        no(\"large2\", large_size),\n        no(\"largeignore1\", large_size + 1),\n        no(\"largeignore2\", large_size + 1),\n    ]\n    f[0].digest = f[0].digest_partial = f[0].digest_samples = \"smallignore\"\n    f[1].digest = f[1].digest_partial = f[1].digest_samples = \"smallignore\"\n    f[2].digest = f[2].digest_partial = f[2].digest_samples = \"small\"\n    f[3].digest = f[3].digest_partial = f[3].digest_samples = \"small\"\n    f[4].digest = f[4].digest_partial = f[4].digest_samples = \"large\"\n    f[5].digest = f[5].digest_partial = f[5].digest_samples = \"large\"\n    f[6].digest = f[6].digest_partial = f[6].digest_samples = \"largeignore\"\n    f[7].digest = f[7].digest_partial = f[7].digest_samples = \"largeignore\"\n\n    r = s.get_dupe_groups(f)\n    # No ignores\n    eq_(len(r), 4)\n    # Ignore smaller\n    s.size_threshold = small_size\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 3)\n    # Ignore larger\n    s.size_threshold = 0\n    s.large_size_threshold = large_size\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 3)\n    # Ignore both\n    s.size_threshold = small_size\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 2)\n\n\ndef test_big_file_partial_hashes(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n\n    smallsize = 1\n    bigsize = 100 * 1024 * 1024  # 100MB\n    s.big_file_size_threshold = bigsize\n\n    f = [no(\"bigfoo\", bigsize), no(\"bigbar\", bigsize), no(\"smallfoo\", smallsize), no(\"smallbar\", smallsize)]\n    f[0].digest = f[0].digest_partial = f[0].digest_samples = \"foobar\"\n    f[1].digest = f[1].digest_partial = f[1].digest_samples = \"foobar\"\n    f[2].digest = f[2].digest_partial = \"bleh\"\n    f[3].digest = f[3].digest_partial = \"bleh\"\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 2)\n\n    # digest_partial is still the same, but the file is actually different\n    f[1].digest = f[1].digest_samples = \"difffoobar\"\n    # here we compare the full digests, as the user disabled the optimization\n    s.big_file_size_threshold = 0\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n\n    # here we should compare the digest_samples, and see they are different\n    s.big_file_size_threshold = bigsize\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n\n\ndef test_min_match_perc_doesnt_matter_for_content_scan(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    f = [no(\"foo\"), no(\"bar\"), no(\"bleh\")]\n    f[0].digest = f[0].digest_partial = f[0].digest_samples = \"foobar\"\n    f[1].digest = f[1].digest_partial = f[1].digest_samples = \"foobar\"\n    f[2].digest = f[2].digest_partial = f[2].digest_samples = \"bleh\"\n    s.min_match_percentage = 101\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    eq_(len(r[0]), 2)\n    s.min_match_percentage = 0\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    eq_(len(r[0]), 2)\n\n\ndef test_content_scan_doesnt_put_digest_in_words_at_the_end(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    f = [no(\"foo\"), no(\"bar\")]\n    f[0].digest = f[0].digest_partial = f[0].digest_samples = (\n        \"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\"\n    )\n    f[1].digest = f[1].digest_partial = f[1].digest_samples = (\n        \"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e\\x0f\"\n    )\n    r = s.get_dupe_groups(f)\n    # FIXME looks like we are missing something here?\n    r[0]\n\n\ndef test_extension_is_not_counted_in_filename_scan(fake_fileexists):\n    s = Scanner()\n    s.min_match_percentage = 100\n    f = [no(\"foo.bar\"), no(\"foo.bleh\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    eq_(len(r[0]), 2)\n\n\ndef test_job(fake_fileexists):\n    def do_progress(progress, desc=\"\"):\n        log.append(progress)\n        return True\n\n    s = Scanner()\n    log = []\n    f = [no(\"foo bar\"), no(\"foo bar\"), no(\"foo bleh\")]\n    s.get_dupe_groups(f, j=job.Job(1, do_progress))\n    eq_(log[0], 0)\n    eq_(log[-1], 100)\n\n\ndef test_mix_file_kind(fake_fileexists):\n    s = Scanner()\n    s.mix_file_kind = False\n    f = [no(\"foo.1\"), no(\"foo.2\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 0)\n\n\ndef test_word_weighting(fake_fileexists):\n    s = Scanner()\n    s.min_match_percentage = 75\n    s.word_weighting = True\n    f = [no(\"foo bar\"), no(\"foo bar bleh\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n    g = r[0]\n    m = g.get_match_of(g.dupes[0])\n    eq_(m.percentage, 75)  # 16 letters, 12 matching\n\n\ndef test_similar_words(fake_fileexists):\n    s = Scanner()\n    s.match_similar_words = True\n    f = [\n        no(\"The White Stripes\"),\n        no(\"The Whites Stripe\"),\n        no(\"Limp Bizkit\"),\n        no(\"Limp Bizkitt\"),\n    ]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 2)\n\n\ndef test_fields(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.FIELDS\n    f = [no(\"The White Stripes - Little Ghost\"), no(\"The White Stripes - Little Acorn\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 0)\n\n\ndef test_fields_no_order(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.FIELDSNOORDER\n    f = [no(\"The White Stripes - Little Ghost\"), no(\"Little Ghost - The White Stripes\")]\n    r = s.get_dupe_groups(f)\n    eq_(len(r), 1)\n\n\ndef test_tag_scan(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.artist = \"The White Stripes\"\n    o1.title = \"The Air Near My Fingers\"\n    o2.artist = \"The White Stripes\"\n    o2.title = \"The Air Near My Fingers\"\n    r = s.get_dupe_groups([o1, o2])\n    eq_(len(r), 1)\n\n\ndef test_tag_with_album_scan(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"artist\", \"album\", \"title\"}\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o3 = no(\"bleh\")\n    o1.artist = \"The White Stripes\"\n    o1.title = \"The Air Near My Fingers\"\n    o1.album = \"Elephant\"\n    o2.artist = \"The White Stripes\"\n    o2.title = \"The Air Near My Fingers\"\n    o2.album = \"Elephant\"\n    o3.artist = \"The White Stripes\"\n    o3.title = \"The Air Near My Fingers\"\n    o3.album = \"foobar\"\n    r = s.get_dupe_groups([o1, o2, o3])\n    eq_(len(r), 1)\n\n\ndef test_that_dash_in_tags_dont_create_new_fields(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"artist\", \"album\", \"title\"}\n    s.min_match_percentage = 50\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.artist = \"The White Stripes - a\"\n    o1.title = \"The Air Near My Fingers - a\"\n    o1.album = \"Elephant - a\"\n    o2.artist = \"The White Stripes - b\"\n    o2.title = \"The Air Near My Fingers - b\"\n    o2.album = \"Elephant - b\"\n    r = s.get_dupe_groups([o1, o2])\n    eq_(len(r), 1)\n\n\ndef test_tag_scan_with_different_scanned(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"track\", \"year\"}\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.artist = \"The White Stripes\"\n    o1.title = \"some title\"\n    o1.track = \"foo\"\n    o1.year = \"bar\"\n    o2.artist = \"The White Stripes\"\n    o2.title = \"another title\"\n    o2.track = \"foo\"\n    o2.year = \"bar\"\n    r = s.get_dupe_groups([o1, o2])\n    eq_(len(r), 1)\n\n\ndef test_tag_scan_only_scans_existing_tags(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"artist\", \"foo\"}\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.artist = \"The White Stripes\"\n    o1.foo = \"foo\"\n    o2.artist = \"The White Stripes\"\n    o2.foo = \"bar\"\n    r = s.get_dupe_groups([o1, o2])\n    eq_(len(r), 1)  # Because 'foo' is not scanned, they match\n\n\ndef test_tag_scan_converts_to_str(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"track\"}\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.track = 42\n    o2.track = 42\n    try:\n        r = s.get_dupe_groups([o1, o2])\n    except TypeError:\n        raise AssertionError()\n    eq_(len(r), 1)\n\n\ndef test_tag_scan_non_ascii(fake_fileexists):\n    s = Scanner()\n    s.scan_type = ScanType.TAG\n    s.scanned_tags = {\"title\"}\n    o1 = no(\"foo\")\n    o2 = no(\"bar\")\n    o1.title = \"foobar\\u00e9\"\n    o2.title = \"foobar\\u00e9\"\n    try:\n        r = s.get_dupe_groups([o1, o2])\n    except UnicodeEncodeError:\n        raise AssertionError()\n    eq_(len(r), 1)\n\n\ndef test_ignore_list(fake_fileexists):\n    s = Scanner()\n    f1 = no(\"foobar\")\n    f2 = no(\"foobar\")\n    f3 = no(\"foobar\")\n    f1.path = Path(\"dir1/foobar\")\n    f2.path = Path(\"dir2/foobar\")\n    f3.path = Path(\"dir3/foobar\")\n    ignore_list = IgnoreList()\n    ignore_list.ignore(str(f1.path), str(f2.path))\n    ignore_list.ignore(str(f1.path), str(f3.path))\n    r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)\n    eq_(len(r), 1)\n    g = r[0]\n    eq_(len(g.dupes), 1)\n    assert f1 not in g\n    assert f2 in g\n    assert f3 in g\n    # Ignored matches are not counted as discarded\n    eq_(s.discarded_file_count, 0)\n\n\ndef test_ignore_list_checks_for_unicode(fake_fileexists):\n    # scanner was calling path_str for ignore list checks. Since the Path changes, it must\n    # be unicode(path)\n    s = Scanner()\n    f1 = no(\"foobar\")\n    f2 = no(\"foobar\")\n    f3 = no(\"foobar\")\n    f1.path = Path(\"foo1\\u00e9\")\n    f2.path = Path(\"foo2\\u00e9\")\n    f3.path = Path(\"foo3\\u00e9\")\n    ignore_list = IgnoreList()\n    ignore_list.ignore(str(f1.path), str(f2.path))\n    ignore_list.ignore(str(f1.path), str(f3.path))\n    r = s.get_dupe_groups([f1, f2, f3], ignore_list=ignore_list)\n    eq_(len(r), 1)\n    g = r[0]\n    eq_(len(g.dupes), 1)\n    assert f1 not in g\n    assert f2 in g\n    assert f3 in g\n\n\ndef test_file_evaluates_to_false(fake_fileexists):\n    # A very wrong way to use any() was added at some point, causing resulting group list\n    # to be empty.\n    class FalseNamedObject(NamedObject):\n        def __bool__(self):\n            return False\n\n    s = Scanner()\n    f1 = FalseNamedObject(\"foobar\", path=\"p1\")\n    f2 = FalseNamedObject(\"foobar\", path=\"p2\")\n    r = s.get_dupe_groups([f1, f2])\n    eq_(len(r), 1)\n\n\ndef test_size_threshold(fake_fileexists):\n    # Only file equal or higher than the size_threshold in size are scanned\n    s = Scanner()\n    f1 = no(\"foo\", 1, path=\"p1\")\n    f2 = no(\"foo\", 2, path=\"p2\")\n    f3 = no(\"foo\", 3, path=\"p3\")\n    s.size_threshold = 2\n    groups = s.get_dupe_groups([f1, f2, f3])\n    eq_(len(groups), 1)\n    [group] = groups\n    eq_(len(group), 2)\n    assert f1 not in group\n    assert f2 in group\n    assert f3 in group\n\n\ndef test_tie_breaker_path_deepness(fake_fileexists):\n    # If there is a tie in prioritization, path deepness is used as a tie breaker\n    s = Scanner()\n    o1, o2 = no(\"foo\"), no(\"foo\")\n    o1.path = Path(\"foo\")\n    o2.path = Path(\"foo/bar\")\n    [group] = s.get_dupe_groups([o1, o2])\n    assert group.ref is o2\n\n\ndef test_tie_breaker_copy(fake_fileexists):\n    # if copy is in the words used (even if it has a deeper path), it becomes a dupe\n    s = Scanner()\n    o1, o2 = no(\"foo bar Copy\"), no(\"foo bar\")\n    o1.path = Path(\"deeper/path\")\n    o2.path = Path(\"foo\")\n    [group] = s.get_dupe_groups([o1, o2])\n    assert group.ref is o2\n\n\ndef test_tie_breaker_same_name_plus_digit(fake_fileexists):\n    # if ref has the same words as dupe, but has some just one extra word which is a digit, it\n    # becomes a dupe\n    s = Scanner()\n    o1 = no(\"foo bar 42\")\n    o2 = no(\"foo bar [42]\")\n    o3 = no(\"foo bar (42)\")\n    o4 = no(\"foo bar {42}\")\n    o5 = no(\"foo bar\")\n    # all numbered names have deeper paths, so they'll end up ref if the digits aren't correctly\n    # used as tie breakers\n    o1.path = Path(\"deeper/path\")\n    o2.path = Path(\"deeper/path\")\n    o3.path = Path(\"deeper/path\")\n    o4.path = Path(\"deeper/path\")\n    o5.path = Path(\"foo\")\n    [group] = s.get_dupe_groups([o1, o2, o3, o4, o5])\n    assert group.ref is o5\n\n\ndef test_partial_group_match(fake_fileexists):\n    # Count the number of discarded matches (when a file doesn't match all other dupes of the\n    # group) in Scanner.discarded_file_count\n    s = Scanner()\n    o1, o2, o3 = no(\"a b\"), no(\"a\"), no(\"b\")\n    s.min_match_percentage = 50\n    [group] = s.get_dupe_groups([o1, o2, o3])\n    eq_(len(group), 2)\n    assert o1 in group\n    # The file that will actually be counted as a dupe is undefined. The only thing we want to test\n    # is that we don't have both\n    if o2 in group:\n        assert o3 not in group\n    else:\n        assert o3 in group\n    eq_(s.discarded_file_count, 1)\n\n\ndef test_dont_group_files_that_dont_exist(tmpdir):\n    # when creating groups, check that files exist first. It's possible that these files have\n    # been moved during the scan by the user.\n    # In this test, we have to delete one of the files between the get_matches() part and the\n    # get_groups() part.\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    p = Path(str(tmpdir))\n    with p.joinpath(\"file1\").open(\"w\") as fp:\n        fp.write(\"foo\")\n    with p.joinpath(\"file2\").open(\"w\") as fp:\n        fp.write(\"foo\")\n    file1, file2 = fs.get_files(p)\n\n    def getmatches(*args, **kw):\n        file2.path.unlink()\n        return [Match(file1, file2, 100)]\n\n    s._getmatches = getmatches\n\n    assert not s.get_dupe_groups([file1, file2])\n\n\ndef test_folder_scan_exclude_subfolder_matches(fake_fileexists):\n    # when doing a Folders scan type, don't include matches for folders whose parent folder already\n    # match.\n    s = Scanner()\n    s.scan_type = ScanType.FOLDERS\n    topf1 = no(\"top folder 1\", size=42)\n    topf1.digest = topf1.digest_partial = topf1.digest_samples = b\"some_digest__1\"\n    topf1.path = Path(\"/topf1\")\n    topf2 = no(\"top folder 2\", size=42)\n    topf2.digest = topf2.digest_partial = topf2.digest_samples = b\"some_digest__1\"\n    topf2.path = Path(\"/topf2\")\n    subf1 = no(\"sub folder 1\", size=41)\n    subf1.digest = subf1.digest_partial = subf1.digest_samples = b\"some_digest__2\"\n    subf1.path = Path(\"/topf1/sub\")\n    subf2 = no(\"sub folder 2\", size=41)\n    subf2.digest = subf2.digest_partial = subf2.digest_samples = b\"some_digest__2\"\n    subf2.path = Path(\"/topf2/sub\")\n    eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2])), 1)  # only top folders\n    # however, if another folder matches a subfolder, keep in in the matches\n    otherf = no(\"other folder\", size=41)\n    otherf.digest = otherf.digest_partial = otherf.digest_samples = b\"some_digest__2\"\n    otherf.path = Path(\"/otherfolder\")\n    eq_(len(s.get_dupe_groups([topf1, topf2, subf1, subf2, otherf])), 2)\n\n\ndef test_ignore_files_with_same_path(fake_fileexists):\n    # It's possible that the scanner is fed with two file instances pointing to the same path. One\n    # of these files has to be ignored\n    s = Scanner()\n    f1 = no(\"foobar\", path=\"path1/foobar\")\n    f2 = no(\"foobar\", path=\"path1/foobar\")\n    eq_(s.get_dupe_groups([f1, f2]), [])\n\n\ndef test_dont_count_ref_files_as_discarded(fake_fileexists):\n    # To speed up the scan, we don't bother comparing contents of files that are both ref files.\n    # However, this causes problems in \"discarded\" counting and we make sure here that we don't\n    # report discarded matches in exact duplicate scans.\n    s = Scanner()\n    s.scan_type = ScanType.CONTENTS\n    o1 = no(\"foo\", path=\"p1\")\n    o2 = no(\"foo\", path=\"p2\")\n    o3 = no(\"foo\", path=\"p3\")\n    o1.digest = o1.digest_partial = o1.digest_samples = \"foobar\"\n    o2.digest = o2.digest_partial = o2.digest_samples = \"foobar\"\n    o3.digest = o3.digest_partial = o3.digest_samples = \"foobar\"\n    o1.is_ref = True\n    o2.is_ref = True\n    eq_(len(s.get_dupe_groups([o1, o2, o3])), 1)\n    eq_(s.discarded_file_count, 0)\n\n\ndef test_prioritize_me(fake_fileexists):\n    # in ScannerME, bitrate goes first (right after is_ref) in prioritization\n    s = ScannerME()\n    o1, o2 = no(\"foo\", path=\"p1\"), no(\"foo\", path=\"p2\")\n    o1.bitrate = 1\n    o2.bitrate = 2\n    [group] = s.get_dupe_groups([o1, o2])\n    assert group.ref is o2\n"
  },
  {
    "path": "core/util.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport time\nimport sys\nimport os\nimport urllib.request\nimport urllib.error\nimport json\nimport semantic_version\nimport logging\nfrom typing import Union\n\nfrom hscommon.util import format_time_decimal\n\n\ndef format_timestamp(t, delta):\n    if delta:\n        return format_time_decimal(t)\n    else:\n        if t > 0:\n            return time.strftime(\"%Y/%m/%d %H:%M:%S\", time.localtime(t))\n        else:\n            return \"---\"\n\n\ndef format_words(w):\n    def do_format(w):\n        if isinstance(w, list):\n            return \"(%s)\" % \", \".join(do_format(item) for item in w)\n        else:\n            return w.replace(\"\\n\", \" \")\n\n    return \", \".join(do_format(item) for item in w)\n\n\ndef format_perc(p):\n    return \"%0.0f\" % p\n\n\ndef format_dupe_count(c):\n    return str(c) if c else \"---\"\n\n\ndef cmp_value(dupe, attrname):\n    value = getattr(dupe, attrname, \"\")\n    return value.lower() if isinstance(value, str) else value\n\n\ndef fix_surrogate_encoding(s, encoding=\"utf-8\"):\n    # ref #210. It's possible to end up with file paths that, while correct unicode strings, are\n    # decoded with the 'surrogateescape' option, which make the string unencodable to utf-8. We fix\n    # these strings here by trying to encode them and, if it fails, we do an encode/decode dance\n    # to remove the problematic characters. This dance is *lossy* but there's not much we can do\n    # because if we end up with this type of string, it means that we don't know the encoding of the\n    # underlying filesystem that brought them. Don't use this for strings you're going to re-use in\n    # fs-related functions because you're going to lose your path (it's going to change). Use this\n    # if you need to export the path somewhere else, outside of the unicode realm.\n    # See http://lucumr.pocoo.org/2013/7/2/the-updated-guide-to-unicode/\n    try:\n        s.encode(encoding)\n    except UnicodeEncodeError:\n        return s.encode(encoding, \"replace\").decode(encoding)\n    else:\n        return s\n\n\ndef executable_folder():\n    return os.path.dirname(os.path.abspath(sys.argv[0]))\n\n\ndef check_for_update(current_version: str, include_prerelease: bool = False) -> Union[None, dict]:\n    request = urllib.request.Request(\n        \"https://api.github.com/repos/arsenetar/dupeguru/releases\",\n        headers={\"Accept\": \"application/vnd.github.v3+json\"},\n    )\n    try:\n        with urllib.request.urlopen(request) as response:\n            if response.status != 200:\n                logging.warn(f\"Error retriving updates. Status: {response.status}\")\n                return None\n            try:\n                response_json = json.loads(response.read())\n            except json.JSONDecodeError as ex:\n                logging.warn(f\"Error parsing updates. {ex.msg}\")\n                return None\n    except urllib.error.URLError as ex:\n        logging.warn(f\"Error retriving updates. {ex.reason}\")\n        return None\n    new_version = semantic_version.Version(current_version)\n    new_url = None\n    for release in response_json:\n        release_version = semantic_version.Version(release[\"name\"])\n        if new_version < release_version and (include_prerelease or not release_version.prerelease):\n            new_version = release_version\n            new_url = release[\"html_url\"]\n    if new_url is not None:\n        return {\"version\": new_version, \"url\": new_url}\n    else:\n        return None\n"
  },
  {
    "path": "help/changelog",
    "content": "=== 4.3.1 (2022-07-08)\n* Fix issue where cache db exceptions could prevent files being hashed (#1015)\n* Add extra guard for non-zero length files without digests to prevent false duplicates\n* Update Italian translations\n\n=== 4.3.0 (2022-07-01)\n* Redirect stdout from custom command to the log files (#1008)\n* Update translations\n* Fix typo in debian control file (#989)\n* Add option to profile scans\n* Update fs.py to optimize stat() calls\n* Fix Error when delete after scan (#988)\n* Update directory scanning to use os.scandir() and DirEntry objects\n* Improve performance of Directories.get_state()\n* Migrate from hscommon.path to pathlib\n* Switch file hashing to xxhash with fallback to md5\n* Add update check feature to about box\n\n=== 4.2.1 (2022-03-25)\n* Default to English on unsupported system language (#976)\n* Fix image viewer zoom datatype issue (#978)\n* Fix errors from window change event (#937, #980)\n* Fix deprecation warning from SQLite\n* Enforce minimum Windows version in installer (#983)\n* Fix help path for local files\n* Drop python 3.6 support\n* VS Code project settings added, yaml validation for GitHub actions\n\n=== 4.2.0 (2021-01-24)\n\n* Add Malay and Turkish\n* Add dark style for windows (#900)\n* Add caching md5 file hashes (#942)\n* Add feature to partially hash large files, with user adjustable preference (#908)\n* Add portable mode (store settings next to executable)\n* Add file association for .dupeguru files on windows\n* Add ability to pass .dupeguru file to load on startup (#902)\n* Add ability to reveal in explorer/finder (#895)\n* Switch audio tag processing from hsaudiotag to mutagen (#440)\n* Add ability to use Qt dialogs instead of native OS dialogs for some file selection operations\n* Add OS and Python details to error dialog to assist in troubleshooting\n* Add preference to ignore large files with threshold (#430)\n* Fix error on close from DetailsPanel (#857, #873)\n* Change reference background color (#894, #898)\n* Remove stripping of unicode characters when matching names (#879)\n* Fix exception when deleting in delta view (#863, #905)\n* Fix dupes only view not updating after re-prioritize results (#757, #910, #911)\n* Fix ability to drag'n'drop file/folder with certain characters in name (#897)\n* Fix window position opening partially offscreen (#653)\n* Fix TypeError is photo mode (#551)\n* Change message for when files are deleted directly (#904)\n* Add more feedback during scan (#700)\n* Add Python version check to build.py (#589)\n* General code cleanups\n* Improvements to using standardized build tooling\n* Moved CI/CD to github actions, added codeql, SonarCloud\n\n=== 4.1.1 (2021-03-21)\n\n* Add Japanese\n* Update internationalization and translations to be up to date with current UI.\n* Minor translation and UI language updates\n* Fix language selection issues on Windows (#760)\n* Add some additional notes about builds on Linux based systems\n* Add import from transifex export to build.py\n\n=== 4.1.0 (2020-12-29)\n\n* Use tabs instead of separate windows (#688)\n* Show the shortcut for \"mark selected\" in results dialog (#656, #641)\n* Add image comparison features to details dialog (#683)\n* Add the ability to use regex based exclusion filters (#705)\n* Change reference row background color, and allow user to adjust the color (#701)\n* Save / Load directories as XML (#706)\n* Workaround for EXIF IFD type mismatch in parsing function (#630, #698)\n* Progress dialog stuck at \"Verified X/X matches\" (#693, #694)\n* Fix word wrap in ignore list dialog (#687)\n* Fix issue with result window action on creation (#685)\n* Colorize details table differences, allow moving rows (#682)\n* Fix loading Result of 'Scan Type: Folders' shows only '---' in every table cell (#677, #676)\n* Fix issue with details and results dialog row trimming (#655, #654)\n* Add option to enable/disable bold font (#646, #314)\n* Use relative icon path for themes to override more easily (#746)\n* Fix issues with Python 3.8 compatibility (#665)\n* Fix flake8 issues (#672)\n* Update to use newer pytest and expand flake8 checking, cleanup various Deprecation Warnings\n* Add warnings to packaging script when files are not built (#691)\n* Use relative icon path for themes to override more easily (#746)\n* Update Packaging for Ubuntu (#593)\n* Minor Build Updates (#627, #575, #628, #614)\n* Update CI builds and add windows CI (#572, #669)\n\n=== 4.0.4 (2019-05-13)\n\n* Update qt/platform.py to support other Unix style OSes (#444)\n* Fix font size scaling issue in properties dialog [qt] (#504)\n* Updates to support Python 3.7\n* Fix issue with result window appearing partially off-screen [qt] (#521)\n* Fix translation error for Simplified Chinese\n* Updates to language files for German (#479)\n* Fix error with multiple close calls to the progress window [qt] (#460, #449)\n* Add Travis CI Builds\n* Un-recurse methods get_files() and get_state() to improve stability (#421)\n* Updates to language files for Italian (#445, #446, #447, #448)\n* Fix issue with cache_shelve (#402, #439)\n* Updated Windows packaging and builds (#438, #456, #461, #491, #474, #490, #565)\n* Handle OS termination signals (#425)\n* Make documentation installation optional\n* Move cocoa UI to dupeguru-cocoa [cocoa]\n\n=== 4.0.3 (2016-11-24)\n\n* Add new picture cache backend: shelve\n* Make shelve picture cache backend the active one on MacOS to fix #394 more\n  elegantly. [cocoa]\n* Remove Sparkle (auto-updates) due to technical limitations. [cocoa]\n\n=== 4.0.2 (2016-10-09)\n\n* Fix systematic crash in Picture Mode under MacOS Sierra. (#394)\n* No change for Linux. Just keeping version in sync.\n\n=== 4.0.1 (2016-08-24)\n\n* Add Greek localization, by Gabriel Koutilellis. (#382)\n* Fix localization base path. [qt] (#378)\n* Fix broken load results dialog. [qt]\n* Fix crash on load results. [cocoa] (#380)\n* Save preferences more predictably. [qt] (#379)\n* Fix picture mode's fuzzy block scanner threshold. (#387)\n\n=== 4.0.0 (2016-07-01)\n\n* Merge Standard, Music and Picture editions in the same application!\n* Improve documentation. (#294)\n* Add Polish, Korean, Spanish and Dutch localizations.\n* qt: Fix wrong use_regexp option propagation to core. (#295)\n* qt: Fix progress window mistakenly showing up on startup. (#357)\n* Bump Python requirement to v3.4.\n* Bump OS X requirement to 10.8\n* Drop Windows support, maybe temporarily.\n  `Details <https://www.hardcoded.net/archive2015#2015-11-01>`_\n* cocoa: Drop iPhoto, Aperture and iTunes support. Was unmaintained and obsolete.\n* Drop \"Audio Contents\" scan type. Was confusing and seldom useful.\n* Change license to GPLv3\n\n=== 3.9.1 (2014-10-17)\n\n* Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254)\n* Fixed ``PermissionError`` on saving results. (#266)\n* Fixed a build problem introduced by Sphinx 1.2.3.\n* Updated German localisation, by Frank Weber.\n\n=== 3.9.0 (2014-04-19)\n\n* This is mostly a dependencies upgrade.\n* Upgraded to Python 3.3.\n* Upgraded to Qt 5.\n* Minimum Windows version is now Windows 7 64bit.\n* Minimum Ubuntu version is now 14.04.\n* Minimum OS X version is now 10.7 (Lion).\n* ... But with a couple of little improvements.\n* Improved documentation.\n* Overwrite subfolders' state when setting states in folder dialog (#248)\n* The error report dialog now brings the user to Github issues.\n\n=== 3.8.0 (2013-12-07)\n\n* Disable symlink/hardlink deletion option when not relevant. (#247)\n* Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228)\n* Make non-numeric delta comparison case insensitive. (#239)\n* Fix surrogate-related UnicodeEncodeError on CSV export. (#210)\n* Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238)\n* Improved documentation.\n* Important internal refactorings.\n* Dropped Ubuntu 12.04 and 12.10 support.\n* Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).\n\n=== 3.7.1 (2013-08-19)\n\n* Fixed folder scan type, which was broken in v3.7.0.\n\n=== 3.7.0 (2013-08-17)\n\n* Improved delta values to support non-numerical values. (#213)\n* Improved the Re-Prioritize dialog's UI. (#224)\n* Added hardlink/symlink support on Windows Vista+. (#220)\n* Dropped 32bit support on Mac OS X.\n* Added Vietnamese localization by Phan Anh.\n\n=== 3.6.1 (2013-04-28)\n\n* Improved \"Make Selection Reference\" to make it clearer. (#222)\n* Improved \"Open Selected\" to allow opening more than one file at once. (#142)\n* Fixed a few typos here and there. (#216 #225)\n* Tweaked the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).\n* Added Arch Linux packaging\n* Added a 64-bit build for Windows.\n* Improved Russian localization by Kyrill Detinov.\n* Improved Brazilian localization by Victor Figueiredo.\n\n=== 3.6.0 (2012-08-08)\n\n* Added \"Export to CSV\". (#189)\n* Added \"Replace with symlinks\" to complement \"Replace with hardlinks\". [Mac, Linux] (#194)\n* dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204)\n* Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198)\n* Fixed result table cells which mistakenly became writable in v3.5.0. [Mac] (#203)\n* Fixed \"Rename Selected\" which was broken since v3.5.0. [Mac] (#202)\n* Fixed a bug where \"Reset to Defaults\" in the Columns menu wouldn't refresh menu items' marked state.\n* Added Brazilian localization by Victor Figueiredo.\n\n=== 3.5.0 (2012-06-01)\n\n* Added a Deletion Options panel.\n* Greatly improved memory usage for big scans.\n* Added a keybinding for the filter field. (#182) [Mac]\n* Upgraded minimum requirements for Ubuntu to 12.04.\n\n=== 3.4.1 (2012-04-14)\n\n* Fixed the \"Folders\" scan type. [Mac]\n* Fixed localization issues. [Windows, Linux]\n\n=== 3.4.0 (2012-03-29)\n\n* Improved results window UI. [Windows, Linux]\n* Added a dialog to edit the Ignore List.\n* Added the ability to sort results by \"marked\" status.\n* Fixed \"Open with default application\". (#190)\n* Fixed a bug where there would be a false reporting of discarded matches. (#195)\n* Fixed various localization glitches.\n* Fixed hard crashes on crash reporting. (#196)\n* Fixed bug where the details panel would show up at inconvenient places in the screen. [Windows, Linux]\n\n=== 3.3.3 (2012-02-01)\n\n* Fixed crash on adding some folders. [Mac OS X]\n* Added Ukrainian localization by Yuri Petrashko.\n\n=== 3.3.2 (2012-01-16)\n\n* Fixed random hard crashes (yeah, again). [Mac OS X]\n* Fixed crash on Export to HTML. [Windows, Linux]\n* Added Armenian localization by Hrant Ohanyan.\n* Added Russian localization by Igor Pavlov.\n\n=== 3.3.1 (2011-12-02)\n\n* Fixed a couple of nasty crashes.\n\n=== 3.3.0 (2011-11-30)\n\n* Added multiple-selection in folder selection dialog for a more efficient folder removal. (#179)\n* Fixed a crash in the prioritize dialog. (#178)\n* Fixed a bug where mass marking with a filter would mark more than filtered duplicates. (#181)\n* Fixed random hard crashes. [Mac OS X] (#183 #184)\n* Added Czech localization by Aleš Nehyba.\n* Added Italian localization by Paolo Rossi.\n\n=== 3.2.1 (2011-10-02)\n\n* Fixed a couple of broken action bindings from v3.2.0.\n\n=== 3.2.0 (2011-09-27)\n\n* Added duplicate re-prioritization dialog. (#138)\n* Added font size preference for duplicate table. (#82)\n* Added Quicklook support. [Mac OS X] (#21)\n* Improved behavior of Mark Selected. (#139)\n* Improved filename sorting. (#169)\n* Added Chinese (Simplified) localization by Eric Dee.\n* Tweaked the fairware system.\n* Upgraded minimum requirements to OS X 10.6 and Ubuntu 11.04.\n\n=== 3.1.2 (2011-08-25)\n\n* Fixed a bug preventing the Folders scan from working. (#172)\n\n=== 3.1.1 (2011-08-24)\n\n* Added German localization by Gregor Tätzner.\n* Improved OS X Lion compatibility. [Mac OS X]\n* Made the file collection phase cancellable. (#168)\n* Fixed glitch in folder window upon selecting a folder state. [Windows, Linux] (#165)\n* Fixed a text coloring glitch in the results. (#156)\n* Fixed glitch in the sorting feature of the Folder column. (#161)\n* Make sure that saved results have the \".dupeguru\" extension. [Linux] (#157)\n\n=== 3.1.0 (2011-04-16)\n\n* Added the \"Folders\" scan type. (#89)\n* Fixed a couple of crashes. (#140 #149)\n\n=== 3.0.2 (2011-03-16)\n\n* Fixed crash after removing marked dupes. (#140)\n* Fixed crash on error handling. [Windows] (#144)\n* Fixed crash on copy/move. [Windows] (#148)\n* Fixed crash when launching dupeGuru from a very long folder name. [Mac OS X] (#119)\n* Fixed a refresh bug in directory panel. (#153)\n* Improved reliability of the \"Send to Trash\" operation. [Linux]\n* Tweaked Fairware reminders.\n\n=== 3.0.1 (2011-01-27)\n\n* Restored the context menu which had been broken in 3.0.0. [Mac OS X] (#133)\n* Fixed a bug where an \"unsaved results\" warning would be issued on quit even with empty results. (#134)\n* Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135)\n* Folders added through drag and drop are added to the recent folders list. (#136)\n* Added a debugging mode. (#132)\n* Fixed french localization glitches.\n\n=== 3.0.0 (2011-01-24)\n\n* Re-designed the UI. (#129)\n* Internationalized dupeGuru and localized it to french. (#32)\n* Changed the format of the help file. (#130)\n\n=== 2.12.3 (2011-01-01)\n\n* Fixed bug causing results to be corrupted after a scan cancellation. (#120)\n* Fixed crash when fetching Fairware unpaid hours. (#121)\n* Fixed crash when replacing files with hardlinks. (#122)\n\n=== 2.12.2 (2010-10-05)\n\n* Fixed delta column colors which were broken since 2.12.0.\n* Fixed column sorting crash. (#108)\n* Fixed occasional crash during scan. (#106)\n\n=== 2.12.1 (2010-09-30)\n*  Re-licensed dupeGuru to BSD and made it [Fairware](http://open.hardcoded.net/about/).\n\n=== 2.12.0 (2010-09-26)\n*  Improved UI with a little revamp.\n*  Added the possibility to place hardlinks to references after having deleted duplicates. [Mac OS X, Linux] (#91)\n*  Added an option to ignore duplicates hardlinking to the same file. [Mac OS X, Linux] (#92)\n*  Added multiple selection in the \"Add Directory\" dialog. [Mac OS X] (#105)\n*  Fixed a bug preventing drag & drop from working in the Directories panel. [Windows, Linux]\n\n=== 2.11.1 (2010-08-26)\n*  Fixed HTML exporting which was broken in 2.11.0.\n\n=== 2.11.0 (2010-08-18)\n*  Added the ability to save results (and reload them) at arbitrary locations.\n*  Improved the way reference files in dupe groups are chosen. (#15)\n*  Remember size/position of all windows between launches. (#102)\n*  Fixed a bug sometimes preventing dupeGuru from reloading previous results.\n*  Fixed a bug sometimes causing the progress dialog to be stuck there. [Mac OS X] (#103)\n*  Removed the Creation Date column, which wasn't displaying the correct value anyway. (#101)\n\n=== 2.10.1 (2010-07-15)\n*  Fixed a couple of crashes. (#95, #97, #100)\n\n=== 2.10.0 (2010-04-13)\n*  Improved error messages when files can't be sent to trash, moved or copied.\n*  Added a custom command invocation action. (#12)\n*  Filters are now applied on whole paths. (#4)\n\n=== 2.9.2 (2010-02-10)\n*  dupeGuru is now 64-bit on Mac OS X!\n*  Fixed a crash upon quitting when support folder is not present. (#83)\n*  Fixed a crash during sorting. (#85)\n*  Fixed selection glitches, especially while renaming. (#93)\n\n=== 2.9.1 (2010-01-13)\n*  Improved memory usage for Contents scans. (#75)\n*  Improved scanning speed when ref directories are involved. (#77)\n*  Show a message dialog at the end of the scan if no duplicates are found. (#81)\n*  Fixed a bug sometimes causing the small files threshold pref to be ignored. [Mac OS X] (#75)\n\n=== 2.9.0 (2009-11-03)\n*  Significantly improved speed and memory usage of big contents-based scans.\n*  Added drag & drop support in the Directories panel. (#9)\n*  Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72)\n*  Dropped support for Mac OS X 10.4 (Tiger)\n\n=== 2.8.2 (2009-10-14)\n*  Improved directory selection in the Directories panel (Windows). (#56)\n*  Fixed a bug preventing dupeGuru from starting on certain machines (Windows). (#68)\n*  Fixed a crash during very big scans. (#70)\n\n=== 2.8.1 (2009-10-02)\n*  Fixed crash with filtering when regular expressions were enabled. (#60)\n*  Fixed crash when setting directories' state. (Mac OS X) (#66)\n*  Fixed crash with Make Reference when certain filters are applied. (Mac OS X) (#55)\n*  Improved error handling during delete/move/copy actions. (#62 #65)\n\n=== 2.8.0 (2009-09-07)\n*  Added support for all kinds of bundle (not just applications) (Mac OS X) (#11)\n*  Re-introduced the Export to XHTML feature to Windows. (#14)\n*  Improved Export to XHTML speed. (#14)\n*  Improved Contents scanning speed for large files. (#33)\n*  Improved the grouping algorithm to reduce the number of discarded files in non-exact scans. (#51)\n*  Stopped showing the same file on the 2 sides of the details panel when a ref file is selected. (#50)\n*  Fixed crashes in the Directories panel. (#46)\n\n=== 2.7.3 (2009-06-20)\n*  Fixed bugs with selection being jumpy during \"Make Reference\" actions and Power Marker\n        switches. (#3)\n*  Fixed crash happening when a file with non-roman characters couldn't be analyzed. (#30)\n*  Fixed crash sometimes happening during the file collection phase in scanning. (#38)\n*  Restored double-click and right-click behavior lost in the PyQt move (Windows). (#34 #35)\n\n=== 2.7.2 (2009-06-10)\n*  Fixed an occasional crash on Copy/Move operations. (#16)\n*  Added automatic exclusion for sensible folders (like system folders). (#20)\n*  Fixed an occasional crash when application files were part of the results (Mac OS X). (#25)\n\n=== 2.7.1 (2009-05-29)\n*  Fixed a bug causing crashes when having application files in the results.\n*  Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files.\n*  Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again.\n\n=== 2.7.0 (2009-05-25)\n*  Converted the Windows GUI to Qt.\n*  Improved the reliability of the scanning process.\n\n=== 2.6.1 (2009-03-27)\n* **Fixed** an occasional crash caused by permission issues.\n* **Fixed** a bug where the \"X discarded\" notice would show a too large number of discarded\n      duplicates.\n\n=== 2.6.0 (2008-09-10)\n\n* **Added** a small file threshold preference.\n* **Added** a notice in the status bar when matches were discarded during the scan.\n* **Improved** duplicate prioritization (smartly chooses which file you will keep).\n* **Improved** scan progress feedback.\n* **Improved** responsiveness of the user interface for certain actions.\n\n=== 2.5.4 (2008-08-10)\n\n* **Improved** the speed of results loading and saving.\n* **Fixed** a crash sometimes occurring during duplicate deletion.\n\n=== 2.5.3 (2008-07-08)\n\n* **Improved** unicode handling for filenames. dupeGuru will now find a lot more duplicates if your files have non-ascii characters in it.\n* **Fixed** \"Clear Ignore List\" crash in Windows.\n\n=== 2.5.2 (2008-01-10)\n\n* **Improved** the handling of low memory situations.\n* **Improved** the directory panel. The \"Remove\" button changes to \"Put Back\" when an excluded directory is selected.\n* **Improved** scan, delete and move speed in situations where there were a lot of duplicates.\n* **Fixed** occasional crashes when moving bundles (such as .app files).\n* **Fixed** occasional crashes when moving a lot of files at once.\n\n=== 2.5.1 (2007-11-22)\n\n* **Added** the \"Remove empty folders\" option.\n* **Fixed** results load/save issues.\n* **Fixed** occasional status bar inaccuracies when the results are filtered.\n\n\n=== 2.5.0 (2007-09-15)\n\n* **Added** post scan filtering.\n* **Fixed** issues with the rename feature under Windows\n* **Fixed** some user interface annoyances under Windows\n\n\n=== 2.4.8 (2007-04-14)\n\n* **Improved** UI responsiveness (using threads) under Mac OS X.\n* **Improved** result load/save speed and memory usage.\n\n=== 2.4.7 (2007-03-10)\n\n* **Fixed** a \"bad file descriptor\" error occasionally popping up.\n* **Fixed** a bug with non-latin directory names.\n\n=== 2.4.6 (2007-02-10)\n\n* **Added** Re-orderable columns. In fact, I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).\n* **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.\n* **Fixed** a bug with all the Delete/Move/Copy actions with certain kinds of files.\n\n=== 2.4.5 (2007-01-11)\n\n* **Fixed** a bug with the Move action.\n\n=== 2.4.4 (2007-01-07)\n\n* **Fixed** a \"ghosting\" bug. Dupes deleted by dupeGuru would sometimes come back in subsequent scans (Windows).\n* **Fixed** bugs sometimes making dupeGuru crash when marking a dupe (Windows).\n* **Fixed** some minor visual glitches (Windows).\n\n=== 2.4.3 (2006-12-08)\n\n* **Fixed** a mishandling of \".app\" files (OS X).\n* **Fixed** a bug preventing files from \"reference\" directories to be displayed in blue in the results (Windows).\n* **Fixed** a bug preventing some files to be sent to the recycle bin (Windows).\n* **Fixed** a bug in the packaging preventing certain Windows configurations to start dupeGuru at all.\n\n=== 2.4.2 (2006-11-18)\n\n* **Fixed** a bug with directory states.\n\n=== 2.4.1 (2006-11-15)\n\n* **Fixed** a bug causing the ignore list not to be saved.\n* **Fixed** a bug sometimes making delete and move operations stall.\n\n=== 2.4.0 (2006-11-10)\n\n* **Changed** the Windows interface. It is now .NET based.\n* **Added** an auto-update feature to the windows version.\n* **Changed** the way power marking works. It is now a mode instead of a separate window.\n* **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.\n* **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.\n\n=== 2.3.4 (2006-11-07)\n\n* **Improved** speed and memory usage of the scanning engine, again. Does it mean there was a lot of improvements to be made? Nah...\n\n=== 2.3.3 (2006-11-02)\n\n* **Improved** speed and memory usage of the scanning engine, especially when the scan results in a lot of duplicates.\n*  Now I wonder if Sparkle is going to work well...\n\n=== 2.3.2 (2006-10-16)\n\n* **Added** an auto-update feature in the Mac OS X version (with Sparkle).\n* **Fixed** a bug preventing some duplicate reports to be created correctly under Windows.\n\n=== 2.3.1 (2006-10-02)\n\n* **Fixed** a bug preventing some duplicates to be found, especially when scanning lots of files.\n\n=== 2.3.0 (2006-09-22)\n\n* **Added** XHTML export feature.\n\n=== 2.2.10 (2006-08-31)\n\n* **Added** sticky columns.\n* **Fixed** an issue with file caching between scans.\n* **Fixed** an issue preventing some duplicates from being deleted/moved/copied.\n\n=== 2.2.9 (2006-08-27)\n\n* **Fixed** an issue with ignore list and unicode.\n* **Fixed** an issue with file attribute fetching sometimes causing dupeGuru to crash.\n* **Fixed** an issue in the directories panel under Windows.\n\n=== 2.2.8 (2006-08-17)\n\n* **Fixed** an issue in the duplicate seeking engine preventing some duplicates to be found.\n\n=== 2.2.7 (2006-08-12)\n\n* **Improved** unicode support.\n* **Improved** the \"Reveal in Finder\" (\"Open Containing Folder\" in Windows) feature so it selects the file in the folder it opens.\n\n=== 2.2.6 (2006-08-07)\n\n* **Improved** the ignore list system.\n* dupeGuru is now a Universal application on Mac OS X.\n\n=== 2.2.5 (2006-07-26)\n\n* **Improved** application (.app) dupe detection on Mac OS X.\n* **Fixed** an issue that occasionally made dupeGuru crash on startup.\n\n=== 2.2.4 (2006-06-27)\n\n* **Fixed** an issue with Move and Copy features.\n\n=== 2.2.3 (2006-06-15)\n\n* **Improved** duplicate scanning speed.\n* **Added** a warning that a file couldn't be renamed if a file with the same name already exists.\n\n=== 2.2.2 (2006-06-07)\n\n* **Added** \"Rename Selected\" feature.\n* **Fixed** some minor issues with \"Reload Last Results\" feature.\n* **Fixed** ignore list issues.\n\n=== 2.2.1 (2006-05-22)\n\n* **Fixed** occasional progress bar woes under Windows.\n* **Fixed** a bug in the registration system under Windows.\n* Nothing has been changed in the Mac OS X version, but I want to keep version in sync.\n\n=== 2.2.0 (2006-05-10)\n\n* **Added** destination path re-creation options.\n* **Added** an ignore list.\n* **Changed** the main icon.\n* **Improved** dramatically the delta values feature.\n\n=== 2.1.2 (2006-04-18)\n\n* **Added** the \"Match similar words\" option.\n* **Fixed** Power marking issues under Mac.\n\n=== 2.1.1 (2006-04-14)\n\n* **Added** the \"Display delta values\" option.\n* **Improved** Power marking sorting speed under Mac.\n* **Fixed** Power marking sorting issues.\n\n=== 2.1.0 (2006-04-03)\n\n* **Added** the Power Marker feature.\n* **Fixed** a column sorting bug. The results would sometimes lose their sort order.\n* **Fixed** a bug with the Make Reference feature. The results sometimes wasn't correctly refreshed after the reference switch.\n\n=== 2.0.1 (2006-03-23)\n\n* **Fixed** an issue occasionally occurring when trying to reload results from removable media that is no longer present.\n\n=== 2.0.0 (2006-03-17)\n\n* Complete rewrite.\n* Now runs on Mac OS X.\n\n=== 1.0.0 (2004-09-24)\n\n* Initial release.\n"
  },
  {
    "path": "help/changelog.tmpl",
    "content": ":tocdepth: 1\n\nChangelog\n=========\n\n**About the word \"crash\":** When reading this changelog, you might be alarmed at the number of fixes\nfor \"crashes\". Be aware that when the word \"crash\" is used here, it refers to \"soft crashes\" which\ndon't cause the application to quit. You simply get an error window that asks you if you want to\nsend the crash report to Hardcoded Software. Crashes that cause the application to quit are called\n\"hard crashes\" in this changelog.\n\n{changelog}\n"
  },
  {
    "path": "help/conf.tmpl",
    "content": "# -*- coding: utf-8 -*-\n#\n# dupeGuru documentation build configuration file, created by\n# sphinx-quickstart on Wed Jan 12 13:20:15 2011.\n#\n# This file is execfile()d with the current directory set to its containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\nimport sys, os\nimport re\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n\n# for autodocs\nsys.path.insert(0, os.path.abspath(os.path.join('..', '..')))\n\n# -- Misc fixes for autodoc\n\ndef fix_nulljob_in_sig(app, what, name, obj, options, signature, return_annotation):\n    if signature:\n        signature = re.sub(r\"<hscommon.jobprogress.job.NullJob object at 0x[\\da-f]+>\", \"nulljob\", signature)\n    return signature, return_annotation\n\ndef setup(app):\n    app.connect('autodoc-process-signature', fix_nulljob_in_sig)\n\nautodoc_member_order = 'groupwise'\n\n# -- General configuration -----------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be extensions\n# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.\nextensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'dupeGuru'\ncopyright = u'2016, Hardcoded Software'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '{version}'\n# The full version, including alpha/beta/rc tags.\nrelease = version\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\nlanguage = '{language}'\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#today = ''\n# Else, today_fmt is used as the format for a strftime call.\n#today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = ['_build']\n\n# The reST default role (used for this markup: `text`) to use for all documents.\n#default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n#modindex_common_prefix = []\n\n\n# -- Options for HTML output ---------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\nhtml_theme = 'haiku'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n#html_theme_path = []\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n#html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#html_logo = None\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n# html_static_path = ['_static']\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\n#html_last_updated_fmt = '%b %d, %Y'\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#html_additional_pages = {}\n\n# If false, no module index is generated.\n# html_domain_indices = False\n\n# If false, no index is generated.\n# html_use_index = False\n\n# If true, the index is split into individual pages for each letter.\n#html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\nhtml_show_sourcelink = False\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n#html_file_suffix = None\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'dupeGurudoc'\n\ntodo_include_todos = True\n"
  },
  {
    "path": "help/de/faq.rst",
    "content": "Häufig gestellte Fragen\n==========================\n\n.. topic:: What is dupeGuru?\n\n    .. only:: edition_se\n\n        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.\n\n    .. only:: edition_me\n\n        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.\n\n    .. only:: edition_pe\n\n        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.\n\n.. topic:: Was macht es besser ala andere Duplikatscanner?\n\n    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 <preferences>`.\n\n.. topic:: Wie sicher ist dupeGuru?\n\n    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.\n\n.. topic:: Was sind die Demo-Einschränkungen von dupeGuru?\n\n    Keine, dupeGuru ist `Fairware <http://open.hardcoded.net/about/>`_.\n\n.. topic:: Die Markierungsbox einer Datei, die ich löschen möchte, ist deaktiviert. Was muss ich tun?\n\n    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.\n\n.. topic:: ich habe einen Ordner aus dem ich wirklich nichts löschen möchte.\n\n    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`.\n\n.. topic:: Was bedeutet diese '(X verworfen)' Nachricht in der Statusbar?\n\n    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.\n\n.. topic:: Ich möchte alle Dateien aus einem bestimmten Ordner markieren. Was kann ich tun?\n\n    Aktiveren Sie den :doc:`Nur Duplikate <results>` 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.\n\n.. only:: edition_se or edition_pe\n\n    .. 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?\n\n        * Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.\n        * Aktivieren Sie den **Deltawerte** Modus.\n        * Gehen Sie auf die \"Größe\" Spalte, um die Ergebnisse nach Größe zu sortieren.\n        * Alle Duplikate unter -300 auswählen.\n        * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.\n        * Alle Duplikate über 300 auswählen\n        * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.\n\n    .. topic:: Ich möchte meine zuletzt geänderten Dateien zur Referenz machen. Was kann ich tun?\n\n        * Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.\n        * Aktivieren Sie den **Deltawerte** Modus.\n        * Gehen Sie auf die \"Modifikation\" Spalte, um die Ergebnisse nach Änderungsdatum zu sortieren.\n        * Klicken Sie erneut auf die \"Modifikation\" Spalte, um die Reihenfolge umzukehren.\n        * Wählen Sie alle Duplikate über 0.\n        * Klicken Sie auf **Mache Ausgewählte zur Referenz**.\n\n    .. topic:: Ich möchte alle Duplikate mit dem Wort copy markieren. Wie mache ich das?\n\n        * **Windows**: Klicken Sie auf **Aktionen --> Filter anwenden**, tippen \"copy\" und klicken auf OK.\n        * **Mac OS X**: Geben Sie \"copy\" in das \"Filter\" Feld in der Werkzeugleiste ein.\n        * Klicken Sie **Markieren --> Alle Markieren**.\n\n.. only:: edition_me\n\n    .. topic:: Ich möchte alle Stücke markieren, die mehr als 3 Sekunden von ihrer Referenz verschieden sind. Was kann ich tun?\n\n        * Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.\n        * Aktivieren Sie den **Deltawerte** Modus.\n        * Klicken Sie auf die \"Zeit\" Spalte, um nach Zeit zu sortieren.\n        * Wählen Sie alle Duplikate unter -00:03.\n        * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.\n        * Wählen Sie alle Duplikate über 00:03.\n        * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.\n\n    .. topic:: Ich möchte meine Stücke mit der höchsten Bitrate zur Referenz machen. Was kann ich tun?\n\n        * Aktivieren Sie den :doc:`Nur Duplikate <results>` Modus.\n        * Aktivieren Sie den **Deltawerte** Modus.\n        * Klicken Sie auf die \"Bitrate\" Spalte, um nach Bitrate zu sortieren.\n        * Klicken Sie erneut auf die \"Bitrate\" Spalte, um die Reihenfolge umzukehren.\n        * Wählen Sie alle Duplikate über 0.\n        * Klicken Sie auf **Mache Ausgewählte zur Referenz**.\n\n    .. topic:: Ich möchte nicht das [live] und [remix] Versionen meiner Stücke als Duplikate erkannt werden. Was kann ich tun?\n\n        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:\n\n        * **Windows**: Klicken Sie auf **Aktionen --> Filter anwenden**, geben \"[*]\" ein und klicken OK.\n        * **Mac OS X**: Geben Sie \"[*]\" in das \"Filter\" Feld der Werkzeugleiste ein.\n        * Klicken Sie auf **Markieren --> Alle Markieren**.\n        * Klicken Sie auf **Entferne Ausgewählte von den Ergebnissen**.\n\n.. 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?\n\n    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 <http://macchampion.com/arbysoft/BatchMod>`_ verwendet werden, um die Dateirechte zu reparieren.\n\n    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.\n\n    .. only:: edition_pe\n\n        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.\n\n    Wenn dies alles fehlschlägt, kontaktieren Sie `HS support <http://www.hardcoded.net/support>`_, wir werden das Problem lösen.\n\n.. todo:: This FAQ qestion is outdated, see english version.\n"
  },
  {
    "path": "help/de/folders.rst",
    "content": "Ordnerauswahl\n================\n\nDas 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.\n\nDas 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.\n\nUm 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.\n\nOrdnerzustände\n--------------\n\nJeder Ordner kann in einem von 3 Zuständen sein:\n\n* **Normal:** Duplikate in diesem Ordner können gelöscht werden.\n* **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.\n* **Ausgeschlossen:** Dateien in diesem Verzeichnis sind nicht im Scan eingeschlossen.\n\nDer 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.\n\nWenn sie einen Zustand für ein Verzeichnis setzen, erben alle Unterordner automatisch diesen Zustand, es sei denn Sie ändern den Zustand der Unterordner explizit.\n\n.. todo:: Add iPhoto/Aperture/iTunes libraries notes\n"
  },
  {
    "path": "help/de/index.rst",
    "content": "dupeGuru Hilfe\n===============\n\n.. only:: edition_se\n\n    Dieses Dokument ist auch auf `Englisch <http://dupeguru.voltaicideas.net/help/en/>`__ und `Französisch <http://dupeguru.voltaicideas.net/help/fr/>`__ verfügbar.\n\n.. only:: edition_se or edition_me\n\n    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.\n\n.. only:: edition_pe\n\n    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.\n\nObwohl 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 <quick_start>` Sektion\n\nEs ist eine gute Idee dupeGuru aktuell zu halten. Sie können die neueste Version auf der http://dupeguru.voltaicideas.net finden.\n\nInhalte:\n\n.. toctree::\n    :maxdepth: 2\n\n    quick_start\n    folders\n    preferences\n    results\n    reprioritize\n    faq\n    changelog\n"
  },
  {
    "path": "help/de/preferences.rst",
    "content": "Einstellungen\n=============\n\n.. only:: edition_se\n\n    **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.\n\n    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.\n\n    **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).\n\n.. only:: edition_me\n\n    **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.\n\n    * **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.\n    * **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).\n    * **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.\n    * **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.\n    * **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.\n    * **Audioinhalt:** Das selbe wie Inhalt, aber nur der Audioinhalt wird verglichen (ohne Metadaten).\n\n    **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).\n\n    **Tags zu scannen:** Bei der Nutzung des **Tags** Scan Typs, können Sie wählen welche Tags verglichen werden sollen.\n\n.. only:: edition_se or edition_me\n\n    **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\")).\n\n    **Ä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.\n\n.. only:: edition_pe\n\n    **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.\n\n    **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.\n\n    **Bilder unterschiedlicher Abmessung gleich:** Wird diese Box gewählt, dürfen Bilder unterschiedlicher Abmessung in einer Duplikategruppe sein..\n\n**Dateitypen dürfen gemischt werden:** Wird diese Box gewählt, dürfen Duplikategruppen Bilder mit unterschiedlichen Dateierweiterungen enthalten.\n\n**Ignoriere Duplikate die mit derselben Datei verlinkt sind:** Ist diese Option aktiviert, wird dupeGuru überprüfen ob Duplikate auf den selben `inode <http://en.wikipedia.org/wiki/Inode>`_ verweisen. Wenn sie es tun, werden sie nicht als Duplikat erkannt. (Nur für OS X und Linux)\n\n**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 <http://www.regular-expressions.info>`_.\n\n**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.\n\n**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave.\n\n* **Zum Ziel:** Alle Dateien werden direkt in das ausgwählte Verzeichnis gesendet, ohne zu versuchen den Quellpfad wiederherzustellen\n* **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.).\n* **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.\n\nAuf jeden Fall behandelt dupeGuru Namenskonflikte indem es dem Ziel-Dateinamen eine Nummer voranstellt, wenn der Dateiname bereits im Zielverzeichnis existiert.\n\n**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.\n\nDas 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).\n\nWenn 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::\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n"
  },
  {
    "path": "help/de/quick_start.rst",
    "content": "Schnellstart\n============\n\nDamit Sie sich schnell mit dupeGuru zurechtfinden, machen wir für den Anfang einen Standardscan mit den Voreinstellungen.\n\n* dupeGuru starten.\n* Zu scannende Ordner entweder mit drag & drop oder dem \"+\" Knopf auswählen.\n* Drücken Sie auf **Scan**.\n* Warten Sie bis der Scanvorgang fertig ist.\n* 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).\n* Wenn eine Datei kein Duplikat ist, wählen Sie es aus und drücken auf **Aktionen-->Entferne Ausgewählte aus den Ergebnissen**.\n* 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**.\n\nDas 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.\n"
  },
  {
    "path": "help/de/reprioritize.rst",
    "content": "Re-Prioritizing duplicates\n==========================\n\ndupeGuru tries to automatically determine which duplicate should go in each group's reference\nposition, but sometimes it gets it wrong. In many cases, clever dupe sorting with \"Delta Values\"\nand \"Dupes Only\" options in addition to the \"Make Selected into Reference\" action does the trick, but\nsometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes into\nplay. You can summon it through the \"Re-Prioritize Results\" item in the \"Actions\" menu.\n\nThis dialog allows you to select criteria according to which a reference dupe will be selected in\neach dupe group. The list of available criteria is on the left and the list of criteria you've\nselected is on the right.\n\nA criteria is a category followed by an argument. For example, \"Size (Highest)\" means that the dupe\nwith the biggest size will win. \"Folder (/foo/bar)\" means that dupes in this folder will win. To add\na criterion to the rightmost list, first select a category in the combobox, then select a\nsubargument in the list below, and then click on the right pointing arrow button.\n\nThe order of the list on the right is important (you can re-order items through drag & drop). When\npicking a dupe for reference position, the first criterion is used. If there's a tie, the second\ncriterion is used and so on and so on. For example, if your arguments are \"Size (Highest)\" and then\n\"Filename (Doesn't end with a number)\", the reference file that will be picked in a group will be\nthe biggest file, and if two or more files have the same size, the one that has a filename that\ndoesn't end with a number will be used. When all criteria result in ties, the order in which dupes\npreviously were in the group will be used.\n"
  },
  {
    "path": "help/de/results.rst",
    "content": "Ergebnisse\n==========\n\nSobald dupeGuru den Duplikatescan beendet hat, werden die Ergebnisse in Form einer Duplikate-Gruppenliste gezeigt.\n\nÜber Duplikatgruppen\n--------------------\n\nEine 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.\n\nSie 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?\n\nWelche 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.\n\nSie 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**.\n\nErgebnisse beurteilen\n---------------------\n\nObwohl 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.\n\nUm 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.\n\nWenn 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.\n\nMarkierung und Auswahl\n----------------------\n\nEin **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.\n\n.. todo:: Add \"Non-numerical delta\" information.\n\nNur Duplikate anzeigen\n----------------------\n\nWird 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.\n\nDie 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:\n\n* Nur Duplikate anzeigen aktivieren\n* Die \"Typ\" Spalte über das \"Spalten\" Menü hinzufügen\n* Auf \"Typ\" klicken, um die Liste zu sortieren\n* Das erste Duplikat mit dem \"exe\" Typ lokalisieren.\n* Es auswählen.\n* Die Liste herunterscrollen und das letzte Duplikat mit dem \"exe\" Typ finden.\n* Die Shift Taste halten und es auswählen.\n* Leertaste drücken, um alle ausgewählten Duplikate zu markieren.\n\nDeltawerte\n----------\n\nWenn 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.\n\nNur Duplikate anzeigen und Deltawerte\n-------------------------------------\n\nDer 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.\n\nSie 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.\n\nFiltern\n-------\n\nDupeGuru 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.\n\n.. todo:: Qt has a toolbar search field now, not a menu item.\n\n**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.\n\n**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\".\n\nIm 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.\n\nFü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 <http://www.regular-expressions.info>`_.\n\nFilter ignorieren, im Einfach- und RegExp-Modus, die Groß- und Kleinschreibung.\n\nDamit 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.\n\nSie 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.\n\nAktionen Menü\n-------------\n\n* **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.\n* **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.\n* **Verschiebe Markierte in den Mülleimer:** Verschiebt alle markierten Duplikate in den Mülleimer.\n* **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 <http://en.wikipedia.org/wiki/Hard_link>`_ . (Nur OS X und Linux)\n* **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.\n* **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.\n* **Entferne Markierte aus den Ergebnissen:** Entfernt alle markierte Duplikate aus den Ergebnissen. Die wirklichen Dateien werden nicht angerührt und bleiben wo sie sind.\n* **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.\n* **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.\n* **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.\n* **Öffne Ausgewählte mit Standardanwendung:** Öffnet die Datei mit der Anwendung die mit dem Dateityp verknüpft ist.\n* **Zeige Ausgewählte:** Öffnet den Ordner der die ausgewählte Datei enthält.\n* **Eigenen Befehl ausführen:** Ruft die in den Einstellungen definierte externe Anwendung auf und nutzt die aktuelle Auswahl als Argumente für den Aufruf.\n* **Ausgewählte umbenennen:** Fragt nach einem neuen Namen und benennt die ausgewählte Datei um.\n\n.. todo:: Add Move and iPhoto/iTunes warning\n.. todo:: Add \"Deletion Options\" section.\n"
  },
  {
    "path": "help/en/contribute.rst",
    "content": "Contribute to dupeGuru\n======================\n\ndupeGuru was started as shareware (thus proprietary) so it doesn't have a legacy of\ncommunity-building. It's `been open source`_ for a while now and, although I've (\"I\" being Virgil\nDupras, author of the software) always wanted to have people other than me working on dupeGuru, I've\nfailed at attracting them.\n\nSince the end of 2013, I've been putting a lot of efforts into dupeGuru's\n:doc:`developer documentation </developer/index>` and I'm more serious about my commitment to create\na community around this project.\n\nSo, whatever your skills, if you're interested in contributing to dupeGuru, please do so. Normally,\nthis documentation should be enough to get you started, but if it isn't, then **please**,\nopen a discussion at https://github.com/arsenetar/dupeguru/discussions.  If there's any situation where you'd\nwish to contribute but some doubt you're having prevent you from going forward, please contact me.\nI'd much prefer to spend the time figuring out with you whether (and how) you can contribute than\ntaking the chance of missing that opportunity.\n\nDevelopment process\n-------------------\n\n* `Source code repository`_\n* `Issue Tracker`_\n* `Issue labels meaning`_\n\ndupeGuru's source code is on GitHub and thus managed in a Git repository. At all times, you should\nbe able to build from source a fresh checkout of the ``master`` branch using instructions from the\n``README.md`` file at the root of this project. If you can't, it's a bug. Please report it.\n\n``master`` is the main development branch, and thus represents what going to be included in the\nnext feature release. When needed, we create maintenance branches for bugfixes of the current\nfeature release.\n\nWhen implementing a big feature, it's possible that it gets its own branch until\nit's stable enough to merge into ``master``.\n\nEvery release is tagged, the tag name containing the edition (for old versions) and its version.\nFor example, release 6.6.0 of dupeGuru ME is tagged ``me6.6.0``. Newer releases are tagged only\nwith the version number (because editions don't exist anymore), for example ``4.0.0``.\n\nOnce you're past building the software, the :doc:`developer documentation </developer/index>` should\nbe enough to get you started with actual development. Then again, proper documentation is a very\ndifficult task and, in the case of dupeGuru, this documentation was practically nonexistent until\nlate in the project, so it's still lacking.\n\nHowever, I'm committed to fix this situation, so if you're in a situation where you lack proper\ndocumentation to figure something out about this code, please contact me.\n\nTasks for non-developers\n------------------------\n\n**Create and comment issues**. The single most useful way for a user who is not a developer to\ncontribute to a software project is by thoroughly documenting a bug or a feature request. Most of\nthe time, what we get as developers are emails like \"the app crashes\" and we spend a lot of time\ntrying to figure out the cause of that bug. By properly describing the nature and context of a crash\n(we learn to do that with experience as a user who reports bugs), you help developers so immensely,\nyou have no idea.\n\nIt's the same thing with feature requests. Description of a feature request, when thoughts have\nalready been given to how such a feature would fit in the current design, are precious to developers\nand help them figure out a clear roadmap for the project.\n\nSo, even if you're not a developer, you can always open a GitHub account and create/comment issues.\nYour contribution will be much appreciated.\n\n**Documentation**. This is a bit trickier because dupeGuru's documentation is written with a rather\ncomplex markup language, `Sphinx`_ (based on `reST`_). To properly work within the documentation,\nyou have to know that language. I don't think that learning this language is outside the realm of\npossibility for a non-developer, but it might be a daunting task.\n\nThat being said, if it's a minor modification to the documentation, nothing stops you from opening\nan issue (there's a label for documentation issues, so this kind of issue is relevant to the\ntracker) describing the change you propose to make and I'll be happy to make the change myself (if\nrelevant, of course).\n\nEven if it's a bigger contribution to the documentation you want to make, I probably wouldn't mind\ndoing the formatting myself. But in that case, it's better to contact me first to make sure that we\nagree on what should be added to the documentation.\n\n**Translation**. Creating or improving an existing translation is a very good way to contribute to\ndupeGuru. For more information about how to do that, you can refer to the `translator guide`_.\n\n.. _been open source: https://www.hardcoded.net/articles/free-as-in-speech-fair-as-in-trade\n.. _Source code repository: https://github.com/arsenetar/dupeguru\n.. _Issue Tracker: https://github.com/arsenetar/issues\n.. _Issue labels meaning: https://github.com/arsenetar/wiki/issue-labels\n.. _Sphinx: http://sphinx-doc.org/\n.. _reST: http://en.wikipedia.org/wiki/ReStructuredText\n.. _translator guide: https://github.com/arsenetar/wiki/Translator-Guide\n"
  },
  {
    "path": "help/en/developer/core/app.rst",
    "content": "core.app\n========\n\n.. automodule:: core.app\n    :members:\n"
  },
  {
    "path": "help/en/developer/core/directories.rst",
    "content": "core.directories\n================\n\n.. automodule:: core.directories\n    :members:\n"
  },
  {
    "path": "help/en/developer/core/engine.rst",
    "content": "core.engine\n===========\n\n.. automodule:: core.engine\n\n    .. autoclass:: Match\n\n    .. autoclass:: Group\n        :members:\n\n    .. autofunction:: build_word_dict\n    .. autofunction:: compare\n    .. autofunction:: compare_fields\n    .. autofunction:: getmatches\n    .. autofunction:: getmatches_by_contents\n    .. autofunction:: get_groups\n    .. autofunction:: merge_similar_words\n    .. autofunction:: reduce_common_words\n\n.. _fields:\n\nFields\n------\n\nFields are groups of words which each represent a significant part of the whole name. This concept\nis sifnificant in music file names, where we often have names like \"My Artist - a very long title\nwith many many words\".\n\nThis title has 10 words. If you run as scan with a bit of tolerance, let's say 90%, you'll be able\nto find a dupe that has only one \"many\" in the song title. However, you would also get false\nduplicates from a title like \"My Giraffe - a very long title with many many words\", which is of\ncourse a very different song and it doesn't make sense to match them.\n\nWhen matching by fields, each field (separated by \"-\") is considered as a separate string to match\nindependently. After all fields are matched, the lowest result is kept. In the \"Giraffe\" example we\ngave, the result would be 50% instead of 90% in normal mode.\n"
  },
  {
    "path": "help/en/developer/core/fs.rst",
    "content": "core.fs\n=======\n\n.. automodule:: core.fs\n    :members:\n"
  },
  {
    "path": "help/en/developer/core/gui/deletion_options.rst",
    "content": "core.gui.deletion_options\n=========================\n\n.. automodule:: core.gui.deletion_options\n    :members:\n"
  },
  {
    "path": "help/en/developer/core/gui/index.rst",
    "content": "core.gui\n========\n\n.. automodule:: core.gui\n    :members:\n\n.. toctree::\n    :maxdepth: 2\n\n    deletion_options\n"
  },
  {
    "path": "help/en/developer/core/index.rst",
    "content": "core\n====\n\n.. toctree::\n    :maxdepth: 2\n\n    app\n    fs\n    engine\n    directories\n    results\n    gui/index\n"
  },
  {
    "path": "help/en/developer/core/results.rst",
    "content": "core.results\n============\n\n.. automodule:: core.results\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/build.rst",
    "content": "hscommon.build\n==============\n\n.. automodule:: hscommon.build\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/conflict.rst",
    "content": "hscommon.conflict\n=================\n\n.. automodule:: hscommon.conflict\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/desktop.rst",
    "content": "hscommon.desktop\n================\n\n.. automodule:: hscommon.desktop\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/base.rst",
    "content": "hscommon.gui.base\n=================\n\n.. automodule:: hscommon.gui.base\n\n    .. autosummary::\n\n        GUIObject\n\n    .. autoclass:: GUIObject\n        :members:\n        :private-members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/column.rst",
    "content": "hscommon.gui.column\n============================\n\n.. automodule:: hscommon.gui.column\n\n    .. autosummary::\n\n        Columns\n        Column\n        ColumnsView\n        PrefAccessInterface\n\n    .. autoclass:: Columns\n        :members:\n        :private-members:\n\n    .. autoclass:: Column\n        :members:\n        :private-members:\n\n    .. autoclass:: ColumnsView\n        :members:\n\n    .. autoclass:: PrefAccessInterface\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/progress_window.rst",
    "content": "hscommon.gui.progress_window\n============================\n\n.. automodule:: hscommon.gui.progress_window\n\n    .. autosummary::\n\n        ProgressWindow\n        ProgressWindowView\n\n    .. autoclass:: ProgressWindow\n        :members:\n        :private-members:\n\n    .. autoclass:: ProgressWindowView\n        :members:\n        :private-members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/selectable_list.rst",
    "content": "hscommon.gui.selectable_list\n============================\n\n.. automodule:: hscommon.gui.selectable_list\n\n    .. autosummary::\n\n        Selectable\n        SelectableList\n        GUISelectableList\n        GUISelectableListView\n\n    .. autoclass:: Selectable\n        :members:\n        :private-members:\n\n    .. autoclass:: SelectableList\n        :members:\n        :private-members:\n\n    .. autoclass:: GUISelectableList\n        :members:\n        :private-members:\n\n    .. autoclass:: GUISelectableListView\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/table.rst",
    "content": "hscommon.gui.table\n==================\n\n.. automodule:: hscommon.gui.table\n\n    .. autosummary::\n\n        Table\n        Row\n        GUITable\n        GUITableView\n\n    .. autoclass:: Table\n        :members:\n        :private-members:\n\n    .. autoclass:: Row\n        :members:\n        :private-members:\n\n    .. autoclass:: GUITable\n        :members:\n        :private-members:\n\n    .. autoclass:: GUITableView\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/text_field.rst",
    "content": "hscommon.gui.text_field\n=======================\n\n.. automodule:: hscommon.gui.text_field\n\n    .. autosummary::\n\n        TextField\n        TextFieldView\n\n    .. autoclass:: TextField\n        :members:\n        :private-members:\n\n    .. autoclass:: TextFieldView\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/gui/tree.rst",
    "content": "hscommon.gui.tree\n=================\n\n.. automodule:: hscommon.gui.tree\n\n    .. autosummary::\n\n        Tree\n        Node\n\n    .. autoclass:: Tree\n        :members:\n        :private-members:\n\n    .. autoclass:: Node\n        :members:\n        :private-members:\n"
  },
  {
    "path": "help/en/developer/hscommon/index.rst",
    "content": "hscommon\n========\n\n.. toctree::\n    :maxdepth: 2\n    :glob:\n\n    build\n    conflict\n    desktop\n    notify\n    path\n    util\n    jobprogress/*\n    gui/*\n"
  },
  {
    "path": "help/en/developer/hscommon/jobprogress/job.rst",
    "content": "hscommon.jobprogress.job\n========================\n\n.. automodule:: hscommon.jobprogress.job\n\n    .. autosummary::\n\n        Job\n        NullJob\n\n    .. autoclass:: Job\n        :members:\n        :private-members:\n\n    .. autoclass:: NullJob\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/jobprogress/performer.rst",
    "content": "hscommon.jobprogress.performer\n==============================\n\n.. automodule:: hscommon.jobprogress.performer\n\n    .. autosummary::\n\n        ThreadedJobPerformer\n\n    .. autoclass:: ThreadedJobPerformer\n        :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/notify.rst",
    "content": "hscommon.notify\n===============\n\n.. automodule:: hscommon.notify\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/path.rst",
    "content": "hscommon.path\n=============\n\n.. automodule:: hscommon.path\n    :members:\n"
  },
  {
    "path": "help/en/developer/hscommon/util.rst",
    "content": "hscommon.util\n=============\n\n.. automodule:: hscommon.util\n    :members:\n"
  },
  {
    "path": "help/en/developer/index.rst",
    "content": "Developer Guide\n===============\n\nWhen looking at a non-trivial codebase for the first time, it's very difficult to understand\nanything of it until you get the \"Big Picture\". This page is meant to, hopefully, make you get\ndupeGuru's big picture.\n\nBranches and tags\n-----------------\n\nThe git repo has one main branch, ``master``. It represents the latest \"stable development commit\",\nthat is, the latest commit that doesn't include in-progress features. This branch should always\nbe buildable, ``tox`` should always run without errors on it.\n\nWhen a feature/bugfix has an atomicity of a single commit, it's alright to commit right into\n``master``. However, if a feature/bugfix needs more than a commit, it should live in a separate\ntopic branch until it's ready.\n\nEvery release is tagged with the version number. For example, there's a ``2.8.2`` tag for the\nv2.8.2 release.\n\nModel/View/Controller... nope!\n------------------------------\n\ndupeGuru's codebase has quite a few design flaws. The Model, View and Controller roles are filled by\ndifferent classes, scattered around. If you're aware of that, it might help you to understand what\nthe heck is going on.\n\nThe central piece of dupeGuru is :class:`core.app.DupeGuru`. It's the only\ninterface to the python's code for the GUI code. A duplicate scan is started with\n:meth:`core.app.DupeGuru.start_scanning()`, directories are added through\n:meth:`core.app.DupeGuru.add_directory()`, etc..\n\nA lot of functionalities of the App are implemented in the platform-specific subclasses of\n:class:`core.app.DupeGuru`, like ``DupeGuru`` in ``cocoa/inter/app.py``, or the ``DupeGuru`` class\nin ``qt/base/app.py``. For example, when performing \"Remove Selected From Results\",\n``RemoveSelected()`` on the cocoa side, and ``remove_duplicates()`` on the PyQt side, are\nrespectively called to perform the thing.\n\n.. _jobs:\n\nJobs\n----\n\nA lot of operations in dupeGuru take a significant amount of time. This is why there's a generalized\nthreaded job mechanism built-in :class:`~core.app.DupeGuru`. First, :class:`~core.app.DupeGuru` has\na ``progress`` member which is an instance of\n:class:`~hscommon.jobprogress.performer.ThreadedJobPerformer`. It lets the GUI code know of the progress\nof the current threaded job. When :class:`~core.app.DupeGuru` needs to start a job, it calls\n``_start_job()`` and the platform specific subclass deals with the details of starting the job.\n\nCore principles\n---------------\n\nThe core of the duplicate matching takes place (for SE and ME, not PE) in :mod:`core.engine`.\nThere's :func:`core.engine.getmatches` which take a list of :class:`core.fs.File` instances and\nreturn a list of ``(firstfile, secondfile, match_percentage)`` matches. Then, there's\n:func:`core.engine.get_groups` which takes a list of matches and returns a list of\n:class:`.Group` instances (a :class:`.Group` is basically a list of :class:`.File` matching\ntogether).\n\nWhen a scan is over, the final result (the list of groups from :func:`.get_groups`) is placed into\n:attr:`core.app.DupeGuru.results`, which is a :class:`core.results.Results` instance. The\n:class:`~.Results` instance is where all the dupe marking, sorting, removing, power marking, etc.\ntakes place.\n\nAPI\n---\n\n.. toctree::\n    :maxdepth: 2\n\n    core/index\n    hscommon/index\n"
  },
  {
    "path": "help/en/faq.rst",
    "content": "Frequently Asked Questions\n==========================\n\n.. contents::\n\nWhat is dupeGuru?\n-----------------\n\ndupeGuru is a tool to find duplicate files on your computer. It has three operational modes:\nStandard, Music and Picture. Each mode has its own specialized preferences.\n\nEach mode has multiple scan types, such as filename, contents, tags. Some scan types feature\nadvanced fuzzy matching algorithm, allowing you to find duplicates that other more rigid duplicate\nscanners can't.\n\nWhat makes it special?\n----------------------\n\nIt's mostly about customizability. There's a lot of scanning options that allow you to get the\ntype of results you're really looking for.\n\nHow safe is it to use dupeGuru?\n-------------------------------\n\nVery safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete.\nFirst, there is the reference folder system that lets you define folders where you absolutely\n**don't** want dupeGuru to let you delete files there, and then there is the group reference system\nthat makes sure that you will **always** keep at least one member of the duplicate group.\n\nHow can I report a bug a suggest a feature?\n-------------------------------------------\n\ndupeGuru is hosted on `GitHub`_ and it's also where issues are tracked. The best way to report a\nbug or suggest a feature is to sign up on GitHub and `open an issue`_.\n\nThe mark box of a file I want to delete is disabled. What must I do?\n--------------------------------------------------------------------\n\nYou cannot mark the reference (The first file) of a duplicate group. However, what you can do is to\npromote a duplicate file to reference. Thus, if a file you want to mark is reference, select a\nduplicate file in the group that you want to promote to reference, and click on\n**Actions-->Make Selected into Reference**. If the reference file is from a reference folder\n(filename written in blue letters), you cannot remove it from the reference position.\n\nI have a folder from which I really don't want to delete files.\n---------------------------------------------------------------\n\nIf you want to be sure that dupeGuru will never delete file from a particular folder, make sure to\nset its state to **Reference** at :doc:`folders`.\n\nWhat is this '(X discarded)' notice in the status bar?\n------------------------------------------------------\n\nIn some cases, some matches are not included in the final results for security reasons. Let me use\nan example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner\ndetermines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru\nhas kind of a problem. It cannot create a duplicate group with A, B and C in it because not all\nfiles in the group would match together. It could create 2 groups: one A-B group and then one A-C\ngroup, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it\nprobably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B\nand A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is\nreally not what you want to do, right? So what dupeGuru does in a case like this is to discard the\nA-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will\nhave a A-C match in your next results.\n\nI want to mark all files from a specific folder. What can I do?\n---------------------------------------------------------------\n\nEnable the :doc:`Dupes Only <results>` mode and click on the Folder column to sort your duplicates\nby folder. It will then be easy for you to select all duplicates from the same folder, and then\npress Space to mark all selected duplicates.\n\nI want to remove all files that are more than 300 KB away from their reference file. What can I do?\n---------------------------------------------------------------------------------------------------\n\n* Enable the :doc:`Dupes Only <results>` mode.\n* Enable the **Delta Values** mode.\n* Click on the \"Size\" column to sort the results by size.\n* Select all duplicates below -300.\n* Click on **Remove Selected from Results**.\n* Select all duplicates over 300.\n* Click on **Remove Selected from Results**.\n\nI want to make my latest modified files reference files. What can I do?\n-----------------------------------------------------------------------\n\n* Enable the :doc:`Dupes Only <results>` mode.\n* Enable the **Delta Values** mode.\n* Click on the \"Modification\" column to sort the results by modification date.\n* Click on the \"Modification\" column again to reverse the sort order.\n* Select all duplicates over 0.\n* Click on **Make Selected into Reference**.\n\nI want to mark all duplicates containing the word \"copy\". How do I do that?\n---------------------------------------------------------------------------\n\n* Type \"copy\" in the \"Filter\" field in the top-right corner of the result window.\n* Click on **Mark --> Mark All**.\n\nI want to remove all songs that are more than 3 seconds away from their reference file. What can I do?\n------------------------------------------------------------------------------------------------------\n\n* Enable the :doc:`Dupes Only <results>` mode.\n* Enable the **Delta Values** mode.\n* Click on the \"Time\" column to sort the results by time.\n* Select all duplicates below -00:03.\n* Click on **Remove Selected from Results**.\n* Select all duplicates over 00:03.\n* Click on **Remove Selected from Results**.\n\nI want to make my highest bitrate songs reference files. What can I do?\n-----------------------------------------------------------------------\n\n* Enable the :doc:`Dupes Only <results>` mode.\n* Enable the **Delta Values** mode.\n* Click on the \"Bitrate\" column to sort the results by bitrate.\n* Click on the \"Bitrate\" column again to reverse the sort order.\n* Select all duplicates over 0.\n* Click on **Make Selected into Reference**.\n\nI don't want [live] and [remix] versions of my songs counted as duplicates. How do I do that?\n---------------------------------------------------------------------------------------------\n\nIf your comparison threshold is low enough, you will probably end up with live and remix\nversions of your songs in your results. There's nothing you can do to prevent that, but there's\nsomething you can do to easily remove them from your results after the scan: post-scan\nfiltering. If, for example, you want to remove every song with anything inside square brackets\n[]:\n\n* Type \"[*]\" in the \"Filter\" field in the top-right corner of the result window.\n* Click on **Mark --> Mark All**.\n* Click on **Actions --> Remove Selected from Results**.\n\nThe \"Filter Hardness\" slider in the preferences won't move!\n-----------------------------------------------------------\n\nThis slider is only relevant for scan types that support \"fuzziness\". Many scan types, such as the\n\"Contents\" type, only support exact matches. When these types are selected, the slider is disabled.\n\nOn some OS, the fact that it's disabled is harder to see than on others, but if you can't move the\nslider, it means that this preference is irrelevant in your current scan type.\n\nI've tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do?\n---------------------------------------------------------------------------------------------------------\n\nMost of the time, the reason why dupeGuru can't send files to Trash is because of file permissions.\nYou need *write* permissions on files you want to send to Trash.\n\nIf dupeGuru still gives you troubles after fixing your permissions, try enabling the \"Directly\ndelete files\" option that is offered to you when you activate Send to Trash. This will not send\nfiles to the Trash, but delete them immediately. In some cases, for example on network storage\n(NAS), this has been known to work when normal deletion didn't.\n\nWhy is Picture mode's contents scan so slow?\n--------------------------------------------\n\nThis scanning method is very different from methods. It can detect duplicate photos even if they\nare not exactly the same. This very cool capability has a cost: time. Every picture has to be\nindividually and fuzzily matched to all others, and this takes a lot of CPU power.\n\nIf all you need to find is exact duplicates, just use the standard mode of dupeGuru with the\nContents scan method. If your photos have EXIF tags, you can also try the \"EXIF\" scan method which\nis much faster.\n\nWhere are user files located?\n-----------------------------\n\nFor some reason, you'd like to remove or edit dupeGuru's user files (debug logs, caches, etc.).\nWhere they're located depends on your platform:\n\n* Linux: ``~/.local/share/data/Hardcoded Software/dupeGuru``\n* Mac OS X: ``~/Library/Application Support/dupeGuru``\n\nPreferences are stored elsewhere:\n\n* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``\n* Mac OS X: In the built-in ``defaults`` system, as ``com.hardcoded-software.dupeguru``\n\n.. _GitHub: https://github.com/arsenetar/dupeguru\n.. _open an issue: https://github.com/arsenetar/dupeguru/wiki/issue-labels\n"
  },
  {
    "path": "help/en/folders.rst",
    "content": "Folder Selection\n================\n\nThe first window you see when you launch dupeGuru is the folder selection window. This windows\ncontains the basic input dupeGuru needs to start a scan:\n\n* An Application Mode selection\n* A Scan Type selection\n* Folders to scan\n\nApplication Mode\n----------------\n\ndupeGuru had three main modes: Standard, Music and Picture.\n\nStandard is for any type of files. This makes this mode the most polyvalent, but it lacks\nspecialized features other modes have.\n\nMusic mode scans only music files, but it supports tags comparison and its results window has many\naudio-related informational columns.\n\nPicture mode scans only pictures, but its contents scan type is a powerful fuzzy matcher that can\nfind pictures that are similar without being exactly the same.\n\nChoosing an application mode not only changes available scan types in the selector below, but also\nchanges available options in the preferences panel. Thus, if you want to fine tune your scan, be\nsure to open the preferences panel **after** you've selected the application mode.\n\nScan Type\n---------\n\nThis selector determines the type of the scan we'll do. See :doc:`scan` for details about scan\ntypes.\n\nFolder List\n-----------\n\nTo add a folder, click on the **+** button. If you added folder before, a popup\nmenu with a list of recent folders you added will pop. You can click on one of\nthem to add it directly to your list. If you click on the first item of the\npopup menu, **Add New Folder...**, you will be prompted for a folder to add. If\nyou never added a folder, no menu will pop and you will directly be prompted\nfor a new folder to add.\n\nAn alternate way to add folders to the list is to drag them in the list.\n\nTo remove a folder, select the folder to remove and click on **-**. If a subfolder is selected when\nyou click the button, the selected folder will be set to **excluded** state (see below) instead of\nbeing removed.\n\nFolder states\n-------------\n\nEvery folder can be in one of these 3 states:\n\n**Normal:**\n    Duplicates found in this folder can be deleted.\n**Reference:**\n    Duplicates found in this folder **cannot** be deleted. Files from this folder can\n    only end up in **reference** position in the dupe group. If more than one file from reference\n    folders end up in the same dupe group, only one will be kept. The others will be removed from\n    the group.\n**Excluded:**\n    Files in this directory will not be included in the scan.\n\nThe default state of a folder is, of course, **Normal**. You can use **Reference** state for a\nfolder if you want to be sure that you won't delete any file from it.\n\nWhen you set the state of a directory, all subfolders of this folder automatically inherit this\nstate unless you explicitly set a subfolder's state.\n\nScan\n----\n\nWhen you're ready, click on the **Scan** button to initiate the scanning process. When it's done,\nyou'll be shown the :doc:`results`.\n"
  },
  {
    "path": "help/en/index.rst",
    "content": "dupeGuru help\n=============\n\nThis help document is also available in these languages:\n\n* `French <http://dupeguru.voltaicideas.net/help/fr>`__\n* `German <http://dupeguru.voltaicideas.net/help/de>`__\n* `Armenian <http://dupeguru.voltaicideas.net/help/hy>`__\n* `Russian <http://dupeguru.voltaicideas.net/help/ru>`__\n* `Ukrainian <http://dupeguru.voltaicideas.net/help/uk>`__\n\ndupeGuru is a tool to find duplicate files on your computer. It has three\nmodes, Standard, Music and Picture, with each mode having its own scan types\nand little features.\n\nAlthough dupeGuru can easily be used without documentation, reading this file\nwill help you to master it. If you are looking for guidance for your first\nduplicate scan, you can take a look at the :doc:`Quick Start <quick_start>`\nsection.\n\nIt is a good idea to keep dupeGuru updated. You can download the latest version on its `homepage`_.\n\nContents:\n\n.. toctree::\n    :maxdepth: 2\n\n    contribute\n    quick_start\n    folders\n    preferences\n    scan\n    results\n    reprioritize\n    faq\n    developer/index\n    changelog\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`search`\n\n.. _homepage: https://dupeguru.voltaicideas.net/\n"
  },
  {
    "path": "help/en/preferences.rst",
    "content": "Preferences\n===========\n\n**Tags to scan:**\n    When using the **Tags** scan type, you can select the tags that will be used for comparison.\n\n**Word weighting:**\n    See :ref:`word-weighting`.\n\n**Match similar words:**\n    See :ref:`similarity-matching`.\n\n**Match pictures of different dimensions:**\n    If you check this box, pictures of different dimensions will be allowed in the same\n    duplicate group.\n\n**Match pictures of different rotations:**\n    If you check this box, pictures of different rotations will be allowed in the same\n    duplicate group.\n\n.. _filter-hardness:\n\n**Filter Hardness:**\n    The threshold needed for two files to be considered duplicates. A lower value means more\n    duplicates. The meaning of the threshold depends on the scanning type (see :doc:`scan`).\n    Only works for :ref:`worded <worded-scan>` and :ref:`picture blocks <picture-blocks-scan>`\n    scans.\n\n**Can mix file kind:**\n    If you check this box, duplicate groups are allowed to have files with different extensions. If\n    you don't check it, well, they aren't!\n\n**Ignore duplicates hardlinking to the same file:**\n    If this option is enabled, dupeGuru will verify duplicates to see if they refer to the same\n    `inode`_. If they do, they will not be considered duplicates. (Only for OS X and Linux)\n\n**Use regular expressions when filtering:**\n    If you check this box, the filtering feature will treat your filter query as a\n    **regular expression**. Explaining them is beyond the scope of this document. A good place to\n    start learning it is `regular-expressions.info`_.\n\n**Remove empty folders after delete or move:**\n    When this option is enabled, folders are deleted after a file is deleted or moved and the folder\n    is empty.\n\n**Copy and Move:**\n    Determines how the Copy and Move operations (in the Action menu) will behave.\n\n* **Right in destination:** All files will be sent directly in the selected destination, without\n  trying to recreate the source path at all.\n* **Recreate relative path:** The source file's path will be re-created in the destination folder up\n  to the root selection in the Directories panel. For example, if you added\n  ``/Users/foobar/SomeFolder`` to your Directories panel and you move\n  ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the destination\n  ``/Users/foobar/MyDestination``, the final destination for the file will be\n  ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` has been trimmed from source's path in\n  the final destination.).\n* **Recreate absolute path:** The source file's path will be re-created in the destination folder in\n  its entirety. For example, if you move ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` to the\n  destination ``/Users/foobar/MyDestination``, the final destination for the file will be\n  ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``.\n\nIn all cases, dupeGuru nicely handles naming conflicts by prepending a number to the destination\nfilename if the filename already exists in the destination.\n\n**Custom Command:**\n    This preference determines the command that will be invoked by the \"Invoke Custom Command\"\n    action. You can invoke any external application through this action. This can be useful if,\n    for example, you have a nice diffing application installed.\n\nThe format of the command is the same as what you would write in the command line, except that there\nare 2 placeholders: **%d** and **%r**. These placeholders will be replaced by the path of the\nselected dupe (%d) and the path of the selected dupe's reference file (%r).\n\nIf the path to your executable contains space characters, you should enclose it in \"\" quotes. You\nshould also enclose placeholders in quotes because it's very possible that paths to dupes and refs\nwill contain spaces. Here's an example custom command::\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n\n.. _inode: http://en.wikipedia.org/wiki/Inode\n.. _regular-expressions.info: http://www.regular-expressions.info\n"
  },
  {
    "path": "help/en/quick_start.rst",
    "content": "Quick Start\n===========\n\nTo get you quickly started with dupeGuru, let's just make a standard scan using default preferences.\n\n* Launch dupeGuru.\n* Add folders to scan with either drag & drop or the \"+\" button.\n* Click on **Scan**.\n* Wait until the scan process is over.\n* 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).\n* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**.\n* 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**.\n\nThat 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.\n"
  },
  {
    "path": "help/en/reprioritize.rst",
    "content": "Re-Prioritizing duplicates\n==========================\n\ndupeGuru tries to automatically determine which duplicate should go in each group's reference\nposition, but sometimes it gets it wrong. In many cases, clever dupe sorting with \"Delta Values\"\nand \"Dupes Only\" options in addition to the \"Make Selected into Reference\" action does the trick,\nbut sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes\ninto play. You can summon it through the \"Re-Prioritize Results\" item in the \"Actions\" menu.\n\nThis dialog allows you to select criteria according to which a reference dupe will be selected in\neach dupe group. The list of available criteria is on the left and the list of criteria you've\nselected is on the right.\n\nA criteria is a category followed by an argument. For example, \"Size (Highest)\" means that the dupe\nwith the biggest size will win. \"Folder (/foo/bar)\" means that dupes in this folder will win. To add\na criterion to the rightmost list, first select a category in the combobox, then select a\nsubargument in the list below, and then click on the right pointing arrow button.\n\nThe order of the list on the right is important (you can re-order items through drag & drop). When\npicking a dupe for reference position, the first criterion is used. If there's a tie, the second\ncriterion is used and so on and so on. For example, if your arguments are \"Size (Highest)\" and then\n\"Filename (Doesn't end with a number)\", the reference file that will be picked in a group will be\nthe biggest file, and if two or more files have the same size, the one that has a filename that\ndoesn't end with a number will be used. When all criteria result in ties, the order in which dupes\npreviously were in the group will be used.\n"
  },
  {
    "path": "help/en/results.rst",
    "content": "Results\n=======\n\n.. contents::\n\nWhen dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list.\n\nAbout duplicate groups\n----------------------\n\nA 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.\n\nYou 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?\n\nWhat 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.\n\nYou can change the reference file of a group manually. To do so, select the duplicate file you want\nto promote to reference, and click on **Actions-->Make Selected into Reference**.\n\nReviewing results\n-----------------\n\nAlthough 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.\n\nTo 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.\n\nIf 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**.\n\nMarking and Selecting\n---------------------\n\nA **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**.\n\nShow Dupes Only\n---------------\n\nWhen 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.\n\nThe 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:\n\n* Enable the Dupes Only mode.\n* Add the \"Kind\" column with the \"Columns\" menu.\n* Click on that \"Kind\" column to sort the list by kind.\n* Locate the first duplicate with a \"exe\" kind.\n* Select it.\n* Scroll down the list to locate the last duplicate with a \"exe\" kind.\n* Hold Shift and click on it.\n* Press Space to mark all selected duplicates.\n\n.. _deltavalues:\n\nDelta Values\n------------\n\nIf you turn this switch on, numerical columns will display the value relative to the duplicate's\nreference instead of the absolute values. These delta values will also be displayed in a different\ncolor, orange,  so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference\nis 1.4 MB, the Size column will display -0.2 MB.\n\nMoreover, non-numerical values will also be in orange if their value is different from their\nreference, and stay black if their value is the same. Combined with column sorting in Dupes Only\nmode, this allows for very powerful post-scan filtering.\n\nDupes Only and Delta Values\n---------------------------\n\nThe Dupes Only mode unveil its true power when you use it with the Delta Values switch turned on.\nWhen you turn it on, relative values will be displayed instead of absolute ones. So if, for example,\nyou want to remove from your results all duplicates that are more than 300 KB away from their\nreference, you could sort the dupes only results by Size, select all duplicates under -300 in the\nSize column, delete them, and then do the same for duplicates over 300 at the bottom of the list.\n\nSame thing for non-numerical values: When Dupes Only and Delta Values are enabled at the same time,\ncolumn sorting groups rows depending on whether they're orange or not. Example: You ran a contents\nscan, but you would only like to delete duplicates that have the same filename? Sort by filename\nand all dupes with their filename attribute being the same as the reference will be grouped\ntogether, their value being in black.\n\nYou could also use it to change the reference priority of your duplicate list. When you make a fresh\nscan, if there are no reference folders, the reference file of every group is the biggest file. If\nyou want to change that, for example, to the latest modification time, you can sort the dupes only\nresults by modification time in **descending** order, select all duplicates with a modification time\ndelta value higher than 0 and click on **Make Selected into Reference**. The reason why you must\nmake the sort order descending is because if 2 files among the same duplicate group are selected\nwhen you click on **Make Selected into Reference**, only the first of the list will be made\nreference, the other will be ignored. And since you want the last modified file to be reference,\nhaving the sort order descending assures you that the first item of the list will be the last\nmodified.\n\nFiltering\n---------\n\ndupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform\nactions on a subset of it. For example, you could easily mark all duplicates with their filename\ncontaining \"copy\" from your results using the filter.\n\nTo use the filtering feature, type your filter in the \"Filter\" search field at the top-right corner\nof the results window. What you type in that box will be applied to the *whole path* of every\nduplicate in the results. Only duplicate *groups* having at least one duplicate matching the filter\nwill be shown.\n\nWhen having groups where not all duplicates match the filter, we still show all duplicates of\nthe group. However, non-matching duplicates are in \"reference mode\". Therefore, you can perform\nactions like \"Mark All\" and be sure to only mark filtered duplicates.\n\nTo go back to unfiltered result, blank out the field or click on the \"X\".\n\nIn simple mode (the default mode), whatever you type as the filter is the string used to perform the\nactual filtering, with the exception of one wildcard: **\\***. Thus, if you type \"[*]\" as your\nfilter, it will match anything with [] brackets in it, whatever is in between those brackets.\n\nFor more advanced filtering, you can turn \"Use regular expressions when filtering\" on. The filtering\nfeature will then use **regular expressions**. A regular expression is a language for matching text.\nExplaining them is beyond the scope of this document. A good place to start learning it is\n`regular-expressions.info`_.\n\nMatches are case insensitive in both simple and regexp mode.\n\nFor the filter to match, your regular expression don't have to match the whole filename, it just\nhave to contain a string matching the expression.\n\nAction Menu\n-----------\n\n**Clear Ignore List:**\n    Remove all ignored matches you added. You have to start a new scan for the\n    newly cleared ignore list to be effective.\n**Export Results to XHTML:**\n    Take the current results, and create an XHTML file out of it. The\n    columns that are visible when you click on this button will be the columns present in the XHTML\n    file. The file will automatically be opened in your default browser.\n**Send Marked to Trash:**\n    Send all marked duplicates to trash, obviously. Before proceeding,\n    you'll be presented deletion options (see below).\n**Move Marked to...:**\n    Prompt you for a destination, and then move all marked files to that\n    destination. Source file's path might be re-created in destination, depending on the\n    \"Copy and Move\" preference.\n**Copy Marked to...:**\n    Prompt you for a destination, and then copy all marked files to that\n    destination. Source file's path might be re-created in destination, depending on the\n    \"Copy and Move\" preference.\n**Remove Marked from Results:**\n    Remove all marked duplicates from results. The actual files will\n    not be touched and will stay where they are.\n**Remove Selected from Results:**\n    Remove all selected duplicates from results. Note that all\n    selected reference files will be ignored, only duplicates can be removed with this action.\n**Make Selected into Reference:**\n    Promote all selected duplicates to reference. If a duplicate is\n    a part of a group having a reference file coming from a reference folder (in blue color), no\n    action will be taken for this duplicate. If more than one duplicate among the same group are\n    selected, only the first of each group will be promoted.\n**Add Selected to Ignore List:**\n    This first removes all selected duplicates from results, and\n    then add the match of that duplicate and the current reference in the ignore list. This match\n    will not come up again in further scan. The duplicate itself might come back, but it will be\n    matched with another reference file. You can clear the ignore list with the Clear Ignore List\n    command.\n**Open Selected with Default Application:**\n    Open the file with the application associated with selected file's type.\n**Reveal Selected in Finder:**\n    Open the folder containing selected file.\n**Invoke Custom Command:**\n    Invokes the external application you've set up in your preferences using the current selection\n    as arguments in the invocation.\n**Rename Selected:**\n    Prompts you for a new name, and then rename the selected file.\n\nDeletion Options\n----------------\n\nThese options affect how duplicate deletion takes place. Most of the time, you don't need to enable\nany of them.\n\n**Link deleted files:**\n    The deleted files are replaced by a link to the reference file. You have a choice of replacing\n    it either with a `symlink`_ or a `hardlink`_. It's better to read the whole\n    wikipedia pages about them to make a informed choice, but in short, a symlink is a shortcut to\n    the file's path. If the original file is deleted or moved, the link is broken. A hardlink is a\n    link to the file *itself*. That link is as good as a \"real\" file. Only when *all* hardlinks to a\n    file are deleted is the file itself deleted.\n\n    On OSX and Linux, this feature is supported fully, but under Windows, it's a bit complicated.\n    Windows XP doesn't support it, but Vista and up support it. However, for the feature to work,\n    dupeGuru has to run with administrative privileges.\n\n**Directly delete files:**\n    Instead of sending files to trash, directly delete them. This is used\n    for troubleshooting and you normally don't need to enable this unless dupeGuru has problems\n    deleting files normally, something that can happens when you try to delete files on network\n    storage (NAS).\n\n.. _regular-expressions.info: http://www.regular-expressions.info\n.. _hardlink: http://en.wikipedia.org/wiki/Hard_link\n.. _symlink: http://en.wikipedia.org/wiki/Symbolic_link\n"
  },
  {
    "path": "help/en/scan.rst",
    "content": "The scanning process\n====================\n\n.. contents::\n\ndupeGuru has 3 basic ways of scanning: :ref:`worded-scan` and :ref:`contents-scan` and\n:ref:`picture blocks <picture-blocks-scan>`. The first two types are for the Standard and Music\nmodes, the last is for the Picture mode. The scanning process is configured through the\n:doc:`Preference pane <preferences>`.\n\n.. _worded-scan:\n\nWorded scans\n------------\n\nWorded scans extract a string from each file and split it into words. The string can come from two\ndifferent sources: **Filename** or **Tags** (Music Edition only).\n\nWhen our source is music tags, we have to choose which tags to use. If, for example, we choose to\nanalyse *artist* and *title* tags, we'd end up with strings like\n\"The White Stripes - Seven Nation Army\".\n\nWords are split by space characters, with all punctuation removed (some are replaced by spaces, some\nby nothing) and all words lowercased. For example, the string \"This guy's song(remix)\" yields\n*this*, *guys*, *song* and *remix*.\n\nOnce this is done, the scanning dance begins. Finding duplicates is only a matter of finding how\nmany words in common two given strings have. If the :ref:`filter hardness <filter-hardness>` is,\nfor example, ``80``, it means that 80% of the words of two strings must match. To determine the\nmatching percentage, dupeGuru first counts the total number of words in **both** strings, then count\nthe number of words matching (every word matching count as 2), and then divide the number of words\nmatching by the total number of words. If the result is higher or equal than the filter hardness,\nwe have a duplicate match. For example, \"a b c d\" and \"c d e\" have a matching percentage of 57\n(4 words matching, 7 total words).\n\nFields\n^^^^^^\n\nSong filenames often come with multiple and distinct parts and this can cause problems. For example,\nlet's take these two songs: \"Dolly Parton - I Will Always Love You\" and\n\"Whitney Houston - I Will Always Love You\". They are clearly not the same song (they come from\ndifferent artists), but they still still have a matching score of 71%! This means that, with a naive\nscanning method, we would get these songs as a false positive as soon as we try to dig a bit deeper\nin our dupe hunt by lowering the threshold a bit.\n\nThis is why we have the \"Fields\" concept. Fields are separated by dashes (``-``). When the\n\"Filename - Fields\" scan type is chosen, each field is compared separately. Our final matching score\nwill only be the lowest of all the fields. In our example, the title has a 100% match, but the\nartist has a 0% match, making our final match score 0.\n\nSometimes, our song filename policy isn't completely homogenous, which means that we can end up with\n\"The White Stripes - Seven Nation Army\" and \"Seven Nation Army - The White Stripes\". This is why\nwe have the \"Filename - Fields (No Order)\" scan type. With this scan type, all fields are compared\nwith each other, and the highest score is kept. Then, the final matching score is the lowest of them\nall. In our case, the final matching score is 100.\n\nNote: Each field is used once. Thus, \"The White Stripes - The White Stripes\" and\n\"The White Stripes - Seven Nation Army\" have a match score of 0 because the second\n\"The White Stripes\" can't be compared with the first field of the other name because it has already\nbeen \"used up\" by the first field. Our final match score would be 0.\n\n*Tags* scanning method is always \"fielded\". When choosing this scan method, we also choose which\ntags are going to be compared, each being a field.\n\n.. _word-weighting:\n\nWord weighting\n^^^^^^^^^^^^^^\n\nWhen enabled, this option slightly changes how matching percentage is calculated by making bigger\nwords worth more. With word weighting, instead of having a value of 1 in the duplicate count and\ntotal word count, every word have a value equal to the number of characters they have. With word\nweighting, \"ab cde fghi\" and \"ab cde fghij\" would have a matching percentage of 53% (19 total\ncharacters, 10 characters matching (4 for \"ab\" and 6 for \"cde\")).\n\n.. _similarity-matching:\n\nSimilarity matching\n^^^^^^^^^^^^^^^^^^^\n\nWhen enabled, similar words will be counted as matches. For example \"The White Stripes\" and\n\"The White Stripe\" would have a match score of 100 instead of 66 with that option turned on.\n\nTwo words are considered similar if they can be made equal with only a few edit operations (removing\na letter, adding one etc.). The process used is not unlike the\n`Levenshtein distance`_. For the technically inclined, the actual function used is\nPython's `get_close_matches`_ with a ``0.8`` cutoff.\n\n**Warning:** Use this option with caution. It is likely that you will get a lot of false positives\nin your results when turning it on. However, it will help you to find duplicates that you wouldn't\nhave found otherwise. The scan process also is significantly slower with this option turned on.\n\n.. _contents-scan:\n\nContents scans\n--------------\n\nContents scans are much simpler than worded scans. We read files and if the contents is exactly the\nsame, we consider the two files duplicates.\n\nThis is, of course, quite longer than comparing filenames and, to avoid needlessly reading whole\nfile contents, we start by looking at file sizes. After having grouped our files by size, we discard\nevery file that is alone in its group. Then, we proceed to read the contents of our remaining files.\n\nMD5 hashes are used to compute compare contents. Yes, it is widely known that forging files having\nthe same MD5 hash is easy, but this file has to be knowingly forged. The possibilities of two files\nhaving the same MD5 hash *and* the same size by accident is still very, very small.\n\nThe :ref:`filter hardness <filter-hardness>` preference is ignored in this scan.\n\nFolders\n^^^^^^^\n\nThis is a special Contents scan type. It works like a normal contents scan, but\ninstead of trying to find duplicate files, it tries to find duplicate folders.\nA folder is duplicate to another if all files it contains have the same\ncontents as the other folder's file.\n\nThis scan is, of course, recursive and subfolders are checked. dupeGuru keeps only the biggest\nfishes. Therefore, if two folders that are considered as matching contain subfolders, these\nsubfolders will not be included in the final results.\n\nWith this mode, we end up with folders as results instead of files.\n\n.. _picture-blocks-scan:\n\nPicture blocks\n--------------\n\ndupeGuru Picture mode stands apart of its two friends. Its scan types are completely different.\nThe first one is its \"Contents\" scan, which is a bit too generic, hence the name we use here,\n\"Picture blocks\".\n\nWe start by opening every picture in RGB bitmap mode, then we \"blockify\" the picture. We create a\n15x15 grid and compute the average color of each grid tile. This is the \"picture analysis\" phase.\nIt's very time consuming and the result is cached in a database (the \"picture cache\").\n\nOnce we've done that, we can start comparing them. Each tile in the grid (an average color) is\ncompared to its corresponding grid on the other picture and a color diff is computer (it's simply\na sum of the difference of R, G and B on each side). All these sums are added up to a final \"score\".\n\nIf that score is smaller or equal to ``100 - threshold``, we have a match.\n\nA threshold of 100 adds an additional constraint that pictures have to be exactly the same (it's\npossible, due to averaging, that the tile comparison yields ``0`` for pictures that aren't exactly\nthe same, but since \"100%\" suggests \"exactly the same\", we discard those ocurrences). If you want\nto get pictures that are very, very similar but still allow a bit of fuzzy differences, go for 99%.\n\nThis second part of the scan is CPU intensive and can take quite a bit of time. This task has been\nmade to take advatange of multi-core CPUs and has been optimized to the best of my abilities, but\nthe fact of the matter is that, due to the fuzziness of the task, we still have to compare every picture\nto every other, making the algorithm quadratic (if ``N`` is the number of pictures to compare, the\nnumber of comparisons to perform is ``N*N``).\n\nThis algorithm is very naive, but in the field, it works rather well. If you master a better\nalgorithm and want to improve dupeGuru, by all means, let me know!\n\nEXIF Timestamp\n--------------\n\nThis one is easy. We read the EXIF information of every picture and extract the ``DateTimeOriginal``\ntag. If the tag is the same for two pictures, they're considered duplicates.\n\n**Warning:** Modified pictures often keep the same EXIF timestamp, so watch out for false positives\nwhen you use that scan type.\n\n.. _Levenshtein distance: http://en.wikipedia.org/wiki/Levenshtein_distance\n.. _get_close_matches: http://docs.python.org/3/library/difflib.html#difflib.get_close_matches\n"
  },
  {
    "path": "help/fr/faq.rst",
    "content": "Foire aux questions\n===================\n\n.. contents::\n\nQu'est-ce que dupeGuru?\n------------------------\n\n.. only:: edition_se\n\n    dupeGuru est un outil pour trouver des doublons parmi vos fichiers. Il peut comparer soit les\n    noms de fichiers, soit le contenu. Le comparateur de nom de fichier peut trouver des doublons\n    même si les noms ne sont pas exactement pareils.\n\n.. only:: edition_me\n\n    dupeGuru Music Editon est un outil pour trouver des doublons parmi vos chansons. Il peut\n    comparer les noms de fichiers, les tags ou bien le contenu. Les comparaisons de nom de fichier\n    ou de tags peuvent trouver des doublons même si les noms de sont pas exactement pareils.\n\n.. only:: edition_pe\n\n    dupeGuru Picture Edition est un outil pour trouver des doublons parmi vos images. Non seulement\n    il permet de trouver les doublons exactes, mais il est aussi capable de trouver les images ayant\n    de légères différences, étant de format différent ou bien ayant une qualité différente.\n\nEn quoi est-il mieux que les autres applications?\n-------------------------------------------------\n\ndupeGuru est hautement configurable. Vous pouvez changer les options de comparaison afin de trouver\nexactement le type de doublons recherché. Plus de détails sur la\n:doc:`page de préférences <preferences>`.\n\ndupeGuru est-il sécuritaire?\n----------------------------\n\nOui. dupeGuru a été conçu afin d'être certain que vous conserviez toujours au moins une copie des\ndoublons que vous trouvez. Il est aussi possible de configurer dupeGuru afin de déterminer certains\ndossier à partir desquels aucun fichier ne sera effacé.\n\nQuelles sont les limitation démo de dupeGuru?\n---------------------------------------------\n\nEn mode de démonstration, les actions sont limitées à 10 doublons par session. En mode `Fairware`_,\nil n'y a pas de limitation.\n\nJe ne peux pas marquer le doublons que je veux effacer, pourquoi?\n-----------------------------------------------------------------\n\nTour groupe de doublons contient au moins un fichier dit \"référence\" et ce fichier ne peut pas être\neffacé. Par contre, ce que vous pouvez faire c'est de le remplacer par un autre fichier du groupe.\nPour ce faire, sélectionnez un fichier du groupe et cliquez sur l'action **Transformer sélectionnés\nen références**.\n\nNotez que si le fichier référence du groupe vient d'un dossier qui a été défini comme dossier\nréférence, ce fichier ne peut pas être déplacé de sa position de référence du groupe.\n\nJ'ai un dossier duquel je ne veut jamais effacer de fichier.\n------------------------------------------------------------\n\nSi vous faites un scan avec un dossier qui ne doit servir que de référence pour effacer des doublons\ndans un autre dossier, changez le type de dossier à \"Référence\" dans la fenêtre de\n:doc:`sélection de dossiers <folders>`.\n\nQue veut dire '(X hors-groupe)' dans la barre de statut?\n--------------------------------------------------------\n\nLors de certaines comparaisons, il est impossible de correctement grouper les paires de doublons et\ncertaines paires doivent être retirées des résultats pour être certain de ne pas effacer de faux\ndoublons. Example: Nous avons 3 fichiers, A, B et C. Nous les comparons en utilisant un petit seuil\nde filtre. La comparaison détermine que A est un double de B, A est un double C, mais que B n'est\n**pas** un double de C. dupeGuru a ici un problème. Il ne peut pas créer un groupe avec A, B et C.\nIl décide donc de jeter C hors du groupe. C'est de là que vient la notice '(X hors-groupe)'.\n\nCette notice veut dire que si jamais vous effacez tout les doubles contenus dans vos résultats et\nque vous faites un nouveau scan, vous pourriez avoir de nouveaux résultats.\n\nJe veux marquer tous les fichiers provenant d'un certain dossier. Quoi faire?\n-----------------------------------------------------------------------------\n\nActivez l'option :doc:`Ne pas montrer les références <results>` et cliquez sur la colonne Dossier\nafin de trier par dossier. Il sera alors facile de sélectionner tous les fichiers de ce dossier\n(avec Shift+selection) puis ensuite d'appuyer sur Espace pour marquer les fichiers sélectionnés.\n\n.. only:: edition_se or edition_pe\n\n    Je veux enlever tous les doublons qui ont une différence de plus de 300KB avec leur référence.\n    ----------------------------------------------------------------------------------------------\n\n    * Activez l'option :doc:`Ne pas montrer les références <results>`.\n    * Activez l'option **Montrer les valeurs en tant que delta**.\n    * Cliquez sur la colonne Taille pour changer le tri.\n    * Sélectionnez tous les fichiers en dessous de -300.\n    * Cliquez sur l'action **Retirer sélectionnés des résultats**.\n    * Sélectionnez tous les fichiers au dessus de 300.\n    * Cliquez sur l'action **Retirer sélectionnés des résultats**.\n\n    Je veux que le fichier avec la plus grande date de dernière modification soit la référence.\n    -------------------------------------------------------------------------------------------\n\n    * Activez l'option :doc:`Ne pas montrer les références <results>`.\n    * Activez l'option **Montrer les valeurs en tant que delta**.\n    * Cliquez sur la colonne Modification (deux fois, afin d'avoir un ordre descendant) pour changer le tri.\n    * Sélectionnez tous les fichiers au dessus de 0.\n    * Cliquez sur l'action **Transformer sélectionnés en références**.\n\n    Je veux marquer tous les fichiers contenant le mot \"copie\".\n    -----------------------------------------------------------\n\n    * Entrez le mot \"copie\" dans le champ \"Filtre\" dans la fenêtre de résultats puis appuyez sur\n      Entrée.\n    * Cliquez sur **Tout Marquer** dans le menu Marquer.\n\n.. only:: edition_me\n\n    Je veux enlever les doublons qui ont une différence de plus de 3 secondes avec leur référence.\n    ----------------------------------------------------------------------------------------------\n\n    * Activez l'option :doc:`Ne pas montrer les références <results>`.\n    * Activez l'option **Montrer les valeurs en tant que delta**.\n    * Cliquez sur la colonne Temps pour changer le tri.\n    * Sélectionnez tous les fichiers en dessous de -00:03.\n    * Cliquez sur l'action **Retirer sélectionnés des résultats**.\n    * Sélectionnez tous les fichiers au dessus de 00:03.\n    * Cliquez sur l'action **Retirer sélectionnés des résultats**.\n\n    Je veux que mes chansons aux bitrate le plus élevé soient mes références.\n    -------------------------------------------------------------------------\n\n    * Activez l'option :doc:`Ne pas montrer les références <results>`.\n    * Activez l'option **Montrer les valeurs en tant que delta**.\n    * Cliquez sur la colonne Bitrate (deux fois, afin d'avoir un ordre descendant) pour changer le tri.\n    * Sélectionnez tous les fichiers au dessus de 0.\n    * Cliquez sur l'action **Transformer sélectionnés en références**.\n\n    Je veux enlever les chansons contenant \"[live]\" ou \"[remix]\" de mes résultat.\n    -----------------------------------------------------------------------------\n\n    Si votre seuil de filtre est assez bas, il se pourrait que vos chansons live ou vos remix soient\n    détectés comme des doublons. Vous n'y pouvez rien, mais ce que vous pouvez faire est d'enlever\n    ces fichiers de vous résultats après le scan. Si, par exemple, vous voulez enlever tous les\n    doublons contenant quelque mot que ce soit entre des caractères \"[]\", faites:\n\n    * Entrez \"[*]\" dans le champ \"Filtre\" dans la fenêtre de résultats puis appuyez sur Entrée.\n    * Cliquez sur **Tout Marquer** dans le menu Marquer.\n    * Cliquez sur l'action **Retirer marqués des résultats**.\n\nJ'essaie d'envoyer mes doublons à la corbeille, mais dupeGuru me dit que je ne peux pas. Pourquoi?\n--------------------------------------------------------------------------------------------------\n\nLa plupart du temps, la raison pour laquelle dupeGuru ne peut pas envoyer des fichiers à la\ncorbeille est un problème de permissions. Vous devez avoir une permission d'écrire dans les fichiers\nque vous voulez effacer. Si vous n'êtes pas familiers avec la ligne de commande, vous pouvez\nutiliser des outils comme `BatChmod`_ pour modifier vos permissions.\n\nSi malgré cela vous ne pouvez toujours pas envoyer vos fichiers à la corbeille, essayez l'option\n\"Supprimer les fichiers directement\" qui vous est offerte lorsque vous procédez à l'effacement des\ndoublons. Cette option fera en sorte de supprimer directement les fichiers sans les faire passer par\nla corbeille. Dans certains cas, ça règle le problème.\n\n.. only:: edition_pe\n\n    Si vous essayez d'effacer des photos dans iPhoto, alors la raison du problème est différente.\n    L'opération rate parce que dupeGuru ne peut pas communiquer avec iPhoto. Il faut garder à\n    l'esprit qu'il ne faut pas toucher à iPhoto pendant l'opération parce que ça peut déranger la\n    communication entre dupeGuru et iPhoto. Aussi, quelque fois, dupeGuru ne peut pas trouver\n    l'application iPhoto. Il faut mieux alors démarrer iPhoto avant l'opération.\n\nDans le pire des cas, `contactez le support HS`_, on trouvera bien.\n\nOù sont les fichiers de configuration de dupeGuru?\n--------------------------------------------------\n\nSi, pour une raison ou une autre, vous voulez effacer ou modifier les fichiers générés par dupeGuru,\nvoici où ils sont:\n\n* Linux: ``~/.local/share/data/Hardcoded Software/dupeGuru``\n* Mac OS X: ``~/Library/Application Support/dupeGuru``\n* Windows: ``\\Users\\<username>\\AppData\\Local\\Hardcoded Software\\dupeGuru``\n\nLes fichiers de préférences sont ailleurs:\n\n* Linux: ``~/.config/Hardcoded Software/dupeGuru.conf``\n* Mac OS X: Dans le système ``defaults`` sous ``com.hardcoded-software.dupeguru``\n* Windows: Dans le Registre, sous ``HKEY_CURRENT_USER\\Software\\Hardcoded Software\\dupeGuru``\n\nPour la Music Edition et Picture Edition, remplacer \"dupeGuru\" par \"dupeGuru Music Edition\" et\n\"dupeGuru Picture Edition\", respectivement.\n\n.. _Fairware: http://open.hardcoded.net/about/\n.. _BatChmod: http://www.lagentesoft.com/batchmod/index.html\n.. _contactez le support HS: http://www.hardcoded.net/support\n"
  },
  {
    "path": "help/fr/folders.rst",
    "content": "Sélection de dossiers\n=====================\n\nLa 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**.\n\nPour 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.\n\nVous pouvez aussi utiliser le drag & drop pour ajouter des dossiers à la liste.\n\nPour 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é.\n\nTypes de dossiers\n-----------------\n\nTout dossier ajouté à la liste est d'un type parmis ces trois:\n\n* **Normal:** Les doublons trouvés dans ce dossier peuvent être effacés.\n* **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.\n* **Excluded:** Les fichiers provenant de ce dossier ne sont pas scannés.\n\nLe 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.\n\nLe type d'un dossier s'applique à ses sous-dossiers, excepté si un sous-dossier a un autre type explicitement défini.\n\n.. only:: edition_pe\n\n    Bibliothèques iPhoto et Aperture\n    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n    dupeGuru PE supporte iPhoto et Aperture, ce qui veut dire qu'il sait comment lire le contenu de\n    ces bibliothèques et comment communiquer avec ces applications pour correctement supprimer des\n    photos de celles-ci. Pour utiliser cette fonctionnalité, vous devez ajouter iPhoto et/ou\n    Aperture avec les boutons spéciaux \"Ajouter librairie iPhoto\" et \"Ajouter librairie Aperture\",\n    qui apparaissent quand on clique sur le petit \"+\". Les dossiers ajoutés seront alors\n    correctement interprétés par dupeGuru.\n\n    Quand une photo est supprimée d'iPhoto, elle est envoyée dans la corbeille d'iPhoto.\n\n    Quand une photo est supprimée d'Aperture, il n'est malheureusement pas possible de l'envoyer\n    dans sa corbeille. Ce que dupeGuru fait à la place, c'est de créer un projet \"dupeGuru Trash\"\n    et d'envoyer les photos dans ce projet. Vous pouvez alors supprimer toutes les photos de ce\n    projet manuellement.\n\n.. only:: edition_me\n\n    Bibliothèques iTunes\n    ^^^^^^^^^^^^^^^^^^^^\n\n    dupeGuru ME supporte iTunes, ce qui veut dire qu'il sait comment lire le contenu de sa\n    bibliothèque et comment communiquer avec iTunes pour correctement supprimer des chansons de sa\n    bibliothèque. Pour utiliser cette fonctionnalité, vous devez ajouter iTunes avec le bouton\n    spécial \"Ajouter librairie iTunes\", qui apparait quand on clique sur le petit \"+\". Le dossier\n    ajouté sera alors correctement interprété par dupeGuru.\n\n    Quand une chanson est supprimée d'iTunes, elle est envoyée à la corebeille du système, comme un\n    fichier normal. La différence ici, c'est qu'après la suppression, iTunes est correctement mis au\n    fait de cette suppression et retire sa référence à cette chanson de sa bibliothèque.\n"
  },
  {
    "path": "help/fr/index.rst",
    "content": "Aide dupeGuru\n===============\n\n.. only:: edition_se\n\n    Ce document est aussi disponible en `anglais <http://dupeguru.voltaicideas.net/help/en/>`__, en `allemand <http://dupeguru.voltaicideas.net/help/de/>`__ et en `arménien <http://dupeguru.voltaicideas.net/help/hy/>`__.\n\n.. only:: edition_se or edition_me\n\n    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.\n\n.. only:: edition_pe\n\n    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.\n\nBien 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 <quick_start>`.\n\nC'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.\n\nContents:\n\n.. toctree::\n    :maxdepth: 2\n\n    quick_start\n    folders\n    preferences\n    results\n    reprioritize\n    faq\n    changelog\n"
  },
  {
    "path": "help/fr/preferences.rst",
    "content": "Préférences\n===========\n\n.. only:: edition_se\n\n    **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.\n\n    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.\n\n    **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).\n\n.. only:: edition_me\n\n    **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.\n\n    * **Nom de fichier:** Le nom de fichier des chansons est comparé, mot-à-mot.\n    * **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).\n    * **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%.\n    * **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.\n    * **Contenu:** Compare le contenu des chansons. Seul un contenu exactement pareil sera considéré comme un doublon.\n    * **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.\n\n    **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).\n\n    **Tags à scanner:** Pour les scans de type **Tags**, cette option détermine les tags qui seront comparés.\n\n.. only:: edition_se or edition_me\n\n    **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\")).\n\n    **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.\n\n.. only:: edition_pe\n\n    **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.\n\n    **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.\n\n    **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.\n\n**Comparer les fichiers de différents types:** Sans cette option, seulement les fichiers du même type seront comparés.\n\n**Ignorer doublons avec hardlink vers le même fichier:** Avec cette option, dupeGuru vérifiera si les doublons pointent vers le même `inode <http://en.wikipedia.org/wiki/Inode>`_. Si oui, ils ne seront pas considérés comme doublons. (Seulement pour OS X et Linux)\n\n**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 <http://www.regular-expressions.info>`_.\n\n**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.\n\n**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:\n\n* **Directement à la destination:** Les fichiers sont envoyés directement dans le dossier cible, sans essayer de recréer leur ancienne hierarchie.\n* **Re-créer chemins relatifs:** Le chemin du fichier relatif au dossier sélectionné dans la :doc:`sélection de dossier <folders>` 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``.\n* **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``.\n\nDans tous les cas, dupeGuru résout les conflits de noms de fichier en ajoutant un numéro en face du nom.\n\n**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.\n\nLe 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).\n\nSi 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::\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n"
  },
  {
    "path": "help/fr/quick_start.rst",
    "content": "Démarrage rapide\n=================\n\nVoici les étapes à suivre pour faire un simple scan par défaut:\n\n* Démarrer dupeGuru.\n* Ajouter les dossiers à scanner soit avec le drag & drop, soit avec le boutton \"+\".\n* Cliquez sur **Scan**.\n* Attendez que le scan soit completé.\n* 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é).\n* Si vous voyer un faux doublon, sélectionnez le puis cliquez sur l'action **Retirer sélectionnés des résultats**.\n* 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**.\n\nCeci 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.\n"
  },
  {
    "path": "help/fr/reprioritize.rst",
    "content": "Re-Prioritizing duplicates\n==========================\n\ndupeGuru tries to automatically determine which duplicate should go in each group's reference\nposition, but sometimes it gets it wrong. In many cases, clever dupe sorting with \"Delta Values\"\nand \"Dupes Only\" options in addition to the \"Make Selected into Reference\" action does the trick,\nbut sometimes, a more powerful option is needed. This is where the Re-Prioritization dialog comes\ninto play. You can summon it through the \"Re-Prioritize Results\" item in the \"Actions\" menu.\n\nThis dialog allows you to select criteria according to which a reference dupe will be selected in\neach dupe group. The list of available criteria is on the left and the list of criteria you've\nselected is on the right.\n\nA criteria is a category followed by an argument. For example, \"Size (Highest)\" means that the dupe\nwith the biggest size will win. \"Folder (/foo/bar)\" means that dupes in this folder will win. To add\na criterion to the rightmost list, first select a category in the combobox, then select a\nsubargument in the list below, and then click on the right pointing arrow button.\n\nThe order of the list on the right is important (you can re-order items through drag & drop). When\npicking a dupe for reference position, the first criterion is used. If there's a tie, the second\ncriterion is used and so on and so on. For example, if your arguments are \"Size (Highest)\" and then\n\"Filename (Doesn't end with a number)\", the reference file that will be picked in a group will be\nthe biggest file, and if two or more files have the same size, the one that has a filename that\ndoesn't end with a number will be used. When all criteria result in ties, the order in which dupes\npreviously were in the group will be used.\n"
  },
  {
    "path": "help/fr/results.rst",
    "content": "Résultats\n==========\n\nQuand dupeGuru a terminé de scanner, la fenêtre de résultat apparaît avec la liste de groupes de doublons trouvés.\n\nÀ propos des groupes de doublons\n---------------------------------\n\nUn 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.\n\nLes 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.\n\nBien 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**.\n\nVérifier les résultats\n------------------------\n\nBien 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.\n\nPour 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.\n\nSi 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.\n\nMarquer et sélectionner\n-----------------------\n\nDans 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.\n\nLes 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**.\n\nNe pas montrer les références\n-------------------------------\n\nQuand 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.\n\nL'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\":\n\n* Activer le mode **Ne pas montrer les références**.\n* Ajouter la colonne \"Type\" par le menu \"Colonnes\".\n* Cliquez sur la colonne Type pour changer le tri.\n* Trouvez le premier fichier avec un type \"exe\".\n* Sélectionnez-le.\n* Trouvez le dernier fichier avec un type \"exe\".\n* Tenez Shift et sélectionnez-le.\n* Appuyez sur espace pour marquer les fichiers sélectionnés.\n\nMontrer les valeurs en tant que delta\n-------------------------------------\n\nSous ce mode, certaines colonnes montreront leur valeurs relativement à la valeur de la référence du\ngroupe (de couleur orange, pour bien les différencier des autres valeurs). Par exemple, si un\nfichier a une taille de 1.2 MB alors que la référence a une taille de 1.4 MB, la valeur affichée\nsous ce mode sera -0.2 MB.\n\nLes valeurs non numériques sont aussi affectées par le mode delta. Par contre, plutôt que montrer la\ndifférence avec la valeur de référence (ce qui est impossible), elles indiquent si elles sont\npareilles en adoptant la couleur orange dans le cas ou la valeur est différent. Il est ainsi\npossible de facilement identifier, par exemple, tous le doublons qui ont un nom de fichier différent\nde leur référence.\n\nLes deux modes ensemble\n-----------------------\n\nQuand on active ces deux modes ensemble, il est alors possible de faire de la sélection de ficher\nassez avancée parce que le tri de fichier se fait alors en fonction des valeurs delta. Il devient\nalors possible de, par exemple, sélectionner tous les fichiers qui ont une différence de plus de 300\nKB par rapport à leur référence, ou d'autres trucs comme ça.\n\nMême chose pour les valeurs non numériques: quand les deux modes sont activés, les règles de tri\npour les valeurs non-numériques change. On commence par grouper les doublon selon si leur valeur est\norange ou non, pour ensuite procéder aux règles de tri normales. Avec ce système, il est alors\nfacile de, par exemple, marquer toues les doublons qui ont un nom de fichier différent de leur\nréférence: il suffit de trier par nom de fichier.\n\nFiltrer les résultats\n---------------------\n\nIl est possible de filtrer les résultats pour agir sur un sous-ensemble de ceux-ci, par exemple tous\nles fichiers qui contiennent le mot \"copie\".\n\nPour filtrer les résultats, entrer le filtrer dans le champ de la barre d'outils, puis appuyer sur\nEntrée. Pour annuler le filtre, appuyez sur le X dans le champ.\n\nEn mode simple (le mode par défaut), ce que vous tapez est ce qui est filtré. Il n'y a qu'un\ncaractère spécial: **\\***. Ainsi, si vous entrez \"[*]\", le filtre cherchera pour tout nom contenant\nles \"[\" et \"]\" avec quelquechose au milieu.\n\nPour un filtrage avancé, activez **Utiliser les expressions régulières pour les filtres** dans les\n:doc:`preferences`. Votre filtre sera alors appliqué comme une `expression régulière`_.\n\nLes filtres sont dans tous les cas insensibles aux majuscules et minuscules.\n\nLes expression régulière pour s'appliquer à un fichier n'ont pas besoin de correspondre au nom\nentier. Une correspondance partielle suffit.\n\nVous remarquerez peut-être que ce ne sont pas tous les fichiers de vos résultats filtrés qui\ns'appliquent au filtre. C'est parce que les groupes ne sont pas brisés par les filtres afin de\npermettre une meilleure mise en context. Par contre, ces fichier seront en mode \"Lecture seule\" et\nne pourront être marqués.\n\nActions\n-------\n\nVoici la liste des actions qu'il est possible d'appliquer aux résultats.\n\n* **Vider la liste de fichiers ignorés:** Ré-initialise la liste des paires de doublons que vous avez ignorés dans le passé.\n* **Exporter vers HTML:** Exporte les résultats vers un fichier HTML et l'ouvre dans votre browser.\n* **Envoyer marqués à la corbeille:** Le nom le dit.\n* **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`.\n* **Copier marqués vers...:** Même chose que le déplacement, sauf que c'est une copie à la place.\n* **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.\n* **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.\n* **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.\n* **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.\n* **Ouvrir sélectionné avec l'application par défaut:** Ouvre le fichier sélectionné avec son application associée.\n* **Ouvrir le dossier contenant le fichier sélectionné:** Le nom dit tout.\n* **Invoquer commande personnalisée:** Invoque la commande personnalisé que vous avez définie dans les :doc:`preferences`.\n* **Renommer sélectionné:** Renomme le fichier sélectionné après vous avoir demandé d'entrer un nouveau nom.\n\n**Déplacer des fichiers dans iPhoto/iTunes:** Attention, quand vous déplacez des fichiers des\nbibliothèques iPhoto ou iTunes, elles ne sont pas vraiment déplacées, mais copiée. Il n'y a pas\nd'action de déplacement possible dans ces bibliothèques.\n\nOptions de suppression\n----------------------\n\nCes options, présentées lors de l'action de suppression de doublons, déterminent comment celle-ci\ns'exécute. La plupart du temps, ces options n'ont pas a être activées.\n\n* **Remplacer les fichiers effacés par des liens:** les fichiers supprimés seront replacés par des\n  liens (`symlink`_ ou `hardlink`_) vers leur fichiers de référence respectifs. Un symlink est un\n  lien symbolique (qui devient caduque si l'original est supprimé) et un hardlink est un lien direct\n  au contenu du fichier (même si l'original est supprimé, le lien reste valide).\n\n  Sur OS X et Linux, cette fonction est supportée pleinement, mais sur Windows, c'est un peu\n  compliqué. Windows XP ne le supporte pas, mais Vista oui. De plus, cette fonction ne peut être\n  utilisée que si dupeGuru roule avec les privilèges administratifs. Ouaip, Windows c'est la joie.\n\n* **Supprimer les fichiers directement:** Plutôt que d'envoyer les doublons à la corbeille,\n  directement les supprimer. Utiliser cette option si vous avez de la difficulté à supprimer des\n  fichiers (ce qui arrive quelquefois quand on travaille avec des partages réseau).\n\n.. _expression régulière: http://www.regular-expressions.info\n.. _hardlink: http://en.wikipedia.org/wiki/Hard_link\n.. _symlink: http://en.wikipedia.org/wiki/Symbolic_link\n"
  },
  {
    "path": "help/hy/faq.rst",
    "content": "﻿Հաճախ Տրվող Հարցեր\n==========================\n\n.. topic:: Ի՞նչ է dupeGuru-ը:\n\n    .. only:: edition_se\n\n        dupeGuru-ն ծրագիր է, որ գտնում է համակարգչի ֆայլերի կրկնօրինակները: Այն կարող է ստուգել ըստ ֆայլի անվան կամ բովանդակության: Ֆայլի անվամբ փնտրման հնարավորությունը ոչ ճշգրիտ համընկնումներ է տալիս երբեմն: Շատ ժամանակ անունները նույնն են, բայց ֆայլերը տարբեր են:\n\n    .. only:: edition_me\n\n        dupeGuru Music Edition-ը ծրագիր է, որ գտնում է համակարգչի երաժշտական ֆայլերի կրկնօրինակները: Այն կարող է հիմնվել ֆայլի անունները ստուգելու վրա՝ ըստ կցապիտակների և բովանդակության: Ֆայլի անունների և կցապիտակների ստուգումը ոչ ճշգրիտ ալգորիթմ է, քանզի այն կարող է գտնել համընկնումներ, որոնք իրականում նույնը չեն:\n\n    .. only:: edition_pe\n\n        dupeGuru Picture Edition (PE՝ կարճ)-ը ծրագիր է, որ գտնում է համակարգչի նկարների ֆայլերի կրկնօրինակները: Այն կարող է գտնել ոչ միայն ճշգրիտ համընկնումները, այլև այն կարող է գտնել տարբեր որակի և տեսակի նկարների (PNG, JPG, GIF և այլն...) համընկնումներ:\n\n.. topic:: Ի՞նչն է այս ծրագիրը առանձնացնում մյուս նմանատիպ ծրագրերից:\n\n    Ստուգելու համակարգը չափազանց նուրբ է: Կարող եք ինքներդ հարմարեցնել այն՝ ստանալու համար այն արդյունքը, ինչը որ Ձեզ պետք է: Կարող եք լրացուցիչ կարդալ այս մասին dupeGuru-ի կարգավորման ընտրանքներում՝ :doc:`Կարգավորումների էջում <preferences>`:\n\n.. topic:: Ո՞րքանով է անվտանգ օգտագործելու dupeGuru-ը:\n\n    Շատ անվտանգ է: dupeGuru-ը նախագծվել է՝ համոզված լինելու համար, որ Դուք չջնջեք այն ֆայլերը, որոնք չպետք է ջնջեք: Նախ, կա հղմամբ համակարգային թղթապանակ, որը հնարավորություն է տալիս Ձեզ որոշելու թղթապանակներ, որտեղ Դուք բացարձակ **չեք** ցանկանում, որ dupeGuru-ն հնարավորություն տա Ձեզ ջնջելու ֆայլերը այստեղից, և ապա կա խմբի հղմամբ համակարգային համակարգ, որը համոզմունք է ստեղծում, որ Դուք **միշտ** պետք է պահեք գոնե մեկ անդամ կրկնօրինակվող խմբի:\n\n.. topic:: Ո՞րոն եք dupeGuru-ի լիցենզիայի սահմանափակումները:\n\n    Փորձնական եղանակում, Դուք կարող եք միայն կատարել գործողություններ 10 կրկնօրինակների հետ միաժամանակ: Ծրագրի\n    `Անվճար տարբերակում <http://open.hardcoded.net/about/>`_ mode, այնուհանդերձ չկան էական սահմանափակումներ:\n\n.. topic::Ջնջելու համար նշելու դաշտի պատուհանը ակտիվ չէ: Ի՞նչ անել:\n\n    Չեք կարող նշել հղումը (Առաջին ֆայլը) կրկնօրինակվող խմբի: Այնուհանդերձ, ինչ կարող եք Դուք անել առաջ մղելու համար կրկնօրինակվող ֆայլը հղմանը: Այսպիսով, եթե ֆայլը ցանկանում եք նշել որպես հղում, ընտրեք կրկնօրինակվող ֆայլը խմբից, որը ցանկանում եք տանել հղման մեջ, և սեղմեք **Գործողություններ-->Դարձնել ընտրվածը հղում**: Եթե հղվող ֆայլը հղման թղթապանակից է (ֆայլի անունը գրված է կապույտ տառերով), Դուք չեք կարող ջնջել այն հղման դիրքից:\n\n.. topic:: Ես ունեմ թղթապանակ, որտեղից ես իրապես չեմ ցանկանում ջնջել ֆայլեր:\n\n    Եթե Դուք ցանկանում եք համոզված լինել, որ dupeGuru-ն երբեք չի ջնջի ֆայլ կոնկրետ թղթապանակից, համոզված եղեք, որ տվել եք կարգը **Հղման** :doc:`folders`:\n\n.. topic:: Ի՞նչ է սա '(X վնասված)'՝ նշված դրության տողում:\n\n    Որոշ դեպքերում, որոշ համընկնումներ ներառված չեն վերջնական արդյունքում երկրորդական պատճառներով: Եկեք նայենք կոնկրետ օրինակի վրա: Մենք ունենք 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 համընկնում հաջորդ արդյունքներում:\n\n.. topic:: Ես ցանկանում եմ նշել բոլոր ֆայլերը որոշված թղթապանակից: Ի՞նչ կարող եմ ես անել:\n\n    Միացնել :doc:`Միայն Սխալները <results>` եղանակը և սեղմեք թղթապանակի սյանը՝ դասավորելու համար կրկնօրինակները ըստ թղթապանակների: Հետագայում հեշտ կլինի ընտրելու բոլոր կրկնօրինակները նույն թղթապանակից և ապա սեղմեք Space՝ ընտրելու ահմար բոլոր կրկնօրինակները:\n\n.. only:: edition_se or edition_pe\n\n    .. topic:: Ես ցանկանում եմ հեռացնել բոլոր ֆայլերը, որոնք 300 ԿԲ-ից ավելի են հղվող ֆայլից: Ի՞նչ կարող եմ ես անել:\n\n        * Միացնել :doc:`Միայն Սխալները <results>` եղանակում:\n        * Միացնել **Դելտա նշանակությունները** եղանակը:\n        * Սեղմեք \"Չափը\" սյանը՝ դասավորելու համար արդյունները ըստ չափի;\n        * Ընտրեք բոլոր կրկնօրինակները՝ -300-ից ցածր:\n        * Սեղմեք **Ջնջել ընտրվածը Արդյունքներից**:\n        * Ընտրեք բոլոր կրկնօրինակերը, որոնք մեծ են 300-ից:\n        * Սեղմեք **Ջնջել ընտրվածները Արդյունքներից**:\n\n    .. topic:: Ես ցանկանում եմ դարձնել վերջին փոփոխված ֆայլերը հղման ֆայլեր: Ի՞նչ կարող եմ ես անել:\n\n        * Միացնել :doc:`Միայն Սխալները <results>` եղանակում:\n        * Միացնել **Դելտա նշանակությունները** եղանակը:\n        * Սեղմեք \"Ըստ փոփոխության\" սյանը՝ արդյունքները ըստ փոփոխման դասավորելու համար:\n        * Սեղմեք \"Ըստ փոփոխության\" սյանը՝ կրկնելու համար դասավորման կարգը:\n        * Ընտրել բոլոր կրկնօրինակները 0-ից բարձր:\n        * Սեղմեք **Դարձնել ընտրվածը հղում**:\n\n    .. topic:: Ես ցանկանում եմ նշել բոլոր այն կրկնօրինակները, որոնք պարունակում են \"պատճենել\" բառը: Ինչպե՞ս դա անել:\n\n        * **Windows**. Սեղմեք **Գործողություններ --> կիրառել ֆիլտրը**, ապա նշեք \"պատճենել\", հետո սեղմեք ԼԱՎ:\n        * **Mac OS X**. Նշեք \"պատճենել\" \"Ֆիլտրում\" դաշտում՝ գործիքների վահանակում:\n        * Սեղմեք **Նշել --> Նշել բոլորը**:\n\n.. only:: edition_me\n\n    .. topic:: Ես ցանկանում եմ հեռացնել բոլոր երգերը, որոնք 3 վայրկյանից հեռու են իրենց հղման ֆայլից: Ի՞նչ կարող եմ ես անել:\n\n        * Միացնել :doc:`Միայն Սխալները <results>` եղանակում:\n        * Միացնել **Դելտա նշանակությունները** եղանակը:\n        * Սեղմեք \"Ժամանակը\" սյանը՝ դասավորելու համար արդյունքները ըստ ժամանակի:\n        * Ընտրեք բոլոր կրկնօրինակները՝ -00:03-ից ցածր:\n        * Սեղմեք **Ջնջել ընտրվածը արդյունքներից**:\n        * Ընտրել բոլոր կրկնօրինակները 00:03-ից բարձր:\n        * Սեղմեք **Ջնջել ընտրվածը արդյունքներից**:\n\n    .. topic:: Ես ցանկանում եմ դարձնել իմ բարձրագույն բիթրեյթ ունեցող երգերը հղման ֆայլեր: Ի՞նչ կարող եմ ես անել:\n\n        * Միացնել :doc:`Միայն Սխալները <results>` եղանակում:\n        * Միացնել **Դելտա նշանակությունները** եղանակը:\n        * Սեղմեք \"Բիթրեյթը\" սյանը՝ դասավորելու համար արդյունքները ըստ բիթրեյթի:\n        * Սեղմեք \"Բիթրեյթը\" սյանը՝ կրկնելու համար դասավորման կարգը:\n        * Ընտրել բոլոր կրկնօրինակները 0-ց բարձր;\n        * Սեղմեք **Դարձնել ընտրվածը հղում**:\n\n    .. topic:: Ես չեմ ցանկանում [live] և [remix] տարբերակները իմ երգերի՝ հաշված որպես կրկնօրինակ: Ինչպե՞ս դա անել:\n\n        Եթե Ձեր համեմատության սահմանը բավականին ցածր է, հնարավոր է Դուք ավարտվեք կենդանի և ռեմիքս տարբերակներով Ձեր երգերի արդյունեքներում: Դուք ոչինչ չեք կարող անել դրա համար, բայց կա ինչ-որ եղանակ՝ դրանք ստուգման արդյունքներից ջնջելու համար: Եթե օրինակի համար, Դուք ցանկանում եք ջնջել ամեն մի երգ, որը գտնվում է գծիկների միջև []:.\n        * **Windows**. Սեղմեք **Գործողություններ --> Կիրառել ֆիլտրը**, ապա տեսակը \"[*]\", ապա սեղմեք ԼԱՎ:\n        * **Mac OS X**. Տեսակը \"[*]\" \"Ֆիլտր\" դաշտում՝ գործիքաշերտի:\n        * Սեղմեք **Նշել --> Նշել բոլորը**:\n        * Սեղմեք **Գործողություններ --> Ջնջել ընտրվածը արդյունքներից**.\n\n.. topic:: Ես փորձում եմ կրկնօրինակները ուղարկել Աղբարկղ, բայց dupeGuru-ն ինձ ասում է, որ չես կարող: Ինչու՞: Ի՞նչ կարող եմ ես անել:\n\n    Շատ ժամանակ, պատճառը, թե ինչու dupeGuru-ն չի կարողանում տեղափոխել ֆայլերը Աղբարկղ, կայանում է ֆայլի լիազորությունների մեջ: Դուք պետք է *գրեք* լիազորությունները ֆայլերում, որոնք որ ցանկանում եք ուղարկել Աղբարկղ: Եթե Ձեզ անծանոթ է Հրամանի տողը, ապա Դուք կարող եք օգտագործել լրացուցիչ գործիքներ, ինչպես օրինակ `BatChmod <http://macchampion.com/arbysoft/BatchMod>`_ լիազորումները նշելու համար:\n\n    Եթե dupeGuru-ն դեռ շարունակում է խնդիրներ առաջ բերել կապված լիազորությունների հետ, ապա կան խնդիրներ կապված՝ \"Տեղափոխել նշվածը...\" որպես շրջանցիկ խորամանկություն: Ուստի ֆայլերը Աղբարկ տեղափոխելիս Դուք ուղարկում եք այն ժամանակավոր թղթապանակ \"Տեղափոխել նշվածը...\" գործողությամբ և ապա Դուք կջնջեք այդ թղթապանակը ձեռադիր;\n\n    .. only:: edition_pe\n\n        Եթե Դուք փորձում եք ջնջել *iPhoto* նկարները, ապա ձախողման պատճառը տարբեր է: Ջնջելը ձախողվել է, որովհետև dupeGuru-ը չի կարողանում համագործակցել iPhoto: Լինել տեղեկացված, որ ջնջումը նորմալ է աշխատում, Դուք չեք նախատեսում խաղարկել ձայն iPhoto-ին, քանսզի dupeGuru-ն աշխատում է: Նաև, երբեմն, Applescript համակարգը չի կողմնորոշվում որտեղ փնտրել iPhoto՝ բացելու համար: Հավանական է, այս դեպքերում պետք է բացել iPhoto-ն *մինչև* Դուք ուղարկեք Ձեր կրկնօրինակները Աղբարկղ:\n\n    Եթե այս ամենը ձախողվի, `կապնվեք HS աջակցության թիմի հետ <http://www.hardcoded.net/support>`_, մենք կփորձեք օգնել Ձեզ:\n\n.. todo:: This FAQ qestion is outdated, see english version.\n"
  },
  {
    "path": "help/hy/folders.rst",
    "content": "﻿Թղթապանակի ընտրություն\n=======================\n\nԱռաջին թղթապանակը, որ Դուք տեսնում եք dupeGuru-ն բացելիս դա թղթապանակի ընտրությունն է: Այս պատուհանը պարունակում է թղթապանակների ցանկը, որոնք կստուգվեն **Ստուգել** սեղմելիս:\n\nԱյս պատուհանը շատ հեշտ է օգտագործել: Եթե ցանկանում եք ավելացնել թղթապանակ, ապա սեղմեք **+** կոճակը: Եթե մինչ այդ ավելացնեք թղթապանակը, ապա կերևա ավելացված վերջին թղթապանակների ցանկը: Կարող եք սեղմել նրանցից մեկի վրա՝ ավելացնելու համար ուղղակի Ձեր ցանկում: Եթե սեղմեք հայտնվող պատուհանի առաջին ֆայլին՝ **Ավելացնել նոր թղթապանակ...**, ապա Ձեզ հարցում կկատարվի թղթապանակ ավելացնելու մասին: Եթե երբեք չեք ավելացրել թղթապանակ, ապա ոչ մի ընտրացանկ չի երևա և Ձեզ ուղղակի հարցում կարվի նոր թղթապանակ ավելացնելու մասին:\n\nԱյլընտրանքյին ճանապարհով թղթապանակներ կարող եք ավելացնել պարզապես դրանք գցելով ցանկում:\n\nԹղթապանակը հեռացնելու համար ընտրեք թղթապանակը, սեղմեք **-**: Եթե ընտրված է ենթաթղթպանակը, երբ Դուք սեղմում եք կոճակին, ընտրված թղթապանակը կնշվի որպես **բացառված** (նայեք այստեղ)՝ ջնջվելու փոխարեն:\n\nԹղթապանակի վիճակը\n------------------\n\nՅուրաքանչյուր թղթապանակ կարող է լինել հետևյալ 3 եղանակներից մեկում.\n\n* **Նորմալ.** Այս թղթապանակում գտնված կրկնօրինակները կարող են ջնջվել:\n* **Հղված.** Կրկնօրինակներ են գտնվել այս թղթապանակում, որոնք **չեն կարող** ջնջվել: Ֆայլերը այս թղթապանակից կարող են միայն ավարտվել **հղում** դիրքով խմբում: Եթե մեկ ֆայլից ավելի են հղման թղթապանակների հղումները, ապա միայն մեկը կպահվի: Մնացածը կջնջվեմ խմբից:\n* **Բացառված.** Ֆայլերը այս թղթապանակում կներառվեն ստուգման մեջ:\n\nԹղթապանակի հիմնական վիճակը, իհարկե՛ **Նորմալ է**: Կարող եք օգտագործել **Հղված** վիճակը թղթապանակի համար, եթե ցանկանում եք համոզված լինել, որ ոչ մի ֆայլ չի ջնջվի:\n\nԵթե նշել եք թղթապանակի վիճակը, բոլոր ենթաթղթապանակները միանգամից կժառանգեն այս վիճակը, եթե վիճակը պարզորոշ տրված է թղթապանակի կարգում:\n\n.. todo:: Add iPhoto/Aperture/iTunes libraries notes\n"
  },
  {
    "path": "help/hy/index.rst",
    "content": "﻿dupeGuru help\n===============\n\n.. only:: edition_se\n\n    Այս փաստաթուղթը հասանելի է նաև՝ `Ֆրանսերեն <http://dupeguru.voltaicideas.net/help/fr/>`__ և `Գերմաներեն <http://dupeguru.voltaicideas.net/help/de/>`__.\n\n.. only:: edition_se or edition_me\n\n    dupeGuru ծրագիր է՝ գտնելու կրկնօրինակ ունեցող ֆայլեր Ձեր համակարգչում: Այն կարող է անգամ ստուգել ֆայլի անունները կան բովանդակությունը: Ֆայլի անվան ստուգման հնարավորությունները ոչ ճշգրիտ համընկման ալգորիթմով, որը կարող է գտնել ֆայլի անվան կրկնօրինակներ, անգամ եթե դրանք նույնը չեն:\n\n.. only:: edition_pe\n\n    dupeGuru Picture Edition-ը (PE՝ կարճ) գործիք է, որը գտնում է նկարների կրկնօրինակները Ձեր համակարգչում: Գտնում է ոչ միայն նույնանման կրկնօրինակները, այլ նաև կարող է գտնել տարբեր տեսակի և որակի նկարներ (PNG, JPG, GIF և այլն...):\n\nՉնայած dupeGuru-ն կարող է հեշտությամբ օգտագործվել առանց օգնության, այնուհանդերձ եթե կարդաք այս ֆայլը, այն մեծապես կօգնի Ձեզ ընկալելու ծրագրի աշխատանքը: Եթե Դուք նայում եք ձեռնարկը կրկնօրինակների առաջին ստուգման համար, ապա կարող եք ընտրել :doc:`Արագ Սկիզբ <quick_start>` հատվածը:\n\nՇատ լավ միտք է պահելու dupeGuru թարմացված: Կարող եք բեռնել վեբ կայքի համապատասխան էջից http://dupeguru.voltaicideas.net:\n\nՊարունակությունը.\n\n.. toctree::\n    :maxdepth: 2\n\n    quick_start\n    folders\n    preferences\n    results\n    reprioritize\n    faq\n    changelog\n"
  },
  {
    "path": "help/hy/preferences.rst",
    "content": "﻿Կարգավորումներ\n================\n\n.. only:: edition_se\n\n    **Ստուգելու տեսակը.** Այս ընտրանքը որոշում է, թե ֆայլերի որ ասպեկտը կհամեմատվի կրկնօրինակված ստուգման հետ: Եթե Դուք ընտրեք **Ֆայլի անունը**, ապա dupeGuru-ն կհամեմատի յուրաքանչյուրը բառ-առ-բառ և կախված է հետևյալ այլ ընտրանքներից, այն կորոշի արդյոք բավական են համընկնող բառերը դիտելու համար 2 ֆայլերի կրկնօրինակները: Եթե ընտրեք միայն **Բովանդակությունը**, ապա նույնատիպ ֆայլերը նույն բովանդակությամբ կհամընկնեն:\n\n    **Թղթապանակներ.** ստուգելու հատուկ տեսակ է: Երբ ընտրեք սա, dupeGuru-ն կստուգի կրկնօրինակ *թղթապանակները*՝ կրկնօրինակ ֆայլերի փոխարեն: Որոշելու համար արդյոք անկախ երկու թղթապանակները կրկնօրինակ են, կստուգվեն թղթապանակների ամբողջ պարունակությունը և եթե **բոլոր** ֆայլերի բովանդակությունը համընկնի, ապա թղթապանակները կորոշվեն որպես կրկնօրինակներ:\n\n    **Ֆիլտրի խստությունը.** Եթե Դուք ընտրեք **Ֆայլի անունը** ստուգելու տեսակը, այս ընտրանքը կորոշի, թե ինչքանով նման պետք է լինեն ֆայլերի անունները, որ dupeGuru-ն ճանաչի դրանք որպես կրկնօրինակներ: Եթե ֆիլտրը առավել խիստ է, օրինակ՝ 80, ապա դա նշանակում է, որ երկու ֆայլերի անունների բառերի 80%-ը պետք է համընկնի: Որոշելու համար համընկնման տոկոսը, dupeGuru-ն նախ հաշվում է բառերի ընդհանուր քիանակը **երկու** ֆայլերի անուններում, ապա հաշվում է համընկնումների քանակը (ամեն բառ համընկնում է 2-ի հաշվին) և բաժանում ընդհանուր գտնված բառերի համընկնումների միջև: Եթե արդյունքը բարձր է կամ հավասար ֆիլտրի խստությանը, ապա մենք ունեք կրկնօրինակի համընկնում: Օրինակ՝ \"a b c d\" և \"c d e\" ունեն համընկնման տոկոս, որը հավասար է 57-ի (4 բառ են համընկնում, 7 ընդհանուր բառից):\n\n.. only:: edition_me\n\n    **Ստուգելու եղանակը.** Այս ընտրանքը որոշում է, թե որ ասպեկտն է ֆայլերի՝ համեմատելի կրկնօրինակման ստուգմանը: Կրկնօրինակների ստուգման բնույթը փոխվում է մեծապես կախված, թե ինչի եք ընտրում այս ընտրանքը:\n\n    * **Ֆայլի անունը.** Ցանկացած երգ ունի իր ֆայլի անվան մասնատումը բառերի և ապա ամեն բառ կհամեմատվի՝ հաշվելու համար համընկնման տոկոսը: Եթե այս տոկոսը ավելի բարձր է կամ հավասար **Ֆիլտրի խստությանը** (նայել՝ մանրամասների համար), dupeGuru-ն կդիտարկի երկու երգերը որպես կրկնօրինակներ:\n    * **Ֆայլի անունը - Դաշտերը.** Ինչպես օրինակ **Ֆայլի անունը**, բացառում է, որ մեկ ֆայլի անունը բաժանվի բառերի, այս բառերը ապա կխմբավորվեն դաշտերում: Դաշտերի բաժանիչը \" - \" է: Համընկնման վերջնական տոկոսը կլինի համընկնման ցածրագույն տոկոսը դաշտերի միջև: Այսպիսով, \"Կատարողը - Վերնագիրը\" և \"Կատարողը - Այլ վերնագիրը\" կունենա համընկման տոկոս՝ 50 (**Ֆայլի անունը** ստուգմամբ, կլինի 75).\n    * **Ֆայլի անունը - Դաշտերը (անկարգ).** Ինչպես օրինակ **Ֆայլի անունը - Դաշտերը** բացառությամբ, որ դաշտի կարգը չի համընկնում: Օրինակ՝ \"Կատարողը - Վերնագիրը\" և \"Վերնագիրը - Կատարողը\" կունենան համընկնման 100 տոկոս՝ 0-ի փոխարեն:\n    * **Կցապիտակներ.** Այս եղանակը կարդում է յուրաքանչյուր երգի կցապիտակները (մետատվյալները) և համեմատում է նրանց դաշտերը: Այս եղանակը, ինչպես օրինակ **Ֆայլի անունը - Դաշտերը** դիտարկում են համընկնման ցածրագույն դաշտը՝ համեմատման վերջնական տոկոսից:\n    * **Պարունակությունը.** Ստուգման այս եղանակը օգտագործում են երգերի բովանդակությունը՝ որոշելու համար, թե որն են կրկնօրինակները: 2 երգերը համընկնեցնելու համար այս եղանակով, դրանք պետք է ունենան **բացառապես նույն բովանդակությունը**:\n    * **Ձայնի բովանդակությունը.** Նույնն են բովանդակությամբ, բայց միայն ձայնի պարունակությունն է համեմատելի (առանց մետատվյալների):\n\n    **Ֆիլտրի խստությունը.** Եթե ընտրում եք ֆայլի անունը կամ կցապիտակը՝ հիմնված ստուգման եղանակի վրա, ապա այս ընտրանքը որոշում է, թե ինչքան են նման երկու ֆայլի անունները/կցապիտակները պետք է լինեն dupeGuru-ի կողմից դիտարկվող կրկնօրինակներ: Եթե ֆիլտրի խստությունը օրինակի համար 80 է, դա նշանակում է, որ երկու ֆայլի անունների բառերի համընկնումը 80% է: Որոշելու համար համընկնման տոկոսը, dupeGuru-ն առաջին հաշվով որոշում է **երկու** ֆայլի անունների առաջին հաշվարկի ընդհանուր քանակը, ապա համընկնող բառերի համընկնման քանակը (բոլոր բառերը համընկնում են 2-ի) և ապա բաժանել համընկնող բառերի թիվը ընդհանուր բառերի թվին: Եթե արդյունքը բարձր է կամ հավասար ֆիլտրի խստությանը, ապա մենք ունենք կրկնօրինակի համընկնում: Օրինակ՝ \"a b c d\" և \"c d e\" ունի համընկնման 57 տոկոս (4 բառերի համընկնում, 7 ընդամենը բառեր):\n\n    **Ստուգվող կցապիտակները.** Երբ օգտագործվում է **Կցապիտակներ** ստուգելու եղանակը, կարող եք ընտրել կցապիտակներ, որոնք կօգտագործվեն համեմատման համար:\n\n.. only:: edition_se or edition_me\n\n    **Բառի կշիռը.** Եթե ընտրում եք **Ֆայլի անունը** ստուգելու եղանակը, ապա այս ընտրանքը որոշակիորեն փոխում է համընկնման տոկոսը հաշվելու եղանակը: Բառի կշռմամբ կրկնօրինակի քանակի փոխարենը 1 նշանակությունը ունենալու համար, ամեն բառը ունի հավասարազոր նշանակություն՝ առկա գրանշանների թվին: Բառի կշռմամբ, \"ab cde fghi\" և \"ab cde fghij\" կունենա համընկնման տոկոս՝ 53% (19 ընդամենը գրանշաններ, 10 գրանշանների համընկնում (4-ը \"ab\"-ի և 6-ը \"cde\"-ի համար)):\n\n    **Նմանատիպ բառերի համընկնում.** Եթե միացնեք այս ընտրանքը, նմանատիպ բառերը կհաշվեն որպես համընկնումներ: Օրինակ՝ \"Սպիտակ շրջանակ\" և \"Սպիտակ շրջանակ\" ունի համընկնման % հավասարազոր 100-ի՝ 66-ի փոխարեն, եթե ընտրանքը միացված է: **Զգուշացում.** Այս ընտրանքը զգուշությամբ օգտագործեք: Հավանական է ստացված տվյալների մեծ մասը կեղծ լինեն: Այնուհանդերձ, այն կօգնի Ձեզ գտնելու կրկնօրինակներ, որոնք այլ ճանապարհով հնարավոր չի եղել գտնել: Ստուգելու ընթացքը նաև նշանակալի դանդաղ է, եթե այս ընտրանքը միացված է:\n\n.. only:: edition_pe\n\n    **Ստուգելու եղանակը.** Այս ընտրանքը որոշում է ստուգելու եղանակը, որը կկիրառվի նկարների նկատմամբ: **Պարունակությունը** ստուգելու եղանակը համեմատում է ակտուալ նկարների բովանդակությունը ոչ ճշգրիտ եղանակով (հնարավորություն տալով գտնելու ոչ միայն անմիջապես կրկնօրինակները, այլ նաև նմանատիպ այլ ֆայլերը): **EXIF Timestamp** ստուգելու եղանակը նայում է նկարի EXIF մետատվյալը (եթե այն կա) և համընկնող նկարները, որոնք որ նույնն են: Սա ավելի արագ է, քան բովանդակությամբ ստուգելը: **Զգուշացում.** Փոփոխված նկարները սովորաբար պահում են նույն EXIF timestamp-ը, ուստի նախ նայեք արդյունքները, ապա գործեք:\n\n    **Ֆիլտրի խստությունը.** *Ստուգում է միայն բովանդակությունը:* Այս ընտարնքի բարձրագույն նիշը, բնորոշում է ֆիլտրի \"խստությունը\" (Այլ կերպ ասաց, արդյունքը ավելի քիչ է լինում): Նույն որակի նկարներից շատերը երբեմն համընկնում են 100%-ով՝ անգամ եթե տեսակը ուրիշ է (PNG և JPG օրինակի համար): Այնուհանդերձ, եթե ցանկանում եք, որ PNG-ն համապատասխանի ցածր որակի JPG-ին, պետք է նշեք ֆիլտրի խստությունը 100-ից ցածր: Ծրագրայինը 95 է:\n\n    **Տարբեր չափերով նկարների համապատասխանեցում.** Եթե ընտրեք սա, տարբեր չափերի նկարները կթույլատրվեն կրկնօրինակվող նույն խմբում:\n\n**Կարող է ուղղել ֆայլի տեսակը.** Եթե ընտրում եք այս վանդակը, ապա կրկնօրինակվող խմբերը կթույլատրվեն ունենալու տարբեր ընդլայնումներ: Եթե չընտրեք, ապա դրանք չեն լինի!\n\n**Անտեսել կրկնօրինակների հղումը նույն ֆայլին file:** Եթե այս ընտրանքը միացված է, dupeGuru-ն կստուգի կրկնօրինակները՝ տեսնելու համար արդյոք դրանք հղվում են նույնին `inode <http://en.wikipedia.org/wiki/Inode>`_: Եթե այո, ապա դրանք չեն որոշվի որպես կրկնօրինակ: (Միայն OS X և Linux-ում)\n\n**Ֆիլտրելիս օգտագործել կանոնավոր սահմանումներ.** Եթե ընտրեք սա, ապա ֆիլտրման հնարավորությունը կդիմի ֆիլտրման հերթին, ինչպես որ **կանոնավոր սահմանում**: Դրա բացատրությունը դուրս կգա այս փաստաթղթի շրջանակից: Ավելին կարող եք կարդալ այստեղ՝ `regular-expressions.info <http://www.regular-expressions.info>`_:\n\n**Ջնջել դատարկ թղթապանակները ջնջելուց կամ տեղափոխելուց.** Երբ այս ընտրանքը միացված է, թղթապանակները կջնջվեն, երբ որ ֆայլը ջնջվի կամ տեղափոխվի և թղթապանակը դատարկ լինի:\n\n**Պատճենել և տեղափոխել.** Որոշում է, թե Պատճենելու և Տեղափոխելու գործողությունները (գործողություն ընտրացանկից):\n\n* **Տեղադրությունից աջ.** Բոլոր ֆայլերը կուղարկվեն ընտրված տեղ՝ առանց փորձելու վերստեղծելու աղբյուրի ճանապարհը բոլորի համար:\n* **Վերստեղծել հարաբերական ճանապարհը.** Աղբյուր ֆայլի ճանապարհը կվերստեղծվի նշանակված թղթապանակում՝ խորքայինից մեկ աստիճան վեր՝ Թղթապանակներ վահանակից: Օրինակ՝ եթե ավելացնեք``/Users/foobar/SomeFolder`` Թղթապանակներ վահանակ և տեղափոխեք ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` նշանակության թղթապանակ ``/Users/foobar/MyDestination``, ֆայլի վերջնական տեղավորությունը կլինի ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` բաժանվել է աղբյուր ճանապարհից վերջնական տեղադրությունում):\n* **Վերստեղծել հարաբերական ճանապարհը.** Աղբյուր ֆայլի ճանապարհը կվերստեղծվի նշանակված թղթապանակում՝ իր հարթությունում: Օրինակ՝ եթե տեղափոխեք ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` նշանակության թղթապանակ ``/Users/foobar/MyDestination``, ֆայլի վերջնական տեղավորությունը կլինի ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``:\n\nԱմեն դեպքում, dupeGuru լավ է հարթում անունների կոնֆլիկտը՝ նախապատրաստելով նշանակության ֆայլի անվան թիվը՝ եթե ֆայլը արդեն առկա է նշված տեղում:\n\n**Ընտրված հրամանը.** Այս կարգավորումը որոշում է հրամանը, որը կկանչվի \"Կանչել Ընտրված հրամանը\" գործողությամբ: Կարող եք կանչել ցանկացած արտաքին ծրագիր՝ այս գործողությամբ: Սա կարող է օգտակար լինել եթե օրինակ փոխարենը ունեք տվյալների փոխանցման լավ ծրագիր:\n\nՀրամանի տեսակը նույնն է, ինչ Դուք կգրեք Հրամանի տողում, բացառությամբ որտեղ կան 2 լրացումներ. **%d** և **%r**: Այս լրացումները կվերագրվեն ընտրված զոհի (%d) ճանապարհով և ընտրված զոհի հղման ֆայլով (%r):\n\nԵթե կատարելի ֆայլի ճանապարհը պարունակում է բացատներ, ապա պետք է փակեք այն \"\" չակերտներով: Նաև պետք է փակեք լրացումները չակերտներով, որովհետև շատ հնարավոր է, որ զոհի ճանապարհները և հղումները կպարունակեն բացատներ: Ահա ընտրված հրամանի օրինակ՝ ::\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n"
  },
  {
    "path": "help/hy/quick_start.rst",
    "content": "﻿Արագ Սկիզբ\n===========\n\nԱրագ սկսելու համար dupeGuru-ն, պարզապես կատարեք ստանդարտ ստուգում՝ օգտագործելով ծրագրային կարգավորումները:\n\n* Բացել dupeGuru-ն:\n* Ավելացնել թղթապանակներ՝ ստուգելու համար նաև վերցնել & գցելը կամ \"+\" կոճակը:\n* Սեղմեք **Ստուգել**:\n* Սպասեք, մինչ ստուգումը կավարտվի:\n* Նայեք ցանկացած կրկնօրինակին (Ֆայլեր, որոնք նշվել են) և ստուգվել, իրականում կրկնօրինակել խմբի հղմանը (Ֆայլը կրկնօրինակելուց առաջ չի նշվում և ընտրված չէ):\n* Եթե ֆայլը սխալ կրկնօրինակ է, ապա ընտրեք այն և սեղմեք **Գործողություններ-->Հեռացնել ընտրվածը Արդյունքներից**:\n* Եթե համոզված եք, որ կրկնօրինակը արդյունքներում կա, ապա սեղմեք **Խմբագրել-->Նշել բոլորը**, և ապա **Գործողություններ-->Ուղարկել Նշվածը Աղբարկղ**:\n\nՍա միայն բազային ստուգում է: Կան բազմաթիվ կարգավորումներ, որոնք հնարավորություն են տալիս նշելու տարբեր արդյունքներ և մի քանի եղանակներ արդյունքների փոփոխման: Մանրամասների համար կարդացեք Օգնության ֆայլը:\n"
  },
  {
    "path": "help/hy/reprioritize.rst",
    "content": "﻿Վերաառաջնայնության կրկնօրինակներ\n================================\n\ndupeGuru-ը փորձում է որոշել, թե որ կրկնօրինակները պետք է գնան յուրաքանչյուր խմբի դիրքում,\nբայց երբեմն սխալ է ստանում: Շատ դեպքերում, խելամիտ դասավորումը \"Դելտա նշանակության\"\nև \"Միայն սխալները\" ընտրանքների ավելացնելով \"Դարձնել ընտրվածը հղում\" գործողության խորամանկություն է, բայց\nերբեմն, պահանջվում են ավելի լավ ընտրանքներ: Ահա այստեղ է, որ  վերաառաջնայնավորման պատուհանը բացվում է:\nԿարող եք կանչել այն \"Վերաառաջնայնավորման արդյունքները\" կետից՝ \"Գործողություններ\" ընտրացանկից:\n\nԱյս պատուհանը հնարավորություն է տալիս Ձեզ ընտրելու չափանիշներ՝ հղման սխալին համապատասխան և կընտրվի\nյուրաքանչյուր սխալի խումբը: Հասանելի չափանիշների ցանկը ձախում է և Ձեր ընտրած չափանիշների ցանկը գտնվում է\nաջում:\n\nՉափանիշն դա բաժինն է, որը հետևում է փաստարկին: Օրինակ՝ \"Չափը (Բարձրագույն)\" նշանակում է, որ սխալը\nհետևում է մեծագույն չափի հաղթողին: \"Թղթապանակը (/foo/bar)\" նշանակում է, որ սխալները թղթապանակում կհաղթեն: Ավելացնելու համար\nփաստարկ ամենաաջ մասում, նախ ընտրեք բաժինը, ապա ընտրեք\nենթափաստարկ հետևյալ ցանկում և ապա սեղմեք կոճակի սլաքի աջ մասում:\n\nՑանկի կարգը աջից շատ կարևոր է (կարող եք վերակարգավորել ֆայլերը վերցնել և գցելու միջոցով): Երբ\nսխալի տեղորոշումը հղման դիրքում է, ապա օգտագործվում է առաջին փաստարկը: Եթե դա կապված է, ապա երկրորդ\nփաստարկն է օգտագործվում և այլն և այլն: Օրինակ, եթե Ձեր փաստարկները  \"Չափը (բարձրագույն)\" են և ապա\n\"Ֆայլի անունը (Չի ավարտվում թվով)\", ապա հղման ֆայլը, որը կընտրվի խմբում, ապա կլինի\nմեծագույն ֆայլը և եթե երկու կամ ավելի ֆայլեր ունեն նույն չափը, ապա մեկը ունի  ֆայլի անուն, որը\nչի ավարտվում թվով, կօգտագործվի: Երբ փաստարկի արդյունքը կապված է, կարգը, որի սխալները\nնախկինում էին, խումբը պետք է օգտագործվի:\n"
  },
  {
    "path": "help/hy/results.rst",
    "content": "﻿Արդյունքները\n=============\n\nԵրբ dupeGuru-ն ավարտի կրկնօրինակների ստուգումը, կցուցադրի արդյունքները կրկնօրինակ խմբերի ցանկում:\n\nԿրկնօրինակ խմբերի մասին\n-------------------------\n\nԿրկնօրինակման խումբը դա ֆայլերի խումբ է, որոնք բոլորը համընկնում են միմյանց: Ամեն խումբ ունի իր **հղվող ֆայլը** և մեկ կամ մի քանի **կրկնօրինակ ֆայլեր**: Հղվող ֆայլը դա խմբի առաջին ֆայլն է: Այն ընտրված չէ, նրանից ցածր և փոխարեն կրկնօրինակ ֆայլերի:\n\nԿարող եք նշել կրկնօրինակ ֆայլերը, բայց երբեք չեք կարող նշել հղվող ֆայլը խմբում: Սա երկրորդ պատճառն է՝ կանխելու dupeGuru-ին ջնջելու ոչ միայն կրկնօրինակ ֆայլերը, այլև դրանց հղումները: Համոզվա՞ծ եք, չէ, որ պետք չէ անել դա:\n\nԻնչն է որոշում, թե որ ֆայլերը հղմամբ են և որ ֆայլերը կրկնօրինակ են՝ թղթապանակի նախնական վիճակում: Ֆայլը հղվող թղթապանակից միշտ հղվում է կրկնօրինակի խմբում: Եթե բոլոր ֆայլերը նորմալ թղթապանակից են, ապա չափն է որոշում, թե որ ֆայլը կլինի կրկնօրինակ խմբի հղումը: dupeGuru-ն ընդունում է, որ Դուք կցանկանաք պահել մեծ ֆայլերը, ուստի դրանք կտեղադրվեն հղման խմբում:\n\nԿարող եք փոխել հղման ֆայլը խմբում ձեռադիր: Դա անելու համար ընտրեք կրկնօրինակ ֆայլը և սեղմեք **Գործողություններ-->Դարձնել ընտրվածը հղմամբ**:\n\nՆայել արդյունքները\n--------------------\n\nՉնայած պարզապես կարող եք սեղմել **Խմբագրել-->Նշել բոլորը** և ապա **Գործողություններ-->Ուղարկել նշվածը Աղբարկղ** արագորեն ջնջելու համար բոլոր կրկնօրինակ ֆայլերը արդյունքներից, միշտ խորհուրդ է տրվում նախ նայել կրկնօրինակները և հետո միայն ջնջել:\n\nՕգնելու համար Ձեզ նայելու արդյունքները, կարող եք օգտագործել **Մանրամասների վահանակը**: Այս վահանակը ցուցադրում է բոլոր մանրամասները ընտրված ընթացիկ ֆայլի, ինչպես նաև հղման մանրամասները: Սա շատ հարմար է արագորեն որոշելու, թե արդյոք կրկնօրինակը իրոքից կրկնօրինակ է, թե ոչ: Կարող եք նաև կրկնակի սեղմեք ֆայլի վրա՝ բացելու համար այն նրա հետ ասոցիացված ծրագրով:\n\nԵթե ունեք շատ սխալ կրկնօրինակներ, ապա ճիշտ կրկնօրինակները (Եթե Ձեր ֆիլտրի խստությունը շատ է ցածր) որոշելու լավագույն եղանակը դրանք նայելն է, ընտրեք ճիշտ կրկնօրինակները և ապա սեղմեք **Գործողություններ-->Ուղարկել նշվածները Աղբարկղ**: Եթե իսկական կրկնօրինակները ավելի շատ են, քան սխալները, ապա կարող եք օգտագործել **Գործողություններ-->Ջնջել նշվածները արդյունքներից**:\n\nՆշում և Ընտրում\n---------------------\n\n**նշվածը** կրկնօրինակ է՝ նշված նշանով, որը ընտրվում է: **ընտրվածը** կրկնօրինակ է, որը ընդգծվում է կամ առանձնացվում է: Ընտրելու բազմակի եղանակները կարող են կատարվել dupeGuru-ում ստանդարտ ճանապարհով (Shift/Command/Control սեղմամբ): կարող եք փոփոխել բոլոր ընտրված կրկնօրինակների վիճակը՝ սեղմելով **space**:\n\nՑուցադրել Միայն Սխալները\n-------------------------\n\nԵթե այս ընտրանքը միացված է, ապա կրկնօրինակները ցուցադրվում են առանց իրենց համապատասխան հղվող ֆայլի։ Կարող եք ընտրել, նծել կամ դասավորել այս ցանկը, ինչպես օոր նորմալ եղանակում։\n\ndupeGuru-ի արդյունքները, երբ այն նորմալ եղանակում է, դասավորվում են համաձայն կրկնօրինակվող խմբերի' **հղվող ֆայլի** ։ Սա նշանակում է, որ եթե Դուք ցանկանաք, օրինակի համար, նշել բոլոր կրկնօրինակները \"exe\" ընդլայնմամբ, ապա չեք կարող պարզապես դասավորել արդյունքները ըստ \"Տեսակի\"՝ ունենալու համար բոլոր exe կրկնօրինակները միասին, որովհետև խումբը կարող է կազմված լինի մեկից ավելի ֆայլերից։ Ահա այստեղ է, որ աշխատում է Միայն Սխալները եղանակը։ Նշելու համար բոլոր \"exe\" կրկնօրինակները, Դուք պարզապես պետք է՝\n\n* Միացնեք Միայն Սխալները եղանակը։\n* Ավելացնեք \"Տեսակը\" սյունը՝ \"Սյուներ\" ընտրացանկին։\n* Սեղմեք \"Տեսակը\" սյանը՝ դասավորելու համար ցանկը ըստ տեսակի։\n* Տեղադրել \"exe\" տեսակի առաջին կրկնօրինակը։\n* Ընտրեք այն։\n* Պտտեք ներքև ցանկում՝ տեղադրելու համար \"exe\" տեսակի վերջին կրկնօրինակը։\n* Սեղմած պահեք Shift-ը և սեղմեք նրա վրա։\n* Սեղմեք Space՝ նշելու համար բոլոր կրկնօրինակները։\n\nԴելտա նշանակությունները\n--------------------------\n\nԵթե միացնեք սա, որոշ սյուներ կցուցադրվեն նշանակություն՝ հարաբերական կրկնօրինակների հղմանը՝ բացարձակ նշանակությունների փոխարեն։ Այս դելտա նշանակությունները նաև կցուցադրվեն տարբեր գույներով, ուստի կարող եք դրանք հեշտությամբ տեսնել։ Օրինակ՝ եթե կրկնօրինակը 1.2 ՄԲ է և եթե նրա հղումը 1.4 ՄԲ է, ապա Չափը սյունում կցուցադրվի -0.2 ՄԲ։\n\nՄիայն Սխալները և Դելտա նշանակությունները\n------------------------------------------\n\nՄիայն Սխալները եղանակը բացում է իր իսկական ուժը, երբ Դուք օգտագործում եք այն Դելտա նշանակությունների հետ։ Երբ միացնեք այն, բացարձակ նշանակությունների փոխարեն կցուցադրվեն հարաբերական նշանակությունները։ Այսպիսով օրինակ, եթե ցանկանում եք արդյունքներից հեռացնել բոլոր կրկնօրինակները, որոնք 300 Կբ-ից մեծ են իրենց հղումներից, ապա չեք կարող դասավորել միայն սխալները արդյունքները ըստ չափի, այդ դեպքում ընտրեք բոլոր կրկնօրինակները, որոնք -300 են Չափը սյունից, ջնջեք դրանք և ապա արեք նույն գործողությունը նաև 300-ից բարձր կրկնօրինակների համար՝ ցանկի ներքևում։\n\nԿարող եք նաև օգտագործել սա՝ փոխելու համար կրկնօրինակման ցանկի հղման առաջնայնությունը։ Նոր ստուգումը կատարելուց հետո, եթե չլինեն հղվող թղթապանակներ, հղվող ֆայլը ամեն խմբի կլինի ամենամեծ ֆայլը։ Եթե ցանկանում եք փոխել այն, օրինակի համար, ըստ վերջին փոփոխման ժամանակի, կարող եք դասավորել միայն սխալները արդյունքները ըստ փոփոխման ժամանակի **նվազման** կարգով, ընտրեք բոլոր կրկնօրինակները, որոնց փոփոխման դելտա ժամանակը բարձր է 0-ից և սեղմեք **Դարձնել ընտրվածը հղում**։ Պատճառը,որ դասավորում եք ըստ նվազման կարգի այն է, որ եթե 2 ֆայլերից, որոնք կընտրվեն նույն կրկնօրինակ խմբում, երբ սեղմեք **Դարձնել ընտրվածը հղում**, ապա ցանկի միայն առաջինը կդառնա հղում, մյուսները կանտեսվեն։ Եվ մինչև Դուք ցանկանաք վերջին փոփոխված ֆայլը դարձնել փոփոխված՝ ունենալով դասավորման նվազման կարգը, ապա ցանկի առաջին ֆայլը կլինի վերջին փոփոխված ֆայլը։\n\n.. todo:: Add \"Non-numerical delta\" information.\n\nՖիլտրում\n---------\n\ndupeGuru-ն աջակցում է հետստուգման ֆիլտրում։ Սրանով Դուք կարող եք սեղմել ներքև արդյունքները, որպեսզի կարողանաք կատարեք գործողություններ դրա հետ։ Օրինակ՝ կարող եք հեշտությամբ նշել բոլոր կրկնօրինակերը նրանց անվան մեջ պարունակող \"պատճեն\" հատկությամբ՝ ֆիլտրի կողմից օգտագործված արդյունքներից։\n\n.. todo:: Qt has a toolbar search field now, not a menu item.\n\n**Windows.** Ֆիլտրելու հնարավորությունը օգտագործելու համար սեղմեք Գործողություններ --> Կիրառել ֆիլտրը, գրեք կիրառվող ֆիլտրը և սեղմեք Կիրառել։ Չֆիլտրված արդյունքներին վերադառնալու համար սեղմեք Գործողություններ --> Չեղարկել ֆիլտրը։\n\n**Mac OS X.** Ֆիլտրելու հնարավորությունը օգտագործելու համար նշեք Ձեր ֆիլտրը \"Ֆիլտր\" որոնման դաշտում գործիքաշերտի։ Չֆիլտրված արդյունքներին վերադառնալու համար սեղմեք դատարկ թողեք դաշտը կամ սեղմեք \"X\"։\n\nՊարզ եղանակում (ծրագրային եղանակն է), ինչ տեսակի ֆիլտր է տողում օգտագործվել փաստացի ֆիլտրման համար, խմբային նիշի բացառությամբ **\\***. Այսպիսով, եթե նշում եք \"[*]\" որպես ֆիլտր, այն կհամընկնի [] փակագծերի հետ, այնուհանդերձ կլինի այդ փակագծերի միջև։\n\nԼրացուցիչ ընդլայնված ֆիլտրման համար, կարող եք միացնել \"Ֆիլտրելիս օգտագործել կանոնավոր սահմանումները\"։ Ապա ֆիլտրման հնարավորությունը կօգտագործվի **կանոնավոր սահմանմամբ** ։ Կանոնավոր սահմանումը դա համապատասխանացման տեքստի լեզուն է։ Առավել մանրամասն կարող եք կարդալ `regular-expressions.info <http://www.regular-expressions.info>`_ կայքում։\n\nՀամապատասխանեցումները զգայուն չեն ո՛չ պարզ, ո՛չ էլ regexp եղանակում։\n\nՀամապատասխանեցման ֆիլտրի դեպքում, Ձեր կանոնավոր սահմանումը չի ունենա ամբողջական ֆայլի անունը, այն միայն կպարունակի սահմանմանը համապատասխան տողին։\n\nԿարող եք տեղեկացնել, որ ոչ բոլոր կրկնօրինակներն են ֆիլտրված արդյունքներում համապատասխանում ֆիլտրին։ Ահա թե ինչու ինչքան շուտ որ պարզ կրկնօրինակը խմբում համապատասխանի ֆիլտրին ամբողջ խումբը կմնա արդյունքներում, ուստի ավելի հեշտ կլինի նայելու կրկնօրինակների կազմը։ Այնուհանդերձ, չհամապատասխանող կրկնօրինակերը \"հղման եղանակում են\"։  Չնայած որ Դուք կարող եք կատարել գործողություններ, ինչպես օրինակ նշել բոլորը և համոզված լինեք, որ միայն նշված են ֆիլտրված կրկնօրինակերը։\n\nԳործողություններ Ընտրացանկը\n----------------------------\n\n* **Մաքրել անտեսման ցանկը.** Հեռացնում է Ձեր ավելացրած բոլոր անտեսված համընկնումները։ Դուք պետք է սկսեք նոր ստուգում, որպեսզի նոր մաքրված անտեսումների ցանկը էֆֆեկտիվ լինի։\n* **Արտածել արդյունքները XHTML-ով.** Վերցնում է ընթացիկ արդյունքները և ստեղծում XHTML ֆայլը։ Սյուննրը, որոնք տեսանելի են այս կոճակը սեղմելիս կլինեն նաև XHTML ֆայլում։ Ֆայլը միանգամից կբացվի հիմնական դիտարկիչում։\n* **Ուղարկել նշվածները Աղբարկղ.** Բոլոր նշված կրկնօրինակերը հեռացնում է Աղբարկղ։\n* **Ջնջել նշվածը և Վերագրել հղմամբ.** Բոլոր նշված կրկնօրինակերը հեռացնում է Աղբարկղ, բայց դա անելուց հետո ջնջված ֆայլերը վերագրվում են ըստ `հղման <http://en.wikipedia.org/wiki/Hard_link>`_ հղվող ֆայլում (Միայն OS X և Linux-ում)\n* **Տեղափոխել նշվածը՝...:** Հարցնում է Ձեզ թղթապանակի մասին և ապա տեղափոխում է բոլոր նշված ֆայլերը այդ թղթապանակ։ Աղբյուր ֆայլերի ճանապարհը կարող է վերստեղծվել նշանակության թղթապանակում՝ կախված \"Պատճենելու և Տեղափոխելու\" կարգավորումներից։\n* **Պատճենել նշվածը՝...:** Հարցնում է Ձեզ թղթապանակի մասին և ապա պատճենում է բոլոր նշված ֆայլերը այդ թղթապանակ։ Աղբյուր ֆայլերի ճանապարհը կարող է վերստեղծվել նշանակության թղթապանակում՝ կախված \"Պատճենելու և Տեղափոխելու\" կարգավորումներից։\n* **Հեռացնել նշվածները արդյունքներից.** Հեռացնում է բոլոր նշված կրկնօրինակները արդյունքներից։ Ակտուալ ֆայլերին դա չի վերաբերվի և դրանք կմնան։\n* **Հեռացնել ընտրվածները արդյունքներից.** Հեռացնում է բոլոր ընտրված կրկնօրինակները արդյունքներից։ Հիշեք, որ ընտրված բոլոր հղվող ֆայլերը կանտեսվեն,այս գործողությամբ կջնջվեն միայն կրկնօրինակերը։\n* **Դարձնել ընտրվածը հղում.** Առաջ է մղում բոլոր ընտրված կրկնօրինակները որպես հղումներ։ Եթե կրկնօրինակը խմբի մասն է, որը ունի հղման թղթապանակ (կապույտ գույնով), ապա ոչ մի գործողություն չի կատարվի դրա համար։ Իսկ եթե միևնույն խմբում կան մեկից ավելի ընտրված կրկնօրինակներ, ապա առաջ կմղվի ամեն խմբից միայն առաջինը։\n* **Ավելացնել ընտրվածը անտեսումների ցանկին.** Նախ բոլոր կրկնօրինակները հեռացվում են արդյունքների ցանկից, ապա ավելացվում է կրկնօրինակի համընկումը և ընթացիկ հղումը անտեսումների ցանկին։ Այս համընկնումը այլևս առաջ չի գա հետագա ստուգումների ժամանակ։ Կրկնօրինակը կարող է հետ բերվել, բայց այն կհամապատասխանի հղման այլ ֆայլի։ Կարող եք մաքրել անտեսումների ցանկը Մաքրել անտեսումների ցանկը հրամանով։\n* **Բացել ընտրվածը հիմական ծրագրով.** Բացում է ֆայլը իր հետ ասոցիացված ծրագրով։\n* **Ցուցադրել ընտրվածը որոնման մեջ.** Բացում է ֆայլը պարունակող թղթապանակը։\n* **Կանչել Ընտրված հրամանը.** Բացում է կարգավորումներոմ Ձեր կողմից նշված արտաքին ծրագիրը։\n* **Անվանափոխել ընտրվածը.** Ձեզ հարցում կկատարվի նոր անվան համար, ապա ընտրված ֆայլը կանվանափոխվի։\n\n.. todo:: Add Move and iPhoto/iTunes warning\n.. todo:: Add \"Deletion Options\" section.\n"
  },
  {
    "path": "help/ru/faq.rst",
    "content": "﻿Часто задаваемые вопросы\n==========================\n\n.. topic:: Что такое dupeGuru?\n\n    .. only:: edition_se\n\n        dupeGuru это инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или контента.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.\n\n    .. only:: edition_me\n\n        dupeGuru Music Edition представляет собой инструмент для поиска дублирующихся песен в вашей музыкальной коллекции. Он может строить свою сканирование файлов, тегам или содержания.Имя файла и тэг проверяет функция нечеткого соответствия алгоритм, который может находить дубликаты файлов или теги, даже если они не совсем то же самое.\n\n    .. only:: edition_pe\n\n        dupeGuru Picture Edition (PE для краткости) представляет собой инструмент для поиска дубликатов фотографий на вашем компьютере. Не только он может найти точные соответствия, но он также может найти дубликаты среди фотографий разного рода (PNG, JPG, GIF и т.д..) И качество.\n\n.. topic:: Что делает его лучше, чем другие сканеры дублировать?\n\n    Сканирования является чрезвычайно гибкой. Вы можете настроить его, чтобы действительно получить, каких результатов вы хотите. Вы можете прочитать больше о опция настройки dupeGuru в :doc:`Настройки <preferences>`.\n\n.. topic:: Насколько безопасно использовать dupeGuru?\n\n    Очень безопасной. dupeGuru был разработан, чтобы убедиться, что вы не удаляете файлы, которые вы не хотели удалить. Во-первых, существует система отсчета папку, которая позволяет определить папки, в которых вы абсолютно не хотите dupeGuru, чтобы вы удаляете файлы там, и тогда есть система контрольной группы, что гарантирует, что вы всегда держать по крайней мере один член группы дубликатов.\n\n.. topic:: Каковы ограничения демо dupeGuru?\n\n    В демо-режиме, вы можете только выполнять действия над 10 дубликаты сразу. в\n    `Fairware <http://open.hardcoded.net/about/>`_ режиме, однако, Есть никаких ограничений.\n\n.. topic:: Знак коробку файл я хочу удалить отключена. Что я должен сделать?\n\n    Вы не можете пометить ссылки (первый файл) дубликат группы. Однако то, что вы можете сделать, заключается в содействии дублировать файл справки. Таким образом, если файл, который Вы хотите, чтобы отметить это ссылки, выделите дубликатов файлов в группу, которую вы хотите продвигать на ссылку, и нажмите на кнопку **Действия -> Добавить выбранной ссылки** . Если ссылка файл из папки ссылки (имя файла написаны на синими буквами), вы не можете удалить его из исходного положения.\n\n.. topic:: У меня есть папка, из которой я действительно не хочу, чтобы удалить файлы.\n\n    IЕсли вы хотите быть уверены, что dupeGuru никогда не будет удалять файл из определенной папки, убедитесь, что установили в состояние **Ссылка** на: документ: `папки`.\n\n.. topic:: Что это за '(X отбрасывается) \"уведомление в строке состояния?\n\n    В некоторых случаях, несколько матчей не включены в окончательные результаты по соображениям безопасности. Позвольте мне привести пример. У нас есть 3 файла: A, B и C. Мы сканируем их с помощью фильтра низких твердости.Сканер определяет, что матчи с B, матчи с С, но делает B ** не ** матч с С. При этом, dupeGuru имеет вид проблемы. Она не может создать дубликат группы А, В и С в это, потому что не все файлы в группе будет соответствовать вместе. Это может создать 2 группы: одна группа AB, а затем одна группа AC, но это не будет, по соображениям безопасности. Давайте думать об этом: если Б не совпадает с С, она, вероятно, означает, что либо B, C или оба на самом деле не дубликаты. Если не было бы 2 группы (АВ и АС), вы бы в конечном итоге удалить оба B и C. И если один из них не дублировать, что на самом деле не то, что вы хотите делать, правильно? Так что dupeGuru делает в таком случае является, чтобы отменить матч AC (и добавляет уведомление в строке состояния). Таким образом, если вы удалите B и повторно запустить сканирование, вам придется соответствовать переменного тока в следующий результат.\n\n.. topic:: Я хочу, чтобы отметить все файлы из определенной папки. Что я могу сделать?\n\n    Включить: документ: `обманутые Только <результаты>` режим и нажать на папку колонки для сортировки дубликатов по папкам. Затем он будет легким для вас, чтобы выбрать все дубликаты из той же папке, а затем нажать клавишу пробел, чтобы отметить все выбранные дубликатов.\n\n.. only:: edition_se or edition_pe\n\n    .. topic:: Я хочу, чтобы удалить все файлы, которые более 300 Кб от их ссылке на файл. Что я могу сделать?\n\n        * Включить :doc:`Только обманутые <results>` режим.\n        * Включить **Значения Делта** режим.\n        * Нажмите на \"размер\" столбца для сортировки результатов по размеру.\n        * Выбрать все дубликаты ниже -300.\n        * Нажмите на **Удалить выбранные из результатов**.\n        * Выбрать все дубликаты более 300.\n        * Нажмите на **Удалить выбранные из результатов**.\n\n    .. topic:: Я хочу, чтобы мои последние измененные файлы файлы справки. Что я могу сделать?\n\n        * Включить: документ: `обманутые Только <результаты>` режим.\n        * Включить **Значения делта**  режим.\n\t* Нажмите на колонку \"Модификация\" для сортировки результатов по дате изменения.\n        * Нажмите на колонку \"Модификация\" снова изменить порядок сортировки.\n        * Выберите все дубликаты на 0.\n        * Нажмите на **Сделать выбранной ссылки**.\n\n    .. topic:: Я хочу, чтобы отметить все дубликаты, содержащие слово \"копия\". Как мне это сделать?\n\n        * **Windows**: Нажмите на **Действия -> Применить фильтр**, затем введите \"копия\", нажмите кнопку ОК.\n        * **Mac OS X**: тип \"копия\" в \"Фильтр\" поле на панели инструментов.\n        * Нажмите на **Отметить  -> Отметить все**.\n\n.. only:: edition_me\n\n    .. topic:: Я хочу, чтобы удалить все песни, которые более чем на 3 секунды от своей ссылке на файл. Что я могу сделать?\n\n         * Включить: документ: `обманутые Только <результаты>` режим.\n         * Включить **Значения делта** режим.\n         * Нажмите на \"Время\" колонку для сортировки результатов по времени.\n         * Выберите все дубликаты ниже -00:03.\n         * Нажмите на **Удалить выбранные из результатов**.\n         * Выберите все дубликаты на 00:03.\n         * Нажмите на **Удалить выбранные из результатов**.\n\n    .. topic:: Я хочу, чтобы мой высокий битрейт файлов песни ссылки. Что я могу сделать?\n\n         * Включить: документ: `обманутые Только <результаты>` режиме\n         * Включить **Значения делта** режим.\n         * Нажмите на кнопку \"Битрейт\" колонку для сортировки результатов по битрейт.\n         * Нажмите на кнопку \"Битрейт\" колонна снова изменить порядок сортировки.\n         * Выберите все дубликаты на 0.\n         * Нажмите на **Сделать выбранной ссылки**.\n\n    .. topic:: Я не хочу [жить] и [ремикс] версии моих песен считаться дубликатами. Как мне это сделать?\n\n        Если ваше сравнение порог достаточно низким, вы, вероятно, в конечном итоге с живой и ремикс версии ваших песен в своих результатах. Там вы ничего не можете сделать, чтобы предотвратить это, но есть кое-что можно сделать, чтобы легко удалить их со своего результаты после сканирования: после сканирования, фильтрации. Если, например, вы хотите удалить все песни с чем-либо в квадратных скобках []:\n\n         * **Windows**: Нажмите на **Действия -> Применить фильтр**, а затем введите \"[*]\", нажмите кнопку ОК.\n         * **Mac OS X**: Тип \"[*]\" в \"Фильтр\" поле на панели инструментов.\n         * Нажмите на Отметить **-> Отметить все**.\n         * Нажмите на **Действия -> Удалить выбранные из результатов**.\n\n.. topic:: Я пытался отправить свои дубликаты в корзину, но dupeGuru говорит мне, он не может это сделать. Почему? Что я могу сделать?\n\n    Большую часть времени, поэтому dupeGuru не можете отправлять файлы в корзину из-за права доступа к файлам. Вы должны написать * * разрешения на файлы, которые вы хотите отправить в корзину. Если вы не знакомы с командной строкой, вы можете использовать утилиты, такие как `BatChmod <http://macchampion.com/arbysoft/BatchMod>` _ исправить Ваши права.\n\n     Если dupeGuru еще дает вам неприятности после фиксации ваших прав, было несколько случаев, когда с помощью \"Перемещение Помечено к ...\" в качестве обходного пути сделали свое дело. Таким образом, вместо отправки файлов в корзину, вы посылаете их во временную папку с \"Переместить Отмеченные к ...\" действия, а затем вы удалите эту временную папку вручную.\n\n    .. only:: edition_pe\n\n        Если вы пытаетесь удалить *Iphoto* фотографии, то причина сбоя иная.Удаление не выполняется, так dupeGuru не может общаться с Iphoto. Учтите, что для удаления корректной работы, вы не должны играть вокруг Iphoto в то время как dupeGuru работает. Кроме того, иногда, система Applescript, кажется, не знают, где найти Iphoto запустить его. Это может помочь в таких случаях для запуска Iphoto *до* вы посылаете дубликатов в корзину.\n\n    Если все это не так, `контакт с поддержки HS <http://www.hardcoded.net/support>`_, мы поможем Вас.\n\n.. todo:: This FAQ qestion is outdated, see english version.\n"
  },
  {
    "path": "help/ru/folders.rst",
    "content": "﻿Выбор папки\r\n================\r\n\r\nПервое окно, вы видите, когда вы запускаете dupeGuru это окно выбора папки. Это окно содержит список папок, которые будут сканироваться при нажатии на **Scan**.\r\n\r\nЭто окно довольно проста в использовании. Если вы хотите добавить папку, нажмите на кнопку **+**. Если вы добавили папки прежде, всплывающее меню со списком последних папки добавил появится. Вы можете нажать на одну из них, чтобы добавить его прямо в свой список. Если нажать на первый пункт всплывающего меню, **Добавить новую папку ...**, вам будет предложено ввести папку добавить. Если вы никогда не добавляется папка, не появится меню, и вы будете непосредственно будет предложено ввести новую папку добавить.\r\n\r\nАльтернативный способ для добавления папок в список, чтобы перетащить их в списке.\r\n\r\nЧтобы удалить папку, выберите папку, удалить, и нажмите на **-**. Если папке выбирается при нажатии кнопки, выбранной папки будет установлен в ** ** исключены состояния (см. ниже), а не удален.\r\n\r\nПапка государств\r\n----------------\r\n\r\nКаждая папка может находиться в одном из этих 3-х государств:\r\n\r\n* **Нормальный:** дубликаты найдены в эту папку можно удалить.\r\n* **Справка:** Дубликаты найти в этой папке не может **быть удалены** . Файлы из этой папки можно только в конечном итоге в **ссылка** позиция в группе обмануть. Если более чем один файл из папки ссылку в конечном итоге в той же группе обмануть, только один, будут сохранены.Другие будут удалены из группы.\r\n* **Не включено:** Файлы в этом каталоге не будет включен в проверку.\r\n\r\nСостояние по умолчанию к папке, конечно, **Нормальный**. Вы можете использовать **Ссылка**  состояние для папки, если вы хотите быть уверены, что вы не будете удалять любые файлы из него.\r\n\r\nКогда вы устанавливаете состояние каталог, все подпапки этой папки автоматически наследует это состояние, если явно не включенное состояние подпапку в.\r\n\r\n.. todo:: Add iPhoto/Aperture/iTunes libraries notes\r\n"
  },
  {
    "path": "help/ru/index.rst",
    "content": "﻿dupeGuru help\n===============\n\nЭтот документ также доступна на `французском <http://dupeguru.voltaicideas.net/help/fr/>`__, `немецком <http://dupeguru.voltaicideas.net/help/de/>`__ и `армянский <http://dupeguru.voltaicideas.net/help/hy/>`__.\n\n.. only:: edition_se or edition_me\n\n    dupeGuru есть инструмент для поиска дубликатов файлов на вашем компьютере. Он может сканировать либо имен файлов или содержимого.Имя файла функций сканирования нечеткого соответствия алгоритма, который позволяет найти одинаковые имена файлов, даже если они не совсем то же самое.\n\n.. only:: edition_pe\n\n    dupeGuru Picture Edition (PE для краткости) представляет собой инструмент для поиска дубликатов фотографий на вашем компьютере. Не только он может найти точные соответствия, но он также может найти дубликаты среди фотографий разного рода (PNG, JPG, GIF и т.д..) И качество.\n\nХотя dupeGuru может быть легко использована без документации, чтение этого файла поможет вам освоить его. Если вы ищете руководство для вашей первой дублировать сканирования, вы можете взглянуть на раздел :doc:`Быстрый <quick_start>` Начало.\n\nЭто хорошая идея, чтобы сохранить dupeGuru обновлен. Вы можете скачать последнюю версию на своей http://dupeguru.voltaicideas.net.\nСодержание:\n\n.. toctree::\n    :maxdepth: 2\n\n    quick_start\n    folders\n    preferences\n    results\n    reprioritize\n    faq\n    changelog\n"
  },
  {
    "path": "help/ru/preferences.rst",
    "content": "﻿Предпочтения\n=============\n\n.. only:: edition_se\n\n    **Тип сканирования:** Этот параметр определяет, какой аспект файлы будут сравниваться в дубликат сканирования. Если выбрать **Имя файла**, dupeGuru будем сравнивать каждое имена файлов слово за слово, и, в зависимости от других параметров ниже, он будет определять, достаточно ли слов соответствие рассмотреть 2 файлов дубликатов. Если выбрать **Содержимое**, только файлы с точно такой же контент будет матч.\n\n    **Папки** типа сканирования немного особенным. Когда вы выбираете его, dupeGuru проведет поиск дубликатов *папки* вместо того, чтобы дубликатов файлов. Для определения того, две папки, дублируют друг друга, все файлы, содержащиеся в папках будут проверяться, и если содержание **все** файлы в матче папки, папки будут считаться дубликатами.\n\n    **Фильтра Твердость:** Если вы выбрали **Имя файла** типа сканирования, эта опция определяет, как похожи два имени должно быть для dupeGuru рассматривать их дубликатов. Если фильтр твердости, например 80, то это означает, что 80% слов из двух имен файлов должны совпадать. Для определения соответствия процент, dupeGuru первой подсчитывает общее количество слов в  **обоих** файла, то подсчитать количество слов соответствия (каждое слово соответствия считаются 2), а затем разделите количество слов соответствия на общее число слов. Если результат больше или равно фильтр твердость, у нас есть дубликаты матча. Например, \"ABCD\" и \"CDE\" имеют соответствующий процент 57 (4 слова соответствия, 7 всего слов).\n\n.. only:: edition_me\n\n    **Тип сканирования:** Этот параметр определяет, какой аспект файлы будут сравниваться в дубликат сканирования.Характер дублировать сканирования варьируется в зависимости от того, что вы выбираете для этой опции.\n\n     * **Имя файла:** Каждая песня будет иметь свой файл разбит на слова, а затем каждое слово будет по сравнению с вычислить соответствующие проценты. Если этот процент выше или равна **жесткость фильтра** (см. ниже подробнее), dupeGuru рассмотрит 2 песни дубликатов.\n     * **Имя файла - Поля: **Как**Имя файла**, за исключением того, что как только имя файла были разделены на слова, эти слова затем группируются в поля.Разделитель полей \"-\".Окончательный процент соответствия будет самым низким соответствующий процент среди полей. Таким образом, \"Исполнитель - Название\" и \"Артист - Другие Название\" будет иметь соответствующий процент 50 (С **Имя файла** сканирования, это будет 75).\n     * **Имя файла - Поля (нет приказа):**Как**Имя файла-Поля**, кроме того, что порядок полей не имеет значения. Например, \"Исполнитель - Название\" и \"Название - Артист\" будет иметь соответствующий процент из 100 вместо 0.\n     * **Теги:** Этот метод считывает метки (метаданные) каждой песни и сравнить их полям. Этот метод, как Супер **- Поля**, считает низкий соответствующее поле в качестве окончательного соответствующий процент.\n     * **Состав:** Этот метод сканирования использовать фактическое содержание песни, чтобы определить, какие являются дубликатами. За 2 песни в соответствии с этим методом, они должны иметь точно **такой же содержания**.\n     * **Аудио контента:** То же содержание, но только в аудио-контент сравнивается (без метаданных).\n\n    **Фильтра Твердость:** Если вы выбрали имя файла или тегами типа сканирования, эта опция определяет, как похожи два имени / теги должны быть для dupeGuru рассматривать их дубликатов. Если фильтр твердости, например 80, то это означает, что 80% слов из двух имен файлов должны совпадать. Для определения соответствия процент, dupeGuru первой подсчитывает общее количество слов в **обоих** файла, то подсчитать количество слов соответствия (каждое слово соответствия считаются 2), а затем разделите количество слов соответствия на общее число слов. Если результат больше или равно фильтр твердость, у нас есть дубликаты матча. Например, \"ABCD\" и \"CDE\" имеют соответствующий процент 57 (4 слова соответствия, 7 всего слов).\n\n    **Теги для сканирования:** При использовании **Тега** Слова типа сканирования, вы можете выбрать теги, которые будут использоваться для сравнения.\n\n.. only:: edition_se or edition_me\n\n    **Слово взвешивания:** Если вы выбрали **Имя файла**  типа сканирования, этот вариант несколько изменений, как соответствующий процент рассчитывается. При слове взвешивания, вместо того, значение 1 в дубликат счета и общее количество слов, каждое слово имеет значение, равное количество символов, которые они имеют. При слове взвешивание, \"AB CDE FGHI\" и \"AB CDE fghij\" будет иметь соответствующий процент 53% (19 Персонажей, 10 символов, соответствующая (4 для \"б\" и 6 \"CDE\")).\n\n    **Совпадения похожих слов:** Если вы включите эту опцию, подобные слова будут засчитаны как спички. Например, \"Белая полоса\" будет совпадать% из 100 вместо 66 с, что функция включена. **Внимание:** Используйте эту опцию с осторожностью. Вполне вероятно, что вы получите много ложных срабатываний в результатах при его включении. Тем не менее, это поможет вам найти дубликаты, что вы не нашли бы в противном случае.Процесс сканирования также значительно медленнее, эта опция включена.\n\n.. only:: edition_pe\n\n    **Тип сканирования:** Этот параметр определяет тип сканирования, которые будут сделаны на ваши картины.**Сканирования** Содержание типа сравнивает фактическое содержание фотографий нечеткие пути (что делает его можно найти не только точными копиями, но и подобные).**EXIF Timestamp** тип сканирования смотрит на метаданные EXIF с фото (если он существует) и соответствует фотографии, которые имеют такой же. Это намного быстрее, чем сканирование содержимого. **Внимание:** Измененные фотографии часто держат же метка EXIF, так что следите за ложных срабатываний, когда вы используете, что тип сканирования.\n\n    **Фильтра Твердость:** *Содержание тип сканирования только.* Чем больше этот параметр, \"тяжелее\" является фильтром (Другими словами, тем меньше результатов Вы получите). Большинство фотографий одного и того же матча качества на 100%, даже если формат отличается (PNG и JPG, например.). Однако, если вы хотите, чтобы соответствовать PNG с более низким качеством JPG, вам придется установить фильтром твердость ниже, чем 100.По умолчанию, 95, это сладкое место.\n\n    **Совпадения рисунки разных размеров:** Если вы установите этот флажок, фотографии разных размеров будет разрешен в том же дубликат группы.\n\n**Можно смешивать файл вида:** Если вы установите этот флажок, дублировать группам разрешается есть файлы с различными расширениями. Если вы не проверить его, ну, они не являются!\n\n**Игнорировать дубликаты hardlinking в тот же файл:** Если эта опция включена, dupeGuru проверит дубликаты, чтобы увидеть если они ссылаются на тот же индексный `дескриптор <http://en.wikipedia.org/wiki/Inode>`_. Если они это сделают, они не будут считаться дубликатами. (Только для OS X и Linux)\n\n**Использование регулярных выражений при фильтрации:** Если вы отметите этот флажок, фильтрация будет рассматривать ваш запрос фильтра, как  **регулярное выражение**. Объясняя их выходит за рамки этого документа.Хорошее место для начала обучения он `regular-expressions.info <http://www.regular-expressions.info>`_.\n\n**Удаление пустых папок после удаления или перемещения:** Когда эта опция включена, папки будут удалены через файл удален или перемещен и папка пуста.\n\n**Копирование и перемещение:** Определяет, как операции копирования и перемещения (в меню Действия) будет себя вести.\n\n* **Право на назначение:** Все файлы будут отправлены непосредственно в пункт назначения, не пытаясь воссоздать исходный путь вообще.\n* **Повторно относительный путь:** путь исходный файл будет воссоздан в папке назначения, вплоть до корневого выделение в панели директорей. Например, если вы добавили ``/Users/foobar/SomeFolder`` на панель Каталоги и перемещении ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до места назначения ``/Users/foobar/MyDestination``, конечным пунктом назначения для файла будет ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` были сокращены с пути источника в конечный пункт назначения.).\n* **Повторно абсолютный путь:** путь исходный файл будет воссоздан в папке назначения в полном комплекте. Например, если вы перемещаете ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до места назначения ``/Users/foobar/MyDestination``, конечным пунктом назначения для файла будет ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``.\n\nВо всех случаях, dupeGuru красиво ручки конфликтов имен путем добавления номера назначения имя файла, если имя файла уже существует в месте назначения.\n\n**Специальной команды:** Это предпочтение определяет команду, которая будет вызываться \"Вызов специальной команды\" действия. Вы можете ссылаться ни на какие внешние приложения через это действие. Это может быть полезно, если, например, у вас есть хорошее приложение сравниваете установлены.\n\nФормат команды такой же, как то, что вы должны написать в командной строке, за исключением того, что Есть 2 заполнителей: **%d** and **%r**. Эти заполнители будут заменены на путь выбран обманут (%d) и путь к ссылке на файл выбранного обмануть (%r).\n\nЕсли путь к исполняемому содержит пробелы, необходимо заключить его в \"\" кавычки. Вы также должны приложить заполнителей в кавычки, потому что это очень возможно, что путь к обманутых и ссылки будут содержать пробелы. Вот пример пользовательской команды:\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n"
  },
  {
    "path": "help/ru/quick_start.rst",
    "content": "﻿Быстрый старт\r\n=============\r\n\r\nЧтобы вы быстро начали с dupeGuru, давайте просто делать сканирование с помощью стандартных настроек по умолчанию.\r\n\r\n* Запуск dupeGuru.\r\n* Добавление папок для сканирования либо перетащить & капли или кнопку \"+\".\r\n* Нажмите на **сканирование**.\r\n* Подождите, пока процесс сканирования завершен.\r\n* Посмотрите на каждый дубликат (файлы, которые отступом) и убедитесь, что это действительно дубликат ссылкой группы (файл выше дублировать без отступа и инвалидов окна знак).\r\n* Если файл ложных дубликатов, выделите ее и нажмите **Действия -> Удалить выбранные из результатов**.\r\n* Если вы уверены, что нет ложных дубликатов в результатах, нажмите на **Изменить -> Отметить Все**, а затем **Действия -> Отправить Помечено в Корзину**.\r\n\r\nЭто только основные сканирования. Есть много настройки вы можете сделать, чтобы получить разные результаты и несколько методов изучения и изменения ваших результатов. Чтобы узнать о них, только что прочитал остальную часть этого файла справки.\n"
  },
  {
    "path": "help/ru/reprioritize.rst",
    "content": "﻿Повторное приоритетов дубликатов\r\n================================\r\n\r\ndupeGuru пытается автоматически определить, какие дубликат должен отправиться в ссылку каждой группы\r\nпозиции, но иногда это делается неправильно. Во многих случаях, умный обмануть сортировки с \"Ценности Дельта\"\r\nи \"обманутые Только\" варианты в дополнение к \"Сделать выбранной ссылки\" действие делает трюк, но\r\nиногда, более мощный вариант не требуется. Здесь изменения приоритетов в диалог вступает в\r\nиграть. Вы можете вызвать его через \"изменить приоритеты Результаты\" пункт в меню \"Действия\".\r\n\r\nЭтот диалог позволяет вам выбрать критерии, по которым ссылка обмануть будут отобраны в\r\nкаждой группе обмануть.Список доступных критериев слева и перечень критериев вы\r\nВыбранная справа.\r\n\r\nКритериев категории следуют аргумент. Например, \"Размер (Высший)\" означает, что обмануть\r\nс крупным размером победит. \"Свойства папки (/ Foo / Bar)\" означает, что обманутые в этой папке будет победить. Для добавления\r\nкритерий правом списке, сначала выберите категорию в выпадающем списке, затем выберите\r\nsubargument в приведенном ниже списке, а затем нажмите на правую стрелку кнопки.\r\n\r\nПорядок списка справа важно (вы можете изменить порядок элементов через перетащить и отпустить). когда\r\nсбор обмануть для справки позицию, первый критерий используется. Если есть галстук, второй\r\nкритерий используется и так далее и так далее. Например, если ваши аргументы \"Размер (высший)\", а затем\r\n\"Имя файла (Не оканчивается на номер)\", ссылке на файл, который будет выбран в группе будет\r\nкрупнейших файл, а если два или несколько файлов имеют одинаковый размер, который имеет имя файла с\r\nне заканчивается номер будет использоваться. Когда все критерии привести к связи, порядок, в котором обманутые\r\nранее были в группе будет использоваться.\n"
  },
  {
    "path": "help/ru/results.rst",
    "content": "﻿Результаты\r\n==========\r\n\r\nКогда dupeGuru завершения сканирования на наличие дубликатов, он покажет его результаты в виде дубликата список группы.\r\n\r\nО дубликат группы\r\n----------------------\r\n\r\nДубликат группа представляет собой группу файлов, которые весь матч вместе. Каждая группа имеет **ссылке**  на файл и одного или более **одинаковых файлов**. Ссылки файл первый файл группы. Его марка окно отключено. Под ним, и с отступом, которые дубликатов файлов.\r\n\r\nВы можете отметить дубликатов файлов, но вы никогда не можете пометить ссылки файл группы. Это мера безопасности, чтобы предотвратить dupeGuru от удаления не только повторяющиеся файлы, но их ссылки. Ты уверен, что не хочу этого, не так ли?\r\n\r\nЧто определяет, какие файлы ссылки и какие файлы являются дубликатами сначала свою папку государства. Файл с ссылкой папка всегда будет ссылка в дубликат группы. Если все файлы из обычной папки, размер определить, какой файл будет ведения дубликат группы. dupeGuru предполагает, что вы всегда хотите сохранить крупнейших файл, так что крупных файлов займет исходное положение.\r\n\r\nВы можете изменить ссылку файл группы вручную. Для этого выберите дубликат файла, который вы хотите продвигать на ссылку, и нажмите на кнопку **Действия -> Добавить выбранной ссылки**.\r\n\r\nПросмотр результатов\r\n--------------------\r\n\r\nХотя вы можете просто нажать на **Правка -> Выделить все, а затем** **Действия -> Отправить Помечено в Корзину** быстро удалить все дубликаты файлов в результатах, всегда рекомендуется пересмотреть все дубликаты перед удаляя их.\r\n\r\nЧтобы помочь вам обзор результатов, вы можете вызвать панель **Подробнее**. Эта панель показывает все детали выбранного файла, а также подробности своей ссылки в. Это очень удобно, чтобы быстро определить, если дубликат действительно дубликат. Вы также можете дважды щелкнуть по файлу, чтобы открыть его и связанные с ним приложения.\r\n\r\nЕсли у вас есть больше ложных дубликатов, чем правда дубликатов (Если Ваш фильтр жесткость очень низкая), лучший способ продолжить бы пересмотреть дубликатов, знак истинного дубликаты и нажмите **Действия -> Отправить Помечено в Корзину** . Если у вас есть более верно, чем ложных дубликатов дубликатов, вместо этого можно пометить все файлы, которые являются ложными дубликатов, а также использовать **Действия -> Удалить Помеченные от результатов**.\r\n\r\nМаркировка и выбор\r\n---------------------\r\n\r\n**Отмеченные** дубликат двух экземплярах с небольшой флажок рядом с ним, имеющие галочки. **Выбран дубликат дубликата** быть выделены. Несколько действий, выбор может быть выполнена в dupeGuru стандартным образом (Shift / Command / Control клик). Вы можете переключать знак состояние всех выбранных дубликаты \", нажав **пространстве**.\r\n\r\nПоказать только обманутые\r\n-------------------------\r\n\r\nКогда этот режим включен, дубликаты отображаются без их соответствующего файла справки. Вы можете выбрать, марка и сортировать этот список, как и в обычном режиме.\r\n\r\nDupeGuru результаты, когда в нормальном режиме, сортируются в соответствии с дубликат группы '**ссылке на файл**. Это означает, что если вы хотите, например, чтобы отметить все дубликаты \"EXE\" расширением, вы не можете просто сортировать результаты по \"Вид\", чтобы иметь все EXE дубликатов вместе, потому что группа может состоять из более чем одного типа файлов . Вот где обманутые Только режим вступает в игру. Чтобы отметить все ваши \"EXE\" дубликаты, вы просто должны:\r\n\r\n* Включить обманутые Только режим.\r\n* Добавить \"Вид\" колонку \"Столбцы\" меню.\r\n* Нажмите на том, что \"Вид\" колонки, чтобы отсортировать список по типу.\r\n* Найдите первый дубликат с \"EXE\" рода.\r\n* Выберите его.\r\n* Прокрутите список, чтобы найти последнего дубликата с \"EXE\" рода.\r\n* Удерживайте Shift и щелкните по нему.\r\n* Нажмите Space, чтобы пометить все выбранные дубликатов.\r\n\r\nДельта значения\r\n---------------\r\n\r\nЕсли включить этот переключатель на некоторые столбцы будут отображать значение по отношению к дубликата ссылке, а не абсолютные значения. Эти дельты значения также будут отображаться в разные цвета, чтобы вы могли заметить их легко. Например, если дубликат 1,2 Мб и свою ссылку в 1,4 Мб, размер столбец отображает -0,2 Мб.\r\n\r\nТолько обманутые и Дельта значения\r\n----------------------------------\r\n\r\nТолько обманутые режиме раскрыть свою истинную силу, когда вы используете его с Делта Значения переключатель включен. Когда вы включите его, относительные значения будет отображаться вместо абсолютных. Так что если, например, вы хотите удалить из результатов все дубликаты, которые являются более 300 Кб от их ссылке, вы можете отсортировать дубликаты только результаты по размеру, выберите все дубликаты при -300 в столбце Размер, удалять их, , а затем сделать то же самое повторяет более 300 в нижней части списка.\r\n\r\nВы можете также использовать его для изменения ссылки приоритет повторяющиеся список. Когда вы делаете свежие сканирования, если Есть нет ссылки папки, ссылке на файл каждой группы является самой большой файл. Если вы хотите изменить, что, например, в последней модификации время, вы можете отсортировать дубликаты только результаты по времени модификации в **убывания** порядке выберите все дубликаты со временем изменения дельты значение больше 0 и нажмите **Убедитесь, выбранной ссылки**. Причина, почему вы должны сделать порядок сортировки по убыванию, потому что если 2 файла среди таких же дубликат группы выбираются при нажатии на **Сделать выбранной ссылки**, только первый из списка будут сделаны ссылки, другие будут проигнорированы . И так как вы хотите Последнее изменение файла для ссылки, имеющие порядок сортировки по убыванию уверяет вас, что первым пунктом в списке будет последнего изменения.\r\n\r\n.. todo:: Add \"Non-numerical delta\" information.\r\n\r\nФильтрация\r\n----------\r\n\r\ndupeGuru поддерживает после сканирования, фильтрации. С его помощью вы можете сузить результаты, чтобы вы могли выполнять действия, на подмножества. Например, вы можете легко пометить все дубликаты с их имя файла, содержащего \"копировать\" из результатов с помощью фильтра.\r\n\r\n.. todo:: Qt has a toolbar search field now, not a menu item.\r\n\r\n**Windows:** Для использования функции фильтрации, нажмите на Действия -> Применить фильтр, запишите фильтр, который вы хотите применить и нажмите ОК. Чтобы вернуться к нефильтрованное результаты, нажмите на Действия -> Отменить фильтр.\r\n\r\n**Mac OS X:** Для использования функции фильтрации, тип фильтра в \"Фильтр\" поле поиска на панели инструментов. Чтобы вернуться к нефильтрованное результате, очистите поле, или нажмите на кнопку \"X\".\r\n\r\nВ простом режиме (режим по умолчанию), что вы вводите в качестве фильтра строку, используемую для выполнения фактической фильтрации, за исключением одной маски: **\\***. Таким образом, если вы введете \"[*]\" как ваш фильтр, он будет соответствовать что-нибудь с [] скобках в нем, все, что между этими скобками.\r\n\r\nДля более продвинутых фильтров, вы можете включить «Использование регулярных выражений при фильтрации\" на. Функция фильтрации будет использовать регулярные выражения. Регулярное выражение языка для согласования текста. Объясняя их выходит за рамки этого документа. Хорошее место для начала обучения он `regular_expressions.info` <http://www.regular-expressions.info>_.\r\n\r\nМатчи не чувствительны к регистру, в простых и регулярных выражений режиме.\r\n\r\nДля фильтра, чтобы соответствовать, регулярное выражение не обязательно должно совпадать целый файл, он просто обязан содержать в цепочку, соответствующую выражению.\r\n\r\nВы могли заметить, что не все дубликаты в результате будут соответствовать вашим фильтром. Это потому, что как только одна копия в матчах группового фильтра, то вся группа останется в результатах, таким образом Вы можете иметь более четкое представление о дубликата контексте. Тем не менее, не соответствующие дубликаты в \"ссылку режиме\". Таким образом, можно выполнять действия, как Марк все и обязательно только знак фильтруется дубликатов.\r\n\r\nДействие меню\r\n-------------\r\n\r\n* **Открытый черный список:** Удалите все игнорируют матчи вы добавили. Вы должны начать новый поиск вновь очищается список игнорируемых чтобы быть эффективными.\r\n* **Экспорт результатов в XHTML:** Возьмите текущие результаты, а также создавать файл XHTML из него. Столбцов, которые видны при нажатии на эту кнопку будет столбцов в файле XHTML. Файл автоматически откроется в браузере по умолчанию.\r\n* **Отправить Помечено в корзину:** Отправить все отмеченные дубликаты, мусор, это очевидно.\r\n* **Удалить Помеченные и замена с Жесткие**: Передает все отмеченные дубликаты, мусор, но после того, как сделали это, удаленные файлы заменяются `жестких <http://en.wikipedia.org/wiki/Hard_link>`_ ссылку к ссылке на файл. (Только для OS X и Linux)\r\n* **Перемещение Помечено в ...:** запросит назначения, а затем переместить все отмеченные файлы в том, что назначения. Путь исходного файла может быть воссоздан в пункт назначения, в зависимости от \"Копирование и перемещение\" предпочтения.\r\n* **Скопируйте Помечено в ...:** запросит у вас место, а затем скопировать все выбранные файлы к этому пункту назначения. Путь исходного файла может быть воссоздан в пункт назначения, в зависимости от \"Копирование и перемещение\" предпочтения.\r\n* **Удалить Помеченные из результатов:** Удалить все отмеченные дубликатов из результата поиска. Сами файлы не будут затронуты и останутся, где они.\r\n* **Удалить выбранные из результатов:** Удалить все выбранные дубликатов из результата поиска. Обратите внимание, что все выбранные файлы ссылки будут игнорироваться, только дубликаты могут быть удалены с этим действием.\r\n* **Сделать Выбранный Справка:** Содействие все выбранные дубликатов ссылки. Если дубликат частью группы, имеющей ссылке на файл ближайшие из ссылки папки (в синий цвет), не будут приняты меры для этого дубликат. Если более чем один дубликат среди той же группы выбраны, только первый из каждой группы будет поощряться.\r\n* **Добавить выбранные в черный список:** Это сначала удаляет все выбранные дубликаты из результатов, а затем добавить матча, которые дублируют и опорный ток в черный список. Этот матч не придет снова в дальнейшей проверки. Копировать себя и, возможно, вернется, но он будет искаться в другой ссылке на файл. Вы можете очистить список игнорируемых с Открытый черный список команды.\r\n* **Открытое Выбранный с приложений по умолчанию:** Откройте файл с помощью приложения, связанного с типом выбранного файла.\r\n* **Показать Выбранный в Finder-е:** Откройте папку, содержащую выбранный файл.\r\n* **Вызов специальной команды:** Вызывает внешнюю программу вы установили в настройках с использованием выделенного фрагмента в качестве аргументов в вызове.\r\n* **Переименования выбрано:** Запрашивает новое имя, а затем переименовать выбранный файл.\r\n\r\n.. todo:: Add Move and iPhoto/iTunes warning\r\n.. todo:: Add \"Deletion Options\" section.\n"
  },
  {
    "path": "help/uk/faq.rst",
    "content": "﻿Часті питання\n==========================\n\n.. topic:: Що таке dupeGuru?\n\n    .. only:: edition_se\n\n        dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або контенту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.\n\n    .. only:: edition_me\n\n        dupeGuru Music Edition являє собою інструмент для пошуку дубльованих пісень у вашій музичній колекції. Він може будувати свою сканування файлів, тегам або змісту. Файл і тег перевіряє функція нечіткого відповідності алгоритм, який може знаходити дублікати файлів або теги, навіть якщо вони не зовсім те ж саме.\n\n    .. only:: edition_pe\n\n        dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість.\n\n.. topic:: Що робить його краще, ніж інші сканери дублювати?\n\n    Сканування є надзвичайно гнучкою. Ви можете налаштувати його, щоб дійсно отримати, яких результатів ви хочете. Ви можете прочитати більше про опція налаштування dupeGuru в :doc:`Установки <preferences>`.\n\n.. topic:: Наскільки безпечно використовувати dupeGuru?\n\n    Дуже безпечною. dupeGuru був розроблений, щоб переконатися, що ви не видаляєте файли, які ви не хотіли видалити. По-перше, існує система відліку папку, яка дозволяє визначити папки, в яких ви абсолютно не ** ** хочете dupeGuru, щоб ви видаляєте файли там, і тоді є система контрольної групи, що гарантує, що ви завжди ** * * тримати принаймні один член групи дублікатів.\n\n.. topic:: Які обмеження демо dupeGuru?\n\n    У демо-режимі, ви можете тільки виконувати дії над 10 дублікати відразу. В\n    `Fairware <http://open.hardcoded.net/about/>`_ mode, однак, Є ніяких обмежень.\n\n.. topic:: Знак коробку файл я хочу видалити відключена. Що я повинен зробити?\n\n    Ви не можете помітити посилання (перший файл) дублікат групи. Однак те, що ви можете зробити, полягає в сприянні дублювати файл довідки. Таким чином, якщо файл, який Ви хочете, щоб відзначити цю посилання, виділіть дублікатів файлів в групу, яку ви хочете просувати на посилання, і натисніть на кнопку **Дії -> Додати вибраної посилання**. Якщо посилання файл з папки посилання (назва файлу написані на синіми літерами), ви не можете видалити його з вихідного положення.\n\n.. topic:: У мене є папка, з якої я справді не хочу, щоб видалити файли.\n\n    Якщо ви хочете бути впевнені, що dupeGuru ніколи не буде видаляти файл з певної папки, переконайтеся, що встановили в стан **Посилання на:** документ: :doc:`folders`.\n\n.. topic:: Що це за '(X відкидається) \"повідомлення в рядку стану?\n\n    У деяких випадках, кілька матчів не включені в остаточні результати з міркувань безпеки. Дозвольте мені навести приклад. У нас є 3 файли: A, B і C. Ми скануємо їх за допомогою фільтра низьких твердості. Сканер визначає, що матчі з B, матчі з С, але робить B ** не ** матч з С. При цьому, dupeGuru має вигляд проблеми. Вона не може створити дублікат групи А, В і С в це, тому що не всі файли в групі буде відповідати разом. Це може створити 2 групи: одна група AB, а потім одна група AC, але це не буде, з міркувань безпеки. Давайте думати про це: якщо Б не співпадає з С, вона, ймовірно, означає, що або B, C або обидва на самому ділі не дублікати. Якщо не було б 2 групи (АВ і АС), ви б у кінцевому підсумку видалити обидва B і C. І якщо один з них не дублювати, що насправді не те, що ви хочете робити, правильно? Так що dupeGuru робить у такому випадку є, щоб відмінити матч AC (і додає повідомлення в рядку стану). Таким чином, якщо ви вилучили B і повторно запустити сканування, вам доведеться відповідати змінного струму в наступний результат.\n\n.. topic:: Я хочу, щоб відзначити всі файли з визначеної папки. Що я можу зробити?\n\n    Включити :doc:`ошукані Тільки <results>` режим і натиснути на папку колонки для сортування дублікатів по папках. Потім він буде легким для вас, щоб вибрати всі дублікати з тієї ж папці, а потім натиснути клавішу пробіл, щоб відзначити всі вибрані дублікатів.\n\n.. only:: edition_se or edition_pe\n\n    .. topic:: Я хочу, щоб видалити всі файли, які більше 300 Кб від їх посиланням на файл. Що я можу зробити?\n\n        * Включити :doc:`ошукані Тільки <results>` режимі.\n        * Включити **Значення Delta** режимі.\n        * Натисніть на \"Розмір\" стовпця для сортування результатів за розміром.\n        * Вибрати всі дублікати нижче -300.\n        * Натисніть на **Видалити вибрані з результатів**.\n        * Вибрати всі дублікати більше 300 осіб.\n        * Натисніть на **Видалити вибрані з результатів**.\n\n    .. topic:: Я хочу, щоб мої останні змінені файли файли довідки. Що я можу зробити?\n\n        * Включити :doc:`ошукані Тільки <results>` режимі.\n        * Включити **Значення Delta** режимі.\n        * Натисніть на \"Модифікація\" колонку для сортування результатів за датою зміни.\n        * Натисніть на \"Модифікація\" колона знову змінити порядок сортування.\n        * Вибрати всі дублікати за 0.\n        * Натисніть на **Зробити вибраної посилання**.\n\n    .. topic:: Я хочу, щоб відзначити все дублікати, що містять слово \"копія\". Як мені це зробити?\n\n        * **Windows**: Натисніть на **Дії -> Застосувати фільтр**, потім введіть \"копія\", натисніть кнопку ОК.\n        * **Mac OS X**: Типу \"копія\" в \"Фільтр\" поле на панелі інструментів.\n        * Натисніть на Марка **-> Позначити всі**.\n\n.. only:: edition_me\n\n    .. topic:: Я хочу, щоб видалити всі пісні, які більш ніж на 3 секунди від своєї посиланням на файл. Що я можу зробити?\n\n        * Включити :doc:`ошукані Тільки <results>` режимі.\n        * Включити **Значення Delta** режимі.\n        * Натисніть на \"Час\" колонку для сортування результатів за часом.\n        * Вибрати всі дублікати нижче -00:03.\n        * Натисніть на **Видалити вибрані з результатів**.\n        * Вибрати всі дублікати за 00:03.\n        * Натисніть на **Видалити вибрані з результатів**.\n\n    .. topic:: Я хочу, щоб мій високий бітрейт файлів пісні посилання. Що я можу зробити?\n\n        * Включити :doc:`ошукані Тільки <results>` режимі.\n        * Включити **Значення Delta** режимі.\n        * Натисніть на \"Бітрейт\" колонку для сортування результатів по бітрейт.\n        * Натисніть на \"Бітрейт\" колона знову змінити порядок сортування.\n        * Вибрати всі дублікати за 0.\n        * Натисніть на **Зробити вибраної посилання**.\n\n    .. topic:: Я не хочу [жити] і [ремікс] версії моїх пісень вважатися дублікатами. Як мені це зробити?\n\n        Якщо ваше порівняння поріг досить низьким, ви, ймовірно, в кінцевому підсумку з живою і ремікс версії ваших пісень у своїх результатах. Там ви нічого не можете зробити, щоб запобігти цьому, але є дещо можна зробити, щоб легко видалити їх зі свого результати після сканування: після сканування, фільтрації. Якщо, наприклад, ви хочете видалити всі пісні з чим-небудь у квадратних дужках []:\n\n        * **Windows**: Натисніть на **Дії -> Застосувати фільтр**, а потім введіть \"[*]\", натисніть кнопку ОК.\n        * **Mac OS X**: Тип \"[*]\" в \"Фільтр\" поле на панелі інструментів.\n        * Натисніть на Марка **-> Позначити всі**.\n        * Натисніть на **Дії -> Видалити вибрані з результатів**.\n\n.. topic:: Я намагався відправити свої дублікати в корзину, але dupeGuru говорить мені, він не може це зробити. Чому? Що я можу зробити?\n\n    Більшу частину часу, тому dupeGuru не можете відправляти файли до кошика через права доступу до файлів. Ви повинні * написати * дозволу на файли, які ви хочете відправити у кошик. Якщо ви не знайомі з командним рядком, ви можете використовувати утиліти, такі як `BatChmod <http://macchampion.com/arbysoft/BatchMod>`_ виправити Ваші права.\n\n    Якщо dupeGuru ще дає вам неприємності після фіксації ваших прав, було кілька випадків, коли за допомогою \"Переміщення Позначено до ...\" як обхідного шляху зробили свою справу. Таким чином, замість відправки файлів в корзину, ви посилаєте їх в тимчасову папку з \"Переміщати Позначено до ...\" дії, а потім видалити цю тимчасову папку вручну.\n\n    .. only:: edition_pe\n\n        Якщо ви намагаєтеся видалити *iPhoto*, то причина збою інша. Видалення не виконується, так dupeGuru не може спілкуватися з iPhoto. Врахуйте, що для видалення коректної роботи, ви не повинні грати навколо iPhoto в той час як dupeGuru працює. Крім того, іноді, система Applescript, здається, не знають, де знайти Iphoto запустити його. Це може допомогти в таких випадках для запуску Iphoto * до * ви посилаєте дублікатів в корзину.\n\n    Якщо все це не так, `контакт УГ підтримки <http://www.hardcoded.net/support>`_, ми зрозуміти це.\n\n.. todo:: This FAQ qestion is outdated, see english version.\n"
  },
  {
    "path": "help/uk/folders.rst",
    "content": "﻿Вибір папки\n================\n\nПерше вікно, ви бачите, коли ви запускаєте dupeGuru це вікно вибору папки. Це вікно містить список папок, які будуть скануватися при натисканні на **Сканування**.Це вікно досить проста у використанні. Якщо ви хочете додати папку, натисніть на кнопку **+**. Якщо ви додали папки перш, спливаюче меню зі списком останніх папки додав з'явиться. Ви можете натиснути на одну з них, щоб додати його прямо в свій список. Якщо натиснути на перший пункт меню, **Додати новий папку ...**, вам буде запропоновано ввести папку додати. Якщо ви ніколи не додається папка, не з'явиться меню, і ви будете безпосередньо буде запропоновано ввести нову папку додати.\n\nАльтернативний спосіб для додавання папок в список, щоб перетягнути їх в списку.\n\nЩоб видалити папку, виберіть папку, видалити, і натисніть на **-**. Якщо папці вибирається при натисканні кнопки, обраної папки буде встановлений в **виключені**  стану (див. нижче), а не видалений.\n\nПапка держав\n-------------\n\nКожна папка може знаходитися в одному з цих 3-х держав:\n\n* ** Нормальний: ** дублікати знайдені в цю папку можна видалити.\n* ** Довідка: ** Дублікати знайти в цій папці  **не може** бути видалені. Файли з цієї папки можна тільки в кінцевому підсумку в **посилання** позиція в групі обдурити. Якщо більш ніж один файл з папки посилання в кінцевому підсумку в тій же групі обдурити, тільки один, будуть збережені. Інші будуть видалені з групи.\n* ** Не включено: ** Файли в цьому каталозі не буде включений у перевірку.\n\nСтан за замовчуванням до папки, звичайно, **Нормальний**. Ви можете використовувати **Посилання**  стан для папки, якщо ви хочете бути впевнені, що ви не будете видаляти будь-які файли з нього.\n\nКоли ви встановлюєте стан каталог, все підпапки цієї папки автоматично успадковує цей стан, якщо явно не включений стан підпапку в.\n\n.. todo:: Add iPhoto/Aperture/iTunes libraries notes\n"
  },
  {
    "path": "help/uk/index.rst",
    "content": "﻿dupeGuru help\n===============\n\n.. only:: edition_se\n\n    Цей документ також доступна на `французькому <http://dupeguru.voltaicideas.net/help/fr/>`__, `німецький <http://dupeguru.voltaicideas.net/help/de/>`__ і `Вірменський <http://dupeguru.voltaicideas.net/help/hy/>`__.\n\n.. only:: edition_se or edition_me\n\n    dupeGuru це інструмент для пошуку дублікатів файлів на вашому комп'ютері. Він може сканувати або імен файлів або вмісту. Файл функцій сканування нечіткого відповідності алгоритму, який дозволяє знайти однакові імена файлів, навіть якщо вони не зовсім те ж саме.\n\n.. only:: edition_pe\n\n    dupeGuru Picture Edition (PE для стислості) являє собою інструмент для пошуку дублікатів фотографій на вашому комп'ютері. Не тільки він може знайти точні відповідності, але він також може знайти дублікати серед фотографій різного роду (PNG, JPG, GIF і т.д..) І якість.\n\nХоча dupeGuru може бути легко використана без документації, читання цього файлу допоможе вам освоїти його. Якщо ви шукаєте керівництво для вашої першої дублювати сканування, ви можете поглянути на: :doc:`Quick Start <quick_start>`\n\nЦе гарна ідея, щоб зберегти dupeGuru оновлено. Ви можете завантажити останню версію на своєму http://dupeguru.voltaicideas.net.\n\nContents:\n\n.. toctree::\n    :maxdepth: 2\n\n    quick_start\n    folders\n    preferences\n    results\n    reprioritize\n    faq\n    changelog\n"
  },
  {
    "path": "help/uk/preferences.rst",
    "content": "﻿Уподобання\n===========\n\n.. only:: edition_se\n\n    **Тип сканування:** Цей параметр визначає, який аспект файли будуть порівнюватися в дублікат сканування. Якщо вибрати **Файл** , dupeGuru будемо порівнювати кожне імена файлів слово за слово, і, залежно від інших параметрів нижче, він буде визначати, чи достатньо слів відповідність розглянути 2 файлів дублікатів. Якщо вибрати **Вміст**, тільки файли з точно такою ж контент буде матч.\n\n     **Папки** типу сканування трохи особливим. Коли ви обираєте його, dupeGuru проведе пошук дублікатів *папки*  замість того, щоб дублікатів файлів. Для визначення того, дві папки, дублюють один одного, всі файли, що містяться в папках будуть перевірятися, і якщо вміст **всі**  файли в матчі папки, папки будуть вважатися дублікатами.\n\n     **Фільтра Твердість:** Якщо ви вибрали **Папки** Файл типу сканування, ця опція визначає, як схожі два імені повинно бути для dupeGuru розглядати їх дублікатів. Якщо фільтр твердості, наприклад 80, то це означає, що 80% слів з ​​двох імен файлів повинні збігатися. Для визначення відповідності відсоток, dupeGuru перший підраховує загальну кількість слів в  **обох** файлу, то підрахувати кількість слів відповідності (кожне слово відповідності вважаються 2), а потім розділіть кількість слів відповідності на загальне число слів. Якщо результат більше або дорівнює фільтр твердість, у нас є дублікати матчу. Наприклад, \"ABCD\" і \"CDE\" мають відповідний відсоток 57 (4 слова відповідності, 7 всього слів).\n\n.. only:: edition_me\n\n    **Тип сканування:** Цей параметр визначає, який аспект файли будуть порівнюватися в дублікат сканування. Характер дублювати сканування варіюється в залежності від того, що ви обираєте для цієї опції.\n\n    * **Файл:** Кожна пісня буде мати свій файл розбитий на слова, а потім кожне слово буде в порівнянні з обчислити відповідні відсотки. Якщо цей відсоток вище або дорівнює **жорсткість фільтра**  (див. нижче детальніше), dupeGuru розгляне 2 пісні дублікатів.\n    * **Файл - Поля:** Як **Файл** , за винятком того, що як тільки ім'я файлу були розділені на слова, ці слова потім групуються в поля. Роздільник полів \"-\". Остаточний відсоток відповідності буде найнижчим відповідний відсоток серед полів. Таким чином, \"Виконавець - Назва\" і \"Артист - Інші Назва\" матиме відповідний відсоток 50 (С **Файл** сканування, це буде 75).\n    * **Файл - Поля (нема наказу):** Як **Супер - Поля**, крім того, що порядок полів не має значення. Наприклад, \"Виконавець - Назва\" і \"Назва - Артист\" матиме відповідний відсоток з 100 замість 0.\n    * **Теги:** Цей метод прочитує мітки (метадані) кожної пісні й порівняти їх полям. Цей метод, як  **Супер - Поля**, вважає низький відповідне поле в якості остаточного відповідний відсоток.\n    * **Склад:** Цей метод сканування використовувати фактичний зміст пісні, щоб визначити, які є дублікатами. За 2 пісні у відповідності з цим методом, вони повинні мати **такий самий змісту**.\n    * **Аудіо контенту:** Те ж зміст, але тільки в аудіо-контент порівнюється (без метаданих).\n\n    **Фільтра Твердість:** Якщо ви вибрали ім'я файлу або тегами типу сканування, ця опція визначає, як схожі два імені / теги повинні бути для dupeGuru розглядати їх дублікатів. Якщо фільтр твердості, наприклад 80, то це означає, що 80% слів з двох імен файлів повинні збігатися. Для визначення відповідності відсоток, dupeGuru перший підраховує загальну кількість слів в **обох** файлу, то підрахувати кількість слів відповідності (кожне слово відповідності вважаються 2), а потім розділіть кількість слів відповідності на загальне число слів. Якщо результат більше або дорівнює фільтр твердість, у нас є дублікати матчу. Наприклад, \"ABCD\" і \"CDE\" мають відповідний відсоток 57 (4 слова відповідності, 7 всього слів).\n\n    **Теги для сканування:** При використанні Слова типу сканування, ви можете вибрати теги, які будуть використовуватися для порівняння.\n\n.. only:: edition_se or edition_me\n\n     **Слово зважування:** ЯКЩО ви вибрать Файл типу сканування, цею ВАРІАНТ Трохи змін, Як відповідній відсоток розраховується. При Слові зважування, Замість того, значення 1 в Дублікат Рахунка и загальна кількість слів, кожне слово має значення, рівну кількість сімволів, які смороду мають. При Слові зважування \", AB CDE FGHI\" і \"AB CDE fghij\" матіме відповідній відсоток 53% (19 Персонажів, 10 сімволів, Що відповідає (4 для \"б\" і 6 \"CDE\")).\n\n     **Матч Схожі слова:** ЯКЩО ви дозволите Цю опцію, подібні слова будуть зараховані Як сірники. Наприклад, \"White Stripes\" і \"Біла смуга\" буде збігатіся% з 100 Замість 66 з, Що функція включена. **Увага:** використову Цю опцію з обережністю. ЦІЛКОМ імовірно, Що ви отрімаєте Багато помилковості спрацьовувань в результатах при йо включенні. Тім не менше, Це Допоможи вам знайте дублікаті, Що ви НЕ знайшлі б в іншому випадка. Процес сканування кож однозначно повільніше, ця опція включена.\n\n.. only:: edition_pe\n\n     **Тип сканування:** Цей параметр визначає тип сканування, які будуть зроблені на ваші картини. **Сканування** Зміст типу порівнює фактичний зміст фотографій нечіткі шляху (що робить його можна знайти не тільки точними копіями, але і подібні). **EXIF Timestamp** тип сканування дивиться на метадані EXIF з фото (якщо він існує) і відповідає фотографії, які мають такий же. Це набагато швидше, ніж сканування вмісту. **Увага:** Змінені фотографії часто тримають ж мітка EXIF, так що слідкуйте за помилкових спрацьовувань, коли ви використовуєте, що тип сканування.\n\n     **Фільтра Твердість:** *Вміст тип сканування тільки*. Чим більше цей параметр, \"важче\" є фільтром (Іншими словами, тим менше результатів Ви отримаєте). Більшість фотографій одного й того ж матчу якості на 100%, навіть якщо формат відрізняється (PNG і JPG, наприклад.). Однак, якщо ви хочете, щоб відповідати PNG з більш низькою якістю JPG, вам доведеться встановити фільтром твердість нижче, ніж 100. За замовчуванням, 95, це солодке місце.\n\n     **Матч малюнки різних розмірів:** Якщо ви встановите цей прапорець, фотографії різних розмірів буде дозволений в тому ж дублікат групи.\n\n**Можна змішувати файл виду:** Якщо ви встановите цей прапорець, дублювати групам дозволяється є файли з різними розширеннями. Якщо ви не перевірити його, ну, вони не є!\n\n**Ігнорувати дублікати hardlinking в той же файл:** Якщо ця опція включена, dupeGuru перевірить дублікати, щоб побачити якщо вони посилаються на той самий індексний\n`дескриптор <http://en.wikipedia.org/wiki/Inode>`__. Якщо вони це зроблять, вони не будуть вважатися дублікатами. (Тільки для OS X і Linux)\n\n**Використання регулярних виразів при фільтрації:** Якщо ви відзначите цей прапорець, фільтрація розглядатиме ваш запит фільтра, як **регулярний вираз**. Пояснюючи їх виходить за рамки цього документа. Гарне місце для початку навчання він `регулярного expressions.info <http://www.regular-expressions.info>`__.\n\n**Видалення порожніх папок після видалення або переміщення:** Коли ця опція включена, папки будуть видалені через файл видалений або переміщений і папка порожня.\n\n**Копіювання і переміщення:** Визначає, як операції копіювання та переміщення (в меню Дії) буде себе вести.\n\n* **Право на призначення:** Всі файли будуть відправлені безпосередньо в пункт призначення, не намагаючись відтворити початковий шлях взагалі.\n* **Повторно відносний шлях:** шлях вихідний файл буде відтворений в папці призначення, аж до кореневого виділення в панелі Directories. Наприклад, якщо ви додали ``/Users/foobar/SomeFolder`` на панель Каталоги і переміщенні ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до місця призначення ``/Users/foobar/MyDestination/SubFolder``, кінцевим пунктом призначення для файлу буде  ``/Users/foobar/MyDestination/SubFolder`` (``SomeFolder`` були скорочені зі шляху джерела в кінцевий пункт призначення.).\n* ** Повторно абсолютний шлях: ** шлях вихідний файл буде відтворений в папці призначення в повному комплекті. Наприклад, якщо ви переміщаєте ``/Users/foobar/SomeFolder/SubFolder/SomeFile.ext`` до місця призначення ``/Users/foobar/MyDestination``, кінцевим пунктом призначення для файлу буде ``/Users/foobar/MyDestination/Users/foobar/SomeFolder/SubFolder``.\n\nУ всіх випадках, dupeGuru красиво ручки конфліктів імен шляхом додавання номера призначення ім'я файлу, якщо ім'я файлу вже існує в місці призначення.\n\n**Спеціальної команди:** Це перевагу визначає команду, яка буде викликатися \"Викликати спеціальної команди\" дії. Ви можете посилатися ні на які зовнішні програми через цю дію. Це може бути корисно, якщо, наприклад, у вас є хороший додаток порівнюєте встановлені.\n\nФормат команди такий же, як те, що ви повинні написати в командному рядку, за винятком того, що Є 2 заповнювачів: **%d** and **%r**. Ці наповнювачі будуть замінені на шлях вибраний обдурити (% г) і шлях до заслання на файл вибраного обдурити (%r).\n\nЯкщо шлях до виконуваного містить прогалини, необхідно укласти його в \"\" лапки. Ви також повинні докласти заповнювачів в лапки, бо це дуже можливо, що шлях до обдурених і посилання будуть містити пробіли. Ось приклад користувальницької команди::\n\n    \"C:\\Program Files\\SuperDiffProg\\SuperDiffProg.exe\" \"%d\" \"%r\"\n"
  },
  {
    "path": "help/uk/quick_start.rst",
    "content": "﻿Швидкий старт\n==============\n\nЩоб ви швидко почали з dupeGuru, давайте просто робити сканування за допомогою стандартних параметрів за замовчуванням.\n\n* Запуск dupeGuru.\n* Додавання папок для сканування або перетягнути & краплі або кнопку \"+\".\n* Натисніть на сканування.\n* Почекайте, поки процес сканування завершено.\n* Подивіться на кожен дублікат (файли, які відступом) і переконайтеся, що це дійсно дублікат посиланням групи (файл вище дублювати без відступу та інвалідів вікна знак).\n* Якщо файл помилкових дублікатів, виділіть її та натисніть **Дії -> Видалити вибрані з результатів**.\n* Якщо ви впевнені, що немає помилкових дублікатів в результатах, натисніть на **Редагувати -> Позначити Всі**, а потім **Дії -> Отправить Позначено до кошику**.\n\nЦе тільки основні сканування. Є багато налаштування ви можете зробити, щоб отримати різні результати і кілька методів вивчення та зміни ваших результатів. Щоб дізнатися про них, щойно прочитав решту цього файлу довідки.\n"
  },
  {
    "path": "help/uk/reprioritize.rst",
    "content": "﻿Повторне пріоритетів дублікатів\n================================\n\ndupeGuru намагається автоматично визначити, які дублікат повинен відправитися в заслання кожної групи\nпозиції, але іноді це робиться неправильно. У багатьох випадках, розумний обдурити сортування з \"Цінності Дельта\"\nі \"ошукані Тільки\" варіанти на додаток до \"Зробити вибраної посилання\" дія робить трюк, але\nіноді, більш потужний варіант не потрібно. Тут зміни пріоритетів в діалог вступає в\nграти. Ви можете викликати його через \"змінити пріоритети Результати\" пункт в меню \"Дії\".\n\nЦей діалог дозволяє вам вибрати критерії, за якими посилання обдурити будуть відібрані в\nкожній групі обдурити. Список доступних критеріїв зліва і перелік критеріїв ви\nОбрана справа.\n\nКритеріїв категорії слідують аргумент. Наприклад, \"Розмір (Вищий)\" означає, що обдурити\nз великим розміром переможе. \"Властивості папки (/Foo/Bar)\" означає, що ошукані в цій папці буде перемогти. для додавання\nкритерій правом списку, спочатку виберіть категорію в спадному списку і виберіть\nsubargument в наведеному нижче списку, а потім натисніть на праву стрілку кнопки.\n\nПорядок списку праворуч важливо (ви можете змінити порядок елементів через перетягнути і відпустити). коли\nзбір обдурити для довідки позицію, перший критерій використовується. Якщо є краватка, другий\nкритерій використовується і так далі і так далі. Наприклад, якщо ваші аргументи \"Розмір (вищий)\", а потім\n\"Файл (Не закінчується на номер)\", заслання на файл, який буде обраний у групі буде\nнайбільших файл, а якщо два або декілька файлів мають однаковий розмір, який має ім'я файлу з\nне закінчується номер буде використовуватися. Коли всі критерії привести до зв'язку, порядок, в якому ошукані\nраніше були в групі буде використовуватися.\n"
  },
  {
    "path": "help/uk/results.rst",
    "content": "﻿Результати\n===========\n\nКоли dupeGuru завершення сканування на наявність дублікатів, він покаже його результати у вигляді дубліката список групи.\n\nПро дублікат групи\n----------------------\n\nДублікат група являє собою групу файлів, які весь матч разом. Кожна група має **посиланням**  на файл і одного або більше **однакових файлів**. Посилання файл перший файл групи. Його марка вікно вимкнено. Під ним, і з відступом, які дублікатів файлів.\n\nВи можете відзначити дублікатів файлів, але ви ніколи не можете помітити посилання файл групи. Це захід безпеки, щоб запобігти dupeGuru від видалення не тільки повторювані файли, але їх посилання. Ти впевнений, що не хочу цього, чи не так?\n\nЩо визначає, які файли посилання і які файли є дублікатами спочатку свою папку держави. Файл з посиланням папка завжди буде посилання в дублікат групи. Якщо всі файли зі звичайної папки, розмір визначити, який файл буде ведення дублікат групи. dupeGuru припускає, що ви завжди хочете зберегти найбільших файл, так що великих файлів займе вихідне положення.\n\nВи можете змінити посилання файл групи вручну. Для цього виберіть дублікат файлу, який ви хочете просувати на посилання, і натисніть на кнопку **Дії -> Додати вибраної посилання**.\n\nПерегляд результатів\n--------------------\n\nХоча ви можете просто натиснути на **Правка -> Виділити все, а потім** **Дії -> Отправить Позначено до кошику** швидко видалити всі дублікати файлів в результатах, завжди рекомендується переглянути всі дублікати перед видаляючи їх.\n\nЩоб допомогти вам огляд результатів, ви можете викликати панель **Докладніше**. Ця панель показує всі деталі обраного файла, а також подробиці свого заслання в. Це дуже зручно, щоб швидко визначити, якщо дублікат дійсно дублікат. Ви також можете двічі клацнути по файлу, щоб відкрити його і пов'язані з ним програми.\n\nЯкщо у вас є більше помилкових дублікатів, ніж правда дублікатів (Якщо Ваш фільтр жорсткість дуже низька), кращий спосіб продовжити б переглянути дублікатів, знак істинного дублікати і натисніть **Дії -> Отправить Позначено до кошику** . Якщо у вас є більш вірно, ніж помилкових дублікатів дублікатів, замість цього можна позначити всі файли, які є помилковими дублікатів, а також використовувати **Дії -> Видалити Помічені від результатів**.\n\nМаркування і вибір\n---------------------\n\n**Зазначені** дублікат двох примірниках з невеликою прапорець поруч з ним, мають галочки. **Обрано**  дублікат дубліката бути виділені. Кілька дій, вибір може бути виконана в dupeGuru стандартним чином (Shift/Command/Control клік). Ви можете перемикати знак стан всіх вибраних дублікати \", натиснувши **просторі**.\n\nПоказати тільки ошукані\n-----------------------\n\nКоли цей режим включений, дублікати відображаються без їх відповідного файлу довідки. Ви можете вибрати, марка і сортувати цей список, як і в звичайному режимі.\n\nDupeGuru результати, коли в нормальному режимі, сортуються відповідно до дублікат групи '**посиланням на файл**. Це означає, що якщо ви хочете, наприклад, щоб відзначити все дублікати \"EXE\" розширенням, ви не можете просто сортувати результати по \"Вид\", щоб мати всі EXE дублікатів разом, тому що група може складатися з більш ніж одного типу файлів . Ось де обдурені Тільки режим вступає в гру. Щоб позначити всі ваші \"EXE\" дублікати, ви просто повинні:\n\n* Включити ошукані Тільки режим.\n* Додати \"Вид\" колонку \"Стовпці\" меню.\n* Натисніть на те, що \"Вид\" колонки, щоб відсортувати список за типом.\n* Знайдена перша дублікат з \"EXE\" роду.\n* Виберіть його.\n* Перейдіть, щоб знайти останнього дубліката з \"EXE\" роду.\n* Утримуйте Shift і клацніть по ньому.\n* Натисніть Space, щоб позначити всі вибрані дублікатів.\n\nДельта значення\n----------------\n\nЯкщо включити цей перемикач на деякі стовпці будуть відображати значення по відношенню до дубліката засланні, а не абсолютні значення. Ці дельти значення також будуть відображатися в різні кольори, щоб ви могли помітити їх легко. Наприклад, якщо дублікат 1,2 Мб і своє посилання в 1,4 Мб, розмір стовпець відображає -0,2 Мб.\n\nТільки ошукані і Дельта значення\n--------------------------------\n\nТільки ошукані режимі розкрити свою дійсну силу, коли ви використовуєте його з Delta Значення перемикач включений. Коли ви дозволите його, відносні значення буде відображатися замість абсолютних. Так що якщо, наприклад, ви хочете видалити з результатів всі дублікати, які є більш 300 Кб від їх посиланню, ви можете відсортувати дублікати тільки результати за розміром, виберіть всі дублікати при -300 в стовпці Розмір, видаляти їх, , а потім зробити те ж саме повторює більше 300 в нижній частині списку.\n\nВи можете також використовувати його для зміни посилання пріоритет повторювані список. Коли ви робите свіжі сканування, якщо Є немає посилання папки, заслання на файл кожної групи є найбільшою файл. Якщо ви хочете змінити, що, наприклад, в останній модифікації час, ви можете відсортувати дублікати тільки результати за часом модифікації в **убування порядку** , виберіть всі дублікати з часом зміни дельти значення більше 0 і натисніть **Переконайтеся, обраної посилання**. Причина, чому ви повинні зробити порядок сортування за спаданням, тому що якщо 2 файли серед таких же дублікат групи вибираються при натисканні на **Зробити вибраної посилання**, тільки перший із списку будуть зроблені посилання, інші будуть проігноровані . І так як ви хочете Остання зміна файлу для посилання, які мають порядок сортування за спаданням запевняє вас, що першим пунктом у списку буде останньої зміни.\n\n.. todo:: Add \"Non-numerical delta\" information.\n\nФільтрація\n-----------\n\ndupeGuru підтримує після сканування, фільтрації. З його допомогою ви можете звузити результати, щоб ви могли виконувати дії, на підмножини. Наприклад, ви можете легко помітити всі дублікати з їх ім'я файлу, що містить \"копіювати\" з результатів за допомогою фільтра.\n\n.. todo:: Qt has a toolbar search field now, not a menu item.\n\n**Windows:** Для використання функції фільтрації, натисніть на Дії -> Застосувати фільтр, запишіть фільтр, який ви хочете застосувати і натисніть ОК. Щоб повернутися до нефільтроване результати, натисніть на Дії -> Скасувати фільтр.\n\n**Mac OS X:** Для використання функції фільтрації, тип фільтра в \"Фільтр\" поле пошуку на панелі інструментів. Щоб повернутися до нефільтроване результаті, очистіть поле, або натисніть на кнопку \"X\".\n\nУ простому режимі (режим), що ви вводите в якості фільтра рядок, що використовується для виконання фактичної фільтрації, за винятком однієї маски: **\\***. Таким чином, якщо ви введете \"[*]\" як ваш фільтр, він буде відповідати що-небудь з [] дужках в ньому, все, що між цими дужками.\n\nДля більш просунутих фільтрів, ви можете включити «Використання регулярних виразів при фільтрації\" на. Функція фільтрації буде використовувати регулярні вирази. Регулярний вираз мови для узгодження тексту. Пояснюючи їх виходить за рамки цього документа. Гарне місце для початку навчання він `регулярного expressions.info <http://www.regular-expressions.info>`__.\n\nМатчі не чутливі до регістру, в простих і регулярних виразів режимі.\n\nДля фільтра, щоб відповідати, регулярний вираз не обов'язково має збігатися цілий файл, він просто зобов'язаний утримувати в ланцюжок, відповідну висловом.\n\nВи могли помітити, що не всі дублікати в результаті будуть відповідати вашим фільтром. Це тому, що як тільки одна копія в матчах групового фільтра, то вся група залишиться в результатах, таким чином Ви можете мати більш чітке уявлення про дубліката контексті. Тим не менш, не відповідні дублікати у \"заслання режимі\". Таким чином, можна виконувати дії, як Марк все і обов'язково тільки знак фільтрується дублікатів.\n\nДія меню\n-----------\n\n* **Відкритий чорний список:** Видаліть всі ігнорують матчі ви додали. Ви повинні почати новий пошук знову очищується список ігнорованих щоб бути ефективними.\n* **Експорт результатів в XHTML:** Візьміть поточні результати, а також створювати файл XHTML з нього. Стовпців, які видно при натисканні на цю кнопку буде стовпців у файлі XHTML. Файл автоматично відкриється в браузері за замовчуванням.\n* **Надіслати Позначено в кошику:** Відправити всі відмічені дублікати, сміття, це очевидно.\n* **Видалити Помічені і заміна з Жорсткі**: Передає всі відмічені дублікати, сміття, але після того, як зробили це, вилучені файли замінюються `жорстких <http://en.wikipedia.org/wiki/Hard_link>`__ посилання до заслання на файл. (Тільки для OS X і Linux)\n* **Переміщення Позначено в ...:** запросить призначення, а потім перемістити всі відмічені файли в тому, що призначення. Шлях вихідного файлу може бути відтворений в пункт призначення, залежно від \"Копіювання і переміщення\" переваги.\n* **Скопіюйте Позначено в ...:** запитає у вас місце, а потім скопіювати всі вибрані файли до цього пункту призначення. Шлях вихідного файлу може бути відтворений в пункт призначення, залежно від \"Копіювання і переміщення\" переваги.\n* **Видалити Помічені з результатів:** Видалити все відмічені дублікатів з результату пошуку. Самі файли не будуть порушені й залишаться, де вони.\n* **Видалити вибрані з результатів:** Видалити всі вибрані дублікатів з результату пошуку. Зверніть увагу, що всі вибрані файли посилання будуть ігноруватися, тільки дублікати можуть бути видалені з цією дією.\n* **Зробити Обраний Довідка:** Сприяння всі вибрані дублікатів посилання. Якщо дублікат частиною групи, що має посиланням на файл найближчі із заслання папки (в синій колір), не будуть прийняті заходи для цього дублікат. Якщо більш ніж один дублікат серед тієї ж групи обрані, тільки перший з кожної групи буде заохочуватися.\n* **Додати обрані в чорний список:** Це спочатку видаляє всі вибрані дублікати з результатів, а потім додати матчу, які дублюють та опорний струм в чорний список. Цей матч не прийде знову в подальшої перевірки. Копіювати себе і, можливо, повернеться, але він буде шукатися в іншій посиланням на файл. Ви можете очистити список ігнорованих з Відкритий чорний список команди.\n* **Відкрите Обраний з додатків за замовчунням:** Відкрийте файл за допомогою програми, пов'язаного з типом обраного файлу.\n* **Розкривати Обраний в Finder:** Відкрийте папку, яка містить вибраний файл.\n* **Викликати спеціальної команди:** Викликає зовнішню програму ви встановили в настройках з використанням виділеного фрагмента в якості аргументів у виклику.\n* **Перейменування обрано:** Запит нове ім'я, а потім перейменувати вибраний файл.\n\n.. todo:: Add Move and iPhoto/iTunes warning\n.. todo:: Add \"Deletion Options\" section.\n"
  },
  {
    "path": "hscommon/LICENSE",
    "content": "Copyright 2014, Hardcoded Software Inc., http://www.hardcoded.net\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n    * 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.\n    * 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.\n\nTHIS 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.\n"
  },
  {
    "path": "hscommon/README",
    "content": "This module is common code used in all Hardcoded Software applications. It has no stable API so\nit is not recommended to actually depend on it. But if you want to copy bits and pieces for your own\napps, be my guest.\n"
  },
  {
    "path": "hscommon/__init__.py",
    "content": ""
  },
  {
    "path": "hscommon/build.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-03-03\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\"\"\"This module is a collection of function to help in HS apps build process.\n\"\"\"\n\nfrom argparse import ArgumentParser\nimport os\nimport sys\nimport os.path as op\nimport shutil\nimport tempfile\nimport plistlib\nfrom subprocess import Popen\nimport re\nimport importlib\nfrom datetime import datetime\nimport glob\nfrom typing import Any, AnyStr, Callable, Dict, List, Union\n\nfrom hscommon.plat import ISWINDOWS\n\n\ndef print_and_do(cmd: str) -> int:\n    \"\"\"Prints ``cmd`` and executes it in the shell.\"\"\"\n    print(cmd)\n    p = Popen(cmd, shell=True)\n    return p.wait()\n\n\ndef _perform(src: os.PathLike, dst: os.PathLike, action: Callable, actionname: str) -> None:\n    if not op.lexists(src):\n        print(\"Copying %s failed: it doesn't exist.\" % src)\n        return\n    if op.lexists(dst):\n        if op.isdir(dst):\n            shutil.rmtree(dst)\n        else:\n            os.remove(dst)\n    print(\"{} {} --> {}\".format(actionname, src, dst))\n    action(src, dst)\n\n\ndef copy_file_or_folder(src: os.PathLike, dst: os.PathLike) -> None:\n    if op.isdir(src):\n        shutil.copytree(src, dst, symlinks=True)\n    else:\n        shutil.copy(src, dst)\n\n\ndef move(src: os.PathLike, dst: os.PathLike) -> None:\n    _perform(src, dst, os.rename, \"Moving\")\n\n\ndef copy(src: os.PathLike, dst: os.PathLike) -> None:\n    _perform(src, dst, copy_file_or_folder, \"Copying\")\n\n\ndef _perform_on_all(pattern: AnyStr, dst: os.PathLike, action: Callable) -> None:\n    # pattern is a glob pattern, example \"folder/foo*\". The file is moved directly in dst, no folder\n    # structure from src is kept.\n    filenames = glob.glob(pattern)\n    for fn in filenames:\n        destpath = op.join(dst, op.basename(fn))\n        action(fn, destpath)\n\n\ndef move_all(pattern: AnyStr, dst: os.PathLike) -> None:\n    _perform_on_all(pattern, dst, move)\n\n\ndef copy_all(pattern: AnyStr, dst: os.PathLike) -> None:\n    _perform_on_all(pattern, dst, copy)\n\n\ndef filereplace(filename: os.PathLike, outfilename: Union[os.PathLike, None] = None, **kwargs) -> None:\n    \"\"\"Reads `filename`, replaces all {variables} in kwargs, and writes the result to `outfilename`.\"\"\"\n    if outfilename is None:\n        outfilename = filename\n    fp = open(filename, encoding=\"utf-8\")\n    contents = fp.read()\n    fp.close()\n    # We can't use str.format() because in some files, there might be {} characters that mess with it.\n    for key, item in kwargs.items():\n        contents = contents.replace(f\"{{{key}}}\", item)\n    fp = open(outfilename, \"wt\", encoding=\"utf-8\")\n    fp.write(contents)\n    fp.close()\n\n\ndef get_module_version(modulename: str) -> str:\n    mod = importlib.import_module(modulename)\n    return mod.__version__\n\n\ndef setup_package_argparser(parser: ArgumentParser):\n    parser.add_argument(\n        \"--sign\",\n        dest=\"sign_identity\",\n        help=\"Sign app under specified identity before packaging (OS X only)\",\n    )\n    parser.add_argument(\n        \"--nosign\",\n        action=\"store_true\",\n        dest=\"nosign\",\n        help=\"Don't sign the packaged app (OS X only)\",\n    )\n    parser.add_argument(\n        \"--src-pkg\",\n        action=\"store_true\",\n        dest=\"src_pkg\",\n        help=\"Build a tar.gz of the current source.\",\n    )\n    parser.add_argument(\n        \"--arch-pkg\",\n        action=\"store_true\",\n        dest=\"arch_pkg\",\n        help=\"Force Arch Linux packaging type, regardless of distro name.\",\n    )\n\n\n# `args` come from an ArgumentParser updated with setup_package_argparser()\ndef package_cocoa_app_in_dmg(app_path: os.PathLike, destfolder: os.PathLike, args) -> None:\n    # Rather than signing our app in XCode during the build phase, we sign it during the package\n    # phase because running the app before packaging can modify it and we want to be sure to have\n    # a valid signature.\n    if args.sign_identity:\n        sign_identity = f\"Developer ID Application: {args.sign_identity}\"\n        result = print_and_do(f'codesign --force --deep --sign \"{sign_identity}\" \"{app_path}\"')\n        if result != 0:\n            print(\"ERROR: Signing failed. Aborting packaging.\")\n            return\n    elif not args.nosign:\n        print(\"ERROR: Either --nosign or --sign argument required.\")\n        return\n    build_dmg(app_path, destfolder)\n\n\ndef build_dmg(app_path: os.PathLike, destfolder: os.PathLike) -> None:\n    \"\"\"Builds a DMG volume with application at ``app_path`` and puts it in ``dest_path``.\n\n    The name of the resulting DMG volume is determined by the app's name and version.\n    \"\"\"\n    print(repr(op.join(app_path, \"Contents\", \"Info.plist\")))\n    with open(op.join(app_path, \"Contents\", \"Info.plist\"), \"rb\") as fp:\n        plist = plistlib.load(fp)\n    workpath = tempfile.mkdtemp()\n    dmgpath = op.join(workpath, plist[\"CFBundleName\"])\n    os.mkdir(dmgpath)\n    print_and_do('cp -R \"{}\" \"{}\"'.format(app_path, dmgpath))\n    print_and_do('ln -s /Applications \"%s\"' % op.join(dmgpath, \"Applications\"))\n    dmgname = \"{}_osx_{}.dmg\".format(\n        plist[\"CFBundleName\"].lower().replace(\" \", \"_\"),\n        plist[\"CFBundleVersion\"].replace(\".\", \"_\"),\n    )\n    print(\"Building %s\" % dmgname)\n    # UDBZ = bzip compression. UDZO (zip compression) was used before, but it compresses much less.\n    print_and_do(\n        'hdiutil create \"{}\" -format UDBZ -nocrossdev -srcdir \"{}\"'.format(op.join(destfolder, dmgname), dmgpath)\n    )\n    print(\"Build Complete\")\n\n\ndef add_to_pythonpath(path: os.PathLike) -> None:\n    \"\"\"Adds ``path`` to both ``PYTHONPATH`` env and ``sys.path``.\"\"\"\n    abspath = op.abspath(path)\n    pythonpath = os.environ.get(\"PYTHONPATH\", \"\")\n    pathsep = \";\" if ISWINDOWS else \":\"\n    pythonpath = pathsep.join([abspath, pythonpath]) if pythonpath else abspath\n    os.environ[\"PYTHONPATH\"] = pythonpath\n    sys.path.insert(1, abspath)\n\n\n# This is a method to hack around those freakingly tricky data inclusion/exlusion rules\n# in setuptools. We copy the packages *without data* in a build folder and then build the plugin\n# from there.\ndef copy_packages(\n    packages_names: List[str],\n    dest: os.PathLike,\n    create_links: bool = False,\n    extra_ignores: Union[List[str], None] = None,\n) -> None:\n    \"\"\"Copy python packages ``packages_names`` to ``dest``, spurious data.\n\n    Copy will happen without tests, testdata, mercurial data or C extension module source with it.\n    ``py2app`` include and exclude rules are **quite** funky, and doing this is the only reliable\n    way to make sure we don't end up with useless stuff in our app.\n    \"\"\"\n    if ISWINDOWS:\n        create_links = False\n    if not extra_ignores:\n        extra_ignores = []\n    ignore = shutil.ignore_patterns(\".hg*\", \"tests\", \"testdata\", \"modules\", \"docs\", \"locale\", *extra_ignores)\n    for package_name in packages_names:\n        if op.exists(package_name):\n            source_path = package_name\n        else:\n            mod = __import__(package_name)\n            source_path = mod.__file__\n            if mod.__file__.endswith(\"__init__.py\"):\n                source_path = op.dirname(source_path)\n        dest_name = op.basename(source_path)\n        dest_path = op.join(dest, dest_name)\n        if op.exists(dest_path):\n            if op.islink(dest_path):\n                os.unlink(dest_path)\n            else:\n                shutil.rmtree(dest_path)\n        print(f\"Copying package at {source_path} to {dest_path}\")\n        if create_links:\n            os.symlink(op.abspath(source_path), dest_path)\n        else:\n            if op.isdir(source_path):\n                shutil.copytree(source_path, dest_path, ignore=ignore)\n            else:\n                shutil.copy(source_path, dest_path)\n\n\ndef build_debian_changelog(\n    changelogpath: os.PathLike,\n    destfile: os.PathLike,\n    pkgname: str,\n    from_version: Union[str, None] = None,\n    distribution: str = \"precise\",\n    fix_version: Union[str, None] = None,\n) -> None:\n    \"\"\"Builds a debian changelog out of a YAML changelog.\n\n    Use fix_version to patch the top changelog to that version (if, for example, there was a\n    packaging error and you need to quickly fix it)\n    \"\"\"\n\n    def desc2list(desc):\n        # We take each item, enumerated with the '*' character, and transform it into a list.\n        desc = desc.replace(\"\\n\", \" \")\n        desc = desc.replace(\"  \", \" \")\n        result = desc.split(\"*\")\n        return [s.strip() for s in result if s.strip()]\n\n    ENTRY_MODEL = (\n        \"{pkg} ({version}) {distribution}; urgency=low\\n\\n{changes}\\n \"\n        \"-- Virgil Dupras <hsoft@hardcoded.net>  {date}\\n\\n\"\n    )\n    CHANGE_MODEL = \"  * {description}\\n\"\n    changelogs = read_changelog_file(changelogpath)\n    if from_version:\n        # We only want logs from a particular version\n        for index, log in enumerate(changelogs):\n            if log[\"version\"] == from_version:\n                changelogs = changelogs[: index + 1]\n                break\n    if fix_version:\n        changelogs[0][\"version\"] = fix_version\n    rendered_logs = []\n    for log in changelogs:\n        version = log[\"version\"]\n        logdate = log[\"date\"]\n        desc = log[\"description\"]\n        rendered_date = logdate.strftime(\"%a, %d %b %Y 00:00:00 +0000\")\n        rendered_descs = [CHANGE_MODEL.format(description=d) for d in desc2list(desc)]\n        changes = \"\".join(rendered_descs)\n        rendered_log = ENTRY_MODEL.format(\n            pkg=pkgname,\n            version=version,\n            changes=changes,\n            date=rendered_date,\n            distribution=distribution,\n        )\n        rendered_logs.append(rendered_log)\n    result = \"\".join(rendered_logs)\n    fp = open(destfile, \"w\")\n    fp.write(result)\n    fp.close()\n\n\nre_changelog_header = re.compile(r\"=== ([\\d.b]*) \\(([\\d\\-]*)\\)\")\n\n\ndef read_changelog_file(filename: os.PathLike) -> List[Dict[str, Any]]:\n    def iter_by_three(it):\n        while True:\n            try:\n                version = next(it)\n                date = next(it)\n                description = next(it)\n            except StopIteration:\n                return\n            yield version, date, description\n\n    with open(filename, encoding=\"utf-8\") as fp:\n        contents = fp.read()\n    splitted = re_changelog_header.split(contents)[1:]  # the first item is empty\n    result = []\n    for version, date_str, description in iter_by_three(iter(splitted)):\n        date = datetime.strptime(date_str, \"%Y-%m-%d\").date()\n        d = {\n            \"date\": date,\n            \"date_str\": date_str,\n            \"version\": version,\n            \"description\": description.strip(),\n        }\n        result.append(d)\n    return result\n\n\ndef fix_qt_resource_file(path: os.PathLike) -> None:\n    # pyrcc5 under Windows, if the locale is non-english, can produce a source file with a date\n    # containing accented characters. If it does, the encoding is wrong and it prevents the file\n    # from being correctly frozen by cx_freeze. To work around that, we open the file, strip all\n    # comments, and save.\n    with open(path, \"rb\") as fp:\n        contents = fp.read()\n    lines = contents.split(b\"\\n\")\n    lines = [line for line in lines if not line.startswith(b\"#\")]\n    with open(path, \"wb\") as fp:\n        fp.write(b\"\\n\".join(lines))\n"
  },
  {
    "path": "hscommon/conflict.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2008-01-08\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\"\"\"When you have to deal with names that have to be unique and can conflict together, you can use\nthis module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name.\n\"\"\"\n\nimport re\nimport os\nimport shutil\n\nfrom errno import EISDIR, EACCES\nfrom pathlib import Path\nfrom typing import Callable, List\n\n# This matches [123], but not [12] (3 digits being the minimum).\n# It also matches [1234] [12345] etc..\n# And only at the start of the string\nre_conflict = re.compile(r\"^\\[\\d{3}\\d*\\] \")\n\n\ndef get_conflicted_name(other_names: List[str], name: str) -> str:\n    \"\"\"Returns name with a ``[000]`` number in front of it.\n\n    The number between brackets depends on how many conlicted filenames\n    there already are in other_names.\n    \"\"\"\n    name = get_unconflicted_name(name)\n    if name not in other_names:\n        return name\n    i = 0\n    while True:\n        newname = \"[%03d] %s\" % (i, name)\n        if newname not in other_names:\n            return newname\n        i += 1\n\n\ndef get_unconflicted_name(name: str) -> str:\n    \"\"\"Returns ``name`` without ``[]`` brackets.\n\n    Brackets which, of course, might have been added by func:`get_conflicted_name`.\n    \"\"\"\n    return re_conflict.sub(\"\", name, 1)\n\n\ndef is_conflicted(name: str) -> bool:\n    \"\"\"Returns whether ``name`` is prepended with a bracketed number.\"\"\"\n    return re_conflict.match(name) is not None\n\n\ndef _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None:\n    \"\"\"Use move() or copy() to move and copy file with the conflict management.\"\"\"\n    if dest_path.is_dir() and not source_path.is_dir():\n        dest_path = dest_path.joinpath(source_path.name)\n    if dest_path.exists():\n        filename = dest_path.name\n        dest_dir_path = dest_path.parent\n        newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename)\n        dest_path = dest_dir_path.joinpath(newname)\n    operation(str(source_path), str(dest_path))\n\n\ndef smart_move(source_path: Path, dest_path: Path) -> None:\n    \"\"\"Same as :func:`smart_copy`, but it moves files instead.\"\"\"\n    _smart_move_or_copy(shutil.move, source_path, dest_path)\n\n\ndef smart_copy(source_path: Path, dest_path: Path) -> None:\n    \"\"\"Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution.\"\"\"\n    try:\n        _smart_move_or_copy(shutil.copy, source_path, dest_path)\n    except OSError as e:\n        # It's a directory, code is 21 on OS X / Linux (EISDIR) and 13 on Windows (EACCES)\n        if e.errno in (EISDIR, EACCES):\n            _smart_move_or_copy(shutil.copytree, source_path, dest_path)\n        else:\n            raise\n"
  },
  {
    "path": "hscommon/desktop.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2013-10-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom enum import Enum\nfrom os import PathLike\nimport os.path as op\nimport logging\n\n\nclass SpecialFolder(Enum):\n    APPDATA = 1\n    CACHE = 2\n\n\ndef open_url(url: str) -> None:\n    \"\"\"Open ``url`` with the default browser.\"\"\"\n    _open_url(url)\n\n\ndef open_path(path: PathLike) -> None:\n    \"\"\"Open ``path`` with its associated application.\"\"\"\n    _open_path(str(path))\n\n\ndef reveal_path(path: PathLike) -> None:\n    \"\"\"Open the folder containing ``path`` with the default file browser.\"\"\"\n    _reveal_path(str(path))\n\n\ndef special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:\n    \"\"\"Returns the path of ``special_folder``.\n\n    ``special_folder`` is a SpecialFolder.* const. The result is the special folder for the current\n    application. The running process' application info is used to determine relevant information.\n\n    You can override the application name with ``appname``. This argument is ingored under Qt.\n    \"\"\"\n    return _special_folder_path(special_folder, portable=portable)\n\n\ntry:\n    from PyQt5.QtCore import QUrl, QStandardPaths\n    from PyQt5.QtGui import QDesktopServices\n    from qt.util import get_appdata\n    from core.util import executable_folder\n    from hscommon.plat import ISWINDOWS, ISOSX\n    import subprocess\n\n    def _open_url(url: str) -> None:\n        QDesktopServices.openUrl(QUrl(url))\n\n    def _open_path(path: str) -> None:\n        url = QUrl.fromLocalFile(str(path))\n        QDesktopServices.openUrl(url)\n\n    def _reveal_path(path: str) -> None:\n        if ISWINDOWS:\n            subprocess.run([\"explorer\", \"/select,\", op.abspath(path)])\n        elif ISOSX:\n            subprocess.run([\"open\", \"-R\", op.abspath(path)])\n        else:\n            _open_path(op.dirname(str(path)))\n\n    def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:\n        if special_folder == SpecialFolder.CACHE:\n            if ISWINDOWS and portable:\n                folder = op.join(executable_folder(), \"cache\")\n            else:\n                folder = QStandardPaths.standardLocations(QStandardPaths.CacheLocation)[0]\n        else:\n            folder = get_appdata(portable)\n        return folder\n\nexcept ImportError:\n    # We're either running tests, and these functions don't matter much or we're in a really\n    # weird situation. Let's just have dummy fallbacks.\n    logging.warning(\"Can't setup desktop functions!\")\n\n    def _open_url(url: str) -> None:\n        # Dummy for tests\n        pass\n\n    def _open_path(path: str) -> None:\n        # Dummy for tests\n        pass\n\n    def _reveal_path(path: str) -> None:\n        # Dummy for tests\n        pass\n\n    def _special_folder_path(special_folder: SpecialFolder, portable: bool = False) -> str:\n        return \"/tmp\"\n"
  },
  {
    "path": "hscommon/gui/__init__.py",
    "content": ""
  },
  {
    "path": "hscommon/gui/base.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\ndef noop(*args, **kwargs):\n    pass\n\n\nclass NoopGUI:\n    def __getattr__(self, func_name):\n        return noop\n\n\nclass GUIObject:\n    \"\"\"Cross-toolkit \"model\" representation of a GUI layer object.\n\n    A ``GUIObject`` is a cross-toolkit \"model\" representation of a GUI layer object, for example, a\n    table. It acts as a cross-toolkit interface to what we call here a :attr:`view`. That\n    view is a toolkit-specific controller to the actual view (an ``NSTableView``, a ``QTableView``,\n    etc.). In our GUIObject, we need a reference to that toolkit-specific controller because some\n    actions have effects on it (for example, prompting it to refresh its data). The ``GUIObject``\n    is typically instantiated before its :attr:`view`, that is why we set it to ``None`` on init.\n    However, the GUI layer is supposed to set the view as soon as its toolkit-specific controller is\n    instantiated.\n\n    When you subclass ``GUIObject``, you will likely want to update its view on instantiation. That\n    is why we call ``self.view.refresh()`` in :meth:`_view_updated`. If you need another type of\n    action on view instantiation, just override the method.\n\n    Most of the time, you will only one to bind a view once in the lifetime of your GUI object.\n    That is why there are safeguards, when setting ``view`` to ensure that we don't double-assign.\n    However, sometimes you want to be able to re-bind another view. In this case, set the\n    ``multibind`` flag to ``True`` and the safeguard will be disabled.\n    \"\"\"\n\n    def __init__(self, multibind: bool = False) -> None:\n        self._view = None\n        self._multibind = multibind\n\n    def _view_updated(self) -> None:\n        \"\"\"(Virtual) Called after :attr:`view` has been set.\n\n        Doing nothing by default, this method is called after :attr:`view` has been set (it isn't\n        called when it's unset, however). Use this for initialization code that requires a view\n        (which is often the whole of the initialization code).\n        \"\"\"\n\n    def has_view(self) -> bool:\n        return (self._view is not None) and (not isinstance(self._view, NoopGUI))\n\n    @property\n    def view(self):\n        \"\"\"A reference to our toolkit-specific view controller.\n\n        *view answering to GUIObject sublass's view protocol*. *get/set*\n\n        This view starts as ``None`` and has to be set \"manually\". There's two times at which we set\n        the view property: On initialization, where we set the view that we'll use for our lifetime,\n        and just before the view is deallocated. We need to unset our view at that time to avoid\n        calls to a deallocated instance (which means a crash).\n\n        To unset our view, we simple assign it to ``None``.\n        \"\"\"\n        return self._view\n\n    @view.setter\n    def view(self, value) -> None:\n        if self._view is None and value is None:\n            # Initial view assignment\n            return\n        if self._view is None or self._multibind:\n            if value is None:\n                value = NoopGUI()\n            self._view = value\n            self._view_updated()\n        else:\n            assert value is None\n            # Instead of None, we put a NoopGUI() there to avoid rogue view callback raising an\n            # exception.\n            self._view = NoopGUI()\n"
  },
  {
    "path": "hscommon/gui/column.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-07-25\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport copy\nfrom typing import Any, List, Tuple, Union\n\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.gui.table import GUITable\n\n\nclass Column:\n    \"\"\"Holds column attributes such as its name, width, visibility, etc.\n\n    These attributes are then used to correctly configure the column on the \"view\" side.\n    \"\"\"\n\n    def __init__(self, name: str, display: str = \"\", visible: bool = True, optional: bool = False) -> None:\n        #: \"programmatical\" (not for display) name. Used as a reference in a couple of place, such\n        #: as :meth:`Columns.column_by_name`.\n        self.name = name\n        #: Immutable index of the column. Doesn't change even when columns are re-ordered. Used in\n        #: :meth:`Columns.column_by_index`.\n        self.logical_index = 0\n        #: Index of the column in the ordered set of columns.\n        self.ordered_index = 0\n        #: Width of the column.\n        self.width = 0\n        #: Default width of the column. This value usually depends on the platform and is set on\n        #: columns initialisation. It will be used if column restoration doesn't contain any\n        #: \"remembered\" widths.\n        self.default_width = 0\n        #: Display name (title) of the column.\n        self.display = display\n        #: Whether the column is visible.\n        self.visible = visible\n        #: Whether the column is visible by default. It will be used if column restoration doesn't\n        #: contain any \"remembered\" widths.\n        self.default_visible = visible\n        #: Whether the column can have :attr:`visible` set to false.\n        self.optional = optional\n\n\nclass ColumnsView:\n    \"\"\"Expected interface for :class:`Columns`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view, the columns controller of a table or outline, is expected to properly respond to\n    callbacks.\n    \"\"\"\n\n    def restore_columns(self) -> None:\n        \"\"\"Update all columns according to the model.\n\n        When this is called, our view has to update the columns title, order and visibility of all\n        columns.\n        \"\"\"\n\n    def set_column_visible(self, colname: str, visible: bool) -> None:\n        \"\"\"Update visibility of column ``colname``.\n\n        Called when the user toggles the visibility of a column, we must update the column\n        ``colname``'s visibility status to ``visible``.\n        \"\"\"\n\n\nclass PrefAccessInterface:\n    \"\"\"Expected interface for :class:`Columns`'s prefaccess.\n\n    *Not actually used in the code. For documentation purposes only.*\n    \"\"\"\n\n    def get_default(self, key: str, fallback_value: Union[Any, None]) -> Any:\n        \"\"\"Retrieve the value for ``key`` in the currently running app's preference store.\n\n        If the key doesn't exist, return ``fallback_value``.\n        \"\"\"\n\n    def set_default(self, key: str, value: Any) -> None:\n        \"\"\"Set the value ``value`` for ``key`` in the currently running app's preference store.\"\"\"\n\n\nclass Columns(GUIObject):\n    \"\"\"Cross-toolkit GUI-enabled column set for tables or outlines.\n\n    Manages a column set's order, visibility and width. We also manage the persistence of these\n    attributes so that we can restore them on the next run.\n\n    Subclasses :class:`.GUIObject`. Expected view: :class:`ColumnsView`.\n\n    :param table: The table the columns belong to. It's from there that we retrieve our column\n                  configuration and it must have a ``COLUMNS`` attribute which is a list of\n                  :class:`Column`. We also call :meth:`~.GUITable.save_edits` on it from time to\n                  time. Technically, this argument can also be a tree, but there's probably some\n                  sorting in the code to do to support this option cleanly.\n    :param prefaccess: An object giving access to user preferences for the currently running app.\n                       We use this to make column attributes persistent. Must follow\n                       :class:`PrefAccessInterface`.\n    :param str savename: The name under which column preferences will be saved. This name is in fact\n                         a prefix. Preferences are saved under more than one name, but they will all\n                         have that same prefix.\n    \"\"\"\n\n    def __init__(self, table: GUITable, prefaccess=None, savename: Union[str, None] = None):\n        GUIObject.__init__(self)\n        self.table = table\n        self.prefaccess = prefaccess\n        self.savename = savename\n        # We use copy here for test isolation. If we don't, changing a column affects all tests.\n        self.column_list: List[Column] = list(map(copy.copy, table.COLUMNS))\n        for i, column in enumerate(self.column_list):\n            column.logical_index = i\n            column.ordered_index = i\n        self.coldata = {col.name: col for col in self.column_list}\n\n    # --- Private\n    def _get_colname_attr(self, colname: str, attrname: str, default: Any) -> Any:\n        try:\n            return getattr(self.coldata[colname], attrname)\n        except KeyError:\n            return default\n\n    def _set_colname_attr(self, colname: str, attrname: str, value: Any) -> None:\n        try:\n            col = self.coldata[colname]\n            setattr(col, attrname, value)\n        except KeyError:\n            pass\n\n    def _optional_columns(self) -> List[Column]:\n        return [c for c in self.column_list if c.optional]\n\n    # --- Override\n    def _view_updated(self) -> None:\n        self.restore_columns()\n\n    # --- Public\n    def column_by_index(self, index: int):\n        \"\"\"Return the :class:`Column` having the :attr:`~Column.logical_index` ``index``.\"\"\"\n        return self.column_list[index]\n\n    def column_by_name(self, name: str):\n        \"\"\"Return the :class:`Column` having the :attr:`~Column.name` ``name``.\"\"\"\n        return self.coldata[name]\n\n    def columns_count(self) -> int:\n        \"\"\"Returns the number of columns in our set.\"\"\"\n        return len(self.column_list)\n\n    def column_display(self, colname: str) -> str:\n        \"\"\"Returns display name for column named ``colname``, or ``''`` if there's none.\"\"\"\n        return self._get_colname_attr(colname, \"display\", \"\")\n\n    def column_is_visible(self, colname: str) -> bool:\n        \"\"\"Returns visibility for column named ``colname``, or ``True`` if there's none.\"\"\"\n        return self._get_colname_attr(colname, \"visible\", True)\n\n    def column_width(self, colname: str) -> int:\n        \"\"\"Returns width for column named ``colname``, or ``0`` if there's none.\"\"\"\n        return self._get_colname_attr(colname, \"width\", 0)\n\n    def columns_to_right(self, colname: str) -> List[str]:\n        \"\"\"Returns the list of all columns to the right of ``colname``.\n\n        \"right\" meaning \"having a higher :attr:`Column.ordered_index`\" in our left-to-right\n        civilization.\n        \"\"\"\n        column = self.coldata[colname]\n        index = column.ordered_index\n        return [col.name for col in self.column_list if (col.visible and col.ordered_index > index)]\n\n    def menu_items(self) -> List[Tuple[str, bool]]:\n        \"\"\"Returns a list of items convenient for quick visibility menu generation.\n\n        Returns a list of ``(display_name, is_marked)`` items for each optional column in the\n        current view (``is_marked`` means that it's visible).\n\n        You can use this to generate a menu to let the user toggle the visibility of an optional\n        column. That is why we only show optional column, because the visibility of mandatory\n        columns can't be toggled.\n        \"\"\"\n        return [(c.display, c.visible) for c in self._optional_columns()]\n\n    def move_column(self, colname: str, index: int) -> None:\n        \"\"\"Moves column ``colname`` to ``index``.\n\n        The column will be placed just in front of the column currently having that index, or to the\n        end of the list if there's none.\n        \"\"\"\n        colnames = self.colnames\n        colnames.remove(colname)\n        colnames.insert(index, colname)\n        self.set_column_order(colnames)\n\n    def reset_to_defaults(self) -> None:\n        \"\"\"Reset all columns' width and visibility to their default values.\"\"\"\n        self.set_column_order([col.name for col in self.column_list])\n        for col in self._optional_columns():\n            col.visible = col.default_visible\n            col.width = col.default_width\n        self.view.restore_columns()\n\n    def resize_column(self, colname: str, newwidth: int) -> None:\n        \"\"\"Set column ``colname``'s width to ``newwidth``.\"\"\"\n        self._set_colname_attr(colname, \"width\", newwidth)\n\n    def restore_columns(self) -> None:\n        \"\"\"Restore's column persistent attributes from the last :meth:`save_columns`.\"\"\"\n        if not (self.prefaccess and self.savename and self.coldata):\n            if (not self.savename) and (self.coldata):\n                # This is a table that will not have its coldata saved/restored. we should\n                # \"restore\" its default column attributes.\n                self.view.restore_columns()\n            return\n        for col in self.column_list:\n            pref_name = f\"{self.savename}.Columns.{col.name}\"\n            coldata = self.prefaccess.get_default(pref_name, fallback_value={})\n            if \"index\" in coldata:\n                col.ordered_index = coldata[\"index\"]\n            if \"width\" in coldata:\n                col.width = coldata[\"width\"]\n            if col.optional and \"visible\" in coldata:\n                col.visible = coldata[\"visible\"]\n        self.view.restore_columns()\n\n    def save_columns(self) -> None:\n        \"\"\"Save column attributes in persistent storage for restoration in :meth:`restore_columns`.\"\"\"\n        if not (self.prefaccess and self.savename and self.coldata):\n            return\n        for col in self.column_list:\n            pref_name = f\"{self.savename}.Columns.{col.name}\"\n            coldata = {\"index\": col.ordered_index, \"width\": col.width}\n            if col.optional:\n                coldata[\"visible\"] = col.visible\n            self.prefaccess.set_default(pref_name, coldata)\n\n    # TODO annotate colnames\n    def set_column_order(self, colnames) -> None:\n        \"\"\"Change the columns order so it matches the order in ``colnames``.\n\n        :param colnames: A list of column names in the desired order.\n        \"\"\"\n        colnames = (name for name in colnames if name in self.coldata)\n        for i, colname in enumerate(colnames):\n            col = self.coldata[colname]\n            col.ordered_index = i\n\n    def set_column_visible(self, colname: str, visible: bool) -> None:\n        \"\"\"Set the visibility of column ``colname``.\"\"\"\n        self.table.save_edits()  # the table on the GUI side will stop editing when the columns change\n        self._set_colname_attr(colname, \"visible\", visible)\n        self.view.set_column_visible(colname, visible)\n\n    def set_default_width(self, colname: str, width: int) -> None:\n        \"\"\"Set the default width or column ``colname``.\"\"\"\n        self._set_colname_attr(colname, \"default_width\", width)\n\n    def toggle_menu_item(self, index: int) -> bool:\n        \"\"\"Toggles the visibility of an optional column.\n\n        You know, that optional column menu you've generated in :meth:`menu_items`? Well, ``index``\n        is the index of them menu item in *that* menu that the user has clicked on to toggle it.\n\n        Returns whether the column in question ends up being visible or not.\n        \"\"\"\n        col = self._optional_columns()[index]\n        self.set_column_visible(col.name, not col.visible)\n        return col.visible\n\n    # --- Properties\n    @property\n    def ordered_columns(self) -> List[Column]:\n        \"\"\"List of :class:`Column` in visible order.\"\"\"\n        return [col for col in sorted(self.column_list, key=lambda col: col.ordered_index)]\n\n    @property\n    def colnames(self) -> List[str]:\n        \"\"\"List of column names in visible order.\"\"\"\n        return [col.name for col in self.ordered_columns]\n"
  },
  {
    "path": "hscommon/gui/progress_window.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom typing import Callable, Tuple, Union\nfrom hscommon.jobprogress.performer import ThreadedJobPerformer\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.gui.text_field import TextField\n\n\nclass ProgressWindowView:\n    \"\"\"Expected interface for :class:`ProgressWindow`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view, some kind window with a progress bar, two labels and a cancel button, is expected\n    to properly respond to its callbacks.\n\n    It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.\n    \"\"\"\n\n    def show(self) -> None:\n        \"\"\"Show the dialog.\"\"\"\n\n    def close(self) -> None:\n        \"\"\"Close the dialog.\"\"\"\n\n    def set_progress(self, progress: int) -> None:\n        \"\"\"Set the progress of the progress bar to ``progress``.\n\n        Not all jobs are equally responsive on their job progress report and it is recommended that\n        you put your progressbar in \"indeterminate\" mode as long as you haven't received the first\n        ``set_progress()`` call to avoid letting the user think that the app is frozen.\n\n        :param int progress: a value between ``0`` and ``100``.\n        \"\"\"\n\n\nclass ProgressWindow(GUIObject, ThreadedJobPerformer):\n    \"\"\"Cross-toolkit GUI-enabled progress window.\n\n    This class allows you to run a long running, job enabled function in a separate thread and\n    allow the user to follow its progress with a progress dialog.\n\n    To use it, you start your long-running job with :meth:`run` and then have your UI layer\n    regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call\n    :meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related\n    functions from the main thread.\n\n    We subclass :class:`.GUIObject` and :class:`.ThreadedJobPerformer`.\n    Expected view: :class:`ProgressWindowView`.\n\n    :param finish_func: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is\n                        an arbitrary id passed to :meth:`run`.\n    :param error_func: A function ``f(jobid, err)`` that is called when an exception is raised and\n                       unhandled during the job. If not specified, the error will be raised in the\n                       main thread. If it's specified, it's your responsibility to raise the error\n                       if you want to. If the function returns ``True``, ``finish_func()`` will be\n                       called as if the job terminated normally.\n    \"\"\"\n\n    def __init__(\n        self,\n        finish_func: Callable[[Union[str, None]], None],\n        error_func: Callable[[Union[str, None], Exception], bool] = None,\n    ) -> None:\n        # finish_func(jobid) is the function that is called when a job is completed.\n        GUIObject.__init__(self)\n        ThreadedJobPerformer.__init__(self)\n        self._finish_func = finish_func\n        self._error_func = error_func\n        #: :class:`.TextField`. It contains that title you gave the job on :meth:`run`.\n        self.jobdesc_textfield = TextField()\n        #: :class:`.TextField`. It contains the job textual update that the function might yield\n        #: during its course.\n        self.progressdesc_textfield = TextField()\n        self.jobid: Union[str, None] = None\n\n    def cancel(self) -> None:\n        \"\"\"Call for a user-initiated job cancellation.\"\"\"\n        # The UI is sometimes a bit buggy and calls cancel() on self.view.close(). We just want to\n        # make sure that this doesn't lead us to think that the user acually cancelled the task, so\n        # we verify that the job is still running.\n        if self._job_running:\n            self.job_cancelled = True\n\n    def pulse(self) -> None:\n        \"\"\"Update progress reports in the GUI.\n\n        Call this regularly from the GUI main run loop. The values might change before\n        :meth:`ProgressWindowView.set_progress` happens.\n\n        If the job is finished, ``pulse()`` will take care of closing the window and re-raising any\n        exception that might have been raised during the job (in the main thread this time). If\n        there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action.\n        \"\"\"\n        last_progress = self.last_progress\n        last_desc = self.last_desc\n        if not self._job_running or last_progress is None:\n            self.view.close()\n            should_continue = True\n            if self.last_error is not None:\n                err = self.last_error.with_traceback(self.last_traceback)\n                if self._error_func is not None:\n                    should_continue = self._error_func(self.jobid, err)\n                else:\n                    raise err\n            if not self.job_cancelled and should_continue:\n                self._finish_func(self.jobid)\n            return\n        if self.job_cancelled:\n            return\n        if last_desc:\n            self.progressdesc_textfield.text = last_desc\n        self.view.set_progress(last_progress)\n\n    def run(self, jobid: str, title: str, target: Callable, args: Tuple = ()):\n        \"\"\"Starts a threaded job.\n\n        The ``target`` function will be sent, as its first argument, a :class:`.Job` instance which\n        it can use to report on its progress.\n\n        :param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.\n        :param title: A title for the task you're starting.\n        :param target: The function that does your famous long running job.\n        :param args: additional arguments that you want to send to ``target``.\n        \"\"\"\n        # target is a function with its first argument being a Job. It can then be followed by other\n        # arguments which are passed as `args`.\n        self.jobid = jobid\n        self.progressdesc_textfield.text = \"\"\n        j = self.create_job()\n        args = tuple([j] + list(args))\n        self.run_threaded(target, args)\n        self.jobdesc_textfield.text = title\n        self.view.show()\n"
  },
  {
    "path": "hscommon/gui/selectable_list.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-09-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom collections.abc import Sequence, MutableSequence\n\nfrom hscommon.gui.base import GUIObject\n\n\nclass Selectable(Sequence):\n    \"\"\"Mix-in for a ``Sequence`` that manages its selection status.\n\n    When mixed in with a ``Sequence``, we enable it to manage its selection status. The selection\n    is held as a list of ``int`` indexes. Multiple selection is supported.\n    \"\"\"\n\n    def __init__(self):\n        self._selected_indexes = []\n\n    # --- Private\n    def _check_selection_range(self):\n        if not self:\n            self._selected_indexes = []\n        if not self._selected_indexes:\n            return\n        self._selected_indexes = [index for index in self._selected_indexes if index < len(self)]\n        if not self._selected_indexes:\n            self._selected_indexes = [len(self) - 1]\n\n    # --- Virtual\n    def _update_selection(self):\n        \"\"\"(Virtual) Updates the model's selection appropriately.\n\n        Called after selection has been updated. Takes the table's selection and does appropriates\n        updates on the view and/or model. Common sense would dictate that when the selection doesn't\n        change, we don't update anything (and thus don't call ``_update_selection()`` at all), but\n        there are cases where it's false. For example, if our list updates its items but doesn't\n        change its selection, we probably want to update the model's selection.\n\n        By default, does nothing.\n\n        Important note: This is only called on :meth:`select`, not on changes to\n        :attr:`selected_indexes`.\n        \"\"\"\n        # A redesign of how this whole thing works is probably in order, but not now, there's too\n        # much breakage at once involved.\n\n    # --- Public\n    def select(self, indexes):\n        \"\"\"Update selection to ``indexes``.\n\n        :meth:`_update_selection` is called afterwards.\n\n        :param list indexes: List of ``int`` that is to become the new selection.\n        \"\"\"\n        if isinstance(indexes, int):\n            indexes = [indexes]\n        self.selected_indexes = indexes\n        self._update_selection()\n\n    # --- Properties\n    @property\n    def selected_index(self):\n        \"\"\"Points to the first selected index.\n\n        *int*. *get/set*.\n\n        Thin wrapper around :attr:`selected_indexes`. ``None`` if selection is empty. Using this\n        property only makes sense if your selectable sequence supports single selection only.\n        \"\"\"\n        return self._selected_indexes[0] if self._selected_indexes else None\n\n    @selected_index.setter\n    def selected_index(self, value):\n        self.selected_indexes = [value]\n\n    @property\n    def selected_indexes(self):\n        \"\"\"List of selected indexes.\n\n        *list of int*. *get/set*.\n\n        When setting the value, automatically removes out-of-bounds indexes. The list is kept\n        sorted.\n        \"\"\"\n        return self._selected_indexes\n\n    @selected_indexes.setter\n    def selected_indexes(self, value):\n        self._selected_indexes = value\n        self._selected_indexes.sort()\n        self._check_selection_range()\n\n\nclass SelectableList(MutableSequence, Selectable):\n    \"\"\"A list that can manage selection of its items.\n\n    Subclasses :class:`Selectable`. Behaves like a ``list``.\n    \"\"\"\n\n    def __init__(self, items=None):\n        Selectable.__init__(self)\n        if items:\n            self._items = list(items)\n        else:\n            self._items = []\n\n    def __delitem__(self, key):\n        self._items.__delitem__(key)\n        self._check_selection_range()\n        self._on_change()\n\n    def __getitem__(self, key):\n        return self._items.__getitem__(key)\n\n    def __len__(self):\n        return len(self._items)\n\n    def __setitem__(self, key, value):\n        self._items.__setitem__(key, value)\n        self._on_change()\n\n    # --- Override\n    def append(self, item):\n        self._items.append(item)\n        self._on_change()\n\n    def insert(self, index, item):\n        self._items.insert(index, item)\n        self._on_change()\n\n    def remove(self, row):\n        self._items.remove(row)\n        self._check_selection_range()\n        self._on_change()\n\n    # --- Virtual\n    def _on_change(self):\n        \"\"\"(Virtual) Called whenever the contents of the list changes.\n\n        By default, does nothing.\n        \"\"\"\n\n    # --- Public\n    def search_by_prefix(self, prefix):\n        # XXX Why the heck is this method here?\n        prefix = prefix.lower()\n        for index, s in enumerate(self):\n            if s.lower().startswith(prefix):\n                return index\n        return -1\n\n\nclass GUISelectableListView:\n    \"\"\"Expected interface for :class:`GUISelectableList`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view, some kind of list view or combobox, is expected to sync with the list's contents by\n    appropriately behave to all callbacks in this interface.\n    \"\"\"\n\n    def refresh(self):\n        \"\"\"Refreshes the contents of the list widget.\n\n        Ensures that the contents of the list widget is synced with the model.\n        \"\"\"\n\n    def update_selection(self):\n        \"\"\"Update selection status.\n\n        Ensures that the list widget's selection is in sync with the model.\n        \"\"\"\n\n\nclass GUISelectableList(SelectableList, GUIObject):\n    \"\"\"Cross-toolkit GUI-enabled list view.\n\n    Represents a UI element presenting the user with a selectable list of items.\n\n    Subclasses :class:`SelectableList` and :class:`.GUIObject`. Expected view:\n    :class:`GUISelectableListView`.\n\n    :param iterable items: If specified, items to fill the list with initially.\n    \"\"\"\n\n    def __init__(self, items=None):\n        SelectableList.__init__(self, items)\n        GUIObject.__init__(self)\n\n    def _view_updated(self):\n        \"\"\"Refreshes the view contents with :meth:`GUISelectableListView.refresh`.\n\n        Overrides :meth:`~hscommon.gui.base.GUIObject._view_updated`.\n        \"\"\"\n        self.view.refresh()\n\n    def _update_selection(self):\n        \"\"\"Refreshes the view selection with :meth:`GUISelectableListView.update_selection`.\n\n        Overrides :meth:`Selectable._update_selection`.\n        \"\"\"\n        self.view.update_selection()\n\n    def _on_change(self):\n        \"\"\"Refreshes the view contents with :meth:`GUISelectableListView.refresh`.\n\n        Overrides :meth:`SelectableList._on_change`.\n        \"\"\"\n        self.view.refresh()\n"
  },
  {
    "path": "hscommon/gui/table.py",
    "content": "# Created By: Eric Mc Sween\n# Created On: 2008-05-29\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom collections.abc import MutableSequence\nfrom collections import namedtuple\nfrom typing import Any, List, Tuple, Union\n\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.gui.selectable_list import Selectable\n\n\n# We used to directly subclass list, but it caused problems at some point with deepcopy\nclass Table(MutableSequence, Selectable):\n    \"\"\"Sortable and selectable sequence of :class:`Row`.\n\n    In fact, the Table is very similar to :class:`.SelectableList` in\n    practice and differs mostly in principle. Their difference lies in the nature of their items\n    they manage. With the Table, rows usually have many properties, presented in columns, and they\n    have to subclass :class:`Row`.\n\n    Usually used with :class:`~hscommon.gui.column.Column`.\n\n    Subclasses :class:`.Selectable`.\n    \"\"\"\n\n    # Should be List[Column], but have circular import...\n    COLUMNS: List = []\n\n    def __init__(self) -> None:\n        Selectable.__init__(self)\n        self._rows: List[\"Row\"] = []\n        self._header: Union[\"Row\", None] = None\n        self._footer: Union[\"Row\", None] = None\n\n    # TODO type hint for key\n    def __delitem__(self, key):\n        self._rows.__delitem__(key)\n        if self._header is not None and ((not self) or (self[0] is not self._header)):\n            self._header = None\n        if self._footer is not None and ((not self) or (self[-1] is not self._footer)):\n            self._footer = None\n        self._check_selection_range()\n\n    # TODO type hint for key\n    def __getitem__(self, key) -> Any:\n        return self._rows.__getitem__(key)\n\n    def __len__(self) -> int:\n        return len(self._rows)\n\n    # TODO type hint for key\n    def __setitem__(self, key, value: Any) -> None:\n        self._rows.__setitem__(key, value)\n\n    def append(self, item: \"Row\") -> None:\n        \"\"\"Appends ``item`` at the end of the table.\n\n        If there's a footer, the item is inserted before it.\n        \"\"\"\n        if self._footer is not None:\n            self._rows.insert(-1, item)\n        else:\n            self._rows.append(item)\n\n    def insert(self, index: int, item: \"Row\") -> None:\n        \"\"\"Inserts ``item`` at ``index`` in the table.\n\n        If there's a header, will make sure we don't insert before it, and if there's a footer, will\n        make sure that we don't insert after it.\n        \"\"\"\n        if (self._header is not None) and (index == 0):\n            index = 1\n        if (self._footer is not None) and (index >= len(self)):\n            index = len(self) - 1\n        self._rows.insert(index, item)\n\n    def remove(self, row: \"Row\") -> None:\n        \"\"\"Removes ``row`` from table.\n\n        If ``row`` is a header or footer, that header or footer will be set to ``None``.\n        \"\"\"\n        if row is self._header:\n            self._header = None\n        if row is self._footer:\n            self._footer = None\n        self._rows.remove(row)\n        self._check_selection_range()\n\n    def sort_by(self, column_name: str, desc: bool = False) -> None:\n        \"\"\"Sort table by ``column_name``.\n\n        Sort key for each row is computed from :meth:`Row.sort_key_for_column`.\n\n        If ``desc`` is ``True``, sort order is reversed.\n\n        If present, header and footer will always be first and last, respectively.\n        \"\"\"\n        if self._header is not None:\n            self._rows.pop(0)\n        if self._footer is not None:\n            self._rows.pop()\n        self._rows.sort(key=lambda row: row.sort_key_for_column(column_name), reverse=desc)\n        if self._header is not None:\n            self._rows.insert(0, self._header)\n        if self._footer is not None:\n            self._rows.append(self._footer)\n\n    # --- Properties\n    @property\n    def footer(self) -> Union[\"Row\", None]:\n        \"\"\"If set, a row that always stay at the bottom of the table.\n\n        :class:`Row`. *get/set*.\n\n        When set to something else than ``None``, ``header`` and ``footer`` represent rows that will\n        always be kept in first and/or last position, regardless of sorting. ``len()`` and indexing\n        will include them, which means that if there's a header, ``table[0]`` returns it and if\n        there's a footer, ``table[-1]`` returns it. To make things short, all list-like functions\n        work with header and footer \"on\". But things get fuzzy for ``append()`` and ``insert()``\n        because these will ensure that no \"normal\" row gets inserted before the header or after the\n        footer.\n\n        Adding and removing footer here and there might seem (and is) hackish, but it's much simpler\n        than the alternative (when, of course, you need such a feature), which is to override magic\n        methods and adjust the results. When we do that, there the slice stuff that we have to\n        implement and it gets quite complex. Moreover, the most frequent operation on a table is\n        ``__getitem__``, and making checks to know whether the key is a header or footer at each\n        call would make that operation, which is the most used, slower.\n        \"\"\"\n        return self._footer\n\n    @footer.setter\n    def footer(self, value: Union[\"Row\", None]) -> None:\n        if self._footer is not None:\n            self._rows.pop()\n        if value is not None:\n            self._rows.append(value)\n        self._footer = value\n\n    @property\n    def header(self) -> Union[\"Row\", None]:\n        \"\"\"If set, a row that always stay at the bottom of the table.\n\n        See :attr:`footer` for details.\n        \"\"\"\n        return self._header\n\n    @header.setter\n    def header(self, value: Union[\"Row\", None]) -> None:\n        if self._header is not None:\n            self._rows.pop(0)\n        if value is not None:\n            self._rows.insert(0, value)\n        self._header = value\n\n    @property\n    def row_count(self) -> int:\n        \"\"\"Number or rows in the table (without counting header and footer).\n\n        *int*. *read-only*.\n        \"\"\"\n        result = len(self)\n        if self._footer is not None:\n            result -= 1\n        if self._header is not None:\n            result -= 1\n        return result\n\n    @property\n    def rows(self) -> List[\"Row\"]:\n        \"\"\"List of rows in the table, excluding header and footer.\n\n        List of :class:`Row`. *read-only*.\n        \"\"\"\n        start = None\n        end = None\n        if self._footer is not None:\n            end = -1\n        if self._header is not None:\n            start = 1\n        return self[start:end]\n\n    @property\n    def selected_row(self) -> \"Row\":\n        \"\"\"Selected row according to :attr:`Selectable.selected_index`.\n\n        :class:`Row`. *get/set*.\n\n        When setting this attribute, we look up the index of the row and set the selected index from\n        there. If the row isn't in the list, selection isn't changed.\n        \"\"\"\n        return self[self.selected_index] if self.selected_index is not None else None\n\n    @selected_row.setter\n    def selected_row(self, value: int) -> None:\n        try:\n            self.selected_index = self.index(value)\n        except ValueError:\n            pass\n\n    @property\n    def selected_rows(self) -> List[\"Row\"]:\n        \"\"\"List of selected rows based on :attr:`.selected_indexes`.\n\n        List of :class:`Row`. *read-only*.\n        \"\"\"\n        return [self[index] for index in self.selected_indexes]\n\n\nclass GUITableView:\n    \"\"\"Expected interface for :class:`GUITable`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view, some kind of table view, is expected to sync with the table's contents by\n    appropriately behave to all callbacks in this interface.\n\n    When in edit mode, the content types by the user is expected to be sent as soon as possible\n    to the :class:`Row`.\n\n    Whenever the user changes the selection, we expect the view to call :meth:`Table.select`.\n    \"\"\"\n\n    def refresh(self) -> None:\n        \"\"\"Refreshes the contents of the table widget.\n\n        Ensures that the contents of the table widget is synced with the model. This includes\n        selection.\n        \"\"\"\n\n    def start_editing(self) -> None:\n        \"\"\"Start editing the currently selected row.\n\n        Begin whatever inline editing support that the view supports.\n        \"\"\"\n\n    def stop_editing(self) -> None:\n        \"\"\"Stop editing if there's an inline editing in effect.\n\n        There's no \"aborting\" implied in this call, so it's appropriate to send whatever the user\n        has typed and might not have been sent down to the :class:`Row` yet. After you've done that,\n        stop the editing mechanism.\n        \"\"\"\n\n\nSortDescriptor = namedtuple(\"SortDescriptor\", \"column desc\")\n\n\nclass GUITable(Table, GUIObject):\n    \"\"\"Cross-toolkit GUI-enabled table view.\n\n    Represents a UI element presenting the user with a sortable, selectable, possibly editable,\n    table view.\n\n    Behaves like the :class:`Table` which it subclasses, but is more focused on being the presenter\n    of some model data to its :attr:`.GUIObject.view`. There's a :meth:`refresh`\n    mechanism which ensures fresh data while preserving sorting order and selection. There's also an\n    editing mechanism which tracks whether (and which) row is being edited (or added) and\n    save/cancel edits when appropriate.\n\n    Subclasses :class:`Table` and :class:`.GUIObject`. Expected view:\n    :class:`GUITableView`.\n    \"\"\"\n\n    def __init__(self) -> None:\n        GUIObject.__init__(self)\n        Table.__init__(self)\n        #: The row being currently edited by the user. ``None`` if no edit is taking place.\n        self.edited: Union[\"Row\", None] = None\n        self._sort_descriptor: Union[SortDescriptor, None] = None\n\n    # --- Virtual\n    def _do_add(self) -> Tuple[\"Row\", int]:\n        \"\"\"(Virtual) Creates a new row, adds it in the table.\n\n        Returns ``(row, insert_index)``.\n        \"\"\"\n        raise NotImplementedError()\n\n    def _do_delete(self) -> None:\n        \"\"\"(Virtual) Delete the selected rows.\"\"\"\n        pass\n\n    def _fill(self) -> None:\n        \"\"\"(Virtual/Required) Fills the table with all the rows that this table is supposed to have.\n\n        Called by :meth:`refresh`. Does nothing by default.\n        \"\"\"\n        pass\n\n    def _is_edited_new(self) -> bool:\n        \"\"\"(Virtual) Returns whether the currently edited row should be considered \"new\".\n\n        This is used in :meth:`cancel_edits` to know whether the cancellation of the edit means a\n        revert of the row's value or the removal of the row.\n\n        By default, always false.\n        \"\"\"\n        return False\n\n    def _restore_selection(self, previous_selection):\n        \"\"\"(Virtual) Restores row selection after a contents-changing operation.\n\n        Before each contents changing operation, we store our previously selected indexes because in\n        many cases, such as in :meth:`refresh`, our selection will be lost. After the operation is\n        over, we call this method with our previously selected indexes (in ``previous_selection``).\n\n        The default behavior is (if we indeed have an empty :attr:`.selected_indexes`) to re-select\n        ``previous_selection``. If it was empty, we select the last row of the table.\n\n        This behavior can, of course, be overriden.\n        \"\"\"\n        if not self.selected_indexes:\n            if previous_selection:\n                self.select(previous_selection)\n            else:\n                self.select([len(self) - 1])\n\n    # --- Public\n    def add(self) -> None:\n        \"\"\"Add a new row in edit mode.\n\n        Requires :meth:`do_add` to be implemented. The newly added row will be selected and in edit\n        mode.\n        \"\"\"\n        self.view.stop_editing()\n        if self.edited is not None:\n            self.save_edits()\n        row, insert_index = self._do_add()\n        self.insert(insert_index, row)\n        self.select([insert_index])\n        self.view.refresh()\n        # We have to set \"edited\" after calling refresh() because some UI are trigger-happy\n        # about calling save_edits() and they do so during calls to refresh(). We don't want\n        # a call to save_edits() during refresh prematurely mess with our newly added item.\n        self.edited = row\n        self.view.start_editing()\n\n    def can_edit_cell(self, column_name: str, row_index: int) -> bool:\n        \"\"\"Returns whether the cell at ``row_index`` and ``column_name`` can be edited.\n\n        A row is, by default, editable as soon as it has an attr with the same name as `column`.\n        If :meth:`Row.can_edit` returns False, the row is not editable at all. You can set\n        editability of rows at the attribute level with can_edit_* properties.\n\n        Mostly just a shortcut to :meth:`Row.can_edit_cell`.\n        \"\"\"\n        row = self[row_index]\n        return row.can_edit_cell(column_name)\n\n    def cancel_edits(self) -> None:\n        \"\"\"Cancels the current edit operation.\n\n        If there's an :attr:`edited` row, it will be re-initialized (with :meth:`Row.load`).\n        \"\"\"\n        if self.edited is None:\n            return\n        self.view.stop_editing()\n        if self._is_edited_new():\n            previous_selection = self.selected_indexes\n            self.remove(self.edited)\n            self._restore_selection(previous_selection)\n            self._update_selection()\n        else:\n            self.edited.load()\n        self.edited = None\n        self.view.refresh()\n\n    def delete(self) -> None:\n        \"\"\"Delete the currently selected rows.\n\n        Requires :meth:`_do_delete` for this to have any effect on the model. Cancels editing if\n        relevant.\n        \"\"\"\n        self.view.stop_editing()\n        if self.edited is not None:\n            self.cancel_edits()\n            return\n        if self:\n            self._do_delete()\n\n    def refresh(self, refresh_view: bool = True) -> None:\n        \"\"\"Empty the table and re-create its rows.\n\n        :meth:`_fill` is called after we emptied the table to create our rows. Previous sort order\n        will be preserved, regardless of the order in which the rows were filled. If there was any\n        edit operation taking place, it's cancelled.\n\n        :param bool refresh_view: Whether we tell our view to refresh after our refill operation.\n                                  Most of the time, it's what we want, but there's some cases where\n                                  we don't.\n        \"\"\"\n        self.cancel_edits()\n        previous_selection = self.selected_indexes\n        del self[:]\n        self._fill()\n        sd = self._sort_descriptor\n        if sd is not None:\n            Table.sort_by(self, column_name=sd.column, desc=sd.desc)\n        self._restore_selection(previous_selection)\n        if refresh_view:\n            self.view.refresh()\n\n    def save_edits(self) -> None:\n        \"\"\"Commit user edits to the model.\n\n        This is done by calling :meth:`Row.save`.\n        \"\"\"\n        if self.edited is None:\n            return\n        row = self.edited\n        self.edited = None\n        row.save()\n\n    def sort_by(self, column_name: str, desc: bool = False) -> None:\n        \"\"\"Sort table by ``column_name``.\n\n        Overrides :meth:`Table.sort_by`. After having performed sorting, calls\n        :meth:`~.Selectable._update_selection` to give you the chance,\n        if appropriate, to update your selected indexes according to, maybe, the selection that you\n        have in your model.\n\n        Then, we refresh our view.\n        \"\"\"\n        Table.sort_by(self, column_name=column_name, desc=desc)\n        self._sort_descriptor = SortDescriptor(column_name, desc)\n        self._update_selection()\n        self.view.refresh()\n\n\nclass Row:\n    \"\"\"Represents a row in a :class:`Table`.\n\n    It holds multiple values to be represented through columns. It's its role to prepare data\n    fetched from model instances into ready-to-present-in-a-table fashion. You will do this in\n    :meth:`load`.\n\n    When you do this, you'll put the result into arbitrary attributes, which will later be fetched\n    by your table for presentation to the user.\n\n    You can organize your attributes in whatever way you want, but there's a convention you can\n    follow if you want to minimize subclassing and use default behavior:\n\n    1. Attribute name = column name. If your attribute is ``foobar``, whenever we refer to\n       ``column_name``, you refer to that attribute with the column name ``foobar``.\n    2. Public attributes are for *formatted* value, that is, user readable strings.\n    3. Underscore prefix is the unformatted (computable) value. For example, you could have\n       ``_foobar`` at ``42`` and ``foobar`` at ``\"42 seconds\"`` (what you present to the user).\n    4. Unformatted values are used for sorting.\n    5. If your column name is a python keyword, add an underscore suffix (``from_``).\n\n    Of course, this is only default behavior. This can be overriden.\n    \"\"\"\n\n    def __init__(self, table: GUITable) -> None:\n        super().__init__()\n        self.table = table\n\n    def _edit(self) -> None:\n        if self.table.edited is self:\n            return\n        assert self.table.edited is None\n        self.table.edited = self\n\n    # --- Virtual\n    def can_edit(self) -> bool:\n        \"\"\"(Virtual) Whether the whole row can be edited.\n\n        By default, always returns ``True``. This is for the *whole* row. For individual cells, it's\n        :meth:`can_edit_cell`.\n        \"\"\"\n        return True\n\n    def load(self) -> None:\n        \"\"\"(Virtual/Required) Loads up values from the model to be presented in the table.\n\n        Usually, our model instances contain values that are not quite ready for display. If you\n        have number formatting, display calculations and other whatnots to perform, you do it here\n        and then you put the result in an arbitrary attribute of the row.\n        \"\"\"\n        raise NotImplementedError()\n\n    def save(self) -> None:\n        \"\"\"(Virtual/Required) Saves user edits into your model.\n\n        If your table is editable, this is called when the user commits his changes. Usually, these\n        are typed up stuff, or selected indexes. You have to do proper parsing and reference\n        linking, and save that stuff into your model.\n        \"\"\"\n        raise NotImplementedError()\n\n    def sort_key_for_column(self, column_name: str) -> Any:\n        \"\"\"(Virtual) Return the value that is to be used to sort by column ``column_name``.\n\n        By default, looks for an attribute with the same name as ``column_name``, but with an\n        underscore prefix (\"unformatted value\"). If there's none, tries without the underscore. If\n        there's none, raises ``AttributeError``.\n        \"\"\"\n        try:\n            return getattr(self, \"_\" + column_name)\n        except AttributeError:\n            return getattr(self, column_name)\n\n    # --- Public\n    def can_edit_cell(self, column_name: str) -> bool:\n        \"\"\"Returns whether cell for column ``column_name`` can be edited.\n\n        By the default, the check is done in many steps:\n\n        1. We check whether the whole row can be edited with :meth:`can_edit`. If it can't, the cell\n           can't either.\n        2. If the column doesn't exist as an attribute, we can't edit.\n        3. If we have an attribute ``can_edit_<column_name>``, return that.\n        4. Check if our attribute is a property. If it's not, it's not editable.\n        5. If our attribute is in fact a property, check whether the property is \"settable\" (has a\n           ``fset`` method). The cell is editable only if the property is \"settable\".\n        \"\"\"\n        if not self.can_edit():\n            return False\n        # '_' is in case column is a python keyword\n        if not hasattr(self, column_name):\n            if hasattr(self, column_name + \"_\"):\n                column_name = column_name + \"_\"\n            else:\n                return False\n        if hasattr(self, \"can_edit_\" + column_name):\n            return getattr(self, \"can_edit_\" + column_name)\n        # If the row has a settable property, we can edit the cell\n        rowclass = self.__class__\n        prop = getattr(rowclass, column_name, None)\n        if prop is None:\n            return False\n        return bool(getattr(prop, \"fset\", None))\n\n    def get_cell_value(self, attrname: str) -> Any:\n        \"\"\"Get cell value for ``attrname``.\n\n        By default, does a simple ``getattr()``, but it is used to allow subclasses to have\n        alternative value storage mechanisms.\n        \"\"\"\n        if attrname == \"from\":\n            attrname = \"from_\"\n        return getattr(self, attrname)\n\n    def set_cell_value(self, attrname: str, value: Any) -> None:\n        \"\"\"Set cell value to ``value`` for ``attrname``.\n\n        By default, does a simple ``setattr()``, but it is used to allow subclasses to have\n        alternative value storage mechanisms.\n        \"\"\"\n        if attrname == \"from\":\n            attrname = \"from_\"\n        setattr(self, attrname, value)\n"
  },
  {
    "path": "hscommon/gui/text_field.py",
    "content": "# Created On: 2012/01/23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.gui.base import GUIObject\nfrom hscommon.util import nonone\n\n\nclass TextFieldView:\n    \"\"\"Expected interface for :class:`TextField`'s view.\n\n    *Not actually used in the code. For documentation purposes only.*\n\n    Our view is expected to sync with :attr:`TextField.text` \"both ways\", that is, update the\n    model's text when the user types something, but also update the text field when :meth:`refresh`\n    is called.\n    \"\"\"\n\n    def refresh(self):\n        \"\"\"Refreshes the contents of the input widget.\n\n        Ensures that the contents of the input widget is actually :attr:`TextField.text`.\n        \"\"\"\n\n\nclass TextField(GUIObject):\n    \"\"\"Cross-toolkit text field.\n\n    Represents a UI element allowing the user to input a text value. Its main attribute is\n    :attr:`text` which acts as the store of the said value.\n\n    When our model value isn't a string, we have a built-in parsing/formatting mechanism allowing\n    us to directly retrieve/set our non-string value through :attr:`value`.\n\n    Subclasses :class:`.GUIObject`. Expected view: :class:`TextFieldView`.\n    \"\"\"\n\n    def __init__(self):\n        GUIObject.__init__(self)\n        self._text = \"\"\n        self._value = None\n\n    # --- Virtual\n    def _parse(self, text):\n        \"\"\"(Virtual) Parses ``text`` to put into :attr:`value`.\n\n        Returns the parsed version of ``text``. Called whenever :attr:`text` changes.\n        \"\"\"\n        return text\n\n    def _format(self, value):\n        \"\"\"(Virtual) Formats ``value`` to put into :attr:`text`.\n\n        Returns the formatted version of ``value``. Called whenever :attr:`value` changes.\n        \"\"\"\n        return value\n\n    def _update(self, newvalue):\n        \"\"\"(Virtual) Called whenever we have a new value.\n\n        Whenever our text/value store changes to a new value (different from the old one), this\n        method is called. By default, it does nothing but you can override it if you want.\n        \"\"\"\n\n    # --- Override\n    def _view_updated(self):\n        self.view.refresh()\n\n    # --- Public\n    def refresh(self):\n        \"\"\"Triggers a view :meth:`~TextFieldView.refresh`.\"\"\"\n        self.view.refresh()\n\n    @property\n    def text(self):\n        \"\"\"The text that is currently displayed in the widget.\n\n        *str*. *get/set*.\n\n        This property can be set. When it is, :meth:`refresh` is called and the view is synced with\n        our value. Always in sync with :attr:`value`.\n        \"\"\"\n        return self._text\n\n    @text.setter\n    def text(self, newtext):\n        self.value = self._parse(nonone(newtext, \"\"))\n\n    @property\n    def value(self):\n        \"\"\"The \"parsed\" representation of :attr:`text`.\n\n        *arbitrary type*. *get/set*.\n\n        By default, it's a mirror of :attr:`text`, but a subclass can override :meth:`_parse` and\n        :meth:`_format` to have anything else. Always in sync with :attr:`text`.\n        \"\"\"\n        return self._value\n\n    @value.setter\n    def value(self, newvalue):\n        if newvalue == self._value:\n            return\n        self._value = newvalue\n        self._text = self._format(newvalue)\n        self._update(self._value)\n        self.refresh()\n"
  },
  {
    "path": "hscommon/gui/tree.py",
    "content": "# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom collections.abc import MutableSequence\n\nfrom hscommon.gui.base import GUIObject\n\n\nclass Node(MutableSequence):\n    \"\"\"Pretty bland node implementation to be used in a :class:`Tree`.\n\n    It has a :attr:`parent`, behaves like a list, its content being its children. Link integrity\n    is somewhat enforced (adding a child to a node will set the child's :attr:`parent`, but that's\n    pretty much as far as we go, integrity-wise. Nodes don't tend to move around much in a GUI\n    tree). We don't even check for infinite node loops. Don't play around these grounds too much.\n\n    Nodes are designed to be subclassed and given meaningful attributes (those you'll want to\n    display in your tree view), but they all have a :attr:`name`, which is given on initialization.\n    \"\"\"\n\n    def __init__(self, name):\n        self._name = name\n        self._parent = None\n        self._path = None\n        self._children = []\n\n    def __repr__(self):\n        return \"<Node %r>\" % self.name\n\n    # --- MutableSequence overrides\n    def __delitem__(self, key):\n        self._children.__delitem__(key)\n\n    def __getitem__(self, key):\n        return self._children.__getitem__(key)\n\n    def __len__(self):\n        return len(self._children)\n\n    def __setitem__(self, key, value):\n        self._children.__setitem__(key, value)\n\n    def append(self, node):\n        self._children.append(node)\n        node._parent = self\n        node._path = None\n\n    def insert(self, index, node):\n        self._children.insert(index, node)\n        node._parent = self\n        node._path = None\n\n    # --- Public\n    def clear(self):\n        \"\"\"Clears the node of all its children.\"\"\"\n        del self[:]\n\n    def find(self, predicate, include_self=True):\n        \"\"\"Return the first child to match ``predicate``.\n\n        See :meth:`findall`.\n        \"\"\"\n        try:\n            return next(self.findall(predicate, include_self=include_self))\n        except StopIteration:\n            return None\n\n    def findall(self, predicate, include_self=True):\n        \"\"\"Yield all children matching ``predicate``.\n\n        :param predicate: ``f(node) --> bool``\n        :param include_self: Whether we can return ``self`` or we return only children.\n        \"\"\"\n        if include_self and predicate(self):\n            yield self\n        for child in self:\n            yield from child.findall(predicate, include_self=True)\n\n    def get_node(self, index_path):\n        \"\"\"Returns the node at ``index_path``.\n\n        :param index_path: a list of int indexes leading to our node. See :attr:`path`.\n        \"\"\"\n        result = self\n        if index_path:\n            for index in index_path:\n                result = result[index]\n        return result\n\n    def get_path(self, target_node):\n        \"\"\"Returns the :attr:`path` of ``target_node``.\n\n        If ``target_node`` is ``None``, returns ``None``.\n        \"\"\"\n        if target_node is None:\n            return None\n        return target_node.path\n\n    @property\n    def children_count(self):\n        \"\"\"Same as ``len(self)``.\"\"\"\n        return len(self)\n\n    @property\n    def name(self):\n        \"\"\"Name for the node, supplied on init.\"\"\"\n        return self._name\n\n    @property\n    def parent(self):\n        \"\"\"Parent of the node.\n\n        If ``None``, we have a root node.\n        \"\"\"\n        return self._parent\n\n    @property\n    def path(self):\n        \"\"\"A list of node indexes leading from the root node to ``self``.\n\n        The path of a node is always related to its :attr:`root`. It's the sequences of index that\n        we have to take to get to our node, starting from the root. For example, if\n        ``node.path == [1, 2, 3, 4]``, it means that ``node.root[1][2][3][4] is node``.\n        \"\"\"\n        if self._path is None:\n            if self._parent is None:\n                self._path = []\n            else:\n                self._path = self._parent.path + [self._parent.index(self)]\n        return self._path\n\n    @property\n    def root(self):\n        \"\"\"Root node of current node.\n\n        To get it, we recursively follow our :attr:`parent` chain until we have ``None``.\n        \"\"\"\n        if self._parent is None:\n            return self\n        else:\n            return self._parent.root\n\n\nclass Tree(Node, GUIObject):\n    \"\"\"Cross-toolkit GUI-enabled tree view.\n\n    This class is a bit too thin to be used as a tree view controller out of the box and HS apps\n    that subclasses it each add quite a bit of logic to it to make it workable. Making this more\n    usable out of the box is a work in progress.\n\n    This class is here (in addition to being a :class:`Node`) mostly to handle selection.\n\n    Subclasses :class:`Node` (it is the root node of all its children) and :class:`.GUIObject`.\n    \"\"\"\n\n    def __init__(self):\n        Node.__init__(self, \"\")\n        GUIObject.__init__(self)\n        #: Where we store selected nodes (as a list of :class:`Node`)\n        self._selected_nodes = []\n\n    # --- Virtual\n    def _select_nodes(self, nodes):\n        \"\"\"(Virtual) Customize node selection behavior.\n\n        By default, simply set :attr:`_selected_nodes`.\n        \"\"\"\n        self._selected_nodes = nodes\n\n    # --- Override\n    def _view_updated(self):\n        self.view.refresh()\n\n    def clear(self):\n        self._selected_nodes = []\n        Node.clear(self)\n\n    # --- Public\n    @property\n    def selected_node(self):\n        \"\"\"Currently selected node.\n\n        *:class:`Node`*. *get/set*.\n\n        First of :attr:`selected_nodes`. ``None`` if empty.\n        \"\"\"\n        return self._selected_nodes[0] if self._selected_nodes else None\n\n    @selected_node.setter\n    def selected_node(self, node):\n        if node is not None:\n            self._select_nodes([node])\n        else:\n            self._select_nodes([])\n\n    @property\n    def selected_nodes(self):\n        \"\"\"List of selected nodes in the tree.\n\n        *List of :class:`Node`*. *get/set*.\n\n        We use nodes instead of indexes to store selection because it's simpler when it's time to\n        manage selection of multiple node levels.\n        \"\"\"\n        return self._selected_nodes\n\n    @selected_nodes.setter\n    def selected_nodes(self, nodes):\n        self._select_nodes(nodes)\n\n    @property\n    def selected_path(self):\n        \"\"\"Currently selected path.\n\n        *:attr:`Node.path`*. *get/set*.\n\n        First of :attr:`selected_paths`. ``None`` if empty.\n        \"\"\"\n        return self.get_path(self.selected_node)\n\n    @selected_path.setter\n    def selected_path(self, index_path):\n        if index_path is not None:\n            self.selected_paths = [index_path]\n        else:\n            self._select_nodes([])\n\n    @property\n    def selected_paths(self):\n        \"\"\"List of selected paths in the tree.\n\n        *List of :attr:`Node.path`*. *get/set*\n\n        Computed from :attr:`selected_nodes`.\n        \"\"\"\n        return list(map(self.get_path, self._selected_nodes))\n\n    @selected_paths.setter\n    def selected_paths(self, index_paths):\n        nodes = []\n        for path in index_paths:\n            try:\n                nodes.append(self.get_node(path))\n            except IndexError:\n                pass\n        self._select_nodes(nodes)\n"
  },
  {
    "path": "hscommon/jobprogress/__init__.py",
    "content": ""
  },
  {
    "path": "hscommon/jobprogress/job.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2004/12/20\n# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\nfrom typing import Any, Callable, Generator, List, Union\n\n\nclass JobCancelled(Exception):\n    \"The user has cancelled the job\"\n\n\nclass JobInProgressError(Exception):\n    \"A job is already being performed, you can't perform more than one at the same time.\"\n\n\nclass JobCountError(Exception):\n    \"The number of jobs started have exceeded the number of jobs allowed\"\n\n\nclass Job:\n    \"\"\"Manages a job's progression and return it's progression through a callback.\n\n    Note that this class is not foolproof. For example, you could call\n    start_subjob, and then call add_progress from the parent job, and nothing\n    would stop you from doing it. However, it would mess your progression\n    because it is the sub job that is supposed to drive the progression.\n    Another example would be to start a subjob, then start another, and call\n    add_progress from the old subjob. Once again, it would mess your progression.\n    There are no stops because it would remove the lightweight aspect of the\n    class (A Job would need to have a Parent instead of just a callback,\n    and the parent could be None. A lot of checks for nothing.).\n    Another one is that nothing stops you from calling add_progress right after\n    SkipJob.\n    \"\"\"\n\n    # ---Magic functions\n    def __init__(self, job_proportions: Union[List[int], int], callback: Callable) -> None:\n        \"\"\"Initialize the Job with 'jobcount' jobs. Start every job with\n        start_job(). Every time the job progress is updated, 'callback' is called\n        'callback' takes a 'progress' int param, and a optional 'desc'\n        parameter. Callback must return false if the job must be cancelled.\n        \"\"\"\n        if not hasattr(callback, \"__call__\"):\n            raise TypeError(\"'callback' MUST be set when creating a Job\")\n        if isinstance(job_proportions, int):\n            job_proportions = [1] * job_proportions\n        self._job_proportions = list(job_proportions)\n        self._jobcount = sum(job_proportions)\n        self._callback = callback\n        self._current_job = 0\n        self._passed_jobs = 0\n        self._progress = 0\n        self._currmax = 1\n\n    # ---Private\n    def _subjob_callback(self, progress: int, desc: str = \"\") -> bool:\n        \"\"\"This is the callback passed to children jobs.\"\"\"\n        self.set_progress(progress, desc)\n        return True  # if JobCancelled has to be raised, it will be at the highest level\n\n    def _do_update(self, desc: str) -> None:\n        \"\"\"Calls the callback function with a % progress as a parameter.\n\n        The parameter is a int in the 0-100 range.\n        \"\"\"\n        if self._current_job:\n            passed_progress = self._passed_jobs * self._currmax\n            current_progress = self._current_job * self._progress\n            total_progress = self._jobcount * self._currmax\n            progress = ((passed_progress + current_progress) * 100) // total_progress\n        else:\n            progress = -1  # indeterminate\n        # It's possible that callback doesn't support a desc arg\n        result = self._callback(progress, desc) if desc else self._callback(progress)\n        if not result:\n            raise JobCancelled()\n\n    # ---Public\n    def add_progress(self, progress: int = 1, desc: str = \"\") -> None:\n        self.set_progress(self._progress + progress, desc)\n\n    def check_if_cancelled(self) -> None:\n        self._do_update(\"\")\n\n    # TODO type hint iterable\n    def iter_with_progress(\n        self, iterable, desc_format: Union[str, None] = None, every: int = 1, count: Union[int, None] = None\n    ) -> Generator[Any, None, None]:\n        \"\"\"Iterate through ``iterable`` while automatically adding progress.\n\n        WARNING: We need our iterable's length. If ``iterable`` is not a sequence (that is,\n        something we can call ``len()`` on), you *have* to specify a count through the ``count``\n        argument. If ``count`` is ``None``, ``len(iterable)`` is used.\n        \"\"\"\n        if count is None:\n            count = len(iterable)\n        desc = \"\"\n        if desc_format:\n            desc = desc_format % (0, count)\n        self.start_job(count, desc)\n        for i, element in enumerate(iterable, start=1):\n            yield element\n            if i % every == 0:\n                if desc_format:\n                    desc = desc_format % (i, count)\n                self.add_progress(progress=every, desc=desc)\n        if desc_format:\n            desc = desc_format % (count, count)\n        self.set_progress(100, desc)\n\n    def start_job(self, max_progress: int = 100, desc: str = \"\") -> None:\n        \"\"\"Begin work on the next job. You must not call start_job more than\n        'jobcount' (in __init__) times.\n        'max' is the job units you are to perform.\n        'desc' is the description of the job.\n        \"\"\"\n        self._passed_jobs += self._current_job\n        try:\n            self._current_job = self._job_proportions.pop(0)\n        except IndexError:\n            raise JobCountError()\n        self._progress = 0\n        self._currmax = max(1, max_progress)\n        self._do_update(desc)\n\n    def start_subjob(self, job_proportions: Union[List[int], int], desc: str = \"\") -> \"Job\":\n        \"\"\"Starts a sub job. Use this when you want to split a job into\n        multiple smaller jobs. Pretty handy when starting a process where you\n        know how many subjobs you will have, but don't know the work unit count\n        for every of them.\n        returns the Job object\n        \"\"\"\n        self.start_job(100, desc)\n        return Job(job_proportions, self._subjob_callback)\n\n    def set_progress(self, progress: int, desc: str = \"\") -> None:\n        \"\"\"Sets the progress of the current job to 'progress', and call the\n        callback\n        \"\"\"\n        self._progress = progress\n        if self._progress > self._currmax:\n            self._progress = self._currmax\n        self._do_update(desc)\n\n\nclass NullJob(Job):\n    def __init__(self, *args, **kwargs) -> None:\n        # Null job does nothing\n        pass\n\n    def add_progress(self, *args, **kwargs) -> None:\n        # Null job does nothing\n        pass\n\n    def check_if_cancelled(self) -> None:\n        # Null job does nothing\n        pass\n\n    def start_job(self, *args, **kwargs) -> None:\n        # Null job does nothing\n        pass\n\n    def start_subjob(self, *args, **kwargs) -> \"NullJob\":\n        return NullJob()\n\n    def set_progress(self, *args, **kwargs) -> None:\n        # Null job does nothing\n        pass\n\n\nnulljob = NullJob()\n"
  },
  {
    "path": "hscommon/jobprogress/performer.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-11-19\n# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom threading import Thread\nimport sys\nfrom typing import Callable, Tuple, Union\n\nfrom hscommon.jobprogress.job import Job, JobInProgressError, JobCancelled\n\n\nclass ThreadedJobPerformer:\n    \"\"\"Run threaded jobs and track progress.\n\n    To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with\n    your work function as a parameter.\n\n    Example:\n\n    j = self._create_job()\n    self._run_threaded(self.some_work_func, (arg1, arg2, j))\n    \"\"\"\n\n    _job_running = False\n    last_error = None\n\n    # --- Protected\n    def create_job(self) -> Job:\n        if self._job_running:\n            raise JobInProgressError()\n        self.last_progress: Union[int, None] = -1\n        self.last_desc = \"\"\n        self.job_cancelled = False\n        return Job(1, self._update_progress)\n\n    def _async_run(self, *args) -> None:\n        target = args[0]\n        args = tuple(args[1:])\n        self._job_running = True\n        self.last_error = None\n        try:\n            target(*args)\n        except JobCancelled:\n            pass\n        except Exception as e:\n            self.last_error = e\n            self.last_traceback = sys.exc_info()[2]\n        finally:\n            self._job_running = False\n            self.last_progress = None\n\n    def reraise_if_error(self) -> None:\n        \"\"\"Reraises the error that happened in the thread if any.\n\n        Call this after the caller of run_threaded detected that self._job_running returned to False\n        \"\"\"\n        if self.last_error is not None:\n            raise self.last_error.with_traceback(self.last_traceback)\n\n    def _update_progress(self, newprogress: int, newdesc: str = \"\") -> bool:\n        self.last_progress = newprogress\n        if newdesc:\n            self.last_desc = newdesc\n        return not self.job_cancelled\n\n    def run_threaded(self, target: Callable, args: Tuple = ()) -> None:\n        if self._job_running:\n            raise JobInProgressError()\n        args = (target,) + args\n        Thread(target=self._async_run, args=args).start()\n"
  },
  {
    "path": "hscommon/loc.py",
    "content": "import os\nimport os.path as op\nimport shutil\nimport tempfile\nfrom typing import Any, List\n\nimport polib\n\nfrom hscommon import pygettext\n\nLC_MESSAGES = \"LC_MESSAGES\"\n\n\ndef get_langs(folder: str) -> List[str]:\n    return [name for name in os.listdir(folder) if op.isdir(op.join(folder, name))]\n\n\ndef files_with_ext(folder: str, ext: str) -> List[str]:\n    return [op.join(folder, fn) for fn in os.listdir(folder) if fn.endswith(ext)]\n\n\ndef generate_pot(folders: List[str], outpath: str, keywords: Any, merge: bool = False) -> None:\n    if merge and not op.exists(outpath):\n        merge = False\n    if merge:\n        _, genpath = tempfile.mkstemp()\n    else:\n        genpath = outpath\n    pyfiles = []\n    for folder in folders:\n        for root, dirs, filenames in os.walk(folder):\n            keep = [fn for fn in filenames if fn.endswith(\".py\")]\n            pyfiles += [op.join(root, fn) for fn in keep]\n    pygettext.main(pyfiles, outpath=genpath, keywords=keywords)\n    if merge:\n        merge_po_and_preserve(genpath, outpath)\n        try:\n            os.remove(genpath)\n        except Exception:\n            print(\"Exception while removing temporary folder %s\\n\", genpath)\n\n\ndef compile_all_po(base_folder: str) -> None:\n    langs = get_langs(base_folder)\n    for lang in langs:\n        pofolder = op.join(base_folder, lang, LC_MESSAGES)\n        pofiles = files_with_ext(pofolder, \".po\")\n        for pofile in pofiles:\n            p = polib.pofile(pofile)\n            p.save_as_mofile(pofile[:-3] + \".mo\")\n\n\ndef merge_locale_dir(target: str, mergeinto: str) -> None:\n    langs = get_langs(target)\n    for lang in langs:\n        if not op.exists(op.join(mergeinto, lang)):\n            continue\n        mofolder = op.join(target, lang, LC_MESSAGES)\n        mofiles = files_with_ext(mofolder, \".mo\")\n        for mofile in mofiles:\n            shutil.copy(mofile, op.join(mergeinto, lang, LC_MESSAGES))\n\n\ndef merge_pots_into_pos(folder: str) -> None:\n    # We're going to take all pot files in `folder` and for each lang, merge it with the po file\n    # with the same name.\n    potfiles = files_with_ext(folder, \".pot\")\n    for potfile in potfiles:\n        refpot = polib.pofile(potfile)\n        refname = op.splitext(op.basename(potfile))[0]\n        for lang in get_langs(folder):\n            po = polib.pofile(op.join(folder, lang, LC_MESSAGES, refname + \".po\"))\n            po.merge(refpot)\n            po.save()\n\n\ndef merge_po_and_preserve(source: str, dest: str) -> None:\n    # Merges source entries into dest, but keep old entries intact\n    sourcepo = polib.pofile(source)\n    destpo = polib.pofile(dest)\n    for entry in sourcepo:\n        if destpo.find(entry.msgid) is not None:\n            # The entry is already there\n            continue\n        destpo.append(entry)\n    destpo.save()\n\n\ndef normalize_all_pos(base_folder: str) -> None:\n    \"\"\"Normalize the format of .po files in base_folder.\n\n    When getting POs from external sources, such as Transifex, we end up with spurious diffs because\n    of a difference in the way line wrapping is handled. It wouldn't be a big deal if it happened\n    once, but these spurious diffs keep overwriting each other, and it's annoying.\n\n    Our PO files will keep polib's format. Call this function to ensure that freshly pulled POs\n    are of the right format before committing them.\n    \"\"\"\n    langs = get_langs(base_folder)\n    for lang in langs:\n        pofolder = op.join(base_folder, lang, LC_MESSAGES)\n        pofiles = files_with_ext(pofolder, \".po\")\n        for pofile in pofiles:\n            p = polib.pofile(pofile)\n            p.save()\n"
  },
  {
    "path": "hscommon/notify.py",
    "content": "# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\"\"\"Very simple inter-object notification system.\n\nThis module is a brain-dead simple notification system involving a :class:`Broadcaster` and a\n:class:`Listener`. A listener can only listen to one broadcaster. A broadcaster can have multiple\nlisteners. If the listener is connected, whenever the broadcaster calls :meth:`~Broadcaster.notify`,\nthe method with the same name as the broadcasted message is called on the listener.\n\"\"\"\n\nfrom collections import defaultdict\nfrom typing import Callable, DefaultDict, List\n\n\nclass Broadcaster:\n    \"\"\"Broadcasts messages that are received by all listeners.\"\"\"\n\n    def __init__(self):\n        self.listeners = set()\n\n    def add_listener(self, listener: \"Listener\") -> None:\n        self.listeners.add(listener)\n\n    def notify(self, msg: str) -> None:\n        \"\"\"Notify all connected listeners of ``msg``.\n\n        That means that each listeners will have their method with the same name as ``msg`` called.\n        \"\"\"\n        for listener in self.listeners.copy():  # listeners can change during iteration\n            if listener in self.listeners:  # disconnected during notification\n                listener.dispatch(msg)\n\n    def remove_listener(self, listener: \"Listener\") -> None:\n        self.listeners.discard(listener)\n\n\nclass Listener:\n    \"\"\"A listener is initialized with the broadcaster it's going to listen to. Initially, it is not connected.\"\"\"\n\n    def __init__(self, broadcaster: Broadcaster) -> None:\n        self.broadcaster = broadcaster\n        self._bound_notifications: DefaultDict[str, List[Callable]] = defaultdict(list)\n\n    def bind_messages(self, messages: str, func: Callable) -> None:\n        \"\"\"Binds multiple message to the same function.\n\n        Often, we perform the same thing on multiple messages. Instead of having the same function\n        repeated again and agin in our class, we can use this method to bind multiple messages to\n        the same function.\n        \"\"\"\n        for message in messages:\n            self._bound_notifications[message].append(func)\n\n    def connect(self) -> None:\n        \"\"\"Connects the listener to its broadcaster.\"\"\"\n        self.broadcaster.add_listener(self)\n\n    def disconnect(self) -> None:\n        \"\"\"Disconnects the listener from its broadcaster.\"\"\"\n        self.broadcaster.remove_listener(self)\n\n    def dispatch(self, msg: str) -> None:\n        if msg in self._bound_notifications:\n            for func in self._bound_notifications[msg]:\n                func()\n        if hasattr(self, msg):\n            method = getattr(self, msg)\n            method()\n\n\nclass Repeater(Broadcaster, Listener):\n    REPEATED_NOTIFICATIONS = None\n\n    def __init__(self, broadcaster: Broadcaster) -> None:\n        Broadcaster.__init__(self)\n        Listener.__init__(self, broadcaster)\n\n    def _repeat_message(self, msg: str) -> None:\n        if not self.REPEATED_NOTIFICATIONS or msg in self.REPEATED_NOTIFICATIONS:\n            self.notify(msg)\n\n    def dispatch(self, msg: str) -> None:\n        Listener.dispatch(self, msg)\n        self._repeat_message(msg)\n"
  },
  {
    "path": "hscommon/path.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/02/21\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\nfrom functools import wraps\nfrom inspect import signature\nfrom pathlib import Path\n\n\ndef pathify(f):\n    \"\"\"Ensure that every annotated :class:`Path` arguments are actually paths.\n\n    When a function is decorated with ``@pathify``, every argument with annotated as Path will be\n    converted to a Path if it wasn't already. Example::\n\n        @pathify\n        def foo(path: Path, otherarg):\n            return path.listdir()\n\n    Calling ``foo('/bar', 0)`` will convert ``'/bar'`` to ``Path('/bar')``.\n    \"\"\"\n    sig = signature(f)\n    pindexes = {i for i, p in enumerate(sig.parameters.values()) if p.annotation is Path}\n    pkeys = {k: v for k, v in sig.parameters.items() if v.annotation is Path}\n\n    def path_or_none(p):\n        return None if p is None else Path(p)\n\n    @wraps(f)\n    def wrapped(*args, **kwargs):\n        args = tuple((path_or_none(a) if i in pindexes else a) for i, a in enumerate(args))\n        kwargs = {k: (path_or_none(v) if k in pkeys else v) for k, v in kwargs.items()}\n        return f(*args, **kwargs)\n\n    return wrapped\n\n\ndef log_io_error(func):\n    \"\"\"Catches OSError, IOError and WindowsError and log them\"\"\"\n\n    @wraps(func)\n    def wrapper(path, *args, **kwargs):\n        try:\n            return func(path, *args, **kwargs)\n        except OSError as e:\n            msg = 'Error \"{0}\" during operation \"{1}\" on \"{2}\": \"{3}\"'\n            classname = e.__class__.__name__\n            funcname = func.__name__\n            logging.warning(msg.format(classname, funcname, str(path), str(e)))\n\n    return wrapper\n"
  },
  {
    "path": "hscommon/plat.py",
    "content": "# Created On: 2011/09/22\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n# Yes, I know, there's the 'platform' unit for this kind of stuff, but the thing is that I got a\n# crash on startup once simply for importing this module and since then I don't trust it. One day,\n# I'll investigate the cause of that crash further.\n\nimport sys\n\nISWINDOWS = sys.platform == \"win32\"\nISOSX = sys.platform == \"darwin\"\nISLINUX = sys.platform.startswith(\"linux\")\n"
  },
  {
    "path": "hscommon/pygettext.py",
    "content": "# This module was taken from CPython's Tools/i18n and dirtily hacked to bypass the need for cmdline\n# invocation.\n\n# Originally written by Barry Warsaw <barry@zope.com>\n#\n# Minimally patched to make it even more xgettext compatible\n# by Peter Funk <pf@artcom-gmbh.de>\n#\n# 2002-11-22 Jürgen Hermann <jh@web.de>\n# Added checks that _() only contains string literals, and\n# command line args are resolved to module lists, i.e. you\n# can now pass a filename, a module or package name, or a\n# directory (including globbing chars, important for Win32).\n# Made docstring fit in 80 chars wide displays using pydoc.\n#\n\nimport os\nimport importlib.machinery\nimport importlib.util\nimport sys\nimport glob\nimport token\nimport tokenize\n\n__version__ = \"1.5\"\n\ndefault_keywords = [\"_\"]\nDEFAULTKEYWORDS = \", \".join(default_keywords)\n\nEMPTYSTRING = \"\"\n\n\n# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's\n# there.\npot_header = \"\"\"\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\\\n\"\n\"Content-Transfer-Encoding: utf-8\\\\n\"\n\"\"\"\n\n\ndef usage(code, msg=\"\"):\n    print(__doc__ % globals(), file=sys.stderr)\n    if msg:\n        print(msg, file=sys.stderr)\n    sys.exit(code)\n\n\nescapes = []\n\n\ndef make_escapes(pass_iso8859):\n    global escapes\n    if pass_iso8859:\n        # Allow iso-8859 characters to pass through so that e.g. 'msgid\n        # \"H?he\"' would result not result in 'msgid \"H\\366he\"'.  Otherwise we\n        # escape any character outside the 32..126 range.\n        mod = 128\n    else:\n        mod = 256\n    for i in range(256):\n        if 32 <= (i % mod) <= 126:\n            escapes.append(chr(i))\n        else:\n            escapes.append(\"\\\\%03o\" % i)\n    escapes[ord(\"\\\\\")] = \"\\\\\\\\\"\n    escapes[ord(\"\\t\")] = \"\\\\t\"\n    escapes[ord(\"\\r\")] = \"\\\\r\"\n    escapes[ord(\"\\n\")] = \"\\\\n\"\n    escapes[ord('\"')] = '\\\\\"'\n\n\ndef escape(s):\n    global escapes\n    s = list(s)\n    for i in range(len(s)):\n        s[i] = escapes[ord(s[i])]\n    return EMPTYSTRING.join(s)\n\n\ndef safe_eval(s):\n    # unwrap quotes, safely\n    return eval(s, {\"__builtins__\": {}}, {})\n\n\ndef normalize(s):\n    # This converts the various Python string types into a format that is\n    # appropriate for .po files, namely much closer to C style.\n    lines = s.split(\"\\n\")\n    if len(lines) == 1:\n        s = '\"' + escape(s) + '\"'\n    else:\n        if not lines[-1]:\n            del lines[-1]\n            lines[-1] = lines[-1] + \"\\n\"\n        for i in range(len(lines)):\n            lines[i] = escape(lines[i])\n        lineterm = '\\\\n\"\\n\"'\n        s = '\"\"\\n\"' + lineterm.join(lines) + '\"'\n    return s\n\n\ndef containsAny(str, set):\n    \"\"\"Check whether 'str' contains ANY of the chars in 'set'\"\"\"\n    return 1 in [c in str for c in set]\n\n\ndef _visit_pyfiles(list, dirname, names):\n    \"\"\"Helper for getFilesForName().\"\"\"\n    # get extension for python source files\n    if \"_py_ext\" not in globals():\n        global _py_ext\n        _py_ext = importlib.machinery.SOURCE_SUFFIXES[0]\n\n    # don't recurse into CVS directories\n    if \"CVS\" in names:\n        names.remove(\"CVS\")\n\n    # add all *.py files to list\n    list.extend([os.path.join(dirname, file) for file in names if os.path.splitext(file)[1] == _py_ext])\n\n\ndef getFilesForName(name):\n    \"\"\"Get a list of module files for a filename, a module or package name,\n    or a directory.\n    \"\"\"\n    if not os.path.exists(name):\n        # check for glob chars\n        if containsAny(name, \"*?[]\"):\n            files = glob.glob(name)\n            file_list = []\n            for file in files:\n                file_list.extend(getFilesForName(file))\n            return file_list\n\n        # try to find module or package\n        try:\n            spec = importlib.util.find_spec(name)\n            name = spec.origin\n        except ImportError:\n            name = None\n        if not name:\n            return []\n\n    if os.path.isdir(name):\n        # find all python files in directory\n        file_list = []\n        os.walk(name, _visit_pyfiles, file_list)\n        return file_list\n    elif os.path.exists(name):\n        # a single file\n        return [name]\n\n    return []\n\n\nclass TokenEater:\n    def __init__(self, options):\n        self.__options = options\n        self.__messages = {}\n        self.__state = self.__waiting\n        self.__data = []\n        self.__lineno = -1\n        self.__freshmodule = 1\n        self.__curfile = None\n\n    def __call__(self, ttype, tstring, stup, etup, line):\n        # dispatch\n        #        import token\n        #        print >> sys.stderr, 'ttype:', token.tok_name[ttype], \\\n        #              'tstring:', tstring\n        self.__state(ttype, tstring, stup[0])\n\n    def __waiting(self, ttype, tstring, lineno):\n        opts = self.__options\n        # Do docstring extractions, if enabled\n        if opts.docstrings and not opts.nodocstrings.get(self.__curfile):\n            # module docstring?\n            if self.__freshmodule:\n                if ttype == tokenize.STRING:\n                    self.__addentry(safe_eval(tstring), lineno, isdocstring=1)\n                    self.__freshmodule = 0\n                elif ttype not in (tokenize.COMMENT, tokenize.NL):\n                    self.__freshmodule = 0\n                return\n            # class docstring?\n            if ttype == tokenize.NAME and tstring in (\"class\", \"def\"):\n                self.__state = self.__suiteseen\n                return\n        if ttype == tokenize.NAME and tstring in opts.keywords:\n            self.__state = self.__keywordseen\n\n    def __suiteseen(self, ttype, tstring, lineno):\n        # ignore anything until we see the colon\n        if ttype == tokenize.OP and tstring == \":\":\n            self.__state = self.__suitedocstring\n\n    def __suitedocstring(self, ttype, tstring, lineno):\n        # ignore any intervening noise\n        if ttype == tokenize.STRING:\n            self.__addentry(safe_eval(tstring), lineno, isdocstring=1)\n            self.__state = self.__waiting\n        elif ttype not in (tokenize.NEWLINE, tokenize.INDENT, tokenize.COMMENT):\n            # there was no class docstring\n            self.__state = self.__waiting\n\n    def __keywordseen(self, ttype, tstring, lineno):\n        if ttype == tokenize.OP and tstring == \"(\":\n            self.__data = []\n            self.__lineno = lineno\n            self.__state = self.__openseen\n        else:\n            self.__state = self.__waiting\n\n    def __openseen(self, ttype, tstring, lineno):\n        if ttype == tokenize.OP and tstring == \")\":\n            # We've seen the last of the translatable strings.  Record the\n            # line number of the first line of the strings and update the list\n            # of messages seen.  Reset state for the next batch.  If there\n            # were no strings inside _(), then just ignore this entry.\n            if self.__data:\n                self.__addentry(EMPTYSTRING.join(self.__data))\n            self.__state = self.__waiting\n        elif ttype == tokenize.STRING:\n            self.__data.append(safe_eval(tstring))\n        elif ttype not in [\n            tokenize.COMMENT,\n            token.INDENT,\n            token.DEDENT,\n            token.NEWLINE,\n            tokenize.NL,\n        ]:\n            # warn if we see anything else than STRING or whitespace\n            print(\n                '*** %(file)s:%(lineno)s: Seen unexpected token \"%(token)s\"'\n                % {\"token\": tstring, \"file\": self.__curfile, \"lineno\": self.__lineno},\n                file=sys.stderr,\n            )\n            self.__state = self.__waiting\n\n    def __addentry(self, msg, lineno=None, isdocstring=0):\n        if lineno is None:\n            lineno = self.__lineno\n        if msg not in self.__options.toexclude:\n            entry = (self.__curfile, lineno)\n            self.__messages.setdefault(msg, {})[entry] = isdocstring\n\n    def set_filename(self, filename):\n        self.__curfile = filename\n        self.__freshmodule = 1\n\n    def write(self, fp):\n        options = self.__options\n        # The time stamp in the header doesn't have the same format as that\n        # generated by xgettext...\n        print(pot_header, file=fp)\n        # Sort the entries.  First sort each particular entry's keys, then\n        # sort all the entries by their first item.\n        reverse = {}\n        for k, v in self.__messages.items():\n            keys = sorted(v.keys())\n            reverse.setdefault(tuple(keys), []).append((k, v))\n        rkeys = sorted(reverse.keys())\n        for rkey in rkeys:\n            rentries = reverse[rkey]\n            rentries.sort()\n            for k, v in rentries:\n                # If the entry was gleaned out of a docstring, then add a\n                # comment stating so.  This is to aid translators who may wish\n                # to skip translating some unimportant docstrings.\n                isdocstring = any(v.values())\n                # k is the message string, v is a dictionary-set of (filename,\n                # lineno) tuples.  We want to sort the entries in v first by\n                # file name and then by line number.\n                v = sorted(v.keys())\n                if not options.writelocations:\n                    pass\n                # location comments are different b/w Solaris and GNU:\n                elif options.locationstyle == options.SOLARIS:\n                    for filename, lineno in v:\n                        d = {\"filename\": filename, \"lineno\": lineno}\n                        print(\"# File: %(filename)s, line: %(lineno)d\" % d, file=fp)\n                elif options.locationstyle == options.GNU:\n                    # fit as many locations on one line, as long as the\n                    # resulting line length doesn't exceeds 'options.width'\n                    locline = \"#:\"\n                    for filename, lineno in v:\n                        d = {\"filename\": filename, \"lineno\": lineno}\n                        s = \" %(filename)s:%(lineno)d\" % d\n                        if len(locline) + len(s) <= options.width:\n                            locline = locline + s\n                        else:\n                            print(locline, file=fp)\n                            locline = \"#:\" + s\n                    if len(locline) > 2:\n                        print(locline, file=fp)\n                if isdocstring:\n                    print(\"#, docstring\", file=fp)\n                print(\"msgid\", normalize(k), file=fp)\n                print('msgstr \"\"\\n', file=fp)\n\n\ndef main(source_files, outpath, keywords=None):\n    global default_keywords\n\n    # for holding option values\n    class Options:\n        # constants\n        GNU = 1\n        SOLARIS = 2\n        # defaults\n        extractall = 0  # FIXME: currently this option has no effect at all.\n        escape = 0\n        keywords = []\n        outfile = \"messages.pot\"\n        writelocations = 1\n        locationstyle = GNU\n        verbose = 0\n        width = 78\n        excludefilename = \"\"\n        docstrings = 0\n        nodocstrings = {}\n\n    options = Options()\n\n    options.outfile = outpath\n    if keywords:\n        options.keywords = keywords\n\n    # calculate escapes\n    make_escapes(options.escape)\n\n    # calculate all keywords\n    options.keywords.extend(default_keywords)\n\n    # initialize list of strings to exclude\n    if options.excludefilename:\n        try:\n            fp = open(options.excludefilename, encoding=\"utf-8\")\n            options.toexclude = fp.readlines()\n            fp.close()\n        except OSError:\n            print(\n                \"Can't read --exclude-file: %s\" % options.excludefilename,\n                file=sys.stderr,\n            )\n            sys.exit(1)\n    else:\n        options.toexclude = []\n\n    # slurp through all the files\n    eater = TokenEater(options)\n    for filename in source_files:\n        if options.verbose:\n            print(\"Working on %s\" % filename)\n        fp = open(filename, encoding=\"utf-8\")\n        closep = 1\n        try:\n            eater.set_filename(filename)\n            try:\n                tokens = tokenize.generate_tokens(fp.readline)\n                for _token in tokens:\n                    eater(*_token)\n            except tokenize.TokenError as e:\n                print(\n                    \"%s: %s, line %d, column %d\" % (e.args[0], filename, e.args[1][0], e.args[1][1]),\n                    file=sys.stderr,\n                )\n        finally:\n            if closep:\n                fp.close()\n\n    fp = open(options.outfile, \"w\", encoding=\"utf-8\")\n    closep = 1\n    try:\n        eater.write(fp)\n    finally:\n        if closep:\n            fp.close()\n"
  },
  {
    "path": "hscommon/sphinxgen.py",
    "content": "# Copyright 2018 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom pathlib import Path\nimport re\nfrom typing import Callable, Dict, Union\n\nfrom hscommon.build import read_changelog_file, filereplace\nfrom sphinx.cmd.build import build_main as sphinx_build\n\nCHANGELOG_FORMAT = \"\"\"\n{version} ({date})\n----------------------\n\n{description}\n\"\"\"\n\n\ndef tixgen(tixurl: str) -> Callable[[str], str]:\n    \"\"\"This is a filter *generator*. tixurl is a url pattern for the tix with a {0} placeholder\n    for the tix #\n    \"\"\"\n    urlpattern = tixurl.format(\"\\\\1\")  # will be replaced buy the content of the first group in re\n    R = re.compile(r\"#(\\d+)\")\n    repl = f\"`#\\\\1 <{urlpattern}>`__\"\n    return lambda text: R.sub(repl, text)\n\n\ndef gen(\n    basepath: Path,\n    destpath: Path,\n    changelogpath: Path,\n    tixurl: str,\n    confrepl: Union[Dict[str, str], None] = None,\n    confpath: Union[Path, None] = None,\n    changelogtmpl: Union[Path, None] = None,\n) -> None:\n    \"\"\"Generate sphinx docs with all bells and whistles.\n\n    basepath: The base sphinx source path.\n    destpath: The final path of html files\n    changelogpath: The path to the changelog file to insert in changelog.rst.\n    tixurl: The URL (with one formattable argument for the tix number) to the ticket system.\n    confrepl: Dictionary containing replacements that have to be made in conf.py. {name: replacement}\n    \"\"\"\n    if confrepl is None:\n        confrepl = {}\n    if confpath is None:\n        confpath = Path(basepath, \"conf.tmpl\")\n    if changelogtmpl is None:\n        changelogtmpl = Path(basepath, \"changelog.tmpl\")\n    changelog = read_changelog_file(changelogpath)\n    tix = tixgen(tixurl)\n    rendered_logs = []\n    for log in changelog:\n        description = tix(log[\"description\"])\n        # The format of the changelog descriptions is in markdown, but since we only use bulled list\n        # and links, it's not worth depending on the markdown package. A simple regexp suffice.\n        description = re.sub(r\"\\[(.*?)\\]\\((.*?)\\)\", \"`\\\\1 <\\\\2>`__\", description)\n        rendered = CHANGELOG_FORMAT.format(version=log[\"version\"], date=log[\"date_str\"], description=description)\n        rendered_logs.append(rendered)\n    confrepl[\"version\"] = changelog[0][\"version\"]\n    changelog_out = Path(basepath, \"changelog.rst\")\n    filereplace(changelogtmpl, changelog_out, changelog=\"\\n\".join(rendered_logs))\n    if Path(confpath).exists():\n        conf_out = Path(basepath, \"conf.py\")\n        filereplace(confpath, conf_out, **confrepl)\n    # Call the sphinx_build function, which is the same as doing sphinx-build from cli\n    try:\n        sphinx_build([str(basepath), str(destpath)])\n    except SystemExit:\n        print(\"Sphinx called sys.exit(), but we're cancelling it because we don't actually want to exit\")\n"
  },
  {
    "path": "hscommon/tests/__init__.py",
    "content": ""
  },
  {
    "path": "hscommon/tests/conflict_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2008-01-08\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport pytest\n\nfrom hscommon.conflict import (\n    get_conflicted_name,\n    get_unconflicted_name,\n    is_conflicted,\n    smart_copy,\n    smart_move,\n)\nfrom pathlib import Path\nfrom hscommon.testutil import eq_\n\n\nclass TestCaseGetConflictedName:\n    def test_simple(self):\n        name = get_conflicted_name([\"bar\"], \"bar\")\n        eq_(\"[000] bar\", name)\n        name = get_conflicted_name([\"bar\", \"[000] bar\"], \"bar\")\n        eq_(\"[001] bar\", name)\n\n    def test_no_conflict(self):\n        name = get_conflicted_name([\"bar\"], \"foobar\")\n        eq_(\"foobar\", name)\n\n    def test_fourth_digit(self):\n        # This test is long because every time we have to add a conflicted name,\n        # a test must be made for every other conflicted name existing...\n        # Anyway, this has very few chances to happen.\n        names = [\"bar\"] + [\"[%03d] bar\" % i for i in range(1000)]\n        name = get_conflicted_name(names, \"bar\")\n        eq_(\"[1000] bar\", name)\n\n    def test_auto_unconflict(self):\n        # Automatically unconflict the name if it's already conflicted.\n        name = get_conflicted_name([], \"[000] foobar\")\n        eq_(\"foobar\", name)\n        name = get_conflicted_name([\"bar\"], \"[001] bar\")\n        eq_(\"[000] bar\", name)\n\n\nclass TestCaseGetUnconflictedName:\n    def test_main(self):\n        eq_(\"foobar\", get_unconflicted_name(\"[000] foobar\"))\n        eq_(\"foobar\", get_unconflicted_name(\"[9999] foobar\"))\n        eq_(\"[000]foobar\", get_unconflicted_name(\"[000]foobar\"))\n        eq_(\"[000a] foobar\", get_unconflicted_name(\"[000a] foobar\"))\n        eq_(\"foobar\", get_unconflicted_name(\"foobar\"))\n        eq_(\"foo [000] bar\", get_unconflicted_name(\"foo [000] bar\"))\n\n\nclass TestCaseIsConflicted:\n    def test_main(self):\n        assert is_conflicted(\"[000] foobar\")\n        assert is_conflicted(\"[9999] foobar\")\n        assert not is_conflicted(\"[000]foobar\")\n        assert not is_conflicted(\"[000a] foobar\")\n        assert not is_conflicted(\"foobar\")\n        assert not is_conflicted(\"foo [000] bar\")\n\n\nclass TestCaseMoveCopy:\n    @pytest.fixture\n    def do_setup(self, request):\n        tmpdir = request.getfixturevalue(\"tmpdir\")\n        self.path = Path(str(tmpdir))\n        self.path.joinpath(\"foo\").touch()\n        self.path.joinpath(\"bar\").touch()\n        self.path.joinpath(\"dir\").mkdir()\n\n    def test_move_no_conflict(self, do_setup):\n        smart_move(self.path.joinpath(\"foo\"), self.path.joinpath(\"baz\"))\n        assert self.path.joinpath(\"baz\").exists()\n        assert not self.path.joinpath(\"foo\").exists()\n\n    def test_copy_no_conflict(self, do_setup):  # No need to duplicate the rest of the tests... Let's just test on move\n        smart_copy(self.path.joinpath(\"foo\"), self.path.joinpath(\"baz\"))\n        assert self.path.joinpath(\"baz\").exists()\n        assert self.path.joinpath(\"foo\").exists()\n\n    def test_move_no_conflict_dest_is_dir(self, do_setup):\n        smart_move(self.path.joinpath(\"foo\"), self.path.joinpath(\"dir\"))\n        assert self.path.joinpath(\"dir\", \"foo\").exists()\n        assert not self.path.joinpath(\"foo\").exists()\n\n    def test_move_conflict(self, do_setup):\n        smart_move(self.path.joinpath(\"foo\"), self.path.joinpath(\"bar\"))\n        assert self.path.joinpath(\"[000] bar\").exists()\n        assert not self.path.joinpath(\"foo\").exists()\n\n    def test_move_conflict_dest_is_dir(self, do_setup):\n        smart_move(self.path.joinpath(\"foo\"), self.path.joinpath(\"dir\"))\n        smart_move(self.path.joinpath(\"bar\"), self.path.joinpath(\"foo\"))\n        smart_move(self.path.joinpath(\"foo\"), self.path.joinpath(\"dir\"))\n        assert self.path.joinpath(\"dir\", \"foo\").exists()\n        assert self.path.joinpath(\"dir\", \"[000] foo\").exists()\n        assert not self.path.joinpath(\"foo\").exists()\n        assert not self.path.joinpath(\"bar\").exists()\n\n    def test_copy_folder(self, tmpdir):\n        # smart_copy also works on folders\n        path = Path(str(tmpdir))\n        path.joinpath(\"foo\").mkdir()\n        path.joinpath(\"bar\").mkdir()\n        smart_copy(path.joinpath(\"foo\"), path.joinpath(\"bar\"))  # no crash\n        assert path.joinpath(\"[000] bar\").exists()\n"
  },
  {
    "path": "hscommon/tests/notify_test.py",
    "content": "# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import eq_\nfrom hscommon.notify import Broadcaster, Listener, Repeater\n\n\nclass HelloListener(Listener):\n    def __init__(self, broadcaster):\n        Listener.__init__(self, broadcaster)\n        self.hello_count = 0\n\n    def hello(self):\n        self.hello_count += 1\n\n\nclass HelloRepeater(Repeater):\n    def __init__(self, broadcaster):\n        Repeater.__init__(self, broadcaster)\n        self.hello_count = 0\n\n    def hello(self):\n        self.hello_count += 1\n\n\ndef create_pair():\n    b = Broadcaster()\n    listener = HelloListener(b)\n    return b, listener\n\n\ndef test_disconnect_during_notification():\n    # When a listener disconnects another listener the other listener will not receive a\n    # notification.\n    # This whole complication scheme below is because the order of the notification is not\n    # guaranteed. We could disconnect everything from self.broadcaster.listeners, but this\n    # member is supposed to be private. Hence, the '.other' scheme\n    class Disconnecter(Listener):\n        def __init__(self, broadcaster):\n            Listener.__init__(self, broadcaster)\n            self.hello_count = 0\n\n        def hello(self):\n            self.hello_count += 1\n            self.other.disconnect()\n\n    broadcaster = Broadcaster()\n    first = Disconnecter(broadcaster)\n    second = Disconnecter(broadcaster)\n    first.other, second.other = second, first\n    first.connect()\n    second.connect()\n    broadcaster.notify(\"hello\")\n    # only one of them was notified\n    eq_(first.hello_count + second.hello_count, 1)\n\n\ndef test_disconnect():\n    # After a disconnect, the listener doesn't hear anything.\n    b, listener = create_pair()\n    listener.connect()\n    listener.disconnect()\n    b.notify(\"hello\")\n    eq_(listener.hello_count, 0)\n\n\ndef test_disconnect_when_not_connected():\n    # When disconnecting an already disconnected listener, nothing happens.\n    b, listener = create_pair()\n    listener.disconnect()\n\n\ndef test_not_connected_on_init():\n    # A listener is not initialized connected.\n    b, listener = create_pair()\n    b.notify(\"hello\")\n    eq_(listener.hello_count, 0)\n\n\ndef test_notify():\n    # The listener listens to the broadcaster.\n    b, listener = create_pair()\n    listener.connect()\n    b.notify(\"hello\")\n    eq_(listener.hello_count, 1)\n\n\ndef test_reconnect():\n    # It's possible to reconnect a listener after disconnection.\n    b, listener = create_pair()\n    listener.connect()\n    listener.disconnect()\n    listener.connect()\n    b.notify(\"hello\")\n    eq_(listener.hello_count, 1)\n\n\ndef test_repeater():\n    b = Broadcaster()\n    r = HelloRepeater(b)\n    listener = HelloListener(r)\n    r.connect()\n    listener.connect()\n    b.notify(\"hello\")\n    eq_(r.hello_count, 1)\n    eq_(listener.hello_count, 1)\n\n\ndef test_repeater_with_repeated_notifications():\n    # If REPEATED_NOTIFICATIONS is not empty, only notifs in this set are repeated (but they're\n    # still dispatched locally).\n    class MyRepeater(HelloRepeater):\n        REPEATED_NOTIFICATIONS = {\"hello\"}\n\n        def __init__(self, broadcaster):\n            HelloRepeater.__init__(self, broadcaster)\n            self.foo_count = 0\n\n        def foo(self):\n            self.foo_count += 1\n\n    b = Broadcaster()\n    r = MyRepeater(b)\n    listener = HelloListener(r)\n    r.connect()\n    listener.connect()\n    b.notify(\"hello\")\n    b.notify(\"foo\")  # if the repeater repeated this notif, we'd get a crash on HelloListener\n    eq_(r.hello_count, 1)\n    eq_(listener.hello_count, 1)\n    eq_(r.foo_count, 1)\n\n\ndef test_repeater_doesnt_try_to_dispatch_to_self_if_it_cant():\n    # if a repeater doesn't handle a particular message, it doesn't crash and simply repeats it.\n    b = Broadcaster()\n    r = Repeater(b)  # doesnt handle hello\n    listener = HelloListener(r)\n    r.connect()\n    listener.connect()\n    b.notify(\"hello\")  # no crash\n    eq_(listener.hello_count, 1)\n\n\ndef test_bind_messages():\n    b, listener = create_pair()\n    listener.bind_messages({\"foo\", \"bar\"}, listener.hello)\n    listener.connect()\n    b.notify(\"foo\")\n    b.notify(\"bar\")\n    b.notify(\"hello\")  # Normal dispatching still work\n    eq_(listener.hello_count, 3)\n"
  },
  {
    "path": "hscommon/tests/path_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2006/02/21\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.path import pathify\nfrom pathlib import Path\n\n\ndef test_pathify():\n    @pathify\n    def foo(a: Path, b, c: Path):\n        return a, b, c\n\n    a, b, c = foo(\"foo\", 0, c=Path(\"bar\"))\n    assert isinstance(a, Path)\n    assert a == Path(\"foo\")\n    assert b == 0\n    assert isinstance(c, Path)\n    assert c == Path(\"bar\")\n\n\ndef test_pathify_preserve_none():\n    # @pathify preserves None value and doesn't try to return a Path\n    @pathify\n    def foo(a: Path):\n        return a\n\n    a = foo(None)\n    assert a is None\n"
  },
  {
    "path": "hscommon/tests/selectable_list_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-09-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import eq_, callcounter, CallLogger\nfrom hscommon.gui.selectable_list import SelectableList, GUISelectableList\n\n\ndef test_in():\n    # When a SelectableList is in a list, doing \"in list\" with another instance returns false, even\n    # if they're the same as lists.\n    sl = SelectableList()\n    some_list = [sl]\n    assert SelectableList() not in some_list\n\n\ndef test_selection_range():\n    # selection is correctly adjusted on deletion\n    sl = SelectableList([\"foo\", \"bar\", \"baz\"])\n    sl.selected_index = 3\n    eq_(sl.selected_index, 2)\n    del sl[2]\n    eq_(sl.selected_index, 1)\n\n\ndef test_update_selection_called():\n    # _update_selection_is called after a change in selection. However, we only do so on select()\n    # calls. I follow the old behavior of the Table class. At the moment, I don't quite remember\n    # why there was a specific select() method for triggering _update_selection(), but I think I\n    # remember there was a reason, so I keep it that way.\n    sl = SelectableList([\"foo\", \"bar\"])\n    sl._update_selection = callcounter()\n    sl.select(1)\n    eq_(sl._update_selection.callcount, 1)\n    sl.selected_index = 0\n    eq_(sl._update_selection.callcount, 1)  # no call\n\n\ndef test_guicalls():\n    # A GUISelectableList appropriately calls its view.\n    sl = GUISelectableList([\"foo\", \"bar\"])\n    sl.view = CallLogger()\n    sl.view.check_gui_calls([\"refresh\"])  # Upon setting the view, we get a call to refresh()\n    sl[1] = \"baz\"\n    sl.view.check_gui_calls([\"refresh\"])\n    sl.append(\"foo\")\n    sl.view.check_gui_calls([\"refresh\"])\n    del sl[2]\n    sl.view.check_gui_calls([\"refresh\"])\n    sl.remove(\"baz\")\n    sl.view.check_gui_calls([\"refresh\"])\n    sl.insert(0, \"foo\")\n    sl.view.check_gui_calls([\"refresh\"])\n    sl.select(1)\n    sl.view.check_gui_calls([\"update_selection\"])\n    # XXX We have to give up on this for now because of a breakage it causes in the tables.\n    # sl.select(1) # don't update when selection stays the same\n    # gui.check_gui_calls([])\n\n\ndef test_search_by_prefix():\n    sl = SelectableList([\"foo\", \"bAr\", \"baZ\"])\n    eq_(sl.search_by_prefix(\"b\"), 1)\n    eq_(sl.search_by_prefix(\"BA\"), 1)\n    eq_(sl.search_by_prefix(\"BAZ\"), 2)\n    eq_(sl.search_by_prefix(\"BAZZ\"), -1)\n"
  },
  {
    "path": "hscommon/tests/table_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2008-08-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import CallLogger, eq_\nfrom hscommon.gui.table import Table, GUITable, Row\n\n\nclass TestRow(Row):\n    __test__ = False\n\n    def __init__(self, table, index, is_new=False):\n        Row.__init__(self, table)\n        self.is_new = is_new\n        self._index = index\n\n    def load(self):\n        # Does nothing for test\n        pass\n\n    def save(self):\n        self.is_new = False\n\n    @property\n    def index(self):\n        return self._index\n\n\nclass TestGUITable(GUITable):\n    __test__ = False\n\n    def __init__(self, rowcount, viewclass=CallLogger):\n        GUITable.__init__(self)\n        self.view = viewclass()\n        self.view.model = self\n        self.rowcount = rowcount\n        self.updated_rows = None\n\n    def _do_add(self):\n        return TestRow(self, len(self), is_new=True), len(self)\n\n    def _is_edited_new(self):\n        return self.edited is not None and self.edited.is_new\n\n    def _fill(self):\n        for i in range(self.rowcount):\n            self.append(TestRow(self, i))\n\n    def _update_selection(self):\n        self.updated_rows = self.selected_rows[:]\n\n\ndef table_with_footer():\n    table = Table()\n    table.append(TestRow(table, 0))\n    footer = TestRow(table, 1)\n    table.footer = footer\n    return table, footer\n\n\ndef table_with_header():\n    table = Table()\n    table.append(TestRow(table, 1))\n    header = TestRow(table, 0)\n    table.header = header\n    return table, header\n\n\n# --- Tests\ndef test_allow_edit_when_attr_is_property_with_fset():\n    # When a row has a property that has a fset, by default, make that cell editable.\n    class TestRow(Row):\n        @property\n        def foo(self):\n            # property only for existence checks\n            pass\n\n        @property\n        def bar(self):\n            # property only for existence checks\n            pass\n\n        @bar.setter\n        def bar(self, value):\n            # setter only for existence checks\n            pass\n\n    row = TestRow(Table())\n    assert row.can_edit_cell(\"bar\")\n    assert not row.can_edit_cell(\"foo\")\n    assert not row.can_edit_cell(\"baz\")  # doesn't exist, can't edit\n\n\ndef test_can_edit_prop_has_priority_over_fset_checks():\n    # When a row has a cen_edit_* property, it's the result of that property that is used, not the\n    # result of a fset check.\n    class TestRow(Row):\n        @property\n        def bar(self):\n            # property only for existence checks\n            pass\n\n        @bar.setter\n        def bar(self, value):\n            # setter only for existence checks\n            pass\n\n        can_edit_bar = False\n\n    row = TestRow(Table())\n    assert not row.can_edit_cell(\"bar\")\n\n\ndef test_in():\n    # When a table is in a list, doing \"in list\" with another instance returns false, even if\n    # they're the same as lists.\n    table = Table()\n    some_list = [table]\n    assert Table() not in some_list\n\n\ndef test_footer_del_all():\n    # Removing all rows doesn't crash when doing the footer check.\n    table, footer = table_with_footer()\n    del table[:]\n    assert table.footer is None\n\n\ndef test_footer_del_row():\n    # Removing the footer row sets it to None\n    table, footer = table_with_footer()\n    del table[-1]\n    assert table.footer is None\n    eq_(len(table), 1)\n\n\ndef test_footer_is_appened_to_table():\n    # A footer is appended at the table's bottom\n    table, footer = table_with_footer()\n    eq_(len(table), 2)\n    assert table[1] is footer\n\n\ndef test_footer_remove():\n    # remove() on footer sets it to None\n    table, footer = table_with_footer()\n    table.remove(footer)\n    assert table.footer is None\n\n\ndef test_footer_replaces_old_footer():\n    table, footer = table_with_footer()\n    other = Row(table)\n    table.footer = other\n    assert table.footer is other\n    eq_(len(table), 2)\n    assert table[1] is other\n\n\ndef test_footer_rows_and_row_count():\n    # rows() and row_count() ignore footer.\n    table, footer = table_with_footer()\n    eq_(table.row_count, 1)\n    eq_(table.rows, table[:-1])\n\n\ndef test_footer_setting_to_none_removes_old_one():\n    table, footer = table_with_footer()\n    table.footer = None\n    assert table.footer is None\n    eq_(len(table), 1)\n\n\ndef test_footer_stays_there_on_append():\n    # Appending another row puts it above the footer\n    table, footer = table_with_footer()\n    table.append(Row(table))\n    eq_(len(table), 3)\n    assert table[2] is footer\n\n\ndef test_footer_stays_there_on_insert():\n    # Inserting another row puts it above the footer\n    table, footer = table_with_footer()\n    table.insert(3, Row(table))\n    eq_(len(table), 3)\n    assert table[2] is footer\n\n\ndef test_header_del_all():\n    # Removing all rows doesn't crash when doing the header check.\n    table, header = table_with_header()\n    del table[:]\n    assert table.header is None\n\n\ndef test_header_del_row():\n    # Removing the header row sets it to None\n    table, header = table_with_header()\n    del table[0]\n    assert table.header is None\n    eq_(len(table), 1)\n\n\ndef test_header_is_inserted_in_table():\n    # A header is inserted at the table's top\n    table, header = table_with_header()\n    eq_(len(table), 2)\n    assert table[0] is header\n\n\ndef test_header_remove():\n    # remove() on header sets it to None\n    table, header = table_with_header()\n    table.remove(header)\n    assert table.header is None\n\n\ndef test_header_replaces_old_header():\n    table, header = table_with_header()\n    other = Row(table)\n    table.header = other\n    assert table.header is other\n    eq_(len(table), 2)\n    assert table[0] is other\n\n\ndef test_header_rows_and_row_count():\n    # rows() and row_count() ignore header.\n    table, header = table_with_header()\n    eq_(table.row_count, 1)\n    eq_(table.rows, table[1:])\n\n\ndef test_header_setting_to_none_removes_old_one():\n    table, header = table_with_header()\n    table.header = None\n    assert table.header is None\n    eq_(len(table), 1)\n\n\ndef test_header_stays_there_on_insert():\n    # Inserting another row at the top puts it below the header\n    table, header = table_with_header()\n    table.insert(0, Row(table))\n    eq_(len(table), 3)\n    assert table[0] is header\n\n\ndef test_refresh_view_on_refresh():\n    # If refresh_view is not False, we refresh the table's view on refresh()\n    table = TestGUITable(1)\n    table.refresh()\n    table.view.check_gui_calls([\"refresh\"])\n    table.view.clear_calls()\n    table.refresh(refresh_view=False)\n    table.view.check_gui_calls([])\n\n\ndef test_restore_selection():\n    # By default, after a refresh, selection goes on the last row\n    table = TestGUITable(10)\n    table.refresh()\n    eq_(table.selected_indexes, [9])\n\n\ndef test_restore_selection_after_cancel_edits():\n    # _restore_selection() is called after cancel_edits(). Previously, only _update_selection would\n    # be called.\n    class MyTable(TestGUITable):\n        def _restore_selection(self, previous_selection):\n            self.selected_indexes = [6]\n\n    table = MyTable(10)\n    table.refresh()\n    table.add()\n    table.cancel_edits()\n    eq_(table.selected_indexes, [6])\n\n\ndef test_restore_selection_with_previous_selection():\n    # By default, we try to restore the selection that was there before a refresh\n    table = TestGUITable(10)\n    table.refresh()\n    table.selected_indexes = [2, 4]\n    table.refresh()\n    eq_(table.selected_indexes, [2, 4])\n\n\ndef test_restore_selection_custom():\n    # After a _fill() called, the virtual _restore_selection() is called so that it's possible for a\n    # GUITable subclass to customize its post-refresh selection behavior.\n    class MyTable(TestGUITable):\n        def _restore_selection(self, previous_selection):\n            self.selected_indexes = [6]\n\n    table = MyTable(10)\n    table.refresh()\n    eq_(table.selected_indexes, [6])\n\n\ndef test_row_cell_value():\n    # *_cell_value() correctly mangles attrnames that are Python reserved words.\n    row = Row(Table())\n    row.from_ = \"foo\"\n    eq_(row.get_cell_value(\"from\"), \"foo\")\n    row.set_cell_value(\"from\", \"bar\")\n    eq_(row.get_cell_value(\"from\"), \"bar\")\n\n\ndef test_sort_table_also_tries_attributes_without_underscores():\n    # When determining a sort key, after having unsuccessfully tried the attribute with the,\n    # underscore, try the one without one.\n    table = Table()\n    row1 = Row(table)\n    row1._foo = \"a\"  # underscored attr must be checked first\n    row1.foo = \"b\"\n    row1.bar = \"c\"\n    row2 = Row(table)\n    row2._foo = \"b\"\n    row2.foo = \"a\"\n    row2.bar = \"b\"\n    table.append(row1)\n    table.append(row2)\n    table.sort_by(\"foo\")\n    assert table[0] is row1\n    assert table[1] is row2\n    table.sort_by(\"bar\")\n    assert table[0] is row2\n    assert table[1] is row1\n\n\ndef test_sort_table_updates_selection():\n    table = TestGUITable(10)\n    table.refresh()\n    table.select([2, 4])\n    table.sort_by(\"index\", desc=True)\n    # Now, the updated rows should be 7 and 5\n    eq_(len(table.updated_rows), 2)\n    r1, r2 = table.updated_rows\n    eq_(r1.index, 7)\n    eq_(r2.index, 5)\n\n\ndef test_sort_table_with_footer():\n    # Sorting a table with a footer keeps it at the bottom\n    table, footer = table_with_footer()\n    table.sort_by(\"index\", desc=True)\n    assert table[-1] is footer\n\n\ndef test_sort_table_with_header():\n    # Sorting a table with a header keeps it at the top\n    table, header = table_with_header()\n    table.sort_by(\"index\", desc=True)\n    assert table[0] is header\n\n\ndef test_add_with_view_that_saves_during_refresh():\n    # Calling save_edits during refresh() called by add() is ignored.\n    class TableView(CallLogger):\n        def refresh(self):\n            self.model.save_edits()\n\n    table = TestGUITable(10, viewclass=TableView)\n    table.add()\n    assert table.edited is not None  # still in edit mode\n"
  },
  {
    "path": "hscommon/tests/tree_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom hscommon.testutil import eq_\nfrom hscommon.gui.tree import Tree, Node\n\n\ndef tree_with_some_nodes():\n    t = Tree()\n    t.append(Node(\"foo\"))\n    t.append(Node(\"bar\"))\n    t.append(Node(\"baz\"))\n    t[0].append(Node(\"sub1\"))\n    t[0].append(Node(\"sub2\"))\n    return t\n\n\ndef test_selection():\n    t = tree_with_some_nodes()\n    assert t.selected_node is None\n    eq_(t.selected_nodes, [])\n    assert t.selected_path is None\n    eq_(t.selected_paths, [])\n\n\ndef test_select_one_node():\n    t = tree_with_some_nodes()\n    t.selected_node = t[0][0]\n    assert t.selected_node is t[0][0]\n    eq_(t.selected_nodes, [t[0][0]])\n    eq_(t.selected_path, [0, 0])\n    eq_(t.selected_paths, [[0, 0]])\n\n\ndef test_select_one_path():\n    t = tree_with_some_nodes()\n    t.selected_path = [0, 1]\n    assert t.selected_node is t[0][1]\n\n\ndef test_select_multiple_nodes():\n    t = tree_with_some_nodes()\n    t.selected_nodes = [t[0], t[1]]\n    eq_(t.selected_paths, [[0], [1]])\n\n\ndef test_select_multiple_paths():\n    t = tree_with_some_nodes()\n    t.selected_paths = [[0], [1]]\n    eq_(t.selected_nodes, [t[0], t[1]])\n\n\ndef test_select_none_path():\n    # setting selected_path to None clears the selection\n    t = Tree()\n    t.selected_path = None\n    assert t.selected_path is None\n\n\ndef test_select_none_node():\n    # setting selected_node to None clears the selection\n    t = Tree()\n    t.selected_node = None\n    eq_(t.selected_nodes, [])\n\n\ndef test_clear_removes_selection():\n    # When clearing a tree, we want to clear the selection as well or else we end up with a crash\n    # when calling selected_paths.\n    t = tree_with_some_nodes()\n    t.selected_path = [0]\n    t.clear()\n    assert t.selected_node is None\n\n\ndef test_selection_override():\n    # All selection changed pass through the _select_node() method so it's easy for subclasses to\n    # customize the tree's behavior.\n    class MyTree(Tree):\n        called = False\n\n        def _select_nodes(self, nodes):\n            self.called = True\n\n    t = MyTree()\n    t.selected_paths = []\n    assert t.called\n    t.called = False\n    t.selected_node = None\n    assert t.called\n\n\ndef test_findall():\n    t = tree_with_some_nodes()\n    r = t.findall(lambda n: n.name.startswith(\"sub\"))\n    eq_(set(r), {t[0][0], t[0][1]})\n\n\ndef test_findall_dont_include_self():\n    # When calling findall with include_self=False, the node itself is never evaluated.\n    t = tree_with_some_nodes()\n    del t._name  # so that if the predicate is called on `t`, we crash\n    r = t.findall(lambda n: not n.name.startswith(\"sub\"), include_self=False)  # no crash\n    eq_(set(r), {t[0], t[1], t[2]})\n\n\ndef test_find_dont_include_self():\n    # When calling find with include_self=False, the node itself is never evaluated.\n    t = tree_with_some_nodes()\n    del t._name  # so that if the predicate is called on `t`, we crash\n    r = t.find(lambda n: not n.name.startswith(\"sub\"), include_self=False)  # no crash\n    assert r is t[0]\n\n\ndef test_find_none():\n    # when find() yields no result, return None\n    t = Tree()\n    assert t.find(lambda n: False) is None  # no StopIteration exception\n"
  },
  {
    "path": "hscommon/tests/util_test.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-01-11\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom io import StringIO\n\nfrom pytest import raises\n\nfrom hscommon.testutil import eq_\nfrom pathlib import Path\nfrom hscommon.util import (\n    nonone,\n    tryint,\n    first,\n    flatten,\n    dedupe,\n    extract,\n    allsame,\n    format_time,\n    format_time_decimal,\n    format_size,\n    multi_replace,\n    delete_if_empty,\n    open_if_filename,\n    FileOrPath,\n    iterconsume,\n    escape,\n    get_file_ext,\n    rem_file_ext,\n    pluralize,\n)\n\n\ndef test_nonone():\n    eq_(\"foo\", nonone(\"foo\", \"bar\"))\n    eq_(\"bar\", nonone(None, \"bar\"))\n\n\ndef test_tryint():\n    eq_(42, tryint(\"42\"))\n    eq_(0, tryint(\"abc\"))\n    eq_(0, tryint(None))\n    eq_(42, tryint(None, 42))\n\n\n# --- Sequence\n\n\ndef test_first():\n    eq_(first([3, 2, 1]), 3)\n    eq_(first(i for i in [3, 2, 1] if i < 3), 2)\n\n\ndef test_flatten():\n    eq_([1, 2, 3, 4], flatten([[1, 2], [3, 4]]))\n    eq_([], flatten([]))\n\n\ndef test_dedupe():\n    reflist = [0, 7, 1, 2, 3, 4, 4, 5, 6, 7, 1, 2, 3]\n    eq_(dedupe(reflist), [0, 7, 1, 2, 3, 4, 5, 6])\n\n\ndef test_extract():\n    wheat, shaft = extract(lambda n: n % 2 == 0, list(range(10)))\n    eq_(wheat, [0, 2, 4, 6, 8])\n    eq_(shaft, [1, 3, 5, 7, 9])\n\n\ndef test_allsame():\n    assert allsame([42, 42, 42])\n    assert not allsame([42, 43, 42])\n    assert not allsame([43, 42, 42])\n    # Works on non-sequence as well\n    assert allsame(iter([42, 42, 42]))\n\n\ndef test_iterconsume():\n    # We just want to make sure that we return *all* items and that we're not mistakenly skipping\n    # one.\n    eq_(list(range(2500)), list(iterconsume(list(range(2500)))))\n    eq_(list(reversed(range(2500))), list(iterconsume(list(range(2500)), reverse=False)))\n\n\n# --- String\n\n\ndef test_escape():\n    eq_(\"f\\\\o\\\\ob\\\\ar\", escape(\"foobar\", \"oa\"))\n    eq_(\"f*o*ob*ar\", escape(\"foobar\", \"oa\", \"*\"))\n    eq_(\"f*o*ob*ar\", escape(\"foobar\", set(\"oa\"), \"*\"))\n\n\ndef test_get_file_ext():\n    eq_(get_file_ext(\"foobar\"), \"\")\n    eq_(get_file_ext(\"foo.bar\"), \"bar\")\n    eq_(get_file_ext(\"foobar.\"), \"\")\n    eq_(get_file_ext(\".foobar\"), \"foobar\")\n\n\ndef test_rem_file_ext():\n    eq_(rem_file_ext(\"foobar\"), \"foobar\")\n    eq_(rem_file_ext(\"foo.bar\"), \"foo\")\n    eq_(rem_file_ext(\"foobar.\"), \"foobar\")\n    eq_(rem_file_ext(\".foobar\"), \"\")\n\n\ndef test_pluralize():\n    eq_(\"0 song\", pluralize(0, \"song\"))\n    eq_(\"1 song\", pluralize(1, \"song\"))\n    eq_(\"2 songs\", pluralize(2, \"song\"))\n    eq_(\"1 song\", pluralize(1.1, \"song\"))\n    eq_(\"2 songs\", pluralize(1.5, \"song\"))\n    eq_(\"1.1 songs\", pluralize(1.1, \"song\", 1))\n    eq_(\"1.5 songs\", pluralize(1.5, \"song\", 1))\n    eq_(\"2 entries\", pluralize(2, \"entry\", plural_word=\"entries\"))\n\n\ndef test_format_time():\n    eq_(format_time(0), \"00:00:00\")\n    eq_(format_time(1), \"00:00:01\")\n    eq_(format_time(23), \"00:00:23\")\n    eq_(format_time(60), \"00:01:00\")\n    eq_(format_time(101), \"00:01:41\")\n    eq_(format_time(683), \"00:11:23\")\n    eq_(format_time(3600), \"01:00:00\")\n    eq_(format_time(3754), \"01:02:34\")\n    eq_(format_time(36000), \"10:00:00\")\n    eq_(format_time(366666), \"101:51:06\")\n    eq_(format_time(0, with_hours=False), \"00:00\")\n    eq_(format_time(1, with_hours=False), \"00:01\")\n    eq_(format_time(23, with_hours=False), \"00:23\")\n    eq_(format_time(60, with_hours=False), \"01:00\")\n    eq_(format_time(101, with_hours=False), \"01:41\")\n    eq_(format_time(683, with_hours=False), \"11:23\")\n    eq_(format_time(3600, with_hours=False), \"60:00\")\n    eq_(format_time(6036, with_hours=False), \"100:36\")\n    eq_(format_time(60360, with_hours=False), \"1006:00\")\n\n\ndef test_format_time_decimal():\n    eq_(format_time_decimal(0), \"0.0 second\")\n    eq_(format_time_decimal(1), \"1.0 second\")\n    eq_(format_time_decimal(23), \"23.0 seconds\")\n    eq_(format_time_decimal(60), \"1.0 minute\")\n    eq_(format_time_decimal(101), \"1.7 minutes\")\n    eq_(format_time_decimal(683), \"11.4 minutes\")\n    eq_(format_time_decimal(3600), \"1.0 hour\")\n    eq_(format_time_decimal(6036), \"1.7 hours\")\n    eq_(format_time_decimal(86400), \"1.0 day\")\n    eq_(format_time_decimal(160360), \"1.9 days\")\n\n\ndef test_format_size():\n    eq_(format_size(1024), \"1 KB\")\n    eq_(format_size(1024, 2), \"1.00 KB\")\n    eq_(format_size(1024, 0, 2), \"1 MB\")\n    eq_(format_size(1024, 2, 2), \"0.01 MB\")\n    eq_(format_size(1024, 3, 2), \"0.001 MB\")\n    eq_(format_size(1024, 3, 2, False), \"0.001\")\n    eq_(format_size(1023), \"1023 B\")\n    eq_(format_size(1023, 0, 1), \"1 KB\")\n    eq_(format_size(511, 0, 1), \"1 KB\")\n    eq_(format_size(9), \"9 B\")\n    eq_(format_size(99), \"99 B\")\n    eq_(format_size(999), \"999 B\")\n    eq_(format_size(9999), \"10 KB\")\n    eq_(format_size(99999), \"98 KB\")\n    eq_(format_size(999999), \"977 KB\")\n    eq_(format_size(9999999), \"10 MB\")\n    eq_(format_size(99999999), \"96 MB\")\n    eq_(format_size(999999999), \"954 MB\")\n    eq_(format_size(9999999999), \"10 GB\")\n    eq_(format_size(99999999999), \"94 GB\")\n    eq_(format_size(999999999999), \"932 GB\")\n    eq_(format_size(9999999999999), \"10 TB\")\n    eq_(format_size(99999999999999), \"91 TB\")\n    eq_(format_size(999999999999999), \"910 TB\")\n    eq_(format_size(9999999999999999), \"9 PB\")\n    eq_(format_size(99999999999999999), \"89 PB\")\n    eq_(format_size(999999999999999999), \"889 PB\")\n    eq_(format_size(9999999999999999999), \"9 EB\")\n    eq_(format_size(99999999999999999999), \"87 EB\")\n    eq_(format_size(999999999999999999999), \"868 EB\")\n    eq_(format_size(9999999999999999999999), \"9 ZB\")\n    eq_(format_size(99999999999999999999999), \"85 ZB\")\n    eq_(format_size(999999999999999999999999), \"848 ZB\")\n\n\ndef test_multi_replace():\n    eq_(\"136\", multi_replace(\"123456\", (\"2\", \"45\")))\n    eq_(\"1 3 6\", multi_replace(\"123456\", (\"2\", \"45\"), \" \"))\n    eq_(\"1 3  6\", multi_replace(\"123456\", \"245\", \" \"))\n    eq_(\"173896\", multi_replace(\"123456\", \"245\", \"789\"))\n    eq_(\"173896\", multi_replace(\"123456\", \"245\", (\"7\", \"8\", \"9\")))\n    eq_(\"17386\", multi_replace(\"123456\", (\"2\", \"45\"), \"78\"))\n    eq_(\"17386\", multi_replace(\"123456\", (\"2\", \"45\"), (\"7\", \"8\")))\n    with raises(ValueError):\n        multi_replace(\"123456\", (\"2\", \"45\"), (\"7\", \"8\", \"9\"))\n    eq_(\"17346\", multi_replace(\"12346\", (\"2\", \"45\"), \"78\"))\n\n\n# --- Files\n\n\nclass TestCaseDeleteIfEmpty:\n    def test_is_empty(self, tmpdir):\n        testpath = Path(str(tmpdir))\n        assert delete_if_empty(testpath)\n        assert not testpath.exists()\n\n    def test_not_empty(self, tmpdir):\n        testpath = Path(str(tmpdir))\n        testpath.joinpath(\"foo\").mkdir()\n        assert not delete_if_empty(testpath)\n        assert testpath.exists()\n\n    def test_with_files_to_delete(self, tmpdir):\n        testpath = Path(str(tmpdir))\n        testpath.joinpath(\"foo\").touch()\n        testpath.joinpath(\"bar\").touch()\n        assert delete_if_empty(testpath, [\"foo\", \"bar\"])\n        assert not testpath.exists()\n\n    def test_directory_in_files_to_delete(self, tmpdir):\n        testpath = Path(str(tmpdir))\n        testpath.joinpath(\"foo\").mkdir()\n        assert not delete_if_empty(testpath, [\"foo\"])\n        assert testpath.exists()\n\n    def test_delete_files_to_delete_only_if_dir_is_empty(self, tmpdir):\n        testpath = Path(str(tmpdir))\n        testpath.joinpath(\"foo\").touch()\n        testpath.joinpath(\"bar\").touch()\n        assert not delete_if_empty(testpath, [\"foo\"])\n        assert testpath.exists()\n        assert testpath.joinpath(\"foo\").exists()\n\n    def test_doesnt_exist(self):\n        # When the 'path' doesn't exist, just do nothing.\n        delete_if_empty(Path(\"does_not_exist\"))  # no crash\n\n    def test_is_file(self, tmpdir):\n        # When 'path' is a file, do nothing.\n        p = Path(str(tmpdir)).joinpath(\"filename\")\n        p.touch()\n        delete_if_empty(p)  # no crash\n\n    def test_ioerror(self, tmpdir, monkeypatch):\n        # if an IO error happens during the operation, ignore it.\n        def do_raise(*args, **kw):\n            raise OSError()\n\n        monkeypatch.setattr(Path, \"rmdir\", do_raise)\n        delete_if_empty(Path(str(tmpdir)))  # no crash\n\n\nclass TestCaseOpenIfFilename:\n    FILE_NAME = \"test.txt\"\n\n    def test_file_name(self, tmpdir):\n        filepath = str(tmpdir.join(self.FILE_NAME))\n        open(filepath, \"wb\").write(b\"test_data\")\n        file, close = open_if_filename(filepath)\n        assert close\n        eq_(b\"test_data\", file.read())\n        file.close()\n\n    def test_opened_file(self):\n        sio = StringIO()\n        sio.write(\"test_data\")\n        sio.seek(0)\n        file, close = open_if_filename(sio)\n        assert not close\n        eq_(\"test_data\", file.read())\n\n    def test_mode_is_passed_to_open(self, tmpdir):\n        filepath = str(tmpdir.join(self.FILE_NAME))\n        open(filepath, \"w\").close()\n        file, close = open_if_filename(filepath, \"a\")\n        eq_(\"a\", file.mode)\n        file.close()\n\n\nclass TestCaseFileOrPath:\n    FILE_NAME = \"test.txt\"\n\n    def test_path(self, tmpdir):\n        filepath = str(tmpdir.join(self.FILE_NAME))\n        open(filepath, \"wb\").write(b\"test_data\")\n        with FileOrPath(filepath) as fp:\n            eq_(b\"test_data\", fp.read())\n\n    def test_opened_file(self):\n        sio = StringIO()\n        sio.write(\"test_data\")\n        sio.seek(0)\n        with FileOrPath(sio) as fp:\n            eq_(\"test_data\", fp.read())\n\n    def test_mode_is_passed_to_open(self, tmpdir):\n        filepath = str(tmpdir.join(self.FILE_NAME))\n        open(filepath, \"w\").close()\n        with FileOrPath(filepath, \"a\") as fp:\n            eq_(\"a\", fp.mode)\n"
  },
  {
    "path": "hscommon/testutil.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-11-14\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport pytest\n\n\ndef eq_(a, b, msg=None):\n    __tracebackhide__ = True\n    assert a == b, msg or \"{!r} != {!r}\".format(a, b)\n\n\ndef callcounter():\n    def f(*args, **kwargs):\n        f.callcount += 1\n\n    f.callcount = 0\n    return f\n\n\nclass CallLogger:\n    \"\"\"This is a dummy object that logs all calls made to it.\n\n    It is used to simulate the GUI layer.\n    \"\"\"\n\n    def __init__(self):\n        self.calls = []\n\n    def __getattr__(self, func_name):\n        def func(*args, **kw):\n            self.calls.append(func_name)\n\n        return func\n\n    def clear_calls(self):\n        del self.calls[:]\n\n    def check_gui_calls(self, expected, verify_order=False):\n        \"\"\"Checks that the expected calls have been made to 'self', then clears the log.\n\n        `expected` is an iterable of strings representing method names.\n        If `verify_order` is True, the order of the calls matters.\n        \"\"\"\n        __tracebackhide__ = True\n        if verify_order:\n            eq_(self.calls, expected)\n        else:\n            eq_(set(self.calls), set(expected))\n        self.clear_calls()\n\n    def check_gui_calls_partial(self, expected=None, not_expected=None, verify_order=False):\n        \"\"\"Checks that the expected calls have been made to 'self', then clears the log.\n\n        `expected` is an iterable of strings representing method names. Order doesn't matter.\n        Moreover, if calls have been made that are not in expected, no failure occur.\n        `not_expected` can be used for a more explicit check (rather than calling `check_gui_calls`\n        with an empty `expected`) to assert that calls have *not* been made.\n        \"\"\"\n        __tracebackhide__ = True\n        if expected is not None:\n            not_called = set(expected) - set(self.calls)\n            assert not not_called, f\"These calls haven't been made: {not_called}\"\n            if verify_order:\n                max_index = 0\n                for call in expected:\n                    index = self.calls.index(call)\n                    if index < max_index:\n                        raise AssertionError(f\"The call {call} hasn't been made in the correct order\")\n                    max_index = index\n        if not_expected is not None:\n            called = set(not_expected) & set(self.calls)\n            assert not called, f\"These calls shouldn't have been made: {called}\"\n        self.clear_calls()\n\n\nclass TestApp:\n    def __init__(self):\n        self._call_loggers = []\n\n    def clear_gui_calls(self):\n        for logger in self._call_loggers:\n            logger.clear_calls()\n\n    def make_logger(self, logger=None):\n        if logger is None:\n            logger = CallLogger()\n        self._call_loggers.append(logger)\n        return logger\n\n    def make_gui(self, name, class_, view=None, parent=None, holder=None):\n        if view is None:\n            view = self.make_logger()\n        if parent is None:\n            # The attribute \"default_parent\" has to be set for this to work correctly\n            parent = self.default_parent\n        if holder is None:\n            holder = self\n        setattr(holder, f\"{name}_gui\", view)\n        gui = class_(parent)\n        gui.view = view\n        setattr(holder, name, gui)\n        return gui\n\n\n# To use @with_app, you have to import app in your conftest.py file.\ndef with_app(setupfunc):\n    def decorator(func):\n        func.setupfunc = setupfunc\n        return func\n\n    return decorator\n\n\n@pytest.fixture\ndef app(request):\n    setupfunc = request.function.setupfunc\n    if hasattr(setupfunc, \"__code__\"):\n        argnames = setupfunc.__code__.co_varnames[: setupfunc.__code__.co_argcount]\n\n        def getarg(name):\n            if name == \"self\":\n                return request.function.__self__\n            else:\n                return request.getfixturevalue(name)\n\n        args = [getarg(argname) for argname in argnames]\n    else:\n        args = []\n    app = setupfunc(*args)\n    return app\n\n\ndef _unify_args(func, args, kwargs, args_to_ignore=None):\n    \"\"\"Unify args and kwargs in the same dictionary.\n\n    The result is kwargs with args added to it. func.func_code.co_varnames is used to determine\n    under what key each elements of arg will be mapped in kwargs.\n\n    if you want some arguments not to be in the results, supply a list of arg names in\n    args_to_ignore.\n\n    if f is a function that takes *args, func_code.co_varnames is empty, so args will be put\n    under 'args' in kwargs.\n\n    def foo(bar, baz)\n    _unifyArgs(foo, (42,), {'baz': 23}) --> {'bar': 42, 'baz': 23}\n    _unifyArgs(foo, (42,), {'baz': 23}, ['bar']) --> {'baz': 23}\n    \"\"\"\n    result = kwargs.copy()\n    if hasattr(func, \"__code__\"):  # built-in functions don't have func_code\n        args = list(args)\n        if getattr(func, \"__self__\", None) is not None:  # bound method, we have to add self to args list\n            args = [func.__self__] + args\n        defaults = list(func.__defaults__) if func.__defaults__ is not None else []\n        arg_count = func.__code__.co_argcount\n        arg_names = list(func.__code__.co_varnames)\n        if len(args) < arg_count:  # We have default values\n            required_arg_count = arg_count - len(args)\n            args = args + defaults[-required_arg_count:]\n        for arg_name, arg in zip(arg_names, args):\n            # setdefault is used because if the arg is already in kwargs, we don't want to use default values\n            result.setdefault(arg_name, arg)\n    else:\n        # 'func' has a *args argument\n        result[\"args\"] = args\n    if args_to_ignore:\n        for kw in args_to_ignore:\n            del result[kw]\n    return result\n\n\ndef log_calls(func):\n    \"\"\"Logs all func calls' arguments under func.calls.\n\n    func.calls is a list of _unify_args() result (dict).\n\n    Mostly used for unit testing.\n    \"\"\"\n\n    def wrapper(*args, **kwargs):\n        unified_args = _unify_args(func, args, kwargs)\n        wrapper.calls.append(unified_args)\n        return func(*args, **kwargs)\n\n    wrapper.calls = []\n    return wrapper\n"
  },
  {
    "path": "hscommon/trans.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-06-23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n# Doing i18n with GNU gettext for the core text gets complicated, so what I do is that I make the\n# GUI layer responsible for supplying a tr() function.\n\nimport locale\nimport logging\nimport os\nimport os.path as op\nfrom typing import Callable, Union\n\nfrom hscommon.plat import ISLINUX\n\n_trfunc = None\n_trget = None\ninstalled_lang = None\n\n\ndef tr(s: str, context: Union[str, None] = None) -> str:\n    if _trfunc is None:\n        return s\n    else:\n        if context:\n            return _trfunc(s, context)\n        else:\n            return _trfunc(s)\n\n\ndef trget(domain: str) -> Callable[[str], str]:\n    # Returns a tr() function for the specified domain.\n    if _trget is None:\n        return lambda s: tr(s, domain)\n    else:\n        return _trget(domain)\n\n\ndef set_tr(\n    new_tr: Callable[[str, Union[str, None]], str],\n    new_trget: Union[Callable[[str], Callable[[str], str]], None] = None,\n) -> None:\n    global _trfunc, _trget\n    _trfunc = new_tr\n    if new_trget is not None:\n        _trget = new_trget\n\n\ndef get_locale_name(lang: str) -> Union[str, None]:\n    # Removed old conversion code as windows seems to support these\n    LANG2LOCALENAME = {\n        \"cs\": \"cs_CZ\",\n        \"de\": \"de_DE\",\n        \"el\": \"el_GR\",\n        \"en\": \"en\",\n        \"es\": \"es_ES\",\n        \"fr\": \"fr_FR\",\n        \"hy\": \"hy_AM\",\n        \"it\": \"it_IT\",\n        \"ja\": \"ja_JP\",\n        \"ko\": \"ko_KR\",\n        \"ms\": \"ms_MY\",\n        \"nl\": \"nl_NL\",\n        \"pl_PL\": \"pl_PL\",\n        \"pt_BR\": \"pt_BR\",\n        \"ru\": \"ru_RU\",\n        \"tr\": \"tr_TR\",\n        \"uk\": \"uk_UA\",\n        \"vi\": \"vi_VN\",\n        \"zh_CN\": \"zh_CN\",\n    }\n    if lang not in LANG2LOCALENAME:\n        return None\n    result = LANG2LOCALENAME[lang]\n    if ISLINUX:\n        result += \".UTF-8\"\n    return result\n\n\n# --- Qt\ndef install_qt_trans(lang: str = None) -> None:\n    from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale\n\n    if not lang:\n        lang = str(QLocale.system().name())[:2]\n    localename = get_locale_name(lang)\n    if localename is not None:\n        try:\n            locale.setlocale(locale.LC_ALL, localename)\n        except locale.Error:\n            logging.warning(\"Couldn't set locale %s\", localename)\n    else:\n        lang = \"en\"\n    qtr1 = QTranslator(QCoreApplication.instance())\n    qtr1.load(\":/qt_%s\" % lang)\n    QCoreApplication.installTranslator(qtr1)\n    qtr2 = QTranslator(QCoreApplication.instance())\n    qtr2.load(\":/%s\" % lang)\n    QCoreApplication.installTranslator(qtr2)\n\n    def qt_tr(s: str, context: Union[str, None] = \"core\") -> str:\n        if context is None:\n            context = \"core\"\n        return str(QCoreApplication.translate(context, s, None))\n\n    set_tr(qt_tr)\n\n\n# --- gettext\ndef install_gettext_trans(base_folder: os.PathLike, lang: str) -> None:\n    import gettext\n\n    def gettext_trget(domain: str) -> Callable[[str], str]:\n        if not lang:\n            return lambda s: s\n        try:\n            return gettext.translation(domain, localedir=base_folder, languages=[lang]).gettext\n        except OSError:\n            return lambda s: s\n\n    default_gettext = gettext_trget(\"core\")\n\n    def gettext_tr(s: str, context: Union[str, None] = None) -> str:\n        if not context:\n            return default_gettext(s)\n        else:\n            trfunc = gettext_trget(context)\n            return trfunc(s)\n\n    set_tr(gettext_tr, gettext_trget)\n    global installed_lang\n    installed_lang = lang\n\n\ndef install_gettext_trans_under_qt(base_folder: os.PathLike, lang: str = None) -> None:\n    # So, we install the gettext locale, great, but we also should try to install qt_*.qm if\n    # available so that strings that are inside Qt itself over which I have no control are in the\n    # right language.\n    from PyQt5.QtCore import QCoreApplication, QTranslator, QLocale, QLibraryInfo\n\n    if not lang:\n        lang = str(QLocale.system().name())[:2]\n    localename = get_locale_name(lang)\n    if localename is None:\n        lang = \"en\"\n        localename = get_locale_name(lang)\n    try:\n        locale.setlocale(locale.LC_ALL, localename)\n    except locale.Error:\n        logging.warning(\"Couldn't set locale %s\", localename)\n    qmname = \"qt_%s\" % lang\n    if ISLINUX:\n        # Under linux, a full Qt installation is already available in the system, we didn't bundle\n        # up the qm files in our package, so we have to load translations from the system.\n        qmpath = op.join(QLibraryInfo.location(QLibraryInfo.TranslationsPath), qmname)\n    else:\n        qmpath = op.join(base_folder, qmname)\n    qtr = QTranslator(QCoreApplication.instance())\n    qtr.load(qmpath)\n    QCoreApplication.installTranslator(qtr)\n    install_gettext_trans(base_folder, lang)\n"
  },
  {
    "path": "hscommon/util.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-01-11\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom math import ceil\nfrom pathlib import Path\nfrom hscommon.path import pathify, log_io_error\n\nfrom typing import IO, Any, Callable, Generator, Iterable, List, Tuple, Union\n\n\ndef nonone(value: Any, replace_value: Any) -> Any:\n    \"\"\"Returns ``value`` if ``value`` is not ``None``. Returns ``replace_value`` otherwise.\"\"\"\n    if value is None:\n        return replace_value\n    else:\n        return value\n\n\ndef tryint(value: Any, default: int = 0) -> int:\n    \"\"\"Tries to convert ``value`` to in ``int`` and returns ``default`` if it fails.\"\"\"\n    try:\n        return int(value)\n    except (TypeError, ValueError):\n        return default\n\n\n# --- Sequence related\n\n\ndef dedupe(iterable: Iterable[Any]) -> List[Any]:\n    \"\"\"Returns a list of elements in ``iterable`` with all dupes removed.\n\n    The order of the elements is preserved.\n    \"\"\"\n    result = []\n    seen = {}\n    for item in iterable:\n        if item in seen:\n            continue\n        seen[item] = 1\n        result.append(item)\n    return result\n\n\ndef flatten(iterables: Iterable[Iterable], start_with: Iterable[Any] = None) -> List[Any]:\n    \"\"\"Takes a list of lists ``iterables`` and returns a list containing elements of every list.\n\n    If ``start_with`` is not ``None``, the result will start with ``start_with`` items, exactly as\n    if ``start_with`` would be the first item of lists.\n    \"\"\"\n    result: List[Any] = []\n    if start_with:\n        result.extend(start_with)\n    for iterable in iterables:\n        result.extend(iterable)\n    return result\n\n\ndef first(iterable: Iterable[Any]):\n    \"\"\"Returns the first item of ``iterable``.\"\"\"\n    try:\n        return next(iter(iterable))\n    except StopIteration:\n        return None\n\n\ndef extract(predicate: Callable[[Any], bool], iterable: Iterable[Any]) -> Tuple[List[Any], List[Any]]:\n    \"\"\"Separates the wheat from the shaft (`predicate` defines what's the wheat), and returns both.\"\"\"\n    wheat = []\n    shaft = []\n    for item in iterable:\n        if predicate(item):\n            wheat.append(item)\n        else:\n            shaft.append(item)\n    return wheat, shaft\n\n\ndef allsame(iterable: Iterable[Any]) -> bool:\n    \"\"\"Returns whether all elements of 'iterable' are the same.\"\"\"\n    it = iter(iterable)\n    try:\n        first_item = next(it)\n    except StopIteration:\n        raise ValueError(\"iterable cannot be empty\")\n    return all(element == first_item for element in it)\n\n\ndef iterconsume(seq: List[Any], reverse: bool = True) -> Generator[Any, None, None]:\n    \"\"\"Iterate over ``seq`` and pops yielded objects.\n\n    Because we use the ``pop()`` method, we reverse ``seq`` before proceeding. If you don't need\n    to do that, set ``reverse`` to ``False``.\n\n    This is useful in tight memory situation where you are looping over a sequence of objects that\n    are going to be discarded afterwards. If you're creating other objects during that iteration\n    you might want to use this to avoid ``MemoryError``.\n    \"\"\"\n    if reverse:\n        seq.reverse()\n    while seq:\n        yield seq.pop()\n\n\n# --- String related\n\n\ndef escape(s: str, to_escape: str, escape_with: str = \"\\\\\") -> str:\n    \"\"\"Returns ``s`` with characters in ``to_escape`` all prepended with ``escape_with``.\"\"\"\n    return \"\".join((escape_with + c if c in to_escape else c) for c in s)\n\n\ndef get_file_ext(filename: str) -> str:\n    \"\"\"Returns the lowercase extension part of filename, without the dot.\"\"\"\n    pos = filename.rfind(\".\")\n    if pos > -1:\n        return filename[pos + 1 :].lower()\n    else:\n        return \"\"\n\n\ndef rem_file_ext(filename: str) -> str:\n    \"\"\"Returns the filename without extension.\"\"\"\n    pos = filename.rfind(\".\")\n    if pos > -1:\n        return filename[:pos]\n    else:\n        return filename\n\n\n# TODO type hint number\ndef pluralize(number, word: str, decimals: int = 0, plural_word: Union[str, None] = None) -> str:\n    \"\"\"Returns a pluralized string with ``number`` in front of ``word``.\n\n    Adds a 's' to s if ``number`` > 1.\n    ``number``: The number to go in front of s\n    ``word``: The word to go after number\n    ``decimals``: The number of digits after the dot\n    ``plural_word``: If the plural rule for word is more complex than adding a 's', specify a plural\n    \"\"\"\n    number = round(number, decimals)\n    plural_format = \"%%1.%df %%s\" % decimals\n    if number > 1:\n        if plural_word is None:\n            word += \"s\"\n        else:\n            word = plural_word\n    return plural_format % (number, word)\n\n\ndef format_time(seconds: int, with_hours: bool = True) -> str:\n    \"\"\"Transforms seconds in a hh:mm:ss string.\n\n    If ``with_hours`` if false, the format is mm:ss.\n    \"\"\"\n    minus = seconds < 0\n    if minus:\n        seconds *= -1\n    m, s = divmod(seconds, 60)\n    if with_hours:\n        h, m = divmod(m, 60)\n        r = \"%02d:%02d:%02d\" % (h, m, s)\n    else:\n        r = \"%02d:%02d\" % (m, s)\n    if minus:\n        return \"-\" + r\n    else:\n        return r\n\n\ndef format_time_decimal(seconds: int) -> str:\n    \"\"\"Transforms seconds in a strings like '3.4 minutes'.\"\"\"\n    minus = seconds < 0\n    if minus:\n        seconds *= -1\n    if seconds < 60:\n        r = pluralize(seconds, \"second\", 1)\n    elif seconds < 3600:\n        r = pluralize(seconds / 60.0, \"minute\", 1)\n    elif seconds < 86400:\n        r = pluralize(seconds / 3600.0, \"hour\", 1)\n    else:\n        r = pluralize(seconds / 86400.0, \"day\", 1)\n    if minus:\n        return \"-\" + r\n    else:\n        return r\n\n\nSIZE_DESC = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\nSIZE_VALS = tuple(1024**i for i in range(1, 9))\n\n\ndef format_size(size: int, decimal: int = 0, forcepower: int = -1, showdesc: bool = True) -> str:\n    \"\"\"Transform a byte count in a formatted string (KB, MB etc..).\n\n    ``size`` is the number of bytes to format.\n    ``decimal`` is the number digits after the dot.\n    ``forcepower`` is the desired suffix. 0 is B, 1 is KB, 2 is MB etc.. if kept at -1, the suffix\n    will be automatically chosen (so the resulting number is always below 1024).\n    if ``showdesc`` is ``True``, the suffix will be shown after the number.\n    Usage example::\n\n        >>> format_size(1234, decimal=2, showdesc=True)\n        '1.21 KB'\n    \"\"\"\n    if forcepower < 0:\n        i = 0\n        while size >= SIZE_VALS[i]:\n            i += 1\n    else:\n        i = forcepower\n    if i > 0:\n        div = SIZE_VALS[i - 1]\n    else:\n        div = 1\n    size_format = \"%%%d.%df\" % (decimal, decimal)\n    negative = size < 0\n    divided_size = (0.0 + abs(size)) / div\n    if decimal == 0:\n        divided_size = ceil(divided_size)\n    else:\n        divided_size = ceil(divided_size * (10**decimal)) / (10**decimal)\n    if negative:\n        divided_size *= -1\n    result = size_format % divided_size\n    if showdesc:\n        result += \" \" + SIZE_DESC[i]\n    return result\n\n\ndef multi_replace(s: str, replace_from: Union[str, List[str]], replace_to: Union[str, List[str]] = \"\") -> str:\n    \"\"\"A function like str.replace() with multiple replacements.\n\n    ``replace_from`` is a list of things you want to replace. Ex: ['a','bc','d']\n    ``replace_to`` is a list of what you want to replace to.\n    If ``replace_to`` is a list and has the same length as ``replace_from``, ``replace_from``\n    items will be translated to corresponding ``replace_to``. A ``replace_to`` list must\n    have the same length as ``replace_from``\n    If ``replace_to`` is a string, all ``replace_from`` occurence will be replaced\n    by that string.\n    ``replace_from`` can also be a str. If it is, every char in it will be translated\n    as if ``replace_from`` would be a list of chars. If ``replace_to`` is a str and has\n    the same length as ``replace_from``, it will be transformed into a list.\n    \"\"\"\n    if isinstance(replace_to, str) and (len(replace_from) != len(replace_to)):\n        replace_to = [replace_to for _ in replace_from]\n    if len(replace_from) != len(replace_to):\n        raise ValueError(\"len(replace_from) must be equal to len(replace_to)\")\n    replace = list(zip(replace_from, replace_to))\n    for r_from, r_to in [r for r in replace if r[0] in s]:\n        s = s.replace(r_from, r_to)\n    return s\n\n\n# --- Files related\n\n\n@log_io_error\n@pathify\ndef delete_if_empty(path: Path, files_to_delete: List[str] = []) -> bool:\n    \"\"\"Deletes the directory at 'path' if it is empty or if it only contains files_to_delete.\"\"\"\n    if not path.exists() or not path.is_dir():\n        return False\n    contents = list(path.glob(\"*\"))\n    if any(p for p in contents if (p.name not in files_to_delete) or p.is_dir()):\n        return False\n    for p in contents:\n        p.unlink()\n    path.rmdir()\n    return True\n\n\ndef open_if_filename(\n    infile: Union[Path, str, IO],\n    mode: str = \"rb\",\n) -> Tuple[IO, bool]:\n    \"\"\"If ``infile`` is a string, it opens and returns it. If it's already a file object, it simply returns it.\n\n    This function returns ``(file, should_close_flag)``. The should_close_flag is True is a file has\n    effectively been opened (if we already pass a file object, we assume that the responsibility for\n    closing the file has already been taken). Example usage::\n\n        fp, shouldclose = open_if_filename(infile)\n        dostuff()\n        if shouldclose:\n            fp.close()\n    \"\"\"\n    if isinstance(infile, Path):\n        return (infile.open(mode), True)\n    if isinstance(infile, str):\n        return (open(infile, mode), True)\n    else:\n        return (infile, False)\n\n\nclass FileOrPath:\n    \"\"\"Does the same as :func:`open_if_filename`, but it can be used with a ``with`` statement.\n\n    Example::\n\n        with FileOrPath(infile):\n            dostuff()\n    \"\"\"\n\n    def __init__(self, file_or_path: Union[Path, str], mode: str = \"rb\") -> None:\n        self.file_or_path = file_or_path\n        self.mode = mode\n        self.mustclose = False\n        self.fp: Union[IO, None] = None\n\n    def __enter__(self) -> IO:\n        self.fp, self.mustclose = open_if_filename(self.file_or_path, self.mode)\n        return self.fp\n\n    def __exit__(self, exc_type, exc_value, traceback) -> None:\n        if self.fp and self.mustclose:\n            self.fp.close()\n"
  },
  {
    "path": "locale/ar/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\\n\"\n\"Language: ar\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"مسار الملف\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"رسالة خطأ\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"مدة\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"معدل البت\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"معدل العينة\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:94\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"اسم الملف\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"مجلد\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"الحجم (ميغا بايت)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"زمن\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"معدل العينة\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"طيب القلب\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:165 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"تعديل\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"عنوان\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"فنان\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"البوم\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"النوع\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"سنة\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"رقم الشاحنة\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"تعليق\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"مباراة ٪\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"الكلمات المستخدمة\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"عدد المخادعين\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"أبعاد\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"الحجم (كيلو بايت)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"الطابع الزمني EXIF\"\n\n#: core\\prioritize.py:158\nmsgid \"Size\"\nmsgstr \"بحجم\"\n"
  },
  {
    "path": "locale/ar/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\\n\"\n\"Language: ar\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"\"\n\n#: core\\app.py:293\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\n#: core\\app.py:304\nmsgid \"No duplicates found.\"\nmsgstr \"\"\n\n#: core\\app.py:319\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:323\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:325\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"\"\n\n#: core\\app.py:330\nmsgid \"Could not load file: {}\"\nmsgstr \"\"\n\n#: core\\app.py:386\nmsgid \"'{}' already is in the list.\"\nmsgstr \"\"\n\n#: core\\app.py:388\nmsgid \"'{}' does not exist.\"\nmsgstr \"\"\n\n#: core\\app.py:396\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\n#: core\\app.py:473\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"\"\n\n#: core\\app.py:475\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\n#: core\\app.py:514\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"\"\n\n#: core\\app.py:520 core\\app.py:781 core\\app.py:791\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"\"\n\n#: core\\app.py:543\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\n#: core\\app.py:705 core\\app.py:717\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"\"\n\n#: core\\app.py:753\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"\"\n\n#: core\\app.py:801\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"\"\n\n#: core\\app.py:817\nmsgid \"Collecting files to scan\"\nmsgstr \"\"\n\n#: core\\app.py:867\nmsgid \"%s (%d discarded)\"\nmsgstr \"\"\n\n#: core\\directories.py:190\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:206\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"اسم الملف\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:21 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:72\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:177\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:185\nmsgid \"Preparing for matching\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:234\nmsgid \"Verified %d/%d matches\"\nmsgstr \"\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"\"\n\n#: core\\pe\\scanner.py:22\nmsgid \"EXIF Timestamp\"\nmsgstr \"الطابع الزمني EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"\"\n\n#: core\\results.py:134\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"\"\n\n#: core\\results.py:141\nmsgid \" filter: %s\"\nmsgstr \"\"\n\n#: core\\scanner.py:90\nmsgid \"Read size of %d/%d files\"\nmsgstr \"\"\n\n#: core\\scanner.py:116\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"\"\n\n#: core\\scanner.py:154\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ar/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Arabic (https://app.transifex.com/voltaicideas/teams/116153/ar/)\\n\"\n\"Language: ar\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"فنان\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"البوم\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"عنوان\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"النوع\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"سنة\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as Github issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to Github\"\nmsgstr \"\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/columns.pot",
    "content": "\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:94\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:165 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"\"\n\n#: core\\prioritize.py:158\nmsgid \"Size\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/core.pot",
    "content": "\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"\"\n\n#: core\\app.py:46\nmsgid \"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?\"\nmsgstr \"\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"\"\n\n#: core\\app.py:289\nmsgid \"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.\"\nmsgstr \"\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"\"\n\n#: core\\app.py:392\nmsgid \"All selected %d matches are going to be ignored in all subsequent scans. Continue?\"\nmsgstr \"\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/cs/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Czech (https://www.transifex.com/voltaicideas/teams/116153/cs/)\\n\"\n\"Language: cs\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Cesta k souboru\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Chybové hlášení\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Doba trvání\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Vzorkovací frekvence\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Název souboru\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Složka\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Velikost (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Čas\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Vzorkovací frekvence\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Typ\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Změna\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Titul\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Umělec\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Žánr\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Rok\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Číslo stopy\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Komentář\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Shoda %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Slov\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Počet kopií\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Rozměry\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Velikost (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Časové razítko EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Velikost\"\n"
  },
  {
    "path": "locale/cs/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Czech (https://app.transifex.com/voltaicideas/teams/116153/cs/)\\n\"\n\"Language: cs\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Neexistují žádné označené duplikáty. Nic se nestalo.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Nejsou k dispozici žádné vybrané duplikáty. Nic se nestalo.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Chystáte se otevřít více souborů najednou. V závislosti na tom, s čím jsou \"\n\"tyto soubory otevřeny, to může způsobit docela nepořádek. Pokračovat?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Vyhledávám duplicity\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Nahrávám\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Přesouvám\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Kopíruji\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Vyhazuji do koše\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Předchozí akce stále nebyla ukončena. Novou zatím nemůžete spustit. Počkejte\"\n\" pár sekund a zkuste to znovu.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Nebyli nalezeny žádné duplicity.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Všechny označené soubory byly úspěšně zkopírovány.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Všechny označené soubory byly úspěšně přesunuty.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Všechny označené soubory byly úspěšně odeslány do koše.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Soubor se nepodařilo načíst: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' již je v seznamu.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' neexistuje.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Všech %d vybraných shod bude v následujících hledáních ignorováno. \"\n\"Pokračovat?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Vyberte adresář, do kterého chcete zkopírovat označené soubory\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Vyberte adresář, kam chcete přesunout označené soubory\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Vyberte cíl pro exportovaný soubor CSV\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Nelze zapisovat do souboru: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Nedefinoval jste žádný uživatelský příkaz. Nadefinujete ho v předvolbách.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Chystáte se z výsledků odstranit %d souborů. Pokračovat?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} duplicitní skupiny byly změněny změně priorit.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Vybrané adresáře neobsahují žádné soubory vhodné k prohledávání.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Shromažďuji prohlížené soubory\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d vyřazeno)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Posíláte-{} soubory do koše.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Regulární výrazy\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Opravdu chcete odstranit všech %d položek ze seznamu výjimek?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Název souboru\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Název souboru - pole\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Název souboru - pole (bez objednávky)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tagy\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Obsah\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analyzováno %d/%d snímků\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Provedeno %d/%d porovnání bloků\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Připravuji porovnávání\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Ověřeno %d/%d shod\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Přečetl EXIF %d/%d obrázků\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Časové razítko EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Zádný\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Končí číslem\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Nekončí číslem\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Nejdelší\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Nejkratší\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Nejvyšší\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Nejnižší\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Nejnovější\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Nejstarší\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplicit označeno.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" filtr: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Načtena metadata %d/%d souborů\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Skoro hotovo! Fidlování s výsledky...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Složky\"\n"
  },
  {
    "path": "locale/cs/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Czech (https://app.transifex.com/voltaicideas/teams/116153/cs/)\\n\"\n\"Language: cs\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Ukončete\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Možnosti\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Seznam ignorovaných\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Vyčistit cache snímků\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Nápověda dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"O aplikaci\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Otevřete protokol ladění\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Opravdu chcete odstranit veškeré uložené analýzy snímků?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Picture cache cleared.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} soubor (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Možnosti mazání\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Propojte odstraněné soubory\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Po smazán duplikát, umístit odkaz cílení referenční soubor nahradit \"\n\"odstraněný soubor.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Pevný odkaz\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symbolický odkaz\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (nepodporovaný)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Přímo smazání souborů\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Místo odesílání souborů do koše je přímo odstraňte. Tato možnost se obvykle \"\n\"používá jako řešení, když nefunguje normální metoda odstranění.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Pokračovat\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Zrušit\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Atribut\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Vybráno\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Referenční\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Nahrát výsledky...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Okno s výsledky\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Přidat složku...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Soubor\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Zobrazit\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Nápověda\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Nahrát nedávné výsledky\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Aplikační režim:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Muzika\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Obrázek\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Typ skenování:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Více možností\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Vyberte složky, které chcete prohledat a stiskněte \\\"Prohledat\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Nahrát výsledky\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Prohledat\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Neuložené výsledky\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Máte neuložené výsledky, opravdu si přejete skončit?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Vyberte složku, kterou chcete přidat do prohledávacího seznamu\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Vyberte soubor s výsledky, který chcete nahrát\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Všechny soubory (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru Výsledek (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Spusťte nové skenování\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Máte neuložené výsledky, opravdu si přejete pokračovat?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Název\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Stav\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Vyjmuto\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normální\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Odebrat vybrané\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Vyprázdnit\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Zavřít\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Detaily\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Prohledávané tagy:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Stopa\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Umělec\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Titul\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Žánr\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Rok\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Váha slov\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Shoda podobných slov\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Různé druhy souborů\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Při filtrování používat regulární výrazy\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Po smazání a přesunu odstranit prázdné složky\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignorovat duplicity ve formě hardlinků na stejný soubor\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Ladící režim (vyžaduje restart)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Porovnávat snímky s různými rozměry\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Tvrdost filtru:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Více výsledků\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Méně výsledků\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Velikost písma:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Jazyk:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Kopírovat a přesunout:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Přímo v cílovém umístění\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Vytvořit s relativní cestou\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Vytvořit s absolutní cestou\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Uživatelský příkaz (argumenty: %d pro duplicity, %r pro odkazy):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru has to restart for language changes to take effect.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Změnit prioritu duplicit\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Do pole vpravo přidejte kritéria a klepnutím na tlačítko OK odešlete \"\n\"duplicity, které těmto kritériím vyhovují do referenčního umístění příslušné\"\n\" skupiny. Více informací naleznete v nápovědě.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problémy!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Při zpracování některých (nebo všech) souborů se vyskytly problémy. Jejich \"\n\"příčina je popsána v tabulce dole. Dotčené soubory nebyli odstraněny z \"\n\"výsledků.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Ukázat vybrané ve správci souborů\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Akce\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Zobrazit pouze duplicity\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Zobrazit rozdíly\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Odeslat označené položky do koše...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Označené přesunout...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Označené kopírovat...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Odstranit označené z výsledků\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Změnit prioritu výsledků...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Odstranit výběr z výsledků\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Přidat výběr na seznam výjimek\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Označit jako referenční\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Vybrané otevřít výchozí aplikací\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Otevřete složku obsahující vybrané\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Vybrané přejmenovat\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Označit vše\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Zrušit označení\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Invertovat označení\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Označit vybrané\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Export do HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Export do CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Uložit výsledky...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Spustit vlastní příkaz\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Označit\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Sloupce\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Výchozí nastavení\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Výsledky\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Jen duplicity\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Hodnoty Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Vyberte soubor pro uložení výsledků\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignorovat soubory menší než\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Výsledky\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Akce\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Přidat novou složku...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Pokročilé\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Automaticky kontrolovat aktualizace\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Základní\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Vše do popředí\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Zkontrolovat aktualizace...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Zavřít okno\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Kopírovat\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Uživatelský příkaz (argumenty: %d pro duplicity, %r pro odkazy):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Vyjmout\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Detaily vybraného souboru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Detaily\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Adresáře\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Předvolby dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Výsledky dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru Website\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Upravit\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Export výsledků do CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exportovat výsledky do XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Méně výsledků\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtr\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Tvrdost filtru:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filtrovat výsledky...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Výběr složky\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Velikost písma:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Skrýt dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Skrýt ostatní\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignorovat soubory menší než:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Nahrát ze souboru...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimalizovat\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Režim\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Více výsledků\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Vložit\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Předvolby...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Rychlý pohled\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Ukončit dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Výchozí nastavení\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Obnovit výchozí\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Odhalit\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Vybrané otevřít ve Finderu\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Vybrat vše\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Vyhodit označené do koše...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Služby\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Zobrazit vše\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Spustit hledání duplicit\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"The name '%@' already exists.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Okno\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Vyloučení Filtry\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Výsledky skenování\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Načíst složky...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Uložit složky...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Vyberte soubor složky, kterou chcete načíst\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Složky (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Vyberte soubor, do kterého chcete uložit své složky\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Složky (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Přidat\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"obnovit výchozí\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Testovací řetězec\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Zadejte python regulární výraz zde...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Sem zadejte cestu k systému souborů nebo název souboru...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Tyto regulární výrazy pythonů (rozlišují velká a malá písmena) odfiltrují soubory během skenování.<br>Pokud se jejich název shoduje s jedním z vybraných regulárních výrazů, bude mít jejich <strong>výchozí stav</strong> na kartě „Složky“ nastaven na „Vyloučeno“.<br>U každého shromážděného souboru se provedou dva testy, aby se zjistilo, zda jej zcela ignorovat nebo ne:<br><li>1. Regulární výrazy bez oddělovače cesty budou porovnávány pouze s názvem souboru.</li>\\n\"\n\"<li>2. Regulární výrazy s alespoň jedním oddělovačem cesty budou porovnány s úplnou cestou k souboru.</li><br>\\n\"\n\"Příklad: pokud chcete odfiltrovat soubory PNG pouze z složky „My Pictures“:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Regulární výraz můžete vyzkoušet pomocí tlačítka „testovací řetězec“ po vložení falešné cesty do testovacího pole:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Odpovídající regulární výrazy budou zvýrazněny.<br>Pokud existuje alespoň jedno zvýraznění, bude testovaná cesta nebo název souboru během skenování ignorována.<br><br>Složky a soubory začínající tečkou \\\".\\\" jsou ve výchozím nastavení odfiltrovány.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Chyba kompilace:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"zvýšení zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"zmenšit zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Normální velikost\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Nejlépe fit\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Režim mezipaměti obrázků:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Přepsat ikony motivů na panelu nástrojů prohlížeče obrázků\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Využít naše vlastní vnitřní ikony namísto ty, které poskytují téma motorem\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Zobrazit posuvníky v prohlížečích obrázků\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Pokud se zobrazený obrázek nevejde do výřezu, zobrazte posuvníky tak, aby se\"\n\" pohled pohyboval.\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Použít výchozí pozici pro panel karet (vyžaduje restart)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Umístěte panel karet pod hlavní nabídku namísto vedle ní.\\n\"\n\"V systému MacOS místo toho vyplní panel karet šířku okna.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Pro reference použijte tučné písmo\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Referenční barva popředí:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Referenční barva pozadí:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Barva popředí Delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Zobrazit záhlaví a lze jej ukotvit\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Zatímco je záhlaví skryto, přetáhněte plovoucí okno pomocí modifikační \"\n\"klávesy\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Záhlaví lze deaktivovat, pouze když je okno ukotveno\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Vertikální záhlaví\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Změňte záhlaví z vodorovné nahoře na svislou na levé straně\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Zobrazit panel karet\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Tyto regulární výrazy pythonů (rozlišují velká a malá písmena) odfiltrují soubory během skenování.<br>Pokud se jejich název shoduje s jedním z vybraných regulárních výrazů, bude mít jejich <strong>výchozí stav</strong> na kartě „Složky“ nastaven na „Vyloučeno“.<br>U každého shromážděného souboru se provedou dva testy, aby se zjistilo, zda jej zcela ignorovat nebo ne:<br><li>1. Regulární výrazy bez oddělovače cesty budou porovnávány pouze s názvem souboru.</li>\\n\"\n\"<li>2. Regulární výrazy s alespoň jedním oddělovačem cesty budou porovnány s úplnou cestou k souboru.</li><br>\\n\"\n\"Příklad: pokud chcete odfiltrovat soubory PNG pouze z složky „My Pictures“:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Regulární výraz můžete vyzkoušet pomocí tlačítka „testovací řetězec“ po vložení falešné cesty do testovacího pole:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Odpovídající regulární výrazy budou zvýrazněny.<br>Pokud existuje alespoň jedno zvýraznění, bude testovaná cesta nebo název souboru během skenování ignorována.<br><br>Složky a soubory začínající tečkou \\\".\\\" jsou ve výchozím nastavení odfiltrovány.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Výsledky\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Obecné rozhraní\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tabulka výsledků\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Okno podrobností\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Všeobecné\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Zobrazit\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"O {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Verze {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Licencován pod GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Chybové hlášení\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Něco se pokazilo. Co takhle nahlásit chybu?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"To, co obvykle opravdu pomáhá, je přidat popis toho, jak jste se dostali k chybě. Dík!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Přejít na GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"česky\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Německy\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"řecky\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Anglicky.\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"španělsky\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Francouzsky\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"arménsky\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"italsky\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japonština\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"korejsky\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malajština\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"holandsky\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"polsky\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"brazilsky\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"rusky\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turečtina\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"ukrajinsky\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"vietnamsky\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"čínsky (zjednodušeně)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Vymazání seznamu\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Hledat...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/de/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\\n\"\n\"Language-Team: German (https://www.transifex.com/voltaicideas/teams/116153/de/)\\n\"\n\"Language: de\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Dateipfad\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Fehlermeldung\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Dauer\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Abtastrate\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Dateiname\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Ordner\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Größe (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Zeit\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Abtastrate\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Typ\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Geändert\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Titel\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Künstler\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Jahr\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Titel Nummer\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Kommentar\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Übereinstimmung %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"genutzte Wörter\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Anzahl der Duplikate\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Auflösung\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Größe (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Zeitstempel\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Größe\"\n"
  },
  {
    "path": "locale/de/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n# Robert M, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Robert M, 2021\\n\"\n\"Language-Team: German (https://app.transifex.com/voltaicideas/teams/116153/de/)\\n\"\n\"Language: de\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Keine markierten Duplikate, daher wurde nichts getan.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Keine ausgewählten Duplikate, daher wurde nichts getan.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Sie sind dabei, sehr viele Dateien gleichzeitig zu öffnen. Das kann zu \"\n\"ziemlichem Durcheinander führen! Trotzdem fortfahren?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Suche nach Duplikaten\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Lade\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Verschiebe\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Kopiere\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Verschiebe in den Papierkorb\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Eine vorherige Aktion ist noch in der Bearbeitung. Sie können noch keine \"\n\"Neue starten. Warten Sie einige Sekunden und versuchen es erneut.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Keine Duplikate gefunden.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Alle markierten Dateien wurden erfolgreich kopiert.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Alle markierten Dateien wurden erfolgreich verschoben.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"Alle markierten Dateien wurden erfolgreich gelöscht.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"\"\n\"Alle markierten Dateien wurden erfolgreich in den Papierkorb verschoben.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Konnte Datei {} nicht laden.\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' ist bereits in der Liste.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' existiert nicht.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Alle %d ausgewählten Dateien werden in zukünftigen Scans ignoriert. \"\n\"Fortfahren?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"\"\n\"Wählen Sie ein Verzeichnis aus, in das markierte Dateien kopiert werden \"\n\"sollen\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\"Wählen Sie ein Verzeichnis aus, in das markierte Dateien verschoben werden \"\n\"sollen\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Zielverzeichnis für den CSV Export angeben\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Konnte Datei {} nicht schreiben.\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Sie haben noch keinen Befehl erstellt. Bitte dies in den Einstellungen vornehmen.\\n\"\n\"Bsp.: \\\"C:\\\\Program Files\\\\Diff\\\\Diff.exe\\\" \\\"%d\\\" \\\"%r\\\"\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"%d Dateien werden aus der Ergebnisliste entfernt. Fortfahren?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} Duplikat-Gruppen wurden durch die Neu-Priorisierung geändert.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Ausgewählte Ordner enthalten keine scannbaren Dateien.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Sammle zu scannende Dateien...\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d verworfen)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"{} Dateien für Scan gesammelt\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"{} Ordner für Scan gesammelt\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"%d Treffer in %d Gruppen gefunden\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Verschiebe {} Datei(en) in den Papierkorb.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Reguläre Ausdrücke\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Möchten Sie wirklich alle %d Einträge aus der Ausnahmeliste löschen?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Dateiname\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Dateiname - Bereiche\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Dateiname - Bereiche (ohne Reihenfolge)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Inhalt\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analysiere Bild %d/%d\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d/%d Chunk-Matches ausgeführt\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Bereite Matching vor\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d/%d verifizierte Übereinstimmungen\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Lese EXIF von Bild %d/%d\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Zeitstempel\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Nichts\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Endet mit Zahl\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Endet nicht mit Zahl\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Längste\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Kürzeste\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Höchste\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Niedrigste\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Neuste\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Älterste\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) Duplikate markiert.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" Filter: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Lese Metadaten von %d/%d Dateien\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Fast fertig! Arrangiere Ergebnisse...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Ordner\"\n"
  },
  {
    "path": "locale/de/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Robert M, 2022\n# Fuan <jcfrt@posteo.net>, 2022\n# Frederik Gschaider <frederik.gschaider@gmail.com>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: German (https://app.transifex.com/voltaicideas/teams/116153/de/)\\n\"\n\"Language: de\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Beenden\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Optionen\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Ausnahme-Liste\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Bilder-Cache leeren\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru Hilfe\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Über dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Debug Log öffnen\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"Möchten Sie wirklich alle zwischengespeicherten Bildanalysen entfernen?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Bilder-Cache geleert.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} Datei (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Lösch-Optionen\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Verlinke gelöschte Dateien\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Doppelte Dateien werden gelöscht, an deren Stelle wird eine Verknüpfung auf \"\n\"die Referenz-Datei erstellt.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(nicht unterstützt)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Ohne Papierkorb löschen\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Anstatt Dateien in den Papierkorb zu verschieben, können Sie diese direkt \"\n\"löschen. Diese Option wird in der Regel genutzt, falls die normale \"\n\"Löschmethode nicht funktioniert.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Fortfahren\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Abbrechen\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Attribut\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Ausgewählt\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Referenz\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Ergebnis laden...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Ergebnisfenster\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Ordner hinzufügen...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Datei\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Ansicht\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Hilfe\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Lade letztes Suchergebnis\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Anwendungsmodus:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Musik\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Bild\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Scantyp:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Optionen\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Zu durchsuchende Ordner auswählen und \\\"Suche starten\\\" drücken.\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Lade Ergebnisse\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Suche starten\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Ungespeicherte Ergebnisse\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\"Sie haben ungespeicherte Ergebnisse. Wollen Sie wirklich dupeGuru beenden?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Wählen Sie einen Ordner aus, um ihn der Scanliste hinzuzufügen\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Wählen Sie eine Ergebnisdatei zum Laden aus\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Alle Dateien (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru Suchergebnisse (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Starte einen neuen Suchlauf\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Sie haben ungespeicherte Ergebnisse. Möchten Sie wirklich fortfahren?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Name\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Zustand\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Ausgeschlossen\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Auswahl löschen\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Liste leeren\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Schließen\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Details\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Folgende Tags scannen:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Track\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Künstler\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Titel\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Jahr\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Wortgewichtung\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Gleiche ähnliche Wörter ab\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Dateitypen dürfen gemischt werden\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Nutze reguläre Ausdrücke beim Filtern\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Entferne leere Ordner beim Löschen oder Verschieben\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignoriere Duplikate mit Hardlinks auf dieselbe Datei\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Debug Modus (Neustart nötig)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Gleiche Bilder mit unterschiedlicher Auflösung ab\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Filter Empfindlichkeit:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Mehr Ergebnisse\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Weniger Ergebnisse\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Schriftgröße:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Sprache:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Kopieren und Verschieben:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Direkt ins Ziel\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Relativen Pfad neu erstellen\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Absoluten Pfad neu erstellen\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru muss neustarten, um die Sprachänderung durchzuführen.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Re-priorisiere Duplikate\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Fügen Sie Kriterien zur rechten Box hinzu. Klicken Sie OK, um die Duplikate,\"\n\" die diesen Kriterien am besten entsprechen, zur Referenzposition der \"\n\"entsprechenden Gruppe zu senden. Lesen Sie die Hilfe für mehr Informationen.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Probleme!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Es gab Probleme bei der Verarbeitung einiger (aller) Dateien. Der Ursache \"\n\"dieser Probleme ist unten genauer beschrieben. Diese Dateien wurden \"\n\"\\\"nicht\\\" aus Ihren Suchergebnissen entfernt.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Zeige Markierte\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Aktionen\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Nur Duplikate anzeigen\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Zeige Delta-Werte\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Verschiebe Markierte in den Papierkorb...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Verschiebe Markierte nach...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Kopiere Markierte nach...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Entferne Markierte aus den Ergebnissen\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Re-priorisiere Ergebnisse...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Entferne Auswahl aus den Ergebnissen\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Füge Auswahl der Ausnahmeliste hinzu\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Mache Auswahl zur Referenz\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Öffne Auswahl mit Standard-Anwendung\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Öffne den Über-Ordner der Auswahl\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Auswahl umbenennen\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Alles markieren\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Nichts markieren\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Auswahl umkehren\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Auswahl markieren\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Exportiere als HTML...\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Exportiere als CSV...\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Speichere Ergebnisse...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Eigenen Befehl ausführen\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Auswählen\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Spalten\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Auf Voreinstellung zurücksetzen\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} (Ergebnisse)\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Nur Duplikate anzeigen\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Zeige Delta-Werte\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Datei zum Speichern der Suchergebnisse auswählen\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignoriere Dateien kleiner als\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Ergebnisse\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Aktion\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Neuer Ordner...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Fortgeschritten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Automatisch nach Updates suchen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Einfach\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Alle nach vorne bringen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Auf Updates prüfen...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Fenster schließen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Kopieren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Eigener Befehl (Variablen: %d für Duplikat, %r für Referenz):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Ausschneiden\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Details der ausgewählten Datei\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Details Panel\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Verzeichnisse\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru Einstellungen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru Ergebnisse\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru Website\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Bearbeiten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Exportiere als CSV...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exportiere als XHTML...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Weniger Suchergebnisse\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filter\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Filter Empfindlichkeit:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filter Suchergebnisse...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Ordner-Auswahlfenster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Schriftgröße:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"dupeGuru ausblenden\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Andere ausblenden\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignoriere Dateien kleiner als:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Lade von Datei...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimieren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Modus\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Mehr Suchergebnisse\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Einfügen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Einstellungen...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Quick Look\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"dupeGuru beenden\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Auf Voreinstellung zurücksetzen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Auf Voreinstellungen zurücksetzen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Zeige\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Zeige Auswahl im Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Alles markieren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Verschiebe Markierte in den Papierkorb...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Alle einblenden\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Starte Duplikat-Scan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Der Name '%@' existiert bereits.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Fenster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Ausschlussfilter\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Scan-Ergebnisse\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Verzeichnisse laden...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Verzeichnisse speichern...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Wählen Sie eine zu ladende Verzeichnisdatei aus\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Verzeichnisse (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"\"\n\"Wählen Sie eine Datei aus, in der Ihre Verzeichnisse gespeichert werden \"\n\"sollen\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Verzeichnisse (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Addieren\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Standardeinstellungen wiederherstellen\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Testzeichenfolge\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Geben Sie hier einen regulären Python-Ausdruck ein...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Geben Sie hier einen Dateisystempfad oder Dateinamen ein...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Diese regulären Python-Ausdrücke (Groß- und Kleinschreibung beachten) filtern Dateien während des Scannens heraus.<br>Der <strong>Standardstatus</strong> 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.<br>Für jede gesammelte Datei werden zwei Tests durchgeführt, um festzustellen, ob sie vollständig ignoriert werden soll oder nicht:<br><li>1. Reguläre Ausdrücke ohne Pfadtrennzeichen werden nur mit dem Dateinamen verglichen.</li>\\n\"\n\"<li>2. Reguläre Ausdrücke mit mindestens einem Pfadtrennzeichen werden mit dem vollständigen Pfad zur Datei verglichen.</li><br>\\n\"\n\"Beispiel: Wenn Sie PNG-Dateien nur aus dem Verzeichnis \\\"Meine Bilder\\\" herausfiltern möchten:<br><code>.*Meine\\\\sBilder\\\\\\\\.*\\\\.png</code><br><br>Sie können den regulären Ausdruck mit der Schaltfläche \\\"Testzeichenfolge\\\" testen, nachdem Sie einen falschen Pfad in das Testfeld eingefügt haben:<br><code>C:\\\\\\\\Nutzer\\\\Meine Bilder\\\\test.png</code><br><br>\\n\"\n\"Übereinstimmende reguläre Ausdrücke werden hervorgehoben.<br>Wenn mindestens eine Markierung vorhanden ist, wird der getestete Pfad oder Dateiname beim Scannen ignoriert.<br><br>Verzeichnisse und Dateien, die mit einem Punkt '.' Beginnen. werden standardmäßig herausgefiltert.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Kompilierungsfehler:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Erhöhen Sie den Zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Verringern Sie den Zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Normale Größe\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Beste Passform\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Bild-Cache-Modus:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Überschreiben Sie Themensymbole in der Viewer-Symbolleiste\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Verwenden Sie unsere eigenen internen Symbole anstelle der von der Theme \"\n\"Engine bereitgestellten\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Bildlaufleisten in Bildbetrachtern anzeigen\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Wenn das angezeigte Bild nicht zum Ansichtsfenster passt, zeigen Sie \"\n\"Bildlaufleisten an, um die Ansicht zu überspannen\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Standardposition für Registerkartenleiste verwenden (Neustart erforderlich)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Platzieren Sie die Registerkartenleiste unter dem Hauptmenü und nicht daneben\\n\"\n\"Unter MacOS füllt die Registerkartenleiste stattdessen die Fensterbreite aus.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Verwenden Sie Fettdruck als Referenz\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Vordergrundfarbe für Referenzen:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Hintergrundfarbe für Referenzen:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Vordergrundfarbe für Delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Zeigt die Titelleiste an und kann angedockt werden\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Während die Titelleiste ausgeblendet ist, ziehen Sie das schwebende Fenster \"\n\"mit der Modifikatortaste herum\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"Die Titelleiste kann nur deaktiviert werden, während das Fenster angedockt \"\n\"ist\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Vertikale Titelleiste\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Ändern Sie die Titelleiste von horizontal oben in vertikal links\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Registerkartenleiste anzeigen\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Diese regulären Python-Ausdrücke (Groß- und Kleinschreibung beachten) filtern Dateien während des Scannens heraus.<br>Der <strong>Standardstatus</strong> 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.<br>Für jede gesammelte Datei werden zwei Tests durchgeführt, um festzustellen, ob sie vollständig ignoriert werden soll oder nicht:<br><li>1. Reguläre Ausdrücke ohne Pfadtrennzeichen werden nur mit dem Dateinamen verglichen.</li>\\n\"\n\"<li>2. Reguläre Ausdrücke mit mindestens einem Pfadtrennzeichen werden mit dem vollständigen Pfad zur Datei verglichen.</li><br>\\n\"\n\"Beispiel: Wenn Sie PNG-Dateien nur aus dem Verzeichnis \\\"Meine Bilder\\\" herausfiltern möchten:<br><code>.*Meine\\\\sBilder\\\\\\\\.*\\\\.png</code><br><br>Sie können den regulären Ausdruck mit der Schaltfläche \\\"Testzeichenfolge\\\" testen, nachdem Sie einen falschen Pfad in das Testfeld eingefügt haben:<br><code>C:\\\\\\\\Nutzer\\\\Meine Bilder\\\\test.png</code><br><br>\\n\"\n\"Übereinstimmende reguläre Ausdrücke werden hervorgehoben.<br>Wenn mindestens eine Markierung vorhanden ist, wird der getestete Pfad oder Dateiname beim Scannen ignoriert.<br><br>Verzeichnisse und Dateien, die mit einem Punkt '.' Beginnen. werden standardmäßig herausgefiltert.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Ergebnisse\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Allgemeine Schnittstelle\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Ergebnistabelle\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Detailfenster\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Allgemeines\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Anzeige\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Dateien partiell hashen die größer sind als\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"Benutzer System-eigene Dialoge\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"Benutzer System-eigene Dialoge für Aktionen wie Datei/Ordern-Auswahl\\n\"\n\"Manche System-eigene Dialoge sind in ihren Funktionen limitiert.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Ignoriere Dateien größer als\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Zwischenspeicher leeren\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"Möchten Sie den Zwischenspeicher wirklich löschen? Dadurch werden alle \"\n\"zwischengespeicherten Datei-Prüfsummen und Bildanalysen entfernt.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Zwischenspeicher geleert.\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Dunklen Stil anwenden\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Profil-Scanvorgang\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\"Erstellen Sie ein Profil des Scanvorgangs und speichern Sie die Protokolle \"\n\"zur Optimierung.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"Die Protokolle befinden sich in: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Fehlerbehebung\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Über {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Version {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Nach Aktualisierungen suchen...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Lizenziert unter GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"Keine Aktualisierung verfügbar.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"Neue Version {} verfügbar, <a href=\\\"{}\\\">hier</a> herunterladen.\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Fehlermeldung\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Etwas ist schief gelaufen. Wie wäre es, den Fehler zu melden?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Fehlerberichte sollten als GitHub-Probleme gemeldet werden. Sie können den obigen Fehler-Traceback kopieren und in eine neue Ausgabe einfügen.\\n\"\n\"\\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\"\\n\"\n\"Was normalerweise wirklich hilft, ist, wenn Sie eine Beschreibung hinzufügen, wie Sie den Fehler erhalten haben. Vielen Dank!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Geh zu GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Tschechisch\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Deutsch\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Griechisch\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Englisch\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Spanisch\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Französisch\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armenisch\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italienisch\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japanisch\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Koreanisch\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malaiisch\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Niederländisch\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polnisch\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brasilianisch\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Russisch\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Türkisch\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ukrainisch\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamesisch\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Chinesisch (Vereinfachtes)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Liste löschen\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Suche...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/el/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Greek (https://www.transifex.com/voltaicideas/teams/116153/el/)\\n\"\n\"Language: el\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Διαδρομή αρχείου\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Μήνυμα σφάλματος\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Διάρκεια\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Ρυθμός δειγματοληψίας\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Όνομα αρχείου\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Φάκελος\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Μέγεθος (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Χρόνος\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Ρυθμός δειγματοληψίας\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Τύπος\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Τροποποίηση\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Τίτλος\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Καλλιτέχνης\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Αλμπουμ\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Είδος\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Έτος\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Αριθμός κομματιού\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Σχόλιο\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Ταύτιση %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Χρησιμοποιημένες λέξεις\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Αριθμός διπλοτύπων\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Διαστάσεις\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Μέγεθος (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Χρονοσήμανση EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Μέγεθος-Διαστάσεις?\"\n"
  },
  {
    "path": "locale/el/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Greek (https://app.transifex.com/voltaicideas/teams/116153/el/)\\n\"\n\"Language: el\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Δεν υπάρχουν μαρκαρισμένα διπλότυπα. Δεν έγινε τίποτα.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Δεν υπάρχουν επιλεγμένα διπλότυπα. Δεν έγινε τίποτα.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Πρόκειται να ανοίξετε πολλά αρχεία ταυτόχρονα. Ανάλογα με το ποιο πρόγραμμα \"\n\"ανοίγουν αυτάτα αρχεία, κάτι τέτοιο μπορεί να προκαλέσει ένα μικρό χάος. \"\n\"Συνέχεια;\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Σάρωση για διπλότυπα\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Φόρτωση\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Μετακίνηση\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Αντιγραφή\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Αποστολή στα σκουπίδια\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Μια προηγούμενη ενέργεια είναι σε εξέλιξη. Δεν μπορείτε να ξεκινήσετε \"\n\"καινούργια ακόμα. Περιμένετε λίγα δευτερόλεπτα, έπειτα προσπαθήστε ξανά.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Δεν βρέθηκαν διπλότυπα.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Όλα τα επιλεγμένα αρχεία αντιγράφηκαν επιτυχώς.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Όλα τα επιλεγμένα αρχεία μετακινήθηκαν επιτυχώς.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Όλα τα επιλεγμένα αρχεία στάλθηκαν με επιτυχία στον κάδο.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Δεν ήταν δυνατή η φόρτωση του αρχείου: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' υπάρχει ήδη στη λίστα.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' δεν υπάρχει.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Όλα τα επιλεγμένα %d στοιχεία θα αγνοηθούν σε μελλοντικές σαρώσεις.Συνέχεια;\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Επιλέξτε έναν κατάλογο για να αντιγράψετε επισημασμένα αρχεία.\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Επιλέξτε έναν κατάλογο για να μετακινήσετε τα επισημασμένα αρχεία.\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Επιλέξτε έναν προορισμό για το εξαγόμενο CSV σας\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Δεν ήταν δυνατή η εγγραφή στο αρχείο: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"Δεν έχετε ορίσει ειδική εντολή. Ρυθμίστε τη στις προτιμήσεις σας. \"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Πρόκειται να αφαιρέσετε %d αρχεία από τα αποτελέσματα. Συνέχεια;\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} ομάδες διπλοτύπων άλλαξαν από το επαναπροσδιορισμό.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Οι επιλεγμένοι φάκελοι δεν περιέχουν σαρώσιμα αρχεία.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Συλλογή αρχείων για σάρωση\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d απορρίφθηκαν)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Στέλνετε {} αρχεία στα σκουπίδια.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Κανονικές εκφράσεις\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Θέλετε να αφαιρέσετε όλα τα %d στοιχεία από τη λίστα αγνόησης; \"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Ονομα αρχείου\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Όνομα αρχείου - Πεδία\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Όνομα αρχείου - Πεδία (Χωρίς παραγγελία)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"ετικέτα\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Περιεχόμενα\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Ανάλυση %d/%d εικόνων\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Εκτέλεση %d/%d μερικής ταυτοποίησης\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Προετοιμασία για σύγκριση\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Πιστοποίηση %d/%d ταυτόσημων\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Ανάγνωση EXIF %d/%d εικόνες\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Χρονική σήμανση EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Καμμία\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Λήγει με αριθμό\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Δεν λήγει με αριθμό\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Μεγαλύτερο\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Μικρότερο\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Υψηλότερη\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Χαμηλότερη\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Νεώτερο\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Παλαιότερο\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) επιλεγμένα διπλότυπα.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" φίλτρο: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Ανάγνωση μεταδεδομένων των %d/%d αρχείων\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Σχεδόν τελείωσα! Παιχνίδι με αποτελέσματα ...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"ντοσιέ\"\n"
  },
  {
    "path": "locale/el/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Greek (https://app.transifex.com/voltaicideas/teams/116153/el/)\\n\"\n\"Language: el\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Έξοδος\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Επιλογές\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Λίστα αγνόησης\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Εκκαθάριση μνήμης cache εικόνων\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Βοήθεια για το dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Σχετικά με το dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Άνοιγμα αρχείου αποσφαλμάτωσης\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"Θέλετε πραγματικά να αφαιρέσετε όλη την αποθηκευμένη ανάλυση εικόνων σας;\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Μνήμη cache εικόνων εκκαθαρίστηκε.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} αρχείο (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Επιλογές διαγραφής\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Σύνδεση διαγεγραμμένων αρχείων\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Μετά την αντιγραφή ενός διπλοτύπου, τοποθετήστε ένα σύνδεσμο που στοχεύει το\"\n\" αρχείοαναφοράς για να αντικαταστήσετε το διαγεγραμμένο αρχείο.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Σκληρός σύνδεσμος\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"συμβολικός σύνδεσμος\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (δεν υποστηρίζεται)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Απευθείας διαγραφή αρχείων\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Αντί για την αποστολή αρχείων στα σκουπίδια, να διαγραφούν άμεσα. Αυτή η \"\n\"επιλογή είναι Συνήθως μια λύση, όταν η κανονική μέθοδο διαγραφής δεν \"\n\"λειτουργεί.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Προχωρήστε\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Ακύρωση\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Χαρακτηριστικό\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Επιλεγμένα\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Αναφορά\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Φόρτωση αποτελεσμάτων...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Παράθυρο αποτελεσμάτων\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Προσθήκη φακέλου...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Αρχείο\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Προβολή\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Βοήθεια\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Φόρτωση πρόσφατων αποτελεσμάτων\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Λειτουργία εφαρμογής:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"μουσική\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Εικόνα\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"πρότυπο\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Είδος σάρωσης\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Περισσότερες επιλογές\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Επιλέξτε τους φακέλους για να σαρώσετε και πατήστε το πλήκτρο \\\"Σάρωση\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Φόρτωση αποτελεσμάτων\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Σάρωση\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Μη αποθηκευμένα αποτελέσματα\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\"Έχετε μη αποθηκευμένα αποτελέσματα, θέλετε πραγματικά να κλείσετε το \"\n\"πρόγραμμα;\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Επιλέξτε ένα φάκελο για να προσθέσετε στη λίστα σάρωσης\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Επιλέξτε αρχείο αποτελεσμάτων προς φόρτωση\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Όλα τα αρχεία (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Αποτελέσματα dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Έναρξη νέας σάρωσης\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Έχετε μη αποθηκευμένα αποτελέσματα, θέλετε πραγματικά να συνεχίσετε;\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Όνομα\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Κατάσταση\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Εξαιρούμενο\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Κανονικό\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Αφαίρεση επιλεγμένων\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Αφαίρεση\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Κλείσιμο\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Λεπτομέρειες\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Ετικέτες προς σάρωση:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Κομμάτι\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Καλλιτέχνης\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Άλμπουμ\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Τίτλος\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Είδος\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Έτος\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Στάθμιση λέξης\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Ταίριασμα παρόμοιων λέξεων\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Να περιλαμβάνουν διαφορετικούς τύπους αρχείων\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Χρήση κανονικών εκφράσεων κατά το φιλτράρισμα\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Κατάργηση κενών φακέλων για διαγραφή ή μετακίνηση\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Αγνόηση διπλοτύπων με hardlinking στο ίδιο αρχείο\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Λειτουργία αποσφαλμάτωσης(απαιτείται επανεκκίνηση)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Ταίριασμα εικόνων διαφορετικών διαστάσεων\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Σκληρότητα φίλτρου\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Περισσότερα αποτελέσματα\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Λιγότερα αποτελέσματα\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Μέγεθος γραμματοσειράς:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Γλώσσα:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Αντιγραφή και μετακίνηση:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Απευθείας στον προορισμό\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Ανασύνθεση σχετικής διαδρομής\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Ανασύνθεση απόλυτης διαδρομής\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Προσωποποιημένη εντολή (παράμετροι: %d για διπλότυπο, %r για αναφορά):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\"Πρέπει να γίνει επανεκκίνηση του dupeGuru για να ισχύσουν οι αλλαγές \"\n\"γλώσσας.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Επαναπροσδιορισμός προτεραιότητας διπλοτύπων\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Προσθέστε κριτήρια στο πλαίσιο δεξιά και κάντε κλικ στο κουμπί OK για να \"\n\"στείλετε τα διπλότυπα που αντιστοιχούν καλύτερα σε αυτά τα κριτήρια, στην \"\n\"αντίστοιχη θέση αναφοράς των ομάδων τους. Διαβάστε το αρχείο βοήθειας για \"\n\"περισσότερες πληροφορίες.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Προβλήματα!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Υπήρχαν προβλήματα επεξεργασίας για κάποιο (ή όλα) από τα αρχεία. Η αιτία \"\n\"τωνΠροβλημάτων περιγράφεται στον παρακάτω πίνακα. Τα εν λόγω αρχεία δεν \"\n\"Αφαιρέθηκαν από τα αποτελέσματα σας.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Αποκάλυψη επιλεγμένων\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Ενέργειες\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Προβολή μόνο διπλοτύπων\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Προβολή τιμών διαφοράς (Delta)\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Αποστολή των μαρκαρισμένων στον Κάδο Ανακύκλωσης...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Μετακίνηση μαρκαρισμένων σε...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Αντιγραφή μαρκαρισμένων σε...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Αφαίρεση μαρκαρισμένων από τα αποτελέσματα\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Επαναπροσδιορισμός προτεραιότητας αποτελεσμάτων...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Αφαίρεση επιλεγμένων από τα αποτελέσματα\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Προσθήκη επιλεγμένων στη λίστα αγνόησης\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Μετατροπή επιλεγμένων σε αναφορά\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Άνοιγμα επιλεγμένου/ων με Προεπιλεγμένη εφαρμογή\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Άνοιγμα φακέλου που περιέχει το/τα επιλεγμένο/α\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Μετονομασία επιλεγμένου/ων\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Μαρκάρισμα όλων\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Ξεμαρκάρισμα όλων\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Αντιστροφή μαρκαρίσματος\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Μαρκάρισμα επιλεγμένου/ων\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Εξαγωγή σε HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Εξαγωγή σε CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Αποθήκευση αποτελεσμάτων...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Εκτέλεση προσαρμοσμένης εντολής\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Μαρκάρισμα\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Στήλες\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Επαναφορά προεπιλογών\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Αποτελέσματα\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Μόνο διπλότυπα\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Τιμές διαφοράς (Delta)\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Επιλέξτε ένα αρχείο για να αποθηκεύσετε τα αποτελέσματα σας\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Αγνόηση αρχείων μικρότερων από\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Αποτελέσματα\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Ενέργεια\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Προσθήκη νέου φακέλου...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Προχωρημένες\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Αυτόματος έλεγχος για ενημερώσεις\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Βασική\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Μεταφορά όλων στο προσκήνιο\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Έλεγχος για αναβάθμιση...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Κλείσιμο παραθύρου\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Αντιγραφή\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Προσωποποιημένη εντολή (παράμετροι: %d για διπλότυπο, %r για αναφορά):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Αποκοπή\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Λεπτομέρειες επιλεγμένου αρχείου\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Πίνακας λεπτομερειών\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Φάκελοι\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Προτιμήσεις dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Αποτελέσματα dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Ιστότοπος dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Επεξεργασία\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Εξαγωγή αποτελεσμάτων σε CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Εξαγωγή αποτελεσμάτων σε XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Λιγότερα αποτελέσματα\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Φίλτρο\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Σκληρότητα φίλτρου:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Φίλτρο αποτελεσμάτων...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Παράθυρο επιλογής φακέλου\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Μέγεθος γραμματοσειράς:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Απόκρυψη dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Απόκρυψη υπολοίπων\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Αγνόηση αρχείων μικρότερων από:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Φόρτωση από αρχείο...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Ελαχιστοποίηση\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Λειτουργία\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Περισσότερα αποτελέσματα\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Επικόλληση\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Προτιμήσεις...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Γρήγηορη προβολή\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Κλείσιμο dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Επαναφορά προεπιλογής\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Επαναφορά προεπιλογών\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Εμφάνιση\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Εμφάνιση επιλεγμένων στην Εύρεση\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Επιλογή όλων\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Αποστολή μαρκαρισμένων στα Σκουπίδια...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Υπηρεσίες\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Εμφάνιση όλων\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Έναρξη σάρωσης διπλοτύπων\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Το όνομα'%@' υπάρχει ήδη.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Παράθυρο\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Ζουμ\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Φίλτρα αποκλεισμού\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Αποτελέσματα σάρωσης\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Φόρτωση καταλόγων...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Αποθήκευση καταλόγων...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Επιλέξτε ένα αρχείο καταλόγων για φόρτωση\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"καταλόγους του dupeguru (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Επιλέξτε ένα αρχείο για να αποθηκεύσετε τους καταλόγους σας\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"καταλόγους του dupeguru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Προσθήκη\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Επαναφέρετε τις προεπιλογές\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Σειρά δοκιμής\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Πληκτρολογήστε μια τυπική έκφραση python εδώ...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Πληκτρολογήστε μια διαδρομή συστήματος ή ένα όνομα αρχείου εδώ...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Αυτές οι τυπικές εκφράσεις python (πεζών-κεφαλαίων) θα φιλτράρουν τα αρχεία κατά τη διάρκεια των σαρώσεων.<br>Οι διευθυντές θα έχουν επίσης την <strong>προεπιλεγμένη κατάστασή</strong> τους σε Εξαίρεση στην καρτέλα Κατάλογοι εάν το όνομά τους συμβαίνει να ταιριάζει με μία από τις επιλεγμένες κανονικές εκφράσεις.<br>Για κάθε αρχείο που συλλέγεται, εκτελούνται δύο δοκιμές για να προσδιοριστεί εάν θα το αγνοηθεί πλήρως ή όχι:<br><li>1. Οι κανονικές εκφράσεις χωρίς διαχωριστικό διαδρομών σε αυτές θα συγκρίνονται μόνο με το όνομα αρχείου.</li>\\n\"\n\"<li>2. Οι κανονικές εκφράσεις με τουλάχιστον ένα διαχωριστικό διαδρομών σε αυτές θα συγκριθούν με την πλήρη διαδρομή προς το αρχείο.</li><br>\\n\"\n\"Παράδειγμα: εάν θέλετε να φιλτράρετε αρχεία .PNG μόνο από τον κατάλογο \\\"Οι εικόνες μου\\\":<br><code>.*Οι\\\\sεικόνες\\\\sμου\\\\\\\\.*\\\\.png</code><br><br>Μπορείτε να δοκιμάσετε την κανονική έκφραση με το κουμπί \\\"δοκιμαστική συμβολοσειρά\\\" αφού επικολλήσετε μια ψεύτικη διαδρομή στο πεδίο δοκιμής:<br><code>C:\\\\\\\\χρήστης\\\\Οι εικόνες μου\\\\test.png</code><br><br>\\n\"\n\"Θα επισημανθεί η αντιστοίχιση των τυπικών εκφράσεων.<br>Εάν υπάρχει τουλάχιστον μία επισήμανση, η διαδρομή ή το όνομα αρχείου που δοκιμάστηκε θα αγνοηθεί κατά τη διάρκεια των σαρώσεων.<br><br>Κατάλογοι και αρχεία που ξεκινούν με τελεία \\\".\\\" φιλτράρονται από προεπιλογή.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Σφάλμα συλλογής:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Αυξήστε το ζουμ\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Μείωση ζουμ\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Κανονικό μέγεθος\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"ταιριάζει καλύτερα\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Λειτουργία προσωρινής μνήμης εικόνας:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"\"\n\"Παράκαμψη εικονιδίων θέματος στη γραμμή εργαλείων του προγράμματος προβολής\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Χρησιμοποιήστε τα δικά μας εσωτερικά εικονίδια αντί αυτών που παρέχονται από\"\n\" τη μηχανή θέματος\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Εμφάνιση γραμμών κύλισης σε προγράμματα προβολής εικόνων\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Όταν η εικόνα που εμφανίζεται δεν ταιριάζει στη θύρα προβολής, εμφανίστε τις\"\n\" γραμμές κύλισης για να εκτείνετε την προβολή γύρω\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Χρησιμοποιήστε την προεπιλεγμένη θέση για τη γραμμή καρτελών (απαιτείται \"\n\"επανεκκίνηση)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Τοποθετήστε τη γραμμή καρτελών κάτω από το κύριο μενού και όχι δίπλα σε αυτό\\n\"\n\"Σε MacOS, η γραμμή καρτελών θα γεμίσει το πλάτος του παραθύρου.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Χρησιμοποιήστε έντονη γραμματοσειρά για αναφορές\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Χρώμα προσκηνίου αναφοράς:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Χρώμα προσκηνίου αναφοράς:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Χρώμα προσκηνίου δέλτα:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Εμφάνιση της γραμμής τίτλου και δυνατότητα σύνδεσης\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Ενώ η γραμμή τίτλου είναι κρυφή, χρησιμοποιήστε το πλήκτρο τροποποίησης για \"\n\"να σύρετε το κυμαινόμενο παράθυρο γύρω\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"Η γραμμή τίτλου μπορεί να απενεργοποιηθεί μόνο όταν το παράθυρο είναι \"\n\"συνδεδεμένο\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Κάθετη γραμμή τίτλου\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Αλλάξτε τη γραμμή τίτλου από οριζόντια στην κορυφή, σε κατακόρυφη στην \"\n\"αριστερή πλευρά\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Εμφάνιση γραμμής καρτελών\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Αυτές οι τυπικές εκφράσεις python (πεζών-κεφαλαίων) θα φιλτράρουν τα αρχεία κατά τη διάρκεια των σαρώσεων.<br>Οι διευθυντές θα έχουν επίσης την <strong>προεπιλεγμένη κατάστασή</strong> τους σε Εξαίρεση στην καρτέλα Κατάλογοι εάν το όνομά τους συμβαίνει να ταιριάζει με μία από τις επιλεγμένες κανονικές εκφράσεις.<br>Για κάθε αρχείο που συλλέγεται, εκτελούνται δύο δοκιμές για να προσδιοριστεί εάν θα το αγνοηθεί πλήρως ή όχι:<br><li>1. Οι κανονικές εκφράσεις χωρίς διαχωριστικό διαδρομών σε αυτές θα συγκρίνονται μόνο με το όνομα αρχείου.</li>\\n\"\n\"<li>2. Οι κανονικές εκφράσεις με τουλάχιστον ένα διαχωριστικό διαδρομών σε αυτές θα συγκριθούν με την πλήρη διαδρομή προς το αρχείο.</li><br>\\n\"\n\"Παράδειγμα: εάν θέλετε να φιλτράρετε αρχεία .PNG μόνο από τον κατάλογο \\\"Οι εικόνες μου\\\":<br><code>.*Οι\\\\sεικόνες\\\\sμου\\\\\\\\.*\\\\.png</code><br><br>Μπορείτε να δοκιμάσετε την κανονική έκφραση με το κουμπί \\\"δοκιμαστική συμβολοσειρά\\\" αφού επικολλήσετε μια ψεύτικη διαδρομή στο πεδίο δοκιμής:<br><code>C:\\\\\\\\χρήστης\\\\Οι εικόνες μου\\\\test.png</code><br><br>\\n\"\n\"Θα επισημανθεί η αντιστοίχιση των τυπικών εκφράσεων.<br>Εάν υπάρχει τουλάχιστον μία επισήμανση, η διαδρομή ή το όνομα αρχείου που δοκιμάστηκε θα αγνοηθεί κατά τη διάρκεια των σαρώσεων.<br><br>Κατάλογοι και αρχεία που ξεκινούν με τελεία \\\".\\\" φιλτράρονται από προεπιλογή.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Αποτελέσματα\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Γενική διεπαφή\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Πίνακας αποτελεσμάτων\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Παράθυρο λεπτομερειών\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Γενικός\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Απεικόνιση\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Σχετικά {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Έκδοση {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Άδεια χρήσης βάσει GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Αναφορά σφάλματος\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Κάτι πήγε στραβά. Μήπως να αναφερθεί το σφάλμα;\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Οι αναφορές σφαλμάτων πρέπει να αναφέρονται ως ζητήματα GitHub. Μπορείτε να αντιγράψετε την ανίχνευση σφαλμάτων παραπάνω και να την επικολλήσετε σε ένα νέο ζήτημα.\\n\"\n\"\\n\"\n\"Βεβαιωθείτε ότι έχετε πραγματοποιήσει αναζήτηση για τυχόν υπάρχοντα ζητήματα εκ των προτέρων. Επίσης, φροντίστε να δοκιμάσετε την πιο πρόσφατη διαθέσιμη έκδοση από το αποθετήριο, καθώς το σφάλμα που αντιμετωπίζετε ενδέχεται να έχει ήδη διορθωθεί.\\n\"\n\"\\n\"\n\"Αυτό που συνήθως βοηθάει συνήθως είναι εάν προσθέσετε μια περιγραφή για το πώς λάβατε το σφάλμα. Ευχαριστώ!\\n\"\n\"\\n\"\n\"Παρόλο που η εφαρμογή θα πρέπει να συνεχίσει να εκτελείται μετά από αυτό το σφάλμα, ενδέχεται να βρίσκεται σε ασταθή κατάσταση, επομένως συνιστάται να κάνετε επανεκκίνηση της εφαρμογής.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Επίσκεψη GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Τσέχικα\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Γερμανικά\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Ελληνικά\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Αγγλικά\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Ισπανικά\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Γαλλικά\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Αρμένικα\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Ιταλικά\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Ιαπωνικά\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Κορεάτικα\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Μαλαϊκά\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Γερμανικά\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Πολωνικά\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Βραζιλιάνικα\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Ρώσικα\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Τουρκικά\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ουκρανέζικα\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Βιετναμέζικα\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Κινέζικα (Απλοποιημένα)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Εκκαθάριση λίστας\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Αναζήτηση...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/en/LC_MESSAGES/columns.po",
    "content": "#\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"File Path\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Error Message\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Duration\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Samplerate\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Filename\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Folder\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Size (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Time\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Sample Rate\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Kind\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modification\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Title\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artist\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Year\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Track Number\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Comment\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Match %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Words Used\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Dupe Count\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensions\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Size (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Timestamp\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Size\"\n"
  },
  {
    "path": "locale/en/LC_MESSAGES/core.po",
    "content": "#\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: core\\app.py:42\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"There are no marked duplicates. Nothing has been done.\"\n\n#: core\\app.py:43\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"There are no selected duplicates. Nothing has been done.\"\n\n#: core\\app.py:44\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\n\n#: core\\app.py:71\nmsgid \"Scanning for duplicates\"\nmsgstr \"Scanning for duplicates\"\n\n#: core\\app.py:72\nmsgid \"Loading\"\nmsgstr \"Loading\"\n\n#: core\\app.py:73\nmsgid \"Moving\"\nmsgstr \"Moving\"\n\n#: core\\app.py:74\nmsgid \"Copying\"\nmsgstr \"Copying\"\n\n#: core\\app.py:75\nmsgid \"Sending to Trash\"\nmsgstr \"Sending to Trash\"\n\n#: core\\app.py:308\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\n\n#: core\\app.py:318\nmsgid \"No duplicates found.\"\nmsgstr \"No duplicates found.\"\n\n#: core\\app.py:333\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"All marked files were copied successfully.\"\n\n#: core\\app.py:334\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"All marked files were moved successfully.\"\n\n#: core\\app.py:335\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"All marked files were successfully sent to Trash.\"\n\n#: core\\app.py:343\nmsgid \"Could not load file: {}\"\nmsgstr \"Could not load file: {}\"\n\n#: core\\app.py:399\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' already is in the list.\"\n\n#: core\\app.py:401\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' does not exist.\"\n\n#: core\\app.py:410\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\n\n#: core\\app.py:486\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Select a directory to copy marked files to\"\n\n#: core\\app.py:487\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Select a directory to move marked files to\"\n\n#: core\\app.py:527\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Select a destination for your exported CSV\"\n\n#: core\\app.py:534 core\\app.py:801 core\\app.py:811\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Couldn't write to file: {}\"\n\n#: core\\app.py:559\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"You have no custom command set up. Set it up in your preferences.\"\n\n#: core\\app.py:727 core\\app.py:740\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"You are about to remove %d files from results. Continue?\"\n\n#: core\\app.py:774\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} duplicate groups were changed by the re-prioritization.\"\n\n#: core\\app.py:821\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"The selected directories contain no scannable file.\"\n\n#: core\\app.py:835\nmsgid \"Collecting files to scan\"\nmsgstr \"Collecting files to scan\"\n\n#: core\\app.py:891\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d discarded)\"\n\n#: core\\engine.py:244 core\\engine.py:288\nmsgid \"0 matches found\"\nmsgstr \"0 matches found\"\n\n#: core\\engine.py:262 core\\engine.py:296\nmsgid \"%d matches found\"\nmsgstr \"%d matches found\"\n\n#: core\\gui\\deletion_options.py:73\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"You are sending {} file(s) to the Trash.\"\n\n#: core\\gui\\exclude_list_table.py:15\nmsgid \"Regular Expressions\"\nmsgstr \"Regular Expressions\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Do you really want to remove all %d items from the ignore list?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Filename\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Filename - Fields\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Filename - Fields (No Order)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:21 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Contents\"\n\n#: core\\pe\\matchblock.py:72\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analyzed %d/%d pictures\"\n\n#: core\\pe\\matchblock.py:181\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Performed %d/%d chunk matches\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Preparing for matching\"\n\n#: core\\pe\\matchblock.py:244\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Verified %d/%d matches\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Read EXIF of %d/%d pictures\"\n\n#: core\\pe\\scanner.py:22\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Timestamp\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"None\"\n\n#: core\\prioritize.py:100\nmsgid \"Ends with number\"\nmsgstr \"Ends with number\"\n\n#: core\\prioritize.py:101\nmsgid \"Doesn't end with number\"\nmsgstr \"Doesn't end with number\"\n\n#: core\\prioritize.py:102\nmsgid \"Longest\"\nmsgstr \"Longest\"\n\n#: core\\prioritize.py:103\nmsgid \"Shortest\"\nmsgstr \"Shortest\"\n\n#: core\\prioritize.py:140\nmsgid \"Highest\"\nmsgstr \"Highest\"\n\n#: core\\prioritize.py:140\nmsgid \"Lowest\"\nmsgstr \"Lowest\"\n\n#: core\\prioritize.py:169\nmsgid \"Newest\"\nmsgstr \"Newest\"\n\n#: core\\prioritize.py:169\nmsgid \"Oldest\"\nmsgstr \"Oldest\"\n\n#: core\\results.py:142\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplicates marked.\"\n\n#: core\\results.py:149\nmsgid \" filter: %s\"\nmsgstr \" filter: %s\"\n\n#: core\\scanner.py:85\nmsgid \"Read size of %d/%d files\"\nmsgstr \"Read size of %d/%d files\"\n\n#: core\\scanner.py:109\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Read metadata of %d/%d files\"\n\n#: core\\scanner.py:147\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Almost done! Fiddling with results...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Folders\"\n"
  },
  {
    "path": "locale/en/LC_MESSAGES/ui.po",
    "content": "#\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Quit\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Options\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Ignore List\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Clear Picture Cache\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru Help\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"About dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Open Debug Log\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Do you really want to remove all your cached picture analysis?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Picture cache cleared.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} file (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Deletion Options\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Link deleted files\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (unsupported)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Directly delete files\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Proceed\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Cancel\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Attribute\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Selected\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Reference\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Load Results...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Results Window\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Add Folder...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"File\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"View\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Help\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Load Recent Results\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Application Mode:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Music\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Picture\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Scan Type:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"More Options\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Select folders to scan and press \\\"Scan\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Load Results\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Scan\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Unsaved results\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"You have unsaved results, do you really want to quit?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Select a folder to add to the scanning list\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Select a results file to load\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"All Files (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru Results (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Start a new scan\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"You have unsaved results, do you really want to continue?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Name\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"State\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Excluded\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Remove Selected\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Clear\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Close\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Details\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Tags to scan:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Track\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artist\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Title\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Year\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Word weighting\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Match similar words\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Can mix file kind\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Use regular expressions when filtering\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Remove empty folders on delete or move\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignore duplicates hardlinking to the same file\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Debug mode (restart required)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Match pictures of different dimensions\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different rotations\"\nmsgstr \"Match pictures of different rotations\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Filter Hardness:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"More Results\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Fewer Results\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Font size:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Language:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Copy and Move:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Right in destination\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Recreate relative path\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Recreate absolute path\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Custom Command (arguments: %d for dupe, %r for ref):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru has to restart for language changes to take effect.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Re-Prioritize duplicates\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problems!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Reveal Selected\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Actions\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Show Dupes Only\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Show Delta Values\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Send Marked to Recycle Bin...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Move Marked to...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Copy Marked to...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Remove Marked from Results\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Re-Prioritize Results...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Remove Selected from Results\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Add Selected to Ignore List\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Make Selected into Reference\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Open Selected with Default Application\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Open Containing Folder of Selected\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Rename Selected\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Mark All\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Mark None\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Invert Marking\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Mark Selected\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Export To HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Export To CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Save Results...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Invoke Custom Command\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Mark\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Columns\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Reset to Defaults\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Results\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Dupes Only\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Delta Values\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Select a file to save your results to\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignore files smaller than\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Results\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Action\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Add New Folder...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Advanced\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Automatically check for updates\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Basic\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Bring All to Front\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Check for update...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Close Window\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Copy\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Custom command (arguments: %d for dupe, %r for ref):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Cut\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Details of Selected File\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Details Panel\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Directories\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru Preferences\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru Results\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru Website\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Edit\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Export Results to CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Export Results to XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Fewer results\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filter\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Filter hardness:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filter Results...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Folder Selection Window\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Font Size:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Hide dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Hide Others\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignore files smaller than:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Load from file...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimize\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Mode\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"More results\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Paste\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Preferences...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Quick Look\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Quit dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Reset to Default\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Reset To Defaults\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Reveal\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Reveal Selected in Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Select All\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Send Marked to Trash...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Show All\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Start Duplicate Scan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"The name '%@' already exists.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Window\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Exclusion Filters\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Scan Results\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Load Directories...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Save Directories...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Select a directories file to load\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Results (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Select a file to save your directories to\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Directories (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Add\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Restore defaults\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Test string\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Type a python regular expression here...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Type a file system path or filename here...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Compilation error: \"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Increase zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Decrease zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Normal size\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Best fit\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Picture cache mode:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Override theme icons in viewer toolbar\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Show scrollbars in image viewers\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Use default position for tab bar (requires restart)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Use bold font for references\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Reference foreground color:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Reference background color:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Delta foreground color:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Show the title bar and can be docked\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"The title bar can only be disabled while the window is docked\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Vertical title bar\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Show tab bar\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Results\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"General Interface\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Result Table\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Details Window\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"General\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Display\"\n"
  },
  {
    "path": "locale/es/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\\n\"\n\"Language-Team: Spanish (https://www.transifex.com/voltaicideas/teams/116153/es/)\\n\"\n\"Language: es\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Ruta de Fichero\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Mensaje de error\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Duración\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Tasa de bits\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Tasa de Muestreo\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nombre de fichero\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Carpeta\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Tamaño (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Hora\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Tasa de Muestreo\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Clase\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modificación\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Título\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Álbum\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Género\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Año\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Pista Número\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Comentario\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Coincidencia %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Palabras Empleadas\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Duplicado Número\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensiones\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Tamaño (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Marca horaria EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Tamaño\"\n"
  },
  {
    "path": "locale/es/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n# IlluminatiWave, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: IlluminatiWave, 2022\\n\"\n\"Language-Team: Spanish (https://app.transifex.com/voltaicideas/teams/116153/es/)\\n\"\n\"Language: es\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"No hay duplicados marcados. No se ha hecho nada.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"No hay duplicados seleccionados. No se ha hecho nada.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Está a punto de abrir muchas imágenes. Dependiendo de los ficheros que se \"\n\"abran, abrirlos puede colgar la máquina. ¿Continuar?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Buscando duplicados\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Cargando\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Moviendo\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Copiando\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Enviando a la Papelera\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Una acción previa sigue ejecutándose. No puede abrir una nueva todavía. \"\n\"Espere unos segundos y vuelva a intentarlo.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"No se han encontrado duplicados.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"\"\n\"Todos los ficheros seleccionados han sido copiados satisfactoriamente.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Todos los ficheros seleccionados se han movidos satisfactoriamente.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"Todos los ficheros seleccionados se han eliminado satisfactoriamente.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Todo los ficheros marcados se han enviado a la papelera exitosamente.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"No se pudo cargar el archivo: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' ya está en la lista.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' no existe.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Todas las %d coincidencias seleccionadas van a ser ignoradas en las \"\n\"subsiguientes exploraciones. ¿Continuar?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Seleccione un directorio donde desee copiar los archivos marcados\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Seleccione un directorio al que desee mover los archivos marcados\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Seleccionar un destino para el CSV seleccionado\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"No se pudo escribir en el archivo: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"No hay comandos configurados. Establézcalos en sus preferencias.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Está a punto de eliminar %d ficheros de resultados. ¿Continuar?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} grupos de duplicados han sido cambiados por la re-priorización.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Las carpetas seleccionadas no contienen ficheros para explorar.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Recopilando ficheros a explorar\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d descartados)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"{} ficheros recopilados para explorar\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"{} carpetas recopiladas para explorar\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"%d coincidencias encontradas en %d grupos\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Enviando {} fichero(s) a la Papelera\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Expresiones regulares\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"¿Desea realmente eliminar todos los %d elementos de la lista de exclusión?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nombre de archivo\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nombre de archivo - Campos\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nombre de archivo - Campos (sin orden)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Etiquetas\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Contenido\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analizadas %d/%d imágenes\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Realizado %d/%d trozos coincidentes\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Preparando para coincidencias\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Verificadas %d/%d coincidencias\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Leído EXIF de %d/%d imágenes\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Marca horaria EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Ninguno\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Termina con un número\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"No termina con un número\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"El más largo\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"El más corto\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"El más alto\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"El más bajo\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"El más nuevo\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"El más antiguo\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplicados marcados.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"filtro: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Leyendo metadatos de %d/%d ficheros\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"¡Casi termino! Jugando con los resultados...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Carpetas\"\n"
  },
  {
    "path": "locale/es/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n# IlluminatiWave, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: IlluminatiWave, 2022\\n\"\n\"Language-Team: Spanish (https://app.transifex.com/voltaicideas/teams/116153/es/)\\n\"\n\"Language: es\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Salir\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Opciones\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Lista de exclusión\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Limpiar el Cache de Fotos\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Ayuda de dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Acerca de dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Abrir Registro de Depuración\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"¿Desea realmente eliminar todo el caché de análisis de imágenes?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Caché de fotos limpio.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} fichero (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Opciones de borrado\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Enlace a ficheros borrados\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Después de borrar un duplicado, poner un enlace señalando el fichero de \"\n\"referencia para reemplazar el fichero borrado.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Enlace permanente\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Enlace Sym\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(no soportado)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Borrar ficheros directamente\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"En lugar de enviar ficheros a la papelera, borrarlos directamente. Esta \"\n\"opción se usa habitualmente como alternativa cuando el método normal de \"\n\"borrado no funciona.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Proceder\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Atributos\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Seleccionado\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Referencia\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Cargar Resultados...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Ventana de Resultados\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Añadir carpeta...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Fichero\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Vista\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Ayuda\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Cargar Resultados Recientes\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Modo de aplicación:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Música\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Imagen\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Estándar\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Tipo de Exploración:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Mas Opciones\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Seleccionar carpetas a explorar y pulsar \\\"Explorar\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Cargar Resultados\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Explorar\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Resultados sin guardar\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Tiene resultados sin guardar, ¿está seguro que quiere salir?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Seleccionar una carpeta a añadir a la lista de exploración\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Seleccionar un fichero de resultados para cargar\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Todos los Ficheros (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Resultados dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Empezar una nueva exploración\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Tiene resultados sin guardar, ¿realmente desea continuar?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nombre\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Estado\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Excluído\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Quitar Seleccionados\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Limpiar\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Cerrar\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Detalles\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Etiquetas a explorar:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Pista\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Álbum\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Título\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Género\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Año\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Ponderación de Palabra\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Coincidencia con palabras similares\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Puede mezclar tipos de fichero\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Use expresiones regulares cuando filtre\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Eliminar carpetas vacías al borrar o mover\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignorar duplicados enlazando al mismo fichero\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Mode de depuración (se requiere reinicio)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Coincidencia de imágenes de distintas dimensiones\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different rotations\"\nmsgstr \"Coincidencia de imágenes de distintas rotaciones\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Dureza del Filtro:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Más Resultados\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Menos Resultados\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Tamaño de fuente:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Idioma:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Copiar y Mover:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Derechos en destino\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Recrear ruta relativa\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Recrear ruta absoluta\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Comando de Usuario (argumentos: %d para duplicado, %r para ref):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\"dupeGuru debe reinicializarse para que los cambios de lenguaje surjan \"\n\"efecto.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Re-Priorizar duplicados\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Añadir criterio en la casilla izquierda y OK para enviar los duplicados \"\n\"correspondientes al mejor de esos criterios a sus respectivos grupos de \"\n\"posición de referencia. Consultar la ayuda para más información.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"¡Problemas!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Problemas procesando algunos (o todos) los ficheros. El origen de los \"\n\"problemas está indicados en la lista inferior. Estos ficheros no se han \"\n\"elimanos de sus resultados. \"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Revelar seleccionados\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Acciones\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Mostrar Sólo Duplicados\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Mostras los Valores Delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Enviar los Marcados a la Papelera de Reciclaje...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Mover los Marcados a...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Copiar los Marcados a...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Quitar los Marcados de los Resultados\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Re-priorizar los Resultados\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Quitar Seleccionados de los Resultados\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Añadir los seleccionados a la Lista de Exclusión\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Poner Seleccionados en Referencia\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Abrir seleccionados con la Aplicación por Defecto\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Abrir la Carpeta Contenedora de los Seleccionados\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Renombrar los Seleccionados\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Marcar Todos\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Marcar Ninguno\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Marcado Inverso\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Marcar Seleccionados\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Exportar a HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Exportar a CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Guardar Resultados...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Invocar Comando de Usuario\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Marcar\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Columnas\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Reiniciar a Valores por Defecto.\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Resultados\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Duplicados Únicamente\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Valores Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Seleccionar un fichero al que guardar los resultados\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignorar ficheros más pequeños de\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Resultados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Acción\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Añadir Nueva Carpeta...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Avanzado\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Comprobar las actualizaciones automáticamente.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Básico\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Traer Todos al Frente\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Comprobando actualizaciones...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Cerrar Ventana\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Copiar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Comando de usuario (argumentos: %d para duplicado, %r para ref):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"cortar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Detalles del Fichero Seleccionado\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Panel de Detalles\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Directorios\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeguru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Preferencias de dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Resultados de dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Web de dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Editar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Exportar resultados a CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exportar resultados a XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Menos resultados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtro\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Dureza del filtro:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Resultados del Filtro...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Ventana de Selección de Carpetas\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Tamaño de Fuente:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Minimizar dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Esconder Otros\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignorar ficheros más pequeños de:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Cargar fichero desde...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimizar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Modo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Más resultados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"De acuerdo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Pegar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Preferencias\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Ojear\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Cerrar dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Reiniciar a Valor por Defecto.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Reiniciar a Valores por Defecto\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Mostrar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Mostrar Seleccionados en Buscar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Seleccionar Todos\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Mover los Marcados a la Papelera...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Servicios\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Mostrar Todos\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Empezar la Exploración de Duplicados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"El nombre '%@' ya existe.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Ventana\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Filtros de exclusión\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Resultados del Escáner\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Cargar Carpetas...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Guardar Carpetas...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Seleccione un archivo de carpetas para cargar\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"Resultados dupeGuru (*.dupeguru)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Seleccionar un fichero al que guardar las carpetas\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"Carpetas dupeGuru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Agregar\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Reiniciar a Valores por Defecto\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Cadena de prueba\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Escriba una expresión regular de Python aquí...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"\"\n\"Escriba una ruta del sistema de archivos o un nombre de archivo aquí...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Estas expresiones regulares de Python (sensibles a mayúsculas y minúsculas) filtrarán los archivos durante los escaneos.<br> Carpetas también tendrá su <strong>establecido en Excluido</strong> en la pestaña Carpetas si su nombre coincide con una de las expresiones regulares seleccionadas.<br>Para cada archivo recopilado, se realizan dos pruebas para determinar si se debe ignorar por completo o no.<br><li>1. Las expresiones regulares sin separador de ruta se compararán solo con el nombre del archivo.</li>\\n\"\n\"<li>2. Las expresiones regulares con al menos un separador de ruta en ellas se compararán con la ruta completa al archivo.</li><br>\\n\"\n\"Ejemplo: si desea filtrar archivos .PNG solo del directorio \\\"Mis imágenes\\\":<br><code>.*Mis\\\\sImágenes\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\Usario\\\\Mis Imágenes\\\\test.png</code><br><br>\\n\"\n\"Se resaltarán las expresiones regulares coincidentes.<br>Si hay al menos un resaltado, la ruta o el nombre de archivo probado se ignorará durante los escaneos.<br><br>Directorios y archivos que comienzan con un punto '.' se ignoran de forma predeterminada.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Error de compilación:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Aumentar el zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Disminuir zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Tamaño normal\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Mejor ajuste\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Modo de caché de imágenes:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Anula los iconos de temas en la barra de herramientas del visor\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Utilice nuestros propios iconos internos en lugar de los proporcionados por \"\n\"el motor de temas\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Mostrar barras de desplazamiento en visores de imágenes\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Cuando la imagen mostrada no se ajusta a la ventana gráfica, muestre barras \"\n\"de desplazamiento para abarcar la vista\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Use la posición predeterminada para la barra de pestañas (requiere \"\n\"reiniciar).\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Coloque la barra de pestañas debajo del menú principal en lugar de al lado.\\n\"\n\"En MacOS, la barra de pestañas llenará el ancho de la ventana.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Utilice fuente en negrita para las referencias.\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Color del texto de referencia:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Color de fondo de referencia:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Color del texto de delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Muestra la barra de título y se puede acoplar.\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Si la barra de título está ocultada, use la tecla modificadora para \"\n\"arrastrar la ventana flotante.\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"La barra de título solo se puede desactivar si la ventana está acoplada.\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Barra de título vertical\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Cambie la barra de título de horizontal en la parte superior a vertical en \"\n\"el lado izquierdo.\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Mostrar barra de pestañas\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Estas expresiones regulares de Python (sensibles a mayúsculas y minúsculas) filtrarán los archivos durante los escaneos.<br> Carpetas también tendrá su <strong>establecido en Excluido</strong> en la pestaña Carpetas si su nombre coincide con una de las expresiones regulares seleccionadas.<br>Para cada archivo recopilado, se realizan dos pruebas para determinar si se debe ignorar por completo o no.<br><li>1. Las expresiones regulares sin separador de ruta se compararán solo con el nombre del archivo.</li>\\n\"\n\"<li>2. Las expresiones regulares con al menos un separador de ruta en ellas se compararán con la ruta completa al archivo.</li><br>\\n\"\n\"Ejemplo: si desea filtrar archivos .PNG solo del directorio \\\"Mis imágenes\\\":<br><code>.*Mis\\\\sImágenes\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\Usario\\\\Mis Imágenes\\\\test.png</code><br><br>\\n\"\n\"Se resaltarán las expresiones regulares coincidentes.<br>Si hay al menos un resaltado, la ruta o el nombre de archivo probado se ignorará durante los escaneos.<br><br>Directorios y archivos que comienzan con un punto '.' se ignoran de forma predeterminada.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Resultados\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Interfaz general\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tabla de resultados\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Ventana de detalles\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"General\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Visualización\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Archivos de hash parcialmente mayores a\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"Usar diálogos nativos del SO\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"Para acciones como la selección de archivos/carpetas, utilice los diálogos nativos del SO\\n\"\n\"Algunos diálogos nativos tienen una funcionalidad limitada.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Ignorar los ficheros mayores a\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Borrar caché\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"¿Seguro que quieres borrar la caché? Esto eliminará todos los hashes de \"\n\"ficheros y análisis de imágenes almacenados en la caché.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Caché eliminada.\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Usar tema oscuro\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Perfilar operación de análisis\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\"Perfilar la operación de análisis y guardar los registros para su \"\n\"optimización.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"Registro guardado en: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Depurar\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Acerca de {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Versión {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Buscando actualizaciones...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Licenciado en GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"Sin actualizaciones disponibles.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"Nueva versión disponible {}, descargar <a href=\\\"{}\\\">aquí</a> \"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Informe de error\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Algo salió mal. ¿Qué tal informar el error?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"Lo que generalmente ayuda es agregar una descripción de cómo obtuvo el error. ¡Gracias!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Ir a GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Checo\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Alemán\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Griego\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Inglés\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Español\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Francés\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armenio\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italiano\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japonés\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Coreano\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malayo\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Holandés\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polaco\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brasileño\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Ruso\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turco\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ucraniano\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamita\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Chino (simplificado)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Limpiar lista\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Búsqueda...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/fr/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: French (https://www.transifex.com/voltaicideas/teams/116153/fr/)\\n\"\n\"Language: fr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Chemin du fichier\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Message d'erreur\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Durée\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Échantillonnage\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nom de fichier\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Dossier\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Taille (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Temps\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Sample Rate\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Type\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modification\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Titre\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artiste\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Année\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Track\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Commentaire\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Match %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Mots\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Nombre de Doublons\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensions\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Taille (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Date EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Taille\"\n"
  },
  {
    "path": "locale/fr/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: French (https://app.transifex.com/voltaicideas/teams/116153/fr/)\\n\"\n\"Language: fr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Aucun doublon marqué. Rien à faire.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Aucun doublon sélectionné. Rien à faire.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Beaucoup de fichiers seront ouverts en même temps. Cela peut gravement \"\n\"encombrer votre système. Continuer?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Scan de doublons en cours\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Chargement en cours\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Déplacement en cours\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Copie en cours\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Envoi de fichiers à la corbeille\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Une action précédente est encore en cours. Attendez quelques secondes avant \"\n\"d'en repartir une nouvelle.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Aucun doublon trouvé.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Tous les fichiers marqués ont été copiés correctement.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Tous les fichiers marqués ont été déplacés correctement.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"\"\n\"Tous les fichiers marqués ont été correctement envoyés à la corbeille.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Impossible d'ouvrir le fichier: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' est déjà dans la liste.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' n'existe pas.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"%d fichiers seront ignorés des prochains scans. Continuer?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Sélectionnez un dossier vers lequel copier les fichiers marqués.\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Sélectionnez un dossier vers lequel déplacer les fichiers marqués.\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Choisissez une destination pour votre exportation CSV\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Impossible d'écrire le fichier: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Vous n'avez pas de commande personnalisée. Ajoutez-la dans vos préférences.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"%d fichiers seront retirés des résultats. Continuer?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} groupes de doublons ont été modifiés par la re-prioritisation.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Les dossiers sélectionnés ne contiennent pas de fichiers valides.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Collecte des fichiers à scanner\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d hors-groupe)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Vous envoyez {} fichier(s) à la corbeille.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Expressions régulières\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"Voulez-vous vider la liste de fichiers ignorés des %d items qu'elle \"\n\"contient?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nom de fichier\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nom de fichier - Champs\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nom de fichier - Champs (sans ordre)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Contenu\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analyzé %d/%d images\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d/%d blocs d'images comparés\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Préparation pour la comparaison\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Vérifié %d/%d paires\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Lu l'EXIF de %d/%d images\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Date EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Aucune\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Chiffres à la fin\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Pas de chiffres à la finr\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Le plus long\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Le plus court\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Plus grand\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Moins grand\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Plus récent\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Moins récent\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) doublons marqués.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" filtre: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Lu les métadonnées de %d/%d fichiers\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Bientôt terminé! Bidouille des résultats...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Dossiers\"\n"
  },
  {
    "path": "locale/fr/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: French (https://app.transifex.com/voltaicideas/teams/116153/fr/)\\n\"\n\"Language: fr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Quitter\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Options\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Liste de doublons ignorés\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Vider la cache d'images\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Aide dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"À propos de dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Ouvrir logs de déboguage\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Voulez-vous vraiment vider la cache de vos analyses précédentes?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"La cache des analyses précédentes a été vidée.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"Fichier {} (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Options de suppression\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Remplacer les fichiers effacés par des liens\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Après avoir effacé un fichier, remplacer celui-ci par un lien vers le \"\n\"fichier référence.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(non pris en charge)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Supprimer les fichiers directement\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Au lieu de passer par la corbeille, supprimer directement. Cette option \"\n\"n'est généralement utilisée qu'en cas de problème.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Continuer\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Annuler\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Attribut\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Sélectionné\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Référence\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Charger résultats...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Fenêtre de résultats\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Ajouter dossier...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Fichier\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Voir\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Aide\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Charger résultats récents\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Mode de l'application:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Musique\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Image\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Type de scan:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Plus d'options\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Sélectionnez les dossiers à scanner puis faites \\\"Scan\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Charger\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Scan\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Résultats non sauvegardés\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Vos résultats ne sont pas sauvegardés. Voulez-vous vraiment quitter?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Sélectionnez un dossier à ajouter à la liste\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Sélectionnez un fichier résultats à charger\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Tout les fichiers (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Résultats dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Commencer un nouveau scan\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"Vos résultats ne sont pas sauvegardés. Voulez-vous vraiment continuer?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nom\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Type\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Exclu\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Effacer sélection\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Vider\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Fermer\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Détails\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Tags à scanner:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Track\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artiste\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Titre\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Année\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Proportionalité des mots\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Comparer les mots similaires\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Comparer les fichiers de différents types\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Utiliser les expressions régulières pour les filtres\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Effacer les dossiers vides après un déplacement\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignorer doublons avec hardlink vers le même fichier\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Mode de déboguage (redémarrage requis)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Comparer les images de tailles différentes\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Seuil du filtre:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Plus de doublons\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Moins de doublons\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Taille de police:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Langue:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Déplacements de fichiers:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Directement à la destination\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Re-créer chemins relatifs\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Re-créer chemins absolus\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Commande perso. (arguments: %d pour doublon, %r pour réf):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru doit redémarrer pour appliquer le changement de langue.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Re-prioriser les doublons\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Ajoutez des critères dans la liste de droite pour envoyer les doublons qui \"\n\"correspondent le plus à ces critère à la position de référence. Une lecture \"\n\"préalable du fichier d'aide est conseillée.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problèmes!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Des problèmes ont été rencontrés lors du traitement de certains fichiers. La\"\n\" nature de ces problèmes est décrite dans la liste ci-dessous. Ces fichiers \"\n\"n'ont pas été retirés des résultats.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Révéler Fichier\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Actions\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Ne pas montrer les références\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Montrer les valeurs en tant que delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Envoyer marqués à la corbeille...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Déplacer marqués vers...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Copier marqués vers...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Retirer marqués des résultats\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Re-prioriser les résultats\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Retirer sélectionnés des résultats\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Ajouter sélectionnés à la liste de fichiers ignorés\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Transformer sélectionnés en références\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Ouvrir sélectionné avec l'application par défaut\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Ouvrir le dossier contenant le fichier sélectionné\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Renommer sélectionné\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Tout marquer\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Tout démarquer\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Inverser le marquage\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Marquer sélectionnés\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Exporter vers HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Exporter vers CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Sauvegarder résultats...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Invoquer commande personnalisée\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Marquer\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Colonnes\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Réinitialiser\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} résultats\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Sans réf.\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Sélectionnez un fichier dans lequel sauvegarder les résultats\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignorer les fichiers plus petits que\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"Résultats de %@\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Action\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Ajouter dossier...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Avancé\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Vérifier automatiquement les mises à jour\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Simple\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Tout ramener au premier plan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Mise à jour...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Fermer\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Copier\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Commande perso. (arguments: %d pour doublon, %r pour réf):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Couper\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Détails du fichier sélectionné\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Fenêtre de détails\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Dossiers\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Préférences de dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Résultats dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Site web de dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Édition\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Exporter les résultats vers CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exporter les résultats vers HTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"moins de doublons\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtre\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Seuil du filtre:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filtrer les résultats...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Fenêtre de sélection de dossiers\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Taille de police:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Masquer dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Masquer les autres\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignorer les fichiers plus petits que:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Charger un fichier...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Placer dans le Dock\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Mode\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"plus de doublons\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Coller\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Préférences...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Quick Look\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Quitter dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Colonnes par défault\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Valeurs par défaut\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Révéler\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Révéler sélectionné dans Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Tout sélectionner\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Envoyer marqués à la corbeille...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Tout afficher\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Commencer à scanner\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Le nom '%@' existe déjà.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Fenêtre\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Réduire/agrandir\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Filtres d'exclusion\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Résultats de scan\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Charger dossiers...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Sauvegarder dossiers...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Sélectionnez un fichier de dossier à charger\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"Dossiers dupeGuru (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Sélectionnez un fichier pour y sauvegarder vos dossiers\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"Dossiers dupeGuru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Ajouter\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Réinitialiser\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Tester chaîne\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Tapez une expression régulière python ici...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Tapez un chemin ou un nom de fichier ici...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ces expressions régulières python (sensible aux majuscules) peuvent ignorer les fichiers pendant les scans. Les dossiers auront également leur <strong>état par défaut</strong> mis sur Exclus dans l'onglet Dossiers si leur nom correspond à une des expressions régulières sélectionnées<br>Pour chaque fichier collecté, deux tests sont faits pour déterminer s'il doit être totalement ignoré:<br><li>1. Les expressions régulières sans séparateur de chemin sont comparées au nom de fichier seul.</li>\\n\"\n\"<li>2. Les expressions régulières avec au moins un séparateur de chemin sont comparées au chemin complet vers le fichier.</li><br>\\n\"\n\"Exemple: si vous voulez uniquement ignorer les fichiers .PNG du dossier \\\"Mes Images\\\":<br><code>.*Mes\\\\sImages\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\Utilisateur\\\\Mes Images\\\\test.png</code><br><br>\\n\"\n\"Les expressions régulières qui fonctionnent seront surlignées.<br>S'il y a au moins un surlignage, le chemin ou nom de fichier testé sera ignoré durant les scans.<br><br>Les dossiers et fichiers commençant par un point '.' sont ignorés par défaut.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Erreur de compilation:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Accroître zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Décroître zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Taille normale\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Meilleur ajustement\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Mode de cache d'images:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Outrepasser le thème d’icônes dans le visualiseur\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Utiliser nos propres icônes plutôt que celles fournies par le moteur du \"\n\"thème\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Afficher les barres de défilement dans visualiseur d'images\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Si l'image affichée ne rentre par dans la fenêtre, afficher les barres de \"\n\"défilement pour faire glisser la vue\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Position par défaut pour la barre d'onglets (redémarrage requis)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Placer la barre d'onglets sous le menu principal plutôt qu'à côté.\\n\"\n\"Sur MacOS, cette barre remplira la fenêtre en largeur.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Utiliser police en gras pour les références\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Couleur du texte des référénces:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Couleur de fond pour les références:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Couleur du texte des delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Affiche barre de titre et peut être ancrée\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Quand la barre de titre est cachée, utilisez la touche méta pour déplacer la\"\n\" fenêtre flottante\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"La barre de titre ne peut être désactivée que quand la fenêtre est ancrée\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Barre de titre verticale\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Placer la barre de titre à la verticale côté gauche plutôt que à \"\n\"l'horizontale en haut\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Afficher la barre d'onglets\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ces expressions régulières python (sensible aux majuscules) peuvent ignorer les fichiers pendant les scans. Les dossiers auront également leur <strong>état par défaut</strong> mis sur Exclus dans l'onglet Dossiers si leur nom correspond à une des expressions régulières sélectionnées<br>Pour chaque fichier collecté, deux tests sont faits pour déterminer s'il doit être totalement ignoré:<br><li>1. Les expressions régulières sans séparateur de chemin sont comparées au nom de fichier seul.</li>\\n\"\n\"<li>2. Les expressions régulières avec au moins un séparateur de chemin sont comparées au chemin complet vers le fichier.</li><br>\\n\"\n\"Exemple: si vous voulez uniquement ignorer les fichiers .PNG du dossier \\\"Mes Images\\\":<br><code>.*Mes\\\\sImages\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\Utilisateur\\\\Mes Images\\\\test.png</code><br><br>\\n\"\n\"Les expressions régulières qui fonctionnent seront surlignées.<br>S'il y a au moins un surlignage, le chemin ou nom de fichier testé sera ignoré durant les scans.<br><br>Les dossiers et fichiers commençant par un point '.' sont ignorés par défaut.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Résultats\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Interface générale\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tableau de résultats\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Fenêtre de détails\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Général\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Affichage\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"A propos de {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Version {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Sous licence GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Rapport d'erreur\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Un problème est survenu. Rapporter l'erreur?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"Décrire comment vous avez rencontré cette erreur est aussi très précieux. Merci!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Aller sur GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Tchèque\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Allemand\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Grecque\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Anglais\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Espagnol\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Français\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Arménien\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italien\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japonais\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Coréen\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malaisien\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Néerlandais\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polonais\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brésilien\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Russe\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turc\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ukrainien\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamien\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Chinois (Simplifié)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Vider la liste\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Recherche...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/hy/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Armenian (https://www.transifex.com/voltaicideas/teams/116153/hy/)\\n\"\n\"Language: hy\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Ֆայլի ճ-ը\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Սխալի գրությունը\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Տևողությունը\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Բիթրեյթը\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Սիմպլրեյթը\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Ֆայլի անունը\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Թղթապանակ\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Չափը (ՄԲ)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Ժամանակը\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Սեմփլրեյթը\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Տեսակ\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Փոփոխությունը\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Անունը\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Կատարողը\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Ալբոմը\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Ժանրը\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Տարին\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Շավիղի համարը\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Մեկնաբանություն\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Համընկնում %-ին\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Բառ է օգտ.\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Խաբկանքի ք-ը\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Չափերը\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Չափը (ԿԲ)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Timestamp\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Չափը\"\n"
  },
  {
    "path": "locale/hy/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Armenian (https://app.transifex.com/voltaicideas/teams/116153/hy/)\\n\"\n\"Language: hy\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Նշված կրկնօրինակներ չկան: Ոչինչ չի արվել.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Ընտրված կրկնօրինակներ չկան: Ոչինչ չի արվել.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Դուք պատրաստվում եք միանգամից շատ ֆայլեր բացել: Կախված այն բանից, թե ինչով \"\n\"են բացվում այդ ֆայլերը, դա անելը կարող է բավականին խառնաշփոթ ստեղծել: \"\n\"Շարունակել?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Ստուգվում են կրկնօրինակները\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Բացվում է\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Տեղափոխվում է\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Պատճենվում է\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Ուղարկվում է Աղբարկղ\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Նախորդ գործողությունը դեռևս ձեռադրում է այստեղ: Չեք կարող սկսել մեկ ուրիշը: \"\n\"Սպասեք մի քանի վայրկյան և կրկին փորձեք:\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Կրկնօրինակներ չկան:\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Բոլոր նշված ֆայլերը հաջողությամբ պատճենվել են:\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Բոլոր նշված ֆայլերը հաջողությամբ տեղափոխվել են:\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Բոլոր նշված ֆայլերը հաջողությամբ Ջնջվել են:\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Հնարավոր չէ բեռնել ֆայլը: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}'-ը արդեն առկա է ցանկում:\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}'-ը գոյություն չունի:\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Ընտրված %d համընկնումները կանտեսվեն հետագա բոլոր ստուգումներից: Շարունակե՞լ:\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Ընտրեք գրացուցակ, որտեղ ցանկանում եք պատճենել նշված ֆայլերը\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\"Խնդրում ենք ընտրել գրացուցակ, որտեղ ցանկանում եք տեղափոխել նշված ֆայլերը\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Ընտրեք նպատակակետ ձեր արտահանված CSV- ի համար\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Չէր կարող գրել է ֆայլը: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"Դուք չեք կատարել Հրամանի ընտրություն: Կատարեք այն կարգավորումներում:\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Դուք պատրաստվում եք ջնջելու %d ֆայլեր: Շարունակե՞լ:\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} կրկնօրինակ խմբերը փոխվել են առաջնահերթության կարգով:\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Ընտրված թղթապանակները պարունակում են չստուգվող ֆայլ:\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Հավաքվում են ֆայլեր՝ ստուգելու համար\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d անպիտան)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Դուք {} ֆայլ եք ուղարկում աղբարկղ:\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Կանոնավոր արտահայտություններ\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Ցանկանու՞մ եք հեռացնել բոլոր %d ֆայլերը անտեսումների ցանկից:\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Ֆայլի անունը\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Ֆայլի անվանումը - Դաշտեր\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Ֆայլի անուն - դաշտեր (պատվեր չկա)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Բովանդակություն\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Ստուգվում է %d/%d նկարները\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Կատարվում է %d/%d տվյալի համընկնում\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Նախապատրաստեցվում է համընկնումը\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Ստուգում է %d/%d համընկնումները\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Կարդալ EXIF-ը d/%d նկարներից\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Timestamp\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Ոչինչ\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Ավարտվում է թվով\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Չի ավարտվում է թվով\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Ամենաերկար\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Ամենակարճը\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Ամենաբարձրը\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Ամենացածրը\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Նորագույնը\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Ամենահինը\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) նշված կրկնօրինակներ:\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"ֆիլտր. %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Կարդալ %d/%d ֆայլերի մետատվյալները\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Գրեթե արված է! Արդյունքների կազմակերպում...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Թղթապանակներ\"\n"
  },
  {
    "path": "locale/hy/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Armenian (https://app.transifex.com/voltaicideas/teams/116153/hy/)\\n\"\n\"Language: hy\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Փակել\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Ընտրանքներ\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"անտեսել ցուցակ\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Մաքրել նկարի պահոցը\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru-ի Օգնությունը\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"dupeGuru-ի մասին\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Բացել Սխալների մատյանը\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Ցանկանու՞մ եք հեռացնել բոլոր պահված նկարները ստուգելուց:\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Նկարի պահոցը մաքրվել է:\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} ֆայլ (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Ջնջումների Ընտրանքներ\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Կապել ջնջված ֆայլերը\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"րկնօրինակը ջնջելուց հետո տեղադրեք հղում, տեղադրել հղում նպատակային հղվող \"\n\"ֆայլը փոխարինել ջնջված ֆայլը.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (չաջակցվող)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Ուղղակիորեն ջնջեք ֆայլերը\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Ֆայլերը աղբարկղ ուղարկելու փոխարեն, դրանք ուղղակիորեն ջնջեք: Այս տարբերակը \"\n\"սովորաբար օգտագործվում է որպես լուծում, երբ ջնջման սովորական մեթոդը չի \"\n\"գործում:\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Շարունակել\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Չեղարկել\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Հատկանիշը\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Ընտրված\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Հղումը\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Բացել արդյունքները...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Արդյունքի պատուհանը\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Ավելացնել թղթապանակ...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Ֆայլ\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Տեսքը\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Օգնություն\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Բացել Վերջին արդյունքները\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Դիմումի ռեժիմ\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Երաժշտություն\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Նկար\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Ստանդարտ\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Ստուգելու տեսակը.\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Լրացուցիչ ընտրանքներ\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Ընտրեք ստուգելու թղթապանակները և սեղմեք  \\\"Ստուգել\\\":\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Բացել արդյունքները\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Ստուգել\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Չպահպանված արդյունքները\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Դուք ունեք չպահպանված արդյունքներ, իրո՞ք փակել:\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Ընտրեք ստուգման ցանկը ավելացնելու թղթապանակը\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Ընտրեք արդյունքի ֆայլը՝ բացելու համար\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Բոլոր ֆայլերը  (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru-ի արդյունքները (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Սկսել նոր ստուգումը\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Ունեք չպահպանված արդյունքներ, իրո՞ք շարունակել:\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Անունը\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Վիճակը\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Բացառված\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Նորմալ\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Հեռացնել ընտրվածը\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Մաքրել\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Փակել\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Մանրամասն\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Ստուգվող կցապիտակները.\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Շավիղը\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Կատարողը\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Ալբոմը\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Վերնագիրը\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Ժանրը\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Տարին\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Բառի չափը\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Ըստ նման բառերի համընկնման\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Կարող է խառը տեսակի\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Օգտ. կանոնավոր սահմանումներ ֆիլտրելիս\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Հեռացնել\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Անտեսել կրկնօրինակները հարդ նույն ֆայլը\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Սխալի եղանակը (պահանջում է վերագործարկում)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Նկարների համընկնում տարբեր չափերով\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Ֆիլտրի կարգը.\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Լր. արդյունքներ\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Քիչ արդյունքներ\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Տառի չափը.\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Լեզուն.\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Պատճենել և Տեղափոխել.\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Նշվածից աջ\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Վերաստեղծել հարաբերական ճ-ը\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Վերաստեղծել բացարձակ ճ-ը\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Հրամանի կատարում (փաստարկներ. %d խաբկանքի, %r հղման համար)\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru-ը պետք է վերագործարկվի՝ լեզուն կիրառելու համար:\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Վերաառաջնավորել կրկնօրինակները\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Ավելացրեք պահանջներ աջ վանդակում և սեղմեք ԼԱՎ՝ ուղարկելու համար պատճեները, \"\n\"որոնք համապատասխանում են այս պահանջներին: Մանրամասները Օգնությունում:\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Խնդիրներ!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Խնդիրներ են առկա որոշ (կամ բոլոր) ֆայլերի հետ գործողություններում: Այդ \"\n\"խնդիրների լուծումը նկարագրված է հետևյալ աղյուսակում: Այս ֆայլերը չեն հեռացվի\"\n\" արդյունքներից:\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Ցուցադրել ընտրվածը\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Գործողություններ\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Ցուցադրել միայն պատճեները\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Ցուցադրել դելտա նշան-ը\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Ուղարկել նշվածները Աղբարկղ...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Տեղափ. նշվածը՝\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Պատճ. նշվածը՝\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Հեռացնել նշվածները ցանկից\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Վերաառաջնայնավորել արդյունքները...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"ՀԵռացնել ընտրվածը արդյունքներից\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Ավելացնել ընտրվածը Անտեսումների ցանկ\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Ընտրված նյութը դարձրեք տեղեկատու նյութ:\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Բացել ընտրվածը Հիմնական ծրագրով\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Բացել ընտրվածը պարունակող թղթապանակը\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Անվանափոխել ընտրվածը\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Նշել բոլորը\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Ոչինչ չնշել\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Ետարկել նշումը\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Նշել ընտրվածը\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Արտածել HTML-ով\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Արտահանել CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Պահպանել արդյունքները...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Անտեսել Հրամանի կատարումը\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Նշել\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Սյուները\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Ետարկել ծրագրայինի\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Արդյունքներ\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Միայն կրկ.\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Դելտա արժեքներ\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Ընտրեք ֆայլը՝ պահպանելու արդյունքները՝\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Անտեսել ֆայլերը, որոնք փոքր են՝\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"ԿԲ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Արդյունքներ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Գործողությունը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Ավելացնել նոր թղթապանակ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Ընդլայնված\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Ինքնաշխատ ստուգել թարմացումները\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Բազային\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Բոլորի առջևում\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Ստուգել թարմացումները...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Փակել պատուհանը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Պատճենել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Հրամանի կատարում (փաստարկները. %d խաբկանքի զոհ, %r հղման համար).\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Կտրել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Դելտա\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Ընտրված ֆայլի մանրամասները\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Մանրամասների վահանակը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Թղթապանակներ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru-ի կարգավորումները\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru-ի արդյունքները\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru-ի վեբ կայքը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Խմբագրել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Արտահանել արդյունքները CSV ձևաչափով:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Արտածել արդյունքները XHTML-ով\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Քիչ արդյունքներ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Ֆիլտրը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Ֆիլտրի կարգը.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Զտել արդյունքները:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Թղթապանակը ընտրելու պատուհանը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Տառատեսակի չափը ՝\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Թաքցնել dupeGuru-ին\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Թաքցնել մյուսները\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Անտեսել նմանատիպ ֆայլերը.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Բացել ֆայլից...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Թաքցնել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Եղանակը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Լր. արդյունքներ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"ԼԱՎ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Տեղադրել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Կարգավորումներ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Quick Look\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Փակել dupeGuru-ը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Ետարկել ծրագրայինի\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Զրոյացնել լռելյայն\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Բացահայտել\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Ցուցադրել ընտրվածը Գտնվածում\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Ընտրել բոլորը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Ուղարկել նշվածները Աղբարկղ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Առայություններ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Ցուցադրել բոլորը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Սկսել կրկնօրինակների ստուգումը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"'%@' անունը արդեն առկա է:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Պատուհանը\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Չափը\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Բացառման ֆիլտրեր\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Սկան արդյունքներ\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"բեռնվածքի տեղեկագրքեր...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Պահել գրացուցակները...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Ընտրեք գրացուցակների ֆայլ `բեռնելու համար\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru տեղեկատուներ (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Ընտրեք ֆայլ, ձեր գրացուցակները պահելու համար\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru տեղեկատուներ (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Ավելացնել\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Վերականգնել Նախնականը\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Թեստային լարային\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Նկարի քեշի ռեժիմ:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Արդյունքներ\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Ընդհանուր միջերես\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Արդյունքների աղյուսակ\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Մանրամասներ Պատուհան\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Գեներալ\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Ցուցադրման\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"{}- ի մասին\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"{}-րդ տարբերակ\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"իցենզավորված տակ GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Սխալների մասին հաղորդագրություն\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Ինչ որ բան այնպես չգնաց. Հաղորդել սխալը?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Error հաշվետվությունները պետք է հրապարակվեն որպես GitHub հարցերի շուրջ: Կարող եք վերևում պատճենել սխալի հետևումը և տեղադրել այն նոր համարում:\\n\"\n\"\\n\"\n\"Խնդրում ենք համոզվեք, որ նախապես փնտրեք արդեն գոյություն ունեցող ցանկացած խնդիր: Նաեւ համոզվեք, որ ստուգել են հենց վերջին տարբերակը մատչելի շտեմարան, քանի որ Bug դուք ապրում գուցե արդեն patched.\\n\"\n\"\\n\"\n\"Սովորաբար այն, ինչ օգնում է իրականում, այն է, եթե ավելացնեք նկարագրությունը, թե ինչպես եք ստացել սխալը: Շնորհակալություն\\n\"\n\"\\n\"\n\"Չնայած այս սխալից հետո ծրագիրը պետք է շարունակի գործել, այն կարող է լինել անկայուն վիճակում, ուստի խորհուրդ է տրվում վերագործարկել ծրագիրը:\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Գնացեք Գիթուբ\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Չեխերեն\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Գերմաներեն\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"հունարեն\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Անգլերեն\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Իսպաներեն\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Ֆրանսերեն\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"հայերեն\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Իտալերեն\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"ճապոներեն\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"կորեերեն\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Մալայերեն\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"հոլանդերեն\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"լեհերեն\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"բրազիլական\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"ռուսերեն\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Թուրքերեն\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"ուկրաիներեն\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"վիետնամերեն\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Չինարեն (Պարզեցված)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Մաքրել ցանկը\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Որոնել...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/it/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\\n\"\n\"Language-Team: Italian (https://www.transifex.com/voltaicideas/teams/116153/it/)\\n\"\n\"Language: it\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Percorso del file\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Messaggio di errore\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Durata\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Campionamento\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nome del file\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Cartella\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Dimensione (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Tempo\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Campionamento\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Tipo\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modificato\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Titolo\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genere\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Anno\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Numero traccia\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Commento\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Somiglianza %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Parole usate\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Conteggio duplicati\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensioni\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Dimensione (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Data EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Dimensione\"\n"
  },
  {
    "path": "locale/it/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2021\n# Emanuele, 2021\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Italian (https://app.transifex.com/voltaicideas/teams/116153/it/)\\n\"\n\"Language: it\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Non ci sono duplicati marcati. Nessuna operazione è stata completata.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"\"\n\"Non ci sono duplicati selezionati. Nessuna operazione è stata completata.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Stai per aprire molti file contemporaneamente. A seconda di quale programma \"\n\"li aprirà, potrebbe crearsi un bel casino. Vuoi continuare?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Scansione per i duplicati\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Caricamento\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Spostamento\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Copia in corso\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Spostamento nel cestino\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Un'azione precedente è ancora in corso. Non puoi cominciarne una nuova. \"\n\"Aspetta qualche secondo e quindi riprova.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Non sono stati trovati dei duplicati.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Tutti i file marcati sono stati copiati correttamente.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Tutti i file marcati sono stati spostati correttamente.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"Tutti i file marcati sono stati cancellati correttamente.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Tutti i file marcati sono stati spostati nel cestino.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Impossibile caricare il file: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' è già nella lista.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' non esiste.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Tutti i %d elementi che coincidono verranno ignorati in tutte le scansioni \"\n\"successive. Continuare?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Seleziona una directory in cui desideri copiare i file contrassegnati\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\"Seleziona una directory in cui desideri spostare i file contrassegnati\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Seleziona una destinazione per il file CSV\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Impossibile modificare il file: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Non hai impostato nessun comando personalizzato. Impostalo nelle tue \"\n\"preferenze.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Stai per rimuovere %d file dai risultati. Continuare?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} gruppi duplicati sono stati cambiati dalla nuova priorirità\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Le cartelle selezionate non contengono file da scansionare.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Raccolta file da scansionare\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d scartati)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"Raccolti {} file da scansionare\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"Raccolte {} cartelle da scansionare\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"%d corrispondenze trovate da %d gruppi\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Stai spostando {} file al Cestino.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Espressioni regolari\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"Vuoi veramente rimuovere tutti i %d elementi dalla lista dei file da \"\n\"ignorare?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nome del file\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nome file - Campi\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nome file - Campi (Nessun Ordine)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tag\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Contenuti\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analizzate %d/%d immagini\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Effettuate %d/%d comparazioni sui sottogruppi di immagini\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Preparazione per la comparazione\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Verificate %d/%d somiglianze\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Leggi dati EXIF da %d/%d immagini\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Timestamp EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Nessuno\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Termina con un numero\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Non termina con un numero\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Più lungo\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Più corto\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Il più alto\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Il più basso\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Il più nuovo\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Il più vecchio\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplicati marcati.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" filtro: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Lettura metadata di %d/%d files\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Quasi finito! Sto organizzando i risultati...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Cartelle\"\n"
  },
  {
    "path": "locale/it/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Emanuele, 2022\n# Fuan <jcfrt@posteo.net>, 2022\n# Giovanni Donisi, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Italian (https://app.transifex.com/voltaicideas/teams/116153/it/)\\n\"\n\"Language: it\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Esci\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Opzioni\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Lista elementi ignorati\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Cancella la cache delle immagini\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Aiuto di dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Informazioni su dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Apri registro eventi\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"Vuoi veramente rimuovere tutte le analisi delle immagini memorizzate nella \"\n\"cache?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"La cache delle immagini è stata cancellata.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} file (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Opzioni di eliminazione\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Collega file eliminati\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Dopo aver selezionato un duplicato, per sostituire il file eliminato \"\n\"posiziona un collegamento che ha come destinazione i file di referenza.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (non supportato)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Elimina file direttamente\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Invece di spostare file nel cestino, eliminali direttamente. Questa opzione \"\n\"di solito è usata come alternativa al sistema di eliminazione standard \"\n\"quando non funziona.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Procedi\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Annulla\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Attributo\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Selezionato\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Riferimento\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Caricamento risultati...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Finestra dei risultati\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Aggiungi Cartella...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"File\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Visualizzazione\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Aiuto\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Carica i risultati recenti\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Modalità applicazione:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Musica\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Foto\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Tipo di scansione:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Più Opzioni\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Seleziona le cartelle da scansionare e premi \\\"Scansiona\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Carica i risultati\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Scansiona\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Risultati non salvati\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Hai dei risultati non salvati. Vuoi veramente chiudere?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"\"\n\"Seleziona una cartella da aggiungere alla lista delle cartelle da \"\n\"scansionare\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Seleziona un risultato (file) da caricare\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Tutti i file (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Risultati dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Inizia una nuova scansione\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Hai dei risultati non salvati. Vuoi veramente continuare?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nome\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Stato\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Escluso\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normale\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Rimuovi Selezionati\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Deseleziona\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Chiudi\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Dettagli\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Etichette da scansionare:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Traccia\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Titolo\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genere\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Anno\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"'Peso' della parola\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Fai coincidere parole simili\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Includi tipi diversi di file\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Usa le espressioni regolari per filtrare\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Rimuovi le cartelle vuote dopo aver cancellato o spostato\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Non considerare gli hardlink come duplicati\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Modalità 'Debug'(è richiesta la riapertura del programma)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Includi immagini di dimensione differente\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Durezza del filtro:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Più risultati\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Meno risultati\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Dimensione carattere:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Lingua:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Copia e sposta:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Tutti gli elementi in una cartella\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Ricrea il percorso relativo\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Ricrea il percorso assoluto\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Comando personalizzato (argomenti: %d per duplicare, %r per riferimento):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"Per cambiare lingua, dupeGuru deve riavviarsi.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Cambia la priorità dei duplicati\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Aggiungi dei criteri di selezione nel riquadro a destra e clicca su 'OK' per\"\n\" inviare i duplicati che meglio corrispondono a questi criteri al loro \"\n\"gruppo di appartenenza. Per maggiori informazioni leggere il file di 'help'.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problemi!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Sono stati riscontrati dei problemi elaborando alcuni (o tutti) i file. La \"\n\"causa di questi problemi è descritta nella tabella sottostante. Questi file \"\n\"non stati rimossi dai vostri risultati.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Mostra i selezionati\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Azioni\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Visualizza solo i duplicati\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Visualizza le differenze\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Sposta elementi marcati nel Cestino...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Sposta elementi marcati nel...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Copia elementi evidenziati nel...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Rimuovi gli elementi marcati dai risultati\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Cambia la priorità dei risultati...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Rimuovi gli elementi selezionati dai risultati\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Aggiungi gli elementi selezionati alla lista da ignorare\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Rendi selezionato un Riferimento\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Apri gli elementi selezionati con l'applicazione predefinita\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Apri cartella degli elementi selezionati\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Rinomina gli elementi selezionati\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Marca tutti\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Deseleziona tutti\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Inverti la selezione\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Marca i selezionati\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Esporta in HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Esporta in CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Salva i risultati...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Invoca comando personalizzato\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Marca\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Colonne\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Ripristina impostazioni predefinite\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Resultati\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Solo duplicati\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Valori Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Seleziona un file dove salvare i tuoi risultati\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignora file più piccoli di\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Resultati\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Azione\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Aggiungi una nuova cartella...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Avanzato\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Controlla gli aggiornamenti automaticamente\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Base\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Porta tutto in primo piano\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Controlla gli aggiornamenti...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Chiudi la finestra\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Copia\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Comando personalizzato (argumenti: %d per duplicare, %r per riferimento):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Taglia\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Dettagli del file selezionato\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Scheda dettagliata\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Cartelle\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Preferenze di dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Risultati di dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Sito Web di dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Modifica\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Esporta risultati in CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Esporta i risultati in formato XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Meno risultati\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtro\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Durezza del filtro:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filtra Risultati...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Finestra di selezione della cartella\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Dimensione Carattere:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Nascondi dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Nascondi gli altri\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignora file più piccoli di:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Carica dal file...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimizza\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Modo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Più risultati\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Incolla\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Preferenze...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Sguardo rapido\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Chiudi dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Ripristina le impostazioni predefinite\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Imposta prefefinite\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Rivelare\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Mostra gli elementi selezionati nel Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Seleziona tutto\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Sposta gli elementi marcati nel cestino...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Servizi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Mostra tutto\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Inizia la scansione dei duplicati\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Il nome '%@' è già esistente.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Finestra\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Filtri di esclusione\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Risultati della scansione\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Carica cartelle...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Salva cartelle...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Seleziona un file delle cartelle da caricare\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"cartelle di dupeGuru (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Seleziona un file in cui salvare le cartelle\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"cartelle di dupeGuru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Addizionare\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Ripristina i valori predefiniti\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Stringa di prova\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Digita qui un'espressione regolare Python...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Digitare un percorso del file system o un nome file qui...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>I direttori avranno anche il loro <strong>stato predefinito</strong> impostato su Escluso nella scheda Directory se il loro nome corrisponde a una delle espressioni regolari selezionate.<br>Per ogni file raccolto vengono eseguiti due test per determinare se ignorarlo o meno completamente:<br><li>1. Le espressioni regolari prive di separatori di percorso verranno confrontate solo con il nome del file.</li>\\n\"\n\"<li>2. Le espressioni regolari contenenti almeno un separatore di percorso verranno confrontate con il percorso completo del file.</li><br>\\n\"\n\"Esempio: se desideri filtrare i file .PNG solo dalla directory \\\"Mie Immagini\\\":<br><code>.*Mie\\\\sImmagini\\\\\\\\.*\\\\.png</code><br><br>Puoi testare l'espressione regolare con il pulsante \\\"stringa di prova\\\" dopo aver incollato un percorso falso nel campo di prova:<br><code>C:\\\\\\\\Utente\\\\Mie Immagini\\\\test.png</code><br><br>\\n\"\n\"Verranno evidenziate le espressioni regolari corrispondenti.<br>Se è presente almeno un'evidenziazione, il percorso o il nome del file testato verrà ignorato durante le scansioni.<br><br>Directory e file che iniziano con un punto \\\".\\\" vengono filtrati per impostazione predefinita.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Errore di compilazione:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Aumenta lo zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Diminuisci lo zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Dimensione normale\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Il più adatto\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Modalità cache immagini:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"\"\n\"Ignora le icone del tema nella barra degli strumenti del visualizzatore\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Usa le nostre icone interne invece di quelle fornite dal motore del tema\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Mostra le barre di scorrimento nei visualizzatori di immagini\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Quando l'immagine visualizzata non si adatta alla visualizzazione, mostra le\"\n\" barre di scorrimento per estendere la visualizzazione\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Usa la posizione predefinita per la barra di tabulazione (richiede il \"\n\"riavvio)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Posiziona la barra di tabulazione sotto il menu principale anziché accanto ad esso\\n\"\n\"Su MacOS, la barra delle schede riempirà invece la larghezza della finestra.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Usa caratteri in grassetto per i riferimenti\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Colore di primo piano per i riferimenti:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Colore di sfondo per i riferimenti:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"colore di primo piano per i delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Mostra la barra del titolo e può essere agganciata\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Mentre la barra del titolo è nascosta, usa il tasto modificatore per \"\n\"trascinare la finestra mobile\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"La barra del titolo può essere disabilitata solo mentre la finestra è \"\n\"agganciata\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Barra del titolo verticale\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Cambia la barra del titolo da orizzontale in alto a verticale sul lato \"\n\"sinistro\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Mostra la barra di tabulazione\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>I direttori avranno anche il loro <strong>stato predefinito</strong> impostato su Escluso nella scheda Directory se il loro nome corrisponde a una delle espressioni regolari selezionate.<br>Per ogni file raccolto vengono eseguiti due test per determinare se ignorarlo o meno completamente:<br><li>1. Le espressioni regolari prive di separatori di percorso verranno confrontate solo con il nome del file.</li>\\n\"\n\"<li>2. Le espressioni regolari contenenti almeno un separatore di percorso verranno confrontate con il percorso completo del file.</li><br>\\n\"\n\"Esempio: se desideri filtrare i file .PNG solo dalla directory \\\"Mie Immagini\\\":<br><code>.*Mie\\\\sImmagini\\\\\\\\.*\\\\.png</code><br><br>Puoi testare l'espressione regolare con il pulsante \\\"stringa di prova\\\" dopo aver incollato un percorso falso nel campo di prova:<br><code>C:\\\\\\\\Utente\\\\Mie Immagini\\\\test.png</code><br><br>\\n\"\n\"Verranno evidenziate le espressioni regolari corrispondenti.<br>Se è presente almeno un'evidenziazione, il percorso o il nome del file testato verrà ignorato durante le scansioni.<br><br>Directory e file che iniziano con un punto \\\".\\\" vengono filtrati per impostazione predefinita.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Risultati\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Interfaccia generale\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tabella dei risultati\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Finestra Dettagli\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Generale\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Schermo\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Calcola hash parziale di file più grandi di\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"Usa le finestre di dialogo native del Sistema Operativo\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"Per azioni come selezione di file/cartelle usa le finestre di dialogo native del Sistema Operativo.\\n\"\n\"Alcune finestre di dialogo native hanno funzionalità limitate.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Ignora file più grandi di\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Svuota cache\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"Vuoi davvero svuotare la cache? Ciò rimuoverà tutti gli hash dei file \"\n\"memorizzati nella cache e le analisi delle immagini.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Cache svuotata\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Usa stile scuro\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Profila l'operazione di scansione\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\"Profila l'operazione di scansione e salva i registri per l'ottimizzazione.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"I log si trovano in: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Debug\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"A proposito di {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Versione {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Controllo degli aggiornamenti...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Distribuito sotto licenza GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"Nessun aggiornamento disponibile.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"È disponibile la nuova versione {}, scaricabile <a href=\\\"{}\\\">qui</a>.\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Rapporto di errore\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Qualcosa è andato storto. Che ne dici di segnalare l'errore?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"Ciò che di solito aiuta davvero è aggiungere una descrizione di come hai ottenuto l'errore. Grazie!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Apri in GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Ceco\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Tedesco\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Greco\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Inglese\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Spagnolo\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Francese\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armeno\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italiano\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Giapponese\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Coreano\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malese\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Olandese\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polacco\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brasiliano\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Russo\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turco\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ucraino\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamita\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Cinese (semplificato)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Cancellare l'elenco\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Ricerca...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ja/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Japanese (https://www.transifex.com/voltaicideas/teams/116153/ja/)\\n\"\n\"Language: ja\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"ファイルパス\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"エラーメッセージ\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"デュレーション\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"ビットレート\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"サンプルレート\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"ファイル名\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"フォルダ\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"サイズ（MB）\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"デュレーション\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"サンプルレート\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"種類\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"変更\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"タイトル\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"アーティスト\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"アルバム\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"ジャンル\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"トラック番号\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"コメント\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"一致率\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"使用した単語\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"重複カウント\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"寸法\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"サイズ（KB）\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIFタイムスタンプ\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"サイズ\"\n"
  },
  {
    "path": "locale/ja/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Yuji Sasaki, 2022\n# Fuan <jcfrt@posteo.net>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2022\\n\"\n\"Language-Team: Japanese (https://app.transifex.com/voltaicideas/teams/116153/ja/)\\n\"\n\"Language: ja\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"チェックを入れた重複はありません。 何も行われませんでした。\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"選択された重複はありません。 何も行われていません。\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"一度に多くのファイルを開こうとしています。 これらのファイルを開く対象によっては、これを行うとかなり混乱する可能性があります。 継続する？\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"重複のスキャン\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"読み込み中\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"移動します\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"コピー中\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"ごみ箱に送信します\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"前のアクションはまだそこにぶら下がっています。 まだ新しいものを始めることはできません。 数秒待ってから、再試行してください。\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"重複は見つかりませんでした。\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"チェックを入れたファイルをすべてコピーしました。\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"チェックを入れたファイルをすべて移動しました。\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"チェックを入れたファイルをすべて削除しました。\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"チェックを入れたファイルをすべてごみ箱に移動しました。\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"ファイルを読み込めませんでした：{}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"「{}」既にリストに含まれています。\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' 存在しません。\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"選択した%d個の一致は、以降のすべてのスキャンで無視されます。 継続する？\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"マークされたファイルをコピーするディレクトリを選択してください\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"マークされたファイルを移動するディレクトリを選択してください\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"エクスポートしたCSVの宛先を選択します。\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"ファイルに書き込めませんでした：{}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"カスタムコマンドは設定されていません。 お好みで設定してください。\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"結果から%d個のファイルを削除しようとしています。 継続する？\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{}重複するグループは、再優先順位付けによって変更されました。\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"選択したディレクトリにはスキャン可能なファイルが含まれていません。\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"スキャンするファイルを収集しています\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d 廃棄)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"{}個のファイルをゴミ箱に送信しています\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"正規表現\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"本当に除外リストから%d個の項目を削除しますか？\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"ファイル名\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"ファイル名 - フィールド\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"ファイル名 - フィールド（順序なし）\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"タグ\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"内容\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"%d/%d 枚の写真を分析しました\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"チャンクマッチを%d/%d回実行しました\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"マッチングの準備\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d/%d件の一致を確認\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"%d/%d枚の写真のEXIFを読みました\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIFタイムスタンプ\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"無し\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"番号で終わっている\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"数字で終わっていない\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"最長\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"最短\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"最高\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"最低\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"最新\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"最古\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s)マークされた重複。\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"フィルタ: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"%d/%dファイルのメタデータを読み取った\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"ほぼ完了しました！ 結果をいじっています...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"フォルダー\"\n"
  },
  {
    "path": "locale/ja/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Japanese (https://app.transifex.com/voltaicideas/teams/116153/ja/)\\n\"\n\"Language: ja\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"終了\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"オプション\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"無視リスト\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"画像キャッシュをクリア\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuruヘルプ\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"dupeGuruついて\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"デバッグログを開く\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"キャッシュされた画像分析をすべて削除しますか？\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"画像キャッシュがクリアされました。\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} ファイル (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"削除オプション\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"削除されたファイルをリンクする\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"重複を削除した後、参照ファイルをターゲットとするリンクを配置して、削除されたファイルを置き換えます。\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"ハードリンク\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"シンボリックリンク\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(非対応)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"ファイルを完全に削除\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"ファイルを直接削除するファイルをゴミ箱に送る代わりに、直接削除します。 このオプションは通常、通常の削除方法が機能しない場合の回避策として使用されます。\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"続行\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"キャンセル\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"属性\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"選択した\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"参照\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"結果をロード...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"結果ウィンドウ\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"フォルダーを追加\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"ファイル\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"ビュー\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"ヘルプ\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"最近の結果を読み込む\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"アプリケーションモード：\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"音楽\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"画像\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"標準\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"スキャンの種類：\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"詳細設定\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"スキャンするフォルダを選択し、「スキャン」を押してください。\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"結果を読み込む\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"スキャン\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"未保存の結果\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"保存されていない結果がありますが、本当に終了しますか？\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"スキャンリストに追加するフォルダを選択してください\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"ロードする結果ファイルを選択してください\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"すべてのファイル (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuruの結果 (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"新しいスキャンを開始\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"保存されていない結果がありますが、本当に続行しますか？\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"名\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"状態\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"除外\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"正常\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"選択を削除\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"クリアー\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"閉じる\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"詳細\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"スキャンするタグ:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"トラック\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"アーティスト\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"アルバム\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"タイトル\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"ジャンル\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"単語の重み付け\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"類似の単語を一致\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"ファイルの種類を混在\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"フィルタに正規表現を使用\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"削除や移動で空になったフォルダを削除\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"同じファイルへの重複ハードリンクを無視\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"デバッグモード（再起動が必要）\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"異なるサイズの写真を一致\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"フィルタの強さ:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"より多くの結果\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"より少ない結果\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"文字サイズ:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"言語:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"コピーと移動:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"目的地で直接\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"相対パスを再作成\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"絶対パスを再作成\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"カスタムコマンド (引数: ％dは重複・％rは参照）:\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"言語の変更を有効にするには、dupeGuruを再起動する必要があります。\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"重複を再優先する\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"右側のボックスに基準を追加し、[OK]をクリックして、これらの基準に最もよく対応する複製をそれぞれのグループの参照位置に送信します。 \"\n\"詳細については、ヘルプファイルをお読みください。\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"問題！\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"一部（またはすべて）のファイルの処理に問題がありました。 これらの問題の原因を次の表に示します。 これらのファイルは結果から削除されませんでした。\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"選択したものを明らかにする\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"作用\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"複製のみを表示\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"デルタ値を表示\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"マークされたものをごみ箱に送る...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"マークされたものを移動...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"マークされたものをコピー...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"結果からマークされたものを削除\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"結果を再優先する...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"結果から選択したアイテムを削除\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"選択したアイテムを無視リストに追加\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"選択したアイテムを参照にする\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"デフォルトのアプリケーションで選択を開く\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"選択したアイテムのコンテナフォルダを開く\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"選択したアイテムの名前を変更する\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"すべてのアイテムをマークする\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"何もマークしない\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"マーキングを反転\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"選択したアイテムにマークを付け\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"HTMLにエクスポート\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"CSVにエクスポート\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"結果を保存する\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"カスタムコマンドを呼び出す\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"マーク\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"コラム\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"既定値にリセット\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{}結果\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"複製のみ\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"デルタ値\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"結果を保存するファイルを選択してください\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"より小さいファイルは無視\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ 結果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"作用\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"新しいフォルダを追加...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"高度\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"更新を自動的にチェック\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"基本\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"すべてを前面に出す\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"更新を確認...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"ウィンドウを閉じる\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"コピー\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"カスタムコマンド (引数：重複の場合は％d、参照の場合は％r）:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"カット\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"デルタ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"選択したファイルの詳細\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"詳細パネル\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"ディレクトリ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuruの設定\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuruの結果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuruウェブサイト\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"編集\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"結果をCSVにエクスポート\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"結果をXHTMLにエクスポート\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"より少ない結果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"フィルタ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"フィルター硬度:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"結果のフィルタリング\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"フォルダ選択ウィンドウ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"フォントサイズ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"dupeGuruを隠す\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"他を隠す\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"以下よりも小さいファイルは無視:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"ファイルからロード...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"最小化する\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"モード\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"より多くの結果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"ペースト\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"環境設定\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"クイックルック\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"dupeGuruを終了\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"デフォルトにリセット\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"既定値にリセット\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"表す\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Finderで選択したものを表示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"すべて選択\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"マークされたアイテムをゴミ箱に送る...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"サービス\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"すべて表示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"重複スキャンを開始\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"'％@'名はすでに存在します。\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"ウィンドウ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"拡大\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"除外フィルター\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"スキャン結果\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"ディレクトリをロード...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"ディレクトリを保存...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"ロードするディレクトリファイルを選択してください\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuruのディレクトリファイル (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"ディレクトリを保存するファイルを選択してください。\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuruのディレクトリファイル (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"追加\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"デフォルトに戻す\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"テスト文字列\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"ここではPythonの正規表現を入力して...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"ここにファイルシステムのパスまたはファイル名を入力してください...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"これらの（大文字と小文字を区別する）Python正規表現は、スキャン中にファイルを除外します。 <br>ディレクターの名前が正規表現の1つと一致する場合、ディレクトリの<strong>デフォルト状態</strong>は[ディレクトリ]タブで[除外]に設定されます。 <br>収集されたファイルごとに、2つのテストがそれぞれに対して実行され、それらをフィルターで除外するかどうかが決定されます:<br><li>1.パス区切り文字が含まれていない正規表現は、ファイル名のみと比較されます。</li>\\n\"\n\"<li>2.パス区切り文字が含まれていない正規表現は、ファイルへのフルパスと比較されます。</li>\\n\"\n\"例：「My Pictures」ディレクトリからのみ.PNGファイルを除外する場合：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>偽のパスを貼り付けることで、テスト文字列機能を使用して正規表現をテストできます:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"一致する正規表現が強調表示されます。<br>ハイライトが少なくとも1つある場合、テストされたパスはスキャン中に無視されます。<br><br>ピリオド「。」で始まるディレクトリとファイル デフォルトでは除外されます。<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"コンパイルエラー:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"ズーム増やす\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"ズームを小さく\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"通常サイズ\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"最適\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"画像キャッシュモード：\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"ビューアツールバーのテーマアイコンを上書きする\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"テーマエンジンによって提供されるアイコンの代わりに、独自の内部アイコンを使用します。\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"画像ビューアにスクロールバーを表示する\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"表示された画像がビューポートに適合しない場合に、ビュー全体にスクロールバーを表示する。\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"タブバーのデフォルトの位置を使用する（再起動が必要）\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"タブバーをメインメニューの横ではなく下に配置する。\\n\"\n\"MacOSでは、代わりにタブバーがウィンドウの幅を埋める。\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"参照のために太字を使用する\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"参照の前景色：\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"参照の背景色：\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"デルタ値の背景色：\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"タイトルバーを表示し、ドッキングできます。\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"タイトルバーが非表示になっているときに、修飾キーを使用してフローティングウィンドウをドラッグできます。\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"タイトルバーは、ウィンドウがドッキングされている間のみ無効にできます。\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"垂直タイトルバー\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"タイトルバーを上部の水平から左側の垂直に変更する\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"タブバーを表示\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"これらの（大文字と小文字を区別する）Python正規表現は、スキャン中にファイルを除外します。 <br>また、ディレクトリの名前が選択した正規表現の1つと一致する場合、ディレクトリの<strong>デフォルト状態</strong>は[ディレクトリ]タブで「除外」に設定されます。<br>収集された各ファイルのために、二つの試験は完全にそれを無視するか否かを決定するために実行される：<br><li>1.パス区切り文字が含まれていない正規表現は、ファイル名のみと比較されます。</li>\\n\"\n\"<li>2.少なくとも1つのパス区切り文字を含む正規表現は、ファイルへのフルパスと比較されます。</li>\\n\"\n\"<br>例：「My Pictures」ディレクトリからのみ.PNGファイルを除外する場合：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>テストフィールドに偽のパスを貼り付けた後、[テスト文字列]ボタンを使用して正規表現をテストできます:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"一致する正規表現が強調表示されます。<br>ハイライトが少なくとも1つある場合、テストされたパスまたはファイル名はスキャン中に無視されます。<br><br>ピリオド「.」で始まるディレクトリとファイル デフォルトでは除外されます。<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"結果\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"一般\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"結果\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"詳細画面\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"一般\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"表示\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"{}について\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"バージョン {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"GPLv3のもとでライセンスされています\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"エラーレポート\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"不明な理由により失敗しました。問題を報告しませんか?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"エラーレポートはGitHubの問題として報告する必要があります。 上記のエラートレースバックをコピーして、新しい問題に貼り付けることができます。\\n\"\n\"\\n\"\n\"事前に既存の問題を検索してください。 また、発生しているバグにはすでにパッチが適用されている可能性があるため、リポジトリから入手できる最新バージョンをテストしてください。\\n\"\n\"\\n\"\n\"通常本当に役立つのは、エラーが発生した方法の説明を追加することです。 ありがとう！\\n\"\n\"\\n\"\n\"このエラーの後もアプリケーションは実行を継続するはずですが、不安定な状態になっている可能性があるため、アプリケーションを再起動することをお勧めします。\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"GitHubに移動\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"チェコ語\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"ドイツ語\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"ギリシャ語\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"英語\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"スペイン語\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"フランス語\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"アルメニア語\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"イタリア語\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"日本語\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"韓国語\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"マレー語\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"オランダ語\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"ポーランド語\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"ブラジル語\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"ロシア語\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"トルコ語\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"ウクライナ語\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"ベトナム語\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"中国語（簡体字）\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"リストをクリア\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"探索...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ko/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Sangdon Lim, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Korean (https://app.transifex.com/voltaicideas/teams/116153/ko/)\\n\"\n\"Language: ko\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"파일 경로\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"오류 메시지\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"길이\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"비트전송률\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"샘플전송률\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:94\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"파일 이름\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"폴더\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"크기 (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"시간\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"샘플전송률\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"종류\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:165 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"수정날짜\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"곡명\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"아티스트\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"앨범\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"장르\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"년도\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"트랙 번호\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"주석\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"일치율%\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"단어 목록\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"중복파일 갯수\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"가로세로 크기\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"크기 (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 타임스탬프\"\n\n#: core\\prioritize.py:158\nmsgid \"Size\"\nmsgstr \"크기\"\n"
  },
  {
    "path": "locale/ko/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2021\n# Sangdon Lim, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Korean (https://app.transifex.com/voltaicideas/teams/116153/ko/)\\n\"\n\"Language: ko\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"아무 파일도 마크되지 않아 작업을 수행하지 않았습니다.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"아무 파일도 선택되지 않아 작업을 수행하지 않았습니다.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"한 번에 많은 파일을 열려고 합니다. 시스템 설정에 따라 너무 많은 프로그램이 실행될 수도 있습니다. 진행할까요?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"중복 파일 검색 중\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"불러오는 중\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"이동중\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"복사중\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"휴지통으로 보내기\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"이전 작업이 아직 진행 중이어서 새 작업을 시작할 수 없습니다. 몇 초 후에 다시 시도해 보세요.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"중복 파일이 없습니다.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"마크된 모든 파일이 성공적으로 복사되었습니다.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"마크된 모든 파일이 성공적으로 이동되었습니다.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"마크된 모든 파일이 성공적으로 삭제되었습니다.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"마크된 모든 파일을 휴지통으로 보냈습니다.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"파일을 불러올 수 없습니다: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' 는 이미 목록에 있습니다.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' 가 존재하지 않습니다.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"선택한 %d개 항목을 검색에서 무시합니다. 진행하시겠습니까?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"마크하신 파일을 복사할 경로를 선택하세요:\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"마크하신 파일을 이동할 경로를 선택하세요:\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"CSV 파일의 저장 경로를 지정해주세요\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"파일에 쓸 수 없습니다 : {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"사용자 지정 명령을 설정하지 않았습니다. 기본 설정에서 설정하십시오.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"결과에서 %d 개의 파일을 제거하려고합니다. 실행하시겠습니까?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} 개의 중복 그룹이 우선 순위 재 지정으로 변경되었습니다.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"선택한 경로에 스캔 가능한 파일이 없습니다.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"스캔 가능 파일 수집중\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d 폐기)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"파일 목록 생성 중: {}개 파일\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"폴더 목록 생성 중: {}개 폴더\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"중복 파일 %d개 확인됨: %d개 그룹\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"{}개 파일을 휴지통으로 보내려고 합니다.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"정규식\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"무시 목록에서 항목 %d개를 정말로 제거 하시겠습니까?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"파일 이름\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"파일 이름 - 필드\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"파일 이름 - 필드 (순서 없음)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"태그\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"내용\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"사진 %d/%d 개 분석됨\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d/%d 청크 매치 수행\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"매칭 준비\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d/%d 일치 확인\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"사진 EXIF 읽는 중: %d/%d\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 타임 스탬프\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"없음\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"숫자로 끝남\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"숫자로 끝나지 않음\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"최장\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"최단\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"최고\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"최저\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"최신\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"가장 오래된\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) 개의 중복 파일을 마크했습니다.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"필터: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"파일 메타데이터 읽는 중: %d/%d\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"거의 완료되었습니다! 결과를 취합하고 있습니다.\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"폴더\"\n"
  },
  {
    "path": "locale/ko/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Sangdon Lim, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Korean (https://app.transifex.com/voltaicideas/teams/116153/ko/)\\n\"\n\"Language: ko\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"나가기\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"설정\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"무시 목록\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"사진 캐시 지우기\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru 도움말\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"dupeGuru에 대하여\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"디버그 로그 열기\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"캐시 된 모든 사진 분석을 정말로 제거 하시겠습니까?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"사진 캐시가 삭제되었습니다.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} 파일 (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"삭제 옵션\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"링크 생성\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"중복 파일들을 삭제한 후 원본 파일을 참조하는 링크로 대체합니다.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"하드링크\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"심볼릭 링크\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(미지원)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"파일 직접삭제\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"파일을 휴지통으로 보내지 않고 바로 삭제합니다. 파일을 휴지통으로 보낼 수 없는 경우 등에 사용할 수 있습니다.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"실행\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"취소\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"속성\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"선택한 파일\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"기준 파일\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"결과 불러오기...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"결과 창\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"폴더 추가하기...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"파일\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"보기\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"도움말\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"최근 결과 불러오기\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"애플리케이션 모드 :\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"음악\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"그림\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"표준\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"스캔 방식:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"설정\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"스캔할 폴더들을 목록에 추가하고 오른쪽 아래의 \\\"스캔\\\" 버튼을 누르십시오.\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"결과 불러오기\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"스캔\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"저장되지 않은 결과\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"결과를 저장하지않고 종료하시겠습니까?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"스캔 목록에 추가 할 폴더를 선택하십시오\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"불러올 결과 파일을 선택하십시오\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"모든 파일 (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru 결과 (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"새 스캔 시작\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"결과를 저장하지않고 진행하시겠습니까?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"이름\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"상태\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"제외\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"일반\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"선택에서 제거\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"삭제\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"닫기\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"파일 속성\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"스캔 태그 :\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"트랙\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"아티스트\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"앨범\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"제목\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"장르\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"년\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"파일 이름 중 긴 단어에 가중치 적용\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"파일 이름 중 비슷한 단어도 중복으로 허용\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"확장자가 다른 파일도 중복 여부 확인\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"필터링 할 때 정규식 사용\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"삭제 또는 이동 후 폴더가 빈 폴더가 되면 폴더 삭제\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"동일한 파일에 대한 중복 하드링크 무시\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"디버그 모드 (다시 시작 필요)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"가로세로 크기가 다른 이미지도 중복 여부 확인\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"필터 민감도:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"결과 더보기\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"결과 줄이기\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"글꼴 크기:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"언어:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"복사 및 이동 :\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"대상 폴더에\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"대상 폴더를 시작으로 상대 경로를 재생성\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"대상 폴더를 시작으로 절대 경로를 재생성\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"사용자 지정 명령 (인수: %d 은 중복, %r 은 참조):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"언어 변경은 dupeGuru의 재시작이 필요합니다.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"중복 우선 순위 재 지정\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"오른쪽 박스에 기준을 추가하고 확인을 눌러 가장 기준에 해당되는 복제를 해당 그룹의 참조 위치로 보냅니다. 상세한 정보는 도움말에 \"\n\"있습니다\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"문제 발생!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"일부 (또는 전체) 파일을 처리하는 데 문제가 있었습니다. 문제의 원인은 아래 표에 표시 되있습니다. 문제되는 파일들은 결과에서 제거되지\"\n\" 않았습니다.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"선택 항목 표시\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"작업\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"중복파일만 보기\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"델타 값 표시\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"마크된 모든 파일을 휴지통으로 보내기\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"마크된 모든 파일을 이동...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"마크된 모든 파일을 복사...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"결과 목록에서 마크된 모든 파일을 제외\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"기준 파일 규칙 재설정\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"결과 목록에서 선택한 파일을 제외\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"무시 목록에 선택 항목 추가\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"선택한 파일을 기준 파일로 설정\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"선택한 파일을 기본 앱으로 열기\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"선택한 파일의 폴더 열기\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"선택한 파일의 이름 변경\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"모든 파일 마크\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"모든 파일의 마크 해제\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"마크 목록 반전\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"선택한 파일 마크\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"HTML로 내보내기\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"CSV로 내보내기\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"결과 저장 ...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"사용자 지정 명령 호출\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"마크\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"열\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"기본값으로 재설정\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} 결과\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"기준 파일 숨기기\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"델타 값\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"결과를 저장할 파일을 선택하십시오.\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"다음보다 작은 파일 무시\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ 결과\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"동작\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"새 폴더 추가...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"고급\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"자동으로 업데이트 확인\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"기본\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"모두 앞으로 가져 오기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"업데이트를 확인...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"창 닫기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"복사\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"사용자 지정 명령 (인수: %d 은 중복, %r 은 참조):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"컷\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"델타\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"선택 파일 세부 정보\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"세부 정보 패널\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"경로\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru 기본 설정\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru 결과\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru 홈페이지\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"편집\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"CSV로 결과 내보내기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"XHTML로 결과 내보내기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"결과 축소\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"필터\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"필터 경도 :\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filter Results...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"폴더 선택 창\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"글꼴 크기 :\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"dupeGuru 숨기기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"다른 숨기기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"이보다 작은 파일 무시 :\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"파일에서 불러오기...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"최소화\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"모드\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"결과 더보기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"확인\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"붙여넣기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"환경 설정...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"빠른보기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"dupeGuru 종료\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"기본값으로 재설정\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"기본값으로 재설정\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"공개\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Finder에서 선택한 항목 표시\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"모두 선택\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"마크된 파일을 휴지통으로 보내기\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"서비스\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"모두 표시\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"중복 스캔 시작\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"'%@' 의 이름이 이미 존재합니다.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"창\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"확대\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"제외 필터\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"스캔 결과\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"경로 불러오는중 ...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"경로 저장중 ...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"불러올 경로를 선택하십시오\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeguru 경로 파일 (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"경로를 저장할 파일을 선택하십시오\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeguru 경로 파일 (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"추가\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"기본값으로 복원\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"테스트 문자열\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"여기에 파이썬 정규식을 입력해주세요 ...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"여기에 파일 시스템 경로 또는 파일 이름을 입력해주세요 ...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"이러한 (대소 문자 구분) 파이썬 정규식은 스캔 중에 파일을 필터링합니다.<br>또한 디렉토리 이름이 선택한 정규식 중 하나와 일치하는 경우 디렉토리 탭에서 <strong>기본 상태</strong>가 제외됨으로 설정됩니다.<br>수집 된 각 파일에 대해 완전히 무시할지 여부를 결정하기 위해 두 가지 테스트가 수행됩니다.<br><li>1. 경로 구분자가없는 정규식은 파일 이름과 만 비교됩니다.</li>\\n\"\n\"<li>2. 경로 구분자가 하나 이상 포함 된 정규식은 파일의 전체 경로와 비교됩니다.</li><br>\\n\"\n\"예 : \\\"내 그림\\\"디렉토리에서만 .PNG 파일을 필터링하려는 경우 :<br><code>.*내\\\\s그림\\\\\\\\.*\\\\.png</code><br><br>테스트 필드에 가짜 경로를 붙여 넣은 후 \\\"test string\\\"버튼으로 정규 표현식을 테스트 할 수 있습니다.<br><code>C:\\\\\\\\사용자\\\\내 그림\\\\test.png</code><br><br>\\n\"\n\"일치하는 정규 표현식이 강조 표시됩니다.<br>강조 표시가 하나 이상있는 경우 검사하는 동안 테스트 된 경로 또는 파일 이름이 무시됩니다.<br><br>마침표 '.'로 시작하는 디렉토리 및 파일 기본적으로 필터링됩니다.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"컴파일 오류 :\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"확대 증가\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"확대 감소\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"보통 크기\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"최고 일치\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"사진 캐시 모드 :\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"뷰어 도구 모음에서 테마 아이콘 재정의\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"테마 엔진에서 제공하는 아이콘 대신 자체 내부 아이콘 사용\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"이미지 뷰어에 스크롤바 표시\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"표시된 이미지가 뷰포트에 맞지 않으면 스크롤바를 표시하여 뷰를 확장합니다.\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"탭 표시 줄에 기본 위치를 사용 (다시 시작해야 함)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"탭 바를 메인 메뉴 옆이 아닌 아래에 놓습니다.\\n\"\n\"MacOS에서는 탭 막대가 대신 창의 너비를 채 웁니다.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"기준 파일을 굵게 표시\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"기준 파일 이름의 색:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"기준 파일 이름의 배경색:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"델타의 전경색 :\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"제목 표시 줄을 표시하고 고정 할 수 있습니다.\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"제목 표시 줄이 숨겨져있는 경우 수정 자 키를 사용하여 부동 창을 주위로 드래그하십시오.\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"제목 표시 줄은 창이 고정되어있는 동안에 만 비활성화 할 수 있습니다.\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"세로 제목 표시 줄\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"제목 표시 줄을 상단 가로에서 왼쪽 세로로 변경\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"탭 표시 줄 표시\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"이러한 (대소 문자 구분) 파이썬 정규식은 스캔 중에 파일을 필터링합니다.<br>또한 디렉토리 이름이 선택한 정규식 중 하나와 일치하는 경우 디렉토리 탭에서 <strong>기본 상태</strong>가 제외됨으로 설정됩니다.<br>수집 된 각 파일에 대해 완전히 무시할지 여부를 결정하기 위해 두 가지 테스트가 수행됩니다.<br><li>1. 경로 구분자가없는 정규식은 파일 이름과 만 비교됩니다.</li>\\n\"\n\"<li>2. 경로 구분자가 하나 이상 포함 된 정규식은 파일의 전체 경로와 비교됩니다.</li><br>\\n\"\n\"예 : \\\"내 그림\\\"디렉토리에서만 .PNG 파일을 필터링하려는 경우 :<br><code>.*내\\\\s그림\\\\\\\\.*\\\\.png</code><br><br>테스트 필드에 가짜 경로를 붙여 넣은 후 \\\"test string\\\"버튼으로 정규 표현식을 테스트 할 수 있습니다.<br><code>C:\\\\\\\\사용자\\\\내 그림\\\\test.png</code><br><br>\\n\"\n\"일치하는 정규 표현식이 강조 표시됩니다.<br>강조 표시가 하나 이상있는 경우 검사하는 동안 테스트 된 경로 또는 파일 이름이 무시됩니다.<br><br>마침표 '.'로 시작하는 디렉토리 및 파일 기본적으로 필터링됩니다.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"결과\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"일반 인터페이스\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"검색 결과\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"세부 정보 창\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"일반\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"디스플레이\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"다음보다 큰 파일은 일부만 해시\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"OS 자체 인터페이스 사용\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"파일 및 폴더 선택에 OS 자체 인터페이스를 사용합니다.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"다음보다 큰 파일 무시\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"캐시 제거\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"캐시를 제거하시겠습니까? 캐시에는 파일 해시 및 이미지 분석 결과가 포함되어 있습니다.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"캐시를 제거했습니다.\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"다크 모드 사용\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"프로파일 스캔 작업\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"최적화를 위해 스캔 프로파일 작업과 로그를 저장합니다.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"로그 저장 경로: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"디버그\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"{} 에 대한정보\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"버전 {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"업데이트 확인 중...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"GPLv3 라이센스\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"새 업데이트가 없습니다.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"새 버전 {}이 있습니다. 다운로드: <a href=\\\"{}\\\">링크</a>\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"오류보고\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"예상치 못한 문제가 발생했습니다. 오류 보고를 추천드립니다.\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"오류 보고서는 GitHub 문제로보고해야합니다. 위의 오류 추적을 복사하여 새 문제에 붙여 넣을 수 있습니다.\\n\"\n\"\\n\"\n\"이미 존재하는 문제에 대해 사전에 검색을 실행하십시오. 또한 경험하고있는 버그가 이미 패치되었을 수 있으므로 저장소에서 사용 가능한 최신 버전을 테스트해야합니다.\\n\"\n\"\\n\"\n\"일반적으로 실제로 도움이되는 것은 오류가 발생한 방법에 대한 설명을 추가하는 것입니다. 감사!\\n\"\n\"\\n\"\n\"이 오류 후에도 응용 프로그램이 계속 실행되어야하지만 불안정한 상태 일 수 있으므로 응용 프로그램을 다시 시작하는 것이 좋습니다.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"GitHub로 이동\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"체코어\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"독일어\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"그리스어\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"영어\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"스페인어\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"프랑스어\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"아르메니아어\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"이탈리아어\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"일본어\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"한국어\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"말레이어\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"네덜란드어\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"폴란드어\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"브라질 언어\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"러시아어\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"터키어\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"우크라이나어\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"베트남어\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"중국어 (간체)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"목록 지우기\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"검색..\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ms/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\\n\"\n\"Language-Team: Malay (https://www.transifex.com/voltaicideas/teams/116153/ms/)\\n\"\n\"Language: ms\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Laluan Fail\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Mesej Ralat\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Tempoh\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Kadar Bit\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Kadar Sampel\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nama Fail\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Folder\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Saiz (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Masa\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Kadar Sampel\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Jenis\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Pengubahsuaian\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Tajuk\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artis\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Tahun\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Nombor Runut\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Komen\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"% Padanan\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Perkataan Diguna\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Jumlah Duplikasi\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensi\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Saiz (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Cap Masa EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Saiz\"\n"
  },
  {
    "path": "locale/ms/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2021\\n\"\n\"Language-Team: Malay (https://app.transifex.com/voltaicideas/teams/116153/ms/)\\n\"\n\"Language: ms\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Tiada duplikasi yang ditandai. Tiada apa yang dilakukan.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Tiada duplikasi yang dipilih. Tiada apa yang dilakukan.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Anda bakal membuka banyak fail serentak. Bergantung kepada apa yang \"\n\"digunakan untuk membuka fail tersebut, ia mungkin menyebabkan sepah. Ingin \"\n\"teruskan?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Mengimbas untuk duplikasi\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Memuatkan\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Memindahkan\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Menyalinkan\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Menghantarkan ke Tong Sampah\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Tindakan sebelum ini masih tergantung. Anda tidak boleh mulakan yang baharu \"\n\"lagi. Tunggu beberapa saat, kemudian cuba lagi.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Tiada duplikasi dijumpai.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Semua fail yang ditandai telah berjaya disalin.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Semua fail yang ditandai telah berjaya dipindah.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"Semua fail yang ditandai telah berjaya dipadam.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Semua fail yang ditandai telah berjaya dihantar ke Tong Sampah.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Tidak mampu memuatkan fail: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' sudah ada dalam senarai.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' tidak wujud.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Kesemua %d padanan yang dipilih akan diabaikan dalam semua imbasan \"\n\"terkemudian. Ingin teruskan?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Pilih direktori dituju untuk salin fail yang ditandai\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Pilih direktori dituju untuk pindah fail yang ditandai\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Pilih tempat tujuan untuk eksport CSV anda\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Tidak mampu menulis ke fail: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Anda tidak ada perintah tersuai ditetapkan. Tetapkannya melalui menu \"\n\"keutamaan anda.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Anda bakal mengalih keluar %d fail dari keputusan. Ingin teruskan?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} kumpulan duplikasi telah diubah oleh pengutamaan semula.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Direktori yang dipilih tidak mempunyai fail yang boleh diimbas.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Mengumpulkan fail untuk diimbas\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d dibuang)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"{} fail dikumpulkan untuk diimbas\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"{} folder dikumpulkan untuk diimbas\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"%d padanan dijumpai dari %d kumpulan\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Anda menghantar {} fail ke Tong Sampah.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Ungkapan Nalar\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"Adakah anda pasti anda ingin alih keluar kesemua %d item dari senarai abai?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nama Fail\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nama Fail - Medan\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nama Fail - Medan (Tiada Tertib)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tag\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Kandungan\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"%d / %d gambar dianalisis\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d / %d padanan ketulan dilaksanakan\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Membuat persediaan untuk pemadanan\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d / %d padanan disahkan\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"EXIF bagi %d / %d gambar dibaca\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Cap masa EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Tiada\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Tamat dengan nombor\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Tidak tamat dengan nombor\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Terpanjang\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Terpendek\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Tertinggi\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Terendah\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Terbaru\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Terlama\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplikasi ditandai.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"penapis: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Metadata bagi %d / %d gambar dibaca\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Hampir selesai! Menyusun keputusan...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Folder\"\n"
  },
  {
    "path": "locale/ms/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n# Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48) <admin@mnh48.moe>, 2023\\n\"\n\"Language-Team: Malay (https://app.transifex.com/voltaicideas/teams/116153/ms/)\\n\"\n\"Language: ms\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Keluar\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Pilihan\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Senarai Abai\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Kosongkan Cache Gambar\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Bantuan dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Mengenai dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Buka Log Nyahpepijat\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"Adakah anda pasti anda ingin alih keluar kesemua analisis gambar cache anda?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Cache gambar dikosongkan.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} fail (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Pilihan Pemadaman\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Pautkan fail dipadam\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Setelah duplikasi dipadam, letak pautan menuju fail rujukan untuk \"\n\"menggantikan fail dipadam.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Pautan Keras\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Pautan Bersimbol\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (tidak disokong)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Padam fail secara terus\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Padam fail secara terus dan bukannya hantar fail ke tong sampah. Pilihan ini\"\n\" selalunya digunakan sebagai penyelesaian apabila kaedah pemadaman biasa \"\n\"tidak berjaya.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Teruskan\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Batal\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Atribut\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Dipilih\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Rujukan\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Muatkan Keputusan...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Tetingkap Keputusan\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Tambah Folder...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Fail\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Lihat\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Bantuan\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Muatkan Keputusan Baru-baru Ini\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Mod Aplikasi:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Muzik\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Gambar\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Piawai\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Jenis Imbasan:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Pilihan Lanjutan\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Pilih folder untuk imbas dan tekan \\\"Imbas\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Muatkan Keputusan\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Imbas\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Keputusan belum disimpan\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\"Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin \"\n\"keluar?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Pilih folder untuk tambah ke senarai imbasan\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Pilih fail keputusan untuk dimuatkan\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Semua Fail (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Keputusan dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Mulakan imbasan baharu\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"Anda mempunyai keputusan yang belum disimpan, adakah anda pasti anda ingin \"\n\"teruskan?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nama\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Keadaan\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Dikecualikan\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Biasa\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Alih Keluar yang Dipilih\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Kosongkan\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Tutup\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Maklumat\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Tag untuk diimbas:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Runut\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artis\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Tajuk\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Tahun\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Pemberatan perkataan\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Padan perkataan serupa\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Boleh campur jenis fail\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Guna ungkapan nalar ketika menapis\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Alih keluar folder kosong semasa pemadaman atau pemindahan\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Abaikan duplikasi yang paut keras ke fail yang sama\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Mod nyahpepijat (perlu mula semula)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Padan gambar dengan dimensi berlainan\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Kekuatan Penapis:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Lebihkan Keputusan\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Kurang Keputusan\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Saiz fon:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Bahasa:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Salin dan Pindah:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Dalam tempat tujuan\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Cipta semula laluan relatif\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Cipta semula laluan mutlak\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Perintah Tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru perlu mula semula untuk menerima kesan perubahan bahasa.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Pengutamaan semula duplikasi\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Tambah kriteria di kotak kanan dan klik OK untuk hantar duplikasi yang \"\n\"paling sepadan dengan kriteria tersebut ke kedudukan rujukan kumpulan \"\n\"masing-masing. Baca fail bantuan untuk maklumat lanjut.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Masalah!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Terdapat masalah semasa memproses sesetengah (atau kesemua) fail. Penyebab \"\n\"masalah ini diterangkan dalam jadual di bawah. Fail tersebut tidak dialih \"\n\"keluar dari keputusan anda.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Dedahkan yang Dipilih\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Tindakan\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Tunjuk Duplikasi Sahaja\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Tunjuk Nilai Delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Hantar yang Ditandai ke Tong Sampah...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Pindah yang Ditandai ke...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Salin yang Ditandai ke...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Alih Keluar yang Ditandai dari Keputusan\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Pengutamaan Semula Keputusan...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Alih Keluar yang Dipilih dari Keputusan\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Tambah yang Dipilih ke Senarai Abai\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Jadikan yang Dipilih menjadi Rujukan\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Buka yang Dipilih dengan Aplikasi Lalai\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Buka Folder yang Mengandungi yang Dipilih\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Namakan Semula yang Dipilih\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Tanda Semua\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Tanda Kosong\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Terbalikkan Penandaan\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Tanda yang Dipilih\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Eksport ke HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Eksport ke CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Simpan Keputusan...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Guna Perintah Tersuai\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Tanda\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Lajur\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Tetap Semula ke Lalai\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Keputusan\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Duplikasi Sahaja\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Nilai Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Pilih fail untuk simpan keputusan anda\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Abaikan fail lebih kecil dari\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Keputusan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Tindakan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Tambah Folder Baharu...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Lanjutan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Periksa kemas kini secara automatik\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Asas\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Bawa Semua ke Hadapan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Periksa kemas kini...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Tutup Tetingkap\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Salin\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Perintah tersuai (argumen: %d untuk duplikasi, %r untuk rujukan):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Potong\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Maklumat Fail yang Dipilih\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Panel Maklumat\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Direktori\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Keutamaan dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Keputusan dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Laman Sesawang dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Sunting\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Eksport Keputusan ke CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Eksport Keputusan ke XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Kurangkan keputusan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Penapis\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Kekuatan penapisan:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Tapis Keputusan...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Tetingkap Pemilihan Folder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Saiz Fon:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Sembunyikan dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Sembunyikan yang Lain\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Abaikan fail lebih kecil dari:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Muatkan dari fail...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Meminimumkan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Mod\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Lebihkan keputusan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Tampal\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Keutamaan...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Lihat Segera\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Keluar dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Tetap Semula ke Lalai\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Tetap Semula ke Lalai\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Dedah\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Dedah yang Dipilih dalam Pencari\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Pilih Semua\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Hantar yang Ditandai ke Tong Sampah...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Perkhidmatan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Tunjuk Semua\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Mulakan Imbasan Duplikasi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Nama '%@' sudah wujud.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Tetingkap\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zum\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Penapis Pengecualian\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Keputusan Imbasan\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Muatkan Direktori...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Simpan Direktori...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Pilih fail direktori untuk dimuatkan\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"Keputusan dupeGuru (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Pilih fail untuk simpan direktori anda\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"Direktori dupeGuru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Tambah\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Tetap semula lalai\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Cuba rentetan\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Taipkan ungkapan nalar python di sini...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Taipkan laluan sistem fail atau nama fail di sini...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.<br>Direktori juga akan ada <strong>keadaan lalai</strong>sendiri ditetapkan kepada  Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu ungkapan nalar.<br>Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan bagi setiap satu fail tersebut untuk menentukan sama ada fail tersebut perlu ditapis keluar:<br><li>1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.</li>\\n\"\n\"<li>2. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.</li><br>\\n\"\n\"Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \\\"My Pictures\\\" sahaja:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Anda boleh cuba ungkapan nalar dengan fungsi cuba rentetan dengan menampal laluan palsu di dalamnya:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Ungkapan nalar yang terpadan akan ditonjolkan.<br>Sekiranya ada sekurang-kurangnya satu tonjolan, laluan yang dicuba akan diabaikan ketika imbasan.<br><br>Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Ralat pengkompilan:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Naikkan zum\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Turunkan zum\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Saiz biasa\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Suaian terbaik\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Mod cache gambar:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Mengataskan ikon tema dalam bar alat pemidang\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Guna ikon dalaman kami sendiri menggantikan apa yang disediakan oleh enjin \"\n\"tema\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Tunjuk bar tatal dalam pemidang imej\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Apabila imej yang dipaparkan tidak muat dalam port pandang, tunjuk bar tatal\"\n\" untuk menggerakkan pemidang\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Guna kedudukan lalai untuk bar tab (perlu mula semula)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Letak bar tab di bawah menu utama dan bukannya di sebelahnya\\n\"\n\"Di MacOS, bar tab akan mengisi lebar tetingkap.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Guna fon tebal untuk rujukan\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Warna latar depan rujukan:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Warna latar belakang rujukan:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Warna latar depan delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Tunjuk bar tajuk dan boleh dilimbungkan\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Apabila bar tajuk disembunyikan, guna kekunci pengubah suai untuk seret \"\n\"tetingkap terapung\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Bar tajuk hanya boleh dilumpuhkan ketika tetingkap dilimbungkan\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Bar tajuk menegak\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Ubah bar tajuk daripada melintang di atas, kepada menegak di sisi kiri\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Tunjuk bar tab\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.<br>Direktori juga akan ada <strong>keadaan lalai</strong> sendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu daripada ungkapan nalar yang dipilih.<br>Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan untuk menentukan sama ada fail tersebut perlu diabaikan sepenuhnya:<br><li>1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.</li>\\n\"\n\"<li>2. Ungkapan nalar dengan sekurang-kurangnya satu pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.</li><br>\\n\"\n\"Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \\\"My Pictures\\\" sahaja:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Anda boleh cuba ungkapan nalar dengan butang \\\"cuba rentetan\\\" selepas menampal laluan palsi dalam medan percubaan:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Ungkapan nalar yang terpadan akan ditonjolkan.<br>Sekiranya ada sekurang-kurangnya satu tonjolan, laluan atau nama fail yang dicuba akan diabaikan ketika imbasan.<br><br>Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Keputusan\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Antara Muka Am\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Jadual Keputusan\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Tetingkap Maklumat\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Am\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Paparan\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Fail cincang separa lebih besar dari\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"Guna dialog OS natif\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"Gunakan dialog natif OS untuk tindakan seperti pemilihan fail/folder.\\n\"\n\"Sesetengah dialog natif mempunyai kefungsian yang terhad.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Abaikan fail lebih besar dari\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Kosongkan Cache\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"Adakah anda pasti anda ingin kosongkan cache? Ini akan alih keluar semua \"\n\"cincang fail dan analisis gambar yang tercache.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Cache dikosongkan.\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Guna gaya gelap\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Bukah operasi imbasan\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"Membukah operasi imbasan dan simpan log untuk pengoptimuman.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"Log terletak di: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Nyahpepijat\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Mengenai {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Versi {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Memeriksa kemas kini...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Dilesenkan bawah GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"Tiada kemas kini tersedia.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"Versi baharu {} tersedia, muat turun <a href=\\\"{}\\\">di sini</a>.\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Laporan Ralat\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Terdapat kesulitan yang terjadi. Apa kata laporkan ralat tersebut?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Laporan ralat patut dilaporkan sebagai isu GitHub. Anda boleh salin runut balik ralat di atas dan tampal dalam isu baharu.\\n\"\n\"\\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\"\\n\"\n\"Ia sangat membantu sekiranya anda tambah keterangan mengenai bagaimana anda dapat ralat tersebut. Terima kasih!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Pergi ke GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Czech\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Jerman\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Yunani\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Inggeris\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Sepanyol\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Perancis\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armenia\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Itali\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Jepun\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Korea\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Melayu\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Belanda\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Poland\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brazil\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Rusia\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turki\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ukraine\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnam\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Cina (Ringkas)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Kosongkan Senarai\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Cari...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\"Pilihan ini adalah untuk pengguna lanjutan atau untuk keadaan khas tertentu,\"\n\" kebanyakan pengguna tidak perlu mengubahsuai pilihan ini.\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"Sertakan pemeriksaan kewujudan selepas selesai imbasan\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"Abaikan perbezaan dalam masa ubahsuai ketika memuatkan cerna tercache\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"Batal?\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"Adakah anda pasti anda ingin batalkan? Semua kemajuan akan hilang.\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ungkapan nalar python (sensitif huruf) ini akan menapis keluar fail ketika imbasan.<br>Direktori juga akan ada <strong>keadaan lalai</strong> sendiri ditetapkan kepada Dikecualikan dalam tab Direktori jika nama tersebut sepadan dengan salah satu daripada ungkapan nalar yang dipilih.<br>Untuk setiap fail yang terhimpun, dua percubaan akan dilaksanakan untuk menentukan sama ada fail tersebut perlu diabaikan sepenuhnya:<br><li>1. Ungkapan nalar tanpa pemisah laluan di dalamnya akan dibandingkan dengan nama fail sahaja.</li>\\n\"\n\"<li>2. Ungkapan nalar dengan sekurang-kurangnya satu pemisah laluan di dalamnya akan dibandingkan dengan laluan penuh ke fail.</li><br>Contoh: jika anda ingin tapis keluar fail .PNG dari direktori \\\"My Pictures\\\" sahaja:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Anda boleh cuba ungkapan nalar dengan butang \\\"cuba rentetan\\\" selepas menampal laluan palsu dalam medan percubaan:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Ungkapan nalar yang terpadan akan ditonjolkan.<br>Sekiranya ada sekurang-kurangnya satu tonjolan, laluan atau nama fail yang dicuba akan diabaikan ketika imbasan.<br><br>Direktori dan fail yang bermula dengan tanda titik '.' ditapis keluar secara lalainya.<br><br>\"\n"
  },
  {
    "path": "locale/nl/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Bas <duvel3@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Bas <duvel3@gmail.com>, 2021\\n\"\n\"Language-Team: Dutch (https://www.transifex.com/voltaicideas/teams/116153/nl/)\\n\"\n\"Language: nl\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Bestandspad\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Foutmelding\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Tijdsduur\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Sample frequentie\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Bestandsnaam\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Map\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Grootte (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Tijd\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Sample Frequentie\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Soort\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Aanpassing\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Titel\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artiest\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Jaar\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Track nummer\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Commentaar\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Zekerheid %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Woorden gebruikt\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Dubbel telling\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Afmetingen\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Grootte (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Tijdstip\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Grootte\"\n"
  },
  {
    "path": "locale/nl/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n# Bas <duvel3@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Bas <duvel3@gmail.com>, 2021\\n\"\n\"Language-Team: Dutch (https://app.transifex.com/voltaicideas/teams/116153/nl/)\\n\"\n\"Language: nl\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Er zijn geen gemarkeerde dubbelingen. Er is niks gedaam\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Er zijn geen dubelingen geselecteerd. Er is niks gedaan\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Je staat op het punt om zeer veel bestanden tegelijkertijd te openen. \"\n\"Afhankelijk met welke applicaties die bestanden worden geopened kan het best\"\n\" een rommeltje worden. Doorgaan?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Dubbelingen aan het opsporen\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Laden\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Verplaatsen\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Kopiëren\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Naar de prullebak verplaatsen\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Er is nog een vorige actie bezig. Je kan nu nog geen nieuwe actie starten. \"\n\"Wacht een paar seconden en probeer het opnieuw\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Geen dubbelingen gevonden\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Alle gemarkeerde bestanden zijn succesvol gekopieerd.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Alle gemarkeerde bestanden zijn succesvol verplaatst.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Alle gemarkeerde bestanden zijn met succes in de prullenbak gedaan.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Kan bestand niet laden: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' staat al in de lijst.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' bestaat niet.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Alle geselecteerde %d overeenkomsten zullen in toekomstige onderzoeken \"\n\"worden overgslagen. Doorgaan?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"\"\n\"Selecteer een map waar u de gemarkeerde bestanden naartoe wilt kopiëren\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\"Selecteer een map waar u de gemarkeerde bestanden naartoe wilt verplaatsen\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Selecteer een locatie voor de CSV export\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Kan niet schrijven naar bestand: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Er is nog geen \\\"aangepaste opdracht\\\" ingericht. Je kan dit doen bij de \"\n\"voorkeuren.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"\"\n\"Je staat op het punt om %d bestanden te verwijderen uit de resultaten. \"\n\"Doorgaan?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"\"\n\"{} dubbelingen groepen waren veranderd door de prioriteits verschuiving.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"\"\n\"De geselecteerde folders bevatten geen bestanden die onderzocht kunnen \"\n\"worden.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Bestanden aan het verzamelen om te onderzoeken\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d weggelaten)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Je verplaatst {} bestand(en) naar de prullenbak\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Normale Uitdrukkingen\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"Weet je zeker dat je alle %d regels uit de overslaan lijst wilt verwijderen?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Bestandsnaam\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Bestandsnaam - Velden\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Bestandsnaam - Velden (geen volgorde)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Inhoud\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"%d van de %d afbeeldingen aan het analyseren\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d van de %d bulk overeenkomsten uitgevoerd\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Voorbereiden voor dubbelingen bepaling\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d van de %d overeenkomsten nagekeken\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"EXIF informatie van %d van de %d afbeeldingen gelezen\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF-tijdstempel\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Geen\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Eindigt met nummer\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Eindigt niet met een nummer\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"langste\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"kortste\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"hoogste\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"laagste\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"nieuwste\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"oudste\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s /%s) dubbelingen gemarkeerd\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"filter: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Metadata van %d/%d bestanden gelezen\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Bijna klaar! Gehannes met resultaten...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Mappen\"\n"
  },
  {
    "path": "locale/nl/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Bas <duvel3@gmail.com>, 2022\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Dutch (https://app.transifex.com/voltaicideas/teams/116153/nl/)\\n\"\n\"Language: nl\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Afsluiten\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Opties\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Overslaan lijst\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Afbeelding cache leegmaken\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru Help\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Over dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Debug Log openen\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Weet je zeker dat je de afbeeldings-analyse cache wilt verwijderen\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Afbeelding cache leeggemaakt.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} bestand (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Verwijderopties\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"link verwijderde bestanden\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Na het verwijderen van een dubbeling, een link leggen tussen het verwijderde\"\n\" bestand en het referentie bestand\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"harde link\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"symbolische link\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(niet ondersteund)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Bestanden direct verwijderen\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"In plaats van de bestanden naar de prullenbak te verplaats worden ze direct \"\n\"verwijderd. Deze optie wordt normaal alleen gekozen als een oplossing als de\"\n\" normale verwijderings methode niet werkt.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Doorgaan\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Afbreken\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Attributen\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Geselecteerd\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Referentie\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Resultaten inlezen...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Resultaten venster\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Folder toevoegen...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Bestand\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Beeld\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Help\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Recente resultaten inlezen\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Toepassingsmodus:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Muziek\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Beeld\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standaard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Onderzoeks type\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Meer Opties\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"\"\n\"Selecteer de folders die onderzocht moeten worden en druk op \\\"onderzoeken\\\"\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Resultaten laden\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"onderzoeken\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"niet opgeslagen resultaten\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\"Je hebt nog niet opgeslagen resultaten, weet je zeker dat je wilt afsluiten?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Selecteer een folder die wil toevoegen aan de onderzoekslijst\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Selecteer een resultaat bestand om te openen\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Alle bestanden (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru resultaten (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Nieuw onderzoek starten\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"Je hebt nog niet opgeslagen resultaten, weet je zeker dat je verder wilt?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Naam\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Status\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Uitgesloten\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normaal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Verwijder selectie\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"opheffen\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"sluiten\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"details\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Labels die onderzocht moeten worden\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Nummer\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artiest\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Titel\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Genre\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Jaar\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Word gewicht\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Vergelijk gelijkwaardige woorden\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Bestandsformaten mogen doorelkaar gebruikt worden\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Gebruik \\\"reguliere expressies\\\" tijdens het filteren\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Verwijder lege folders tijdens weggooien of verplaatsen\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Negeer dubbelingen die hard gelinkt zijn aan het zelfde bestand\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Debug stand (herstart is nodig)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Afbeeldingen met afwijkende afmetingen toch gebruiken bij onderzoek\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Filter sterkte\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Meer resultaten\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Minder resultaten\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Font grote:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Taal:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Kopieeren en verplaatsen\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Direct in bestemming\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Hercreeer relatieve bestandslocatie\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Creer volledige bestands locatie\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Aangepaste opdracht (opties: %d voor dubbeling, %r voor referentie):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru moet herstart worden om de taal wijziging door te voeren\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Her-priotarisatie van dubbelingen\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Voeg criteria toe aan de rechter kant en klik op OK om dubbeligen die het \"\n\"meeste overeenkomen met deze criteria te verplaatsen naar de overeenkomende \"\n\"referentie groep. Lees het help bestand voor meer informatie. \"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problemen!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Er waren problemen bij het verwerken van sommige (of alle) bestanden. De \"\n\"oorzaak hiervan staat beschreven in de tabel hieronder. Deze bestanden \"\n\"bestanden zijn niet verwijderd uit de resultaten.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Toon selectie\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Acties\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Alleen dubbelingen tonen\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Alleen het verschil tonen\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Verplaats de gemarkeerde bestanden naar de prullenbak\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Verplaats gemarkeerde naar ...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Kopieer gemarkeerde naar ...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Verwijder gemarkeerde regels uit het resultaat\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Resultaat her-priotariseren\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Verwijder geselecteerde uit het resultaat\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Voeg geselecteerde toe aan de overslaan lijst\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Maak van de selectie een referentie\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Open de selectie met de standaard toepassing\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Open de folder van de selectie\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Hernoem de selectie\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Alles markeren\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Niks markeren\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Markering omdraaien\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Geselecteerde markeren\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Exporteer naar HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Exporteer naar CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Resultaten opslaan...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Voer aangepaste opdracht uit\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Markeer\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Kolommen\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Terug naar standaard instellingen\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} resultaten\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Alleen dubbelingen\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Verschillen\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Selecteerd een bestand om het resultaat op te slaan\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Negeer bestanden kleiner dan\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ resultaten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Actie\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Nieuwe folder toevoegen...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Geavanceerd\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"automatisch controleren voor nieuwere versie\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Basis\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Alles naar voren halen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Controleer  nieuwere versie\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Venster sluiten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"kopieer\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Aangepaste opdracht (opties: %d voor dubbeling, %r voor referentie):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"verwijder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"verschil\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"details van het geselecteerde bestand\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"details paneel\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Folders\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru voorkeuren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru resultaten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru website\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Bewerken\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Exporteer resultaat naar CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exporteer resultaat naar XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Minder resultaten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filter\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Filter sterkte\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filter resultaten ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Folder selectie venster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Grootte lettertype:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"dupeGuru verbergen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Overige verbergen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Negeer bestanden kleiner dan\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"laden vanuit bestand...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimaliseren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Modus\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Meer resultaten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Plakken\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Voorkeuren ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Snel kijken\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"dupeGuru afsluiten\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Terug naar standaard instellingen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Terug naar standaard instellingen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Toon bestand\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Toon folder van bestand\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Alles selecteren\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Verplaats de gemarkeerde bestanden naar de prullenbak\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Services\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Alles tonen\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Start dubbelingen onderzoek\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"de naam '%@\\\" bestaat al.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Venster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Zoom\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Uitsluitingsfilters\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Scanresultaten\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Mappen laden ...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Mappen opslaan ...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Selecteer een directory-bestand om te laden\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru-mappen (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Selecteer een bestand om uw mappen in op te slaan\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru-mappen (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Toevoegen\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Herstel de standaardinstellingen\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Test tekenreeks\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Typ hier een reguliere expressie voor python...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Typ hier een bestandssysteempad of bestandsnaam ...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Deze (hoofdlettergevoelige) reguliere expressies van Python filteren bestanden uit tijdens scans.<br>Mappen hebben ook hun <strong>standaardstatus</strong> ingesteld op \\\"Uitgesloten\\\" in het tabblad Mappen als hun naam overeenkomt met een van de reguliere expressies. <br>Voor elk verzameld bestand worden twee tests uitgevoerd op elk van hen om te bepalen of ze al dan niet moeten worden uitgefilterd:<br><li>1. Reguliere expressies zonder padscheidingsteken worden alleen vergeleken met de bestandsnaam.</li>\\n\"\n\"<li>2. Reguliere expressies zonder padscheidingsteken worden vergeleken met het volledige pad naar het bestand.</li><br>\\n\"\n\"Voorbeeld: als u de PNG-bestanden alleen uit de map \\\"My Pictures\\\" wilt filteren:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>U kunt de reguliere expressie testen met de functie teststring door er een neppad in te plakken:<br><code> C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Overeenkomende reguliere expressies worden gemarkeerd.<br>Als er ten minste één highlight is, wordt het geteste pad genegeerd tijdens scans.<br><br>Mappen en bestanden die beginnen met een punt '.' worden standaard uitgefilterd.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Compilatiefout:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Zoom vergroten\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Zoom verkleinen\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Normale grootte\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Beste pasvorm\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Fotocachemodus:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Overschrijf themapictogrammen in de werkbalk van de viewer\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Gebruik onze eigen interne pictogrammen in plaats van die van de thema-\"\n\"engine\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Toon schuifbalken in afbeeldingsviewers\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Als de weergegeven afbeelding niet in de viewport past, laat dan \"\n\"schuifbalken zien om de weergave rondom te omspannen\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Gebruik de standaardpositie voor de tabbalk (opnieuw opstarten vereist)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Plaats de tabbalk onder het hoofdmenu in plaats van ernaast.\\n\"\n\"Op MacOS vult de tabbalk in plaats daarvan de breedte van het venster.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Gebruik vet lettertype voor referenties\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Referentie voorgrondkleur:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Referentie achtergrondkleur:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Delta voorgrondkleur:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Toon de titelbalk en kan worden gedokt\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Terwijl de titelbalk verborgen is, gebruik maken van de speciale toets op \"\n\"het zwevende venster slepen\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"De titelbalk kan alleen worden uitgeschakeld terwijl het venster is gedokt\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Verticale titelbalk\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Wijzig de titelbalk van horizontaal geplaatst, verticale links\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Tabbladbalk weergeven\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Deze (hoofdlettergevoelige) reguliere expressies van Python filteren bestanden uit tijdens scans.<br>Mappen hebben ook hun <strong>standaardstatus</strong> ingesteld op Uitgesloten in het tabblad Mappen als hun naam toevallig overeenkomt met een van de geselecteerde reguliere expressies. <br>Voor elk verzameld bestand worden twee tests uitgevoerd om te bepalen of het al dan niet volledig moet worden genegeerd:<br><li>1. Reguliere expressies zonder padscheidingsteken worden alleen vergeleken met de bestandsnaam.</li>\\n\"\n\"<li>2. Reguliere expressies met ten minste één padscheidingsteken worden vergeleken met het volledige pad naar het bestand.</li><br>\\n\"\n\"<br>Voorbeeld: als u .PNG-bestanden alleen uit de map \\\"My Pictures\\\" wilt filteren:<code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>U kunt de reguliere expressie testen met de \\\"test string\\\" -knop nadat u een neppad in het testveld hebt geplakt:<br><code> C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Overeenkomende reguliere expressies worden gemarkeerd. <br>Als er ten minste één markering is, wordt het geteste pad of de bestandsnaam genegeerd tijdens het scannen.<br><br>Mappen en bestanden die beginnen met een punt '.' worden standaard uitgefilterd.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Resultaten\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Algemene interface\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Resultaattabel\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Details Venster\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Algemeen\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Scherm\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Over {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Versie {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Licentie verleend onder GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Foutenrapport\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Er is iets fout gegaan. Hoe zit het met het melden van de fout?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Foutrapporten moeten worden gerapporteerd als GitHub-problemen. U kunt de bovenstaande foutopsporing kopiëren en in een nieuwe uitgave plakken.\\n\"\n\"\\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\"\\n\"\n\"Wat meestal echt helpt, is als je een beschrijving toevoegt van hoe je de fout hebt gekregen. Bedankt!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Ga naar GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Tsjechisch\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Duits\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Grieks\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Engels\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Spaans\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Frans\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armeens\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italiaans\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japans\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Koreaans\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Maleis\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Nederlands\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Pools\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Braziliaans\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Russisch\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turks\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Oekraïens\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamees\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"(Versimpeld) Chinees\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Lijst leegmaken\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Zoeken...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/pl_PL/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Polish (Poland) (https://www.transifex.com/voltaicideas/teams/116153/pl_PL/)\\n\"\n\"Language: pl_PL\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Ścieżka pliku\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Komunikat o błędzie\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Trwanie\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Szybkość transmisji\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Samplerate\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nazwa pliku\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Katalog\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Rozmiar (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Czas\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Częstotliwość próbkowania\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Rodzaj\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modyfikacja\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Tytuł\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artysta\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Gatunek\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Rok\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Numer utworu\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Komentarz\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Mecz %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Użyte słowa\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Liczba duplikatów\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Wymiary\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Rozmiar (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Sygnatura czasowa EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Rozmiar\"\n"
  },
  {
    "path": "locale/pl_PL/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Polish (Poland) (https://app.transifex.com/voltaicideas/teams/116153/pl_PL/)\\n\"\n\"Language: pl_PL\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Brak wykrytych duplikatów. Nic nie zrobiono.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Brak wybranych duplikatów. Nic nie zrobiono.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Masz zamiar otworzyć wiele plików jednocześnie. W zależności od tego, za \"\n\"pomocą czego te pliki są otwierane, może to spowodować spory bałagan. \"\n\"Kontyntynuj?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Wyszukiwanie duplikatów\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Ładuję\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Przenoszę\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Kopiowanie\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Wysyłam do kosza\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Wciąż wisi tam poprzednia akcja. Nie możesz jeszcze rozpocząć nowego. \"\n\"Poczekaj kilka sekund i spróbuj ponownie.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Nie znaleziono duplikatów.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Wszystkie zaznaczone pliki zostały pomyślnie skopiowane.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Wszystkie zaznaczone pliki zostały pomyślnie przeniesione.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Wszystkie zaznaczone pliki zostały pomyślnie wysłane do kosza.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Nie udało się załadować pliku: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' jest już na liście.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' nie istnieje.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Wszystkie zaznaczone %d duplikaty będą ignorowane w kolejnych skanach. \"\n\"Kontynuować?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Wybierz katalog, do którego chcesz skopiować zaznaczone pliki\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Wybierz katalog, do którego chcesz przenieść zaznaczone pliki\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Wybierz miejsce docelowe dla eksportowanego pliku CSV\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Nie udało się zapisać do pliku: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Nie masz skonfigurowanego polecenia niestandardowego. Ustaw to w swoich \"\n\"preferencjach.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Zamierzasz usunąć %d plików z wyników. Kontyntynuj?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} zduplikowanych grup zmieniono przez ponowne ustalenie priorytetów.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Wybrane katalogi nie zawierają plik skanowalną.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Zbieranie plików do skanowania\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s(%d odrzucone)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Wysyłasz {} plików do Kosza\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Wyrażenia regularne\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Czy na pewno chcesz usunąć wszystkie %d pozycji z listy ignorowanych?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nazwa pliku\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nazwa pliku - pola\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nazwa pliku - pola (bez kolejności)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tagi\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Treść\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Analizowane %d/%d zdjęć\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Wykonano %d/%d dopasowań fragmentów\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Przygotowanie do dopasowania\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Zweryfikowane %d/%d meczów\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Przeczytaj EXIF z %d/%d zdjęć\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Sygnatura czasowa EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Nie\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Kończy się numerem\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Nie kończy się liczbą\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Najdłużej\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Najkrótsza\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Najwyższa\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Najniższa\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Najnowsza\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Najstarszy\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplikaty oznakowane.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" filtr: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Przeczytaj metadane %d/%d plików\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Prawie skończone! Porządkowanie wyników...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Katalogi\"\n"
  },
  {
    "path": "locale/pl_PL/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Polish (Poland) (https://app.transifex.com/voltaicideas/teams/116153/pl_PL/)\\n\"\n\"Language: pl_PL\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Wyjdź\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Opcje\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Lista ignorowanych\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Wyczyść pamięć podręczną obrazów\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Pomoc dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"O dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Otwórz Log aplikacji\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Czy na pewno chcesz usunąć całą analizę obrazów z pamięci podręcznej?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Obraz cache wyczyszczone.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"Plik {} (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Opcje usuwania\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Twórz dowiązania dla usuniętych plików\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Po usunięciu duplikatu, utwórz link wskazujący na plik referencyjny w \"\n\"miejsce usuniętego pliku.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Twardy link\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(niepodparta)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Usuwaj pliki zamiast przenosić do kosza\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Zamiast przenosić pliki do kosza, usuwaj je. Ta opcja może służyć za \"\n\"obejście, jeśli przenoszenie do kosza nie działa.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Kontynuuj\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Anuluj\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Właściwość\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Zaznaczony\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Odwołanie\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Wczytaj wyniki...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Okno wyników\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Dodaj folder...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Plik\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Widok\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Pomoc\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Wczytaj ostatnie wyniki\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Tryb aplikacji\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Muzyka\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Obrazek\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standard\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Rodzaj skanowania:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Więcej opcji\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Wybierz foldery do przeskanowania i wciśnij \\\"Skanuj\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Wczytaj wyniki\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Skanuj\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Niezapisane wyniki\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Masz niezapisane wyniki wyszukiwania, czy na pewno chcesz wyjść?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Wybierz folder aby dodać go do listy do przeskanowania\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Wybierz plik wyniku skanowania do wczytania\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Wszystkie pliki (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Wyniki dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Rozpocznij nowe skanowanie\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"Masz niezapisane wyniki wyszukiwania, czy na pewno chcesz kontynuować?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nazwa\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Stan\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Wykluczony\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normalny\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Usuń zaznaczone\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Wyczyść\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Zamknij\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Szczegóły\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Tagi do skanowania\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"ścieżka\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artysta\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Tytuł\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Gatunek\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Rok\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Ważenie słów\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Dopasuj podobne słowa\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Może mieszać rodzaj pliku\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Podczas filtrowania używaj wyrażeń regularnych\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Usuń puste foldery podczas usuwania lub przenoszenia\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignoruj duplikaty dowiązujące do tego samego pliku\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"tryb debugowania (wymaga restartu)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Zgadzają zdjęć z różnych wymiarach\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Siła filtru:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Więcej wyników\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Mniej wyników\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Wielkość czcionki:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Język:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Kopiuj i przenieś:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"W miejscu przeznaczenia\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Ponowne utworzenie ścieżki względnej\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Ponownie utworzyć ścieżkę bezwzględną\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Własne polecenie (argumenty: %d dla duplikatu, %r dla pliku referencyjnego):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\"dupeGuru musi zostać ponownie uruchomiona, aby zmiany języka zaczęły \"\n\"obowiązywać.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Zmień priorytet duplikatów\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Dodaj kryteria w prawym polu i kliknij OK, aby wysłać duplikaty, które \"\n\"najlepiej odpowiadają tym kryteriom, do pozycji odniesienia ich odpowiedniej\"\n\" grupy. Przeczytaj plik pomocy, aby uzyskać więcej informacji.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problemy!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Wystąpiły problemy podczas przetwarzania niektórych (lub wszystkich) plików.\"\n\" Przyczyny tych problemów opisano w poniższej tabeli. Te pliki nie zostały \"\n\"usunięte z wyników.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Pokaż wybrane\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Działania\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Tylko pokaż Duplikaty\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Pokaż wartości delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Wyślij Oznaczono do Kosza...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Przenieś zaznaczone do ...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Kopiuj zaznaczone do ...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Usuń Oznaczono z wyników\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Zmień priorytety wyników...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Usuń wybrane z wyników\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Dodaj wybrane do listy ignorowanych\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Zmień wybrany element w element referencyjny\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Otwórz wybrane z domyślną aplikacją\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Otwórz folder zawierający wybrany element\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Zmień nazwę wybranych\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Zaznacz wszystko\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Zaznacz brak\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Odwróć znakowanie\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Zaznacz wybrane\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Eksportuj do HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Eksportuj do CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Zapisz wyniki...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Wywołaj polecenie niestandardowe\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Zaznacz\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Kolumny\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Przywróć domyślne\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Wyniki\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Tylko duplikaty\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Wartości delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Wybierz plik, aby zapisać wyniki do\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignoruj pliki mniejsze niż\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Wyniki\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Akcję\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Dodaj nowy folder...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Zaawansowane\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Automatycznie sprawdzać aktualizacje\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Podstawowe\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Przenieś wszystko na wierzch\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Sprawdź aktualizacje...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Zamknij okno\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Kopia\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Własne polecenie (argumenty: %d dla duplikatu, %r dla pliku referencyjnego):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Wytnij\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Szczegóły wybranego pliku\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Panel szczegółów\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Katalogi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru Preferencje\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru Wyniki\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru Witryna\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Edytuj\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Eksportuj wyniki do CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Eksportuj wyniki do XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Mniej wyników\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtr\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Twardość filtra:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filtruj wyniki...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Okno wyboru folderu\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Rozmiar czcionki:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Ukryj dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Ukryj inne\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignoruj pliki mniejsze niż:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Wczytaj z pliku...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Zminimalizować\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Tryb\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Więcej wyników\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Wklej\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Preferencje...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Szybkie spojrzenie\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Zamknij dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Przywróć domyślne\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Przywróć domyślne\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Odsłonić\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Pokaż wybrane w Finderze\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Zaznacz wszystko\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Wyślij Oznaczono do Kosza...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Usługi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Pokaż wszystkie\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Rozpocznij skanowanie duplikatów\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Nazwa '%@' już istnieje.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Okno\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Powiększenie\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Filtry wykluczenia\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Wyniki skanowania\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Wczytaj katalogi...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Zapisz katalogi...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Wybierz plik katalogów do załadowania\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Katalogi (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Wybierz plik, w którym chcesz zapisać swoje katalogi\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Katalogi (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Dodaj\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Przywróć domyślne\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Ciąg testowy\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Wpisz python wyrażenie regularne tutaj...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Wpisz ścieżkę systemu plików lub nazwę pliku tutaj...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Te wyrażenia regularne Pythona (uwzględniające wielkość liter) odfiltrują pliki podczas skanowania.<br>Katalogi będą miały również <strong>domyślny stan</strong> ustawiony na Wykluczone na karcie Katalogi, jeśli ich nazwa będzie pasować do jednego z wybranych wyrażeń regularnych.<br>Dla każdego zebranego pliku wykonywane są dwa testy, aby określić, czy należy go całkowicie zignorować:<br><li>1. Wyrażenia regularne bez separatora ścieżek będą porównywane tylko z nazwą pliku.</li>\\n\"\n\"<li>2. Wyrażenia regularne zawierające przynajmniej jeden separator ścieżki zostaną porównane z pełną ścieżką do pliku.</li><br>\\n\"\n\"Przykład: jeśli chcesz odfiltrować pliki PNG tylko z katalogu \\\"My Pictures\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Możesz przetestować wyrażenie regularne przyciskiem „ciąg testowy” po wklejeniu fałszywej ścieżki w polu testowym:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Pasujące wyrażenia regularne zostaną podświetlone.<br>Jeśli istnieje co najmniej jedno podświetlenie, testowana ścieżka lub nazwa pliku zostanie zignorowana podczas skanowania.<br><br>Katalogi i pliki zaczynające się od kropki \\\".\\\" są domyślnie odfiltrowywane.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Błąd kompilacji:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Zwiększ zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Zmniejsz zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Normalny rozmiar\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Najlepiej dopasowana\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Tryb pamięci podręcznej obrazów:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Zastąp ikony motywów na pasku narzędzi przeglądarki.\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Użyj naszych własnych wewnętrznych ikon zamiast ikon dostarczonych przez \"\n\"silnik motywu.\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Pokaż paski przewijania w widzów obrazu\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Jeśli wyświetlany obraz nie mieści się w rzutni, pokaż paski przewijania, \"\n\"aby przesunąć widok\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Użyj domyślnej pozycji paska kart (wymaga ponownego uruchomienia)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Umieść pasek kart poniżej menu głównego zamiast obok.\\n\"\n\"W systemie MacOS pasek kart będzie zamiast tego wypełniał szerokość okna.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Użyj pogrubioną czcionką o referencje\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Kolor pierwszego planu dla referencyjnego:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Kolor tła dla referencyjnego:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Kolor pierwszego planu delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Pokaż pasek tytułu i można go zadokować\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Gdy pasek tytułu jest ukryty, użyj klawisza modyfikującego, aby przeciągnąć \"\n\"pływające okno\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Pasek tytułu można wyłączyć tylko wtedy, gdy okno jest zadokowane\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Pionowy pasek tytułu\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Zmień pasek tytułu z poziomego u góry na pionowy po lewej stronie\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Pokaż pasek kart\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Te wyrażenia regularne Pythona (uwzględniające wielkość liter) odfiltrują pliki podczas skanowania.<br>Katalogi będą miały również <strong>domyślny stan</strong> ustawiony na Wykluczone na karcie Katalogi, jeśli ich nazwa będzie pasować do jednego z wybranych wyrażeń regularnych.<br>Dla każdego zebranego pliku wykonywane są dwa testy, aby określić, czy należy go całkowicie zignorować:<br><li>1. Wyrażenia regularne bez separatora ścieżek będą porównywane tylko z nazwą pliku.</li>\\n\"\n\"<li>2. Wyrażenia regularne zawierające przynajmniej jeden separator ścieżki zostaną porównane z pełną ścieżką do pliku.</li><br>\\n\"\n\"Przykład: jeśli chcesz odfiltrować pliki PNG tylko z katalogu \\\"My Pictures\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Możesz przetestować wyrażenie regularne przyciskiem „ciąg testowy” po wklejeniu fałszywej ścieżki w polu testowym:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Pasujące wyrażenia regularne zostaną podświetlone.<br>Jeśli istnieje co najmniej jedno podświetlenie, testowana ścieżka lub nazwa pliku zostanie zignorowana podczas skanowania.<br><br>Katalogi i pliki zaczynające się od kropki \\\".\\\" są domyślnie odfiltrowywane.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Wyniki\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Interfejs ogólny\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tabela wyników\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Okno szczegółów\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Generał\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Pokaz\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"O {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Wersja {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Licencjonowany w ramach GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Raport błędów\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Coś poszło nie tak. Co powiesz na zgłoszenie błędu?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"To, co zwykle naprawdę pomaga, to dodanie opisu tego, w jaki sposób wystąpił błąd. Dzięki!\\n\"\n\"\\n\"\n\"Chociaż aplikacja powinna nadal działać po tym błędzie, może być w stanie niestabilnym, dlatego zaleca się ponowne uruchomienie aplikacji.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Przejdź do GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Czech\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Niemiecki\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Grecki\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Angielski\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Hiszpański\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Francuski\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Ormiański\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Włoski\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japońsku\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Koreański\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malajski\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Holenderski\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polskie\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brazylijski\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Rosyjski\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turecki\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ukraiński\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Wietnamski\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Chiński (uproszczony)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Wyczyść listę\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Szukaj...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/pt_BR/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\\n\"\n\"Language-Team: Portuguese (Brazil) (https://www.transifex.com/voltaicideas/teams/116153/pt_BR/)\\n\"\n\"Language: pt_BR\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Caminho\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Mensagem de Erro\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Duração\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Taxa de Bits\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Amostragem\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Nome do Arquivo\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Pasta\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Tamanho\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Duração\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Tamanho da Amostra\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Tipo\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Modificado\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Nome\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Álbum\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Gênero\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Ano\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Número da Faixa\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Comentário\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"% Precisão\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Palavras Usadas\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Duplicatas\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Dimensões\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Tamanho\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Timestamp EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Tamanho\"\n"
  },
  {
    "path": "locale/pt_BR/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Portuguese (Brazil) (https://app.transifex.com/voltaicideas/teams/116153/pt_BR/)\\n\"\n\"Language: pt_BR\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Não há duplicatas marcadas. Nada foi feito.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Não há duplicatas selecionadas. Nada foi feito.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Você está prestes a abrir muitos arquivos de uma vez. Problemas podem surgir\"\n\" dependendo de qual app seja usado para abri-los. Deseja continuar?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Buscando por duplicatas\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Carregando\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Movendo\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Copiando\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Movendo para o Lixo\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Ainda há uma ação em andamento. Não é possível iniciar outra agora. Espere \"\n\"alguns segundos e tente novamente.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Nenhuma duplicata encontrada.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Todos os arquivos marcados foram copiados corretamente.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Todos os arquivos marcados foram relocados corretamente.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Todos os arquivos marcados foram movidos para o Lixo corretamente.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Não foi possível carregar o arquivo: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"‘{}’ já está na lista.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"‘{}’ não existe.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"Excluir %d duplicata(s) selecionada(s) de escaneamentos posteriores?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Selecione um diretório onde deseja copiar os arquivos marcados\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Selecione um diretório para onde deseja mover os arquivos marcados\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Selecione uma pasta para o CSV exportado\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Não foi possível gravar no arquivo: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"Você não possui nenhum comando personalizado. Crie um nas preferências.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Remover %d arquivo(s) dos resultados?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} grupos de duplicatas alterados ao repriorizar.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"As pastas selecionadas não contém arquivos escaneáveis.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Juntando arquivos para escanear\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d rejeitado(s))\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Você está movendo {} arquivo(s) para o Lixo.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Expressões regulares\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Tem certeza de que deseja remover todos os %d itens da lista Ignorar?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Nome do arquivo\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Nome do arquivo - campos\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Nome do arquivo - campos (sem pedido)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Conteúdo\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"%d/%d fotos analizadas\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d/%d resultados em blocos executados\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Preparando para comparação\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d/%d resultados verificados\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"EXIF lido em %d/%d fotos\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Timestamp EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Nenhum\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Termina com número\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Não termina com número\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Mais longo\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Mais curto\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Maior\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Menor\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Mais recente\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Mais antigo\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) duplicatas marcadas.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" filtro: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Metadados lidos em %d/%d arquivos\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Quase pronto! Mexendo nos resultados ...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Pastas\"\n"
  },
  {
    "path": "locale/pt_BR/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Portuguese (Brazil) (https://app.transifex.com/voltaicideas/teams/116153/pt_BR/)\\n\"\n\"Language: pt_BR\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Encerrar\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Opções\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Lista Ignorar\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Apagar Cache de Fotos\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Ajuda dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Sobre o dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Abrir Registro de Depuração\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Deseja remover todo o cache das fotos já analizadas?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Cache de fotos apagado.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"Arquivo {} (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Opções de Apagamento\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Criar link dos arquivos apagados\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Após apagar uma duplicata, cria um link direcionado ao arquivo original para\"\n\" substituir o arquivo apagado.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Symlink\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(incompatível)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Apagar arquivos imediatamente\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Apaga os arquivos imediatamente ao invés de movê-los para o Lixo. Essa opção\"\n\" é usada como alternativa para quando o método normal falha.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Continuar\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Cancelar\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Atributo\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Seleção\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Referência\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Carregar…\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Janela de Resultados\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Adicionar Pasta…\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Arquivo\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Visualização\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Ajuda\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Carregar Resultados Recentes\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Modo de Aplicação:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Música\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Imagem\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Padrão\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Tipo de Scan:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Mais opções\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Selecione as pastas desejadas e clique em “Escanear”.\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Carregar\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Escanear\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Resultados não salvos\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Você possui resultados não salvos, deseja encerrar assim mesmo?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Selecione uma pasta para adicionar ao escaneamento\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Selecione um resultado para carregar\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Todos os Arquivos (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Resultados do dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Iniciar um novo escaneamento\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Você possui resultados não salvos, deseja continuar mesmo assim?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Nome\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Estado\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Excluído\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Normal\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Remover Seleção\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Limpar\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Fechar\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Detalhes\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Escanear Tags:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Faixa\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Artista\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Álbum\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Nome\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Gênero\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Ano\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Importância da palavra\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Coincidir palavras similares\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Pode misturar tipo de arquivo\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Usar expressões regulares ao filtrar\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Remover pastas vazias ao apagar ou mover\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ignorar duplicatas de hardlink a um mesmo arquivo\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Modo de Depuração (requer reinício)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Coincidir fotos de dimensões diferentes\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different rotations\"\nmsgstr \"Coincidir fotos de rotações diferentes\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Pressão do Filtro:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"+ Resultados\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"- Resultados\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Tam. fonte:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Idioma:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Copiar e Mover:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Exatamente no destino\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Recriar caminho relativo\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Recriar caminho absoluto\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Comando personalizado (argumentos: %d (dup), %r (ref)):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\"É necessário reiniciar o dupeGuru para que as mudanças de idioma surtam \"\n\"efeito.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Repriorizar duplicatas\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Adicione critérios à caixa da direita e clique em OK para elevar as \"\n\"duplicatas à posição de referência em seus respectivos grupos, baseado nos \"\n\"critérios escolhidos. Leia a Ajuda para maiores informações.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Problemas!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Problemas ao processar alguns (ou todos) os arquivos. A causa dos problemas \"\n\"está detalhada abaixo. Esses arquivos não foram removidos dos seus \"\n\"resultados.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Mostrar Seleção\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Ações\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Mostrar Somente Duplicatas\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Mostrar Valores Delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Mover Marcados para o Lixo…\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Mover Marcados para…\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Copiar Marcados para…\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Remover Marcados dos Resultados\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Repriorizar Resultados…\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Remover Seleção dos Resultados\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Adicionar Seleção à Lista Ignorar\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Fazer da Seleção Referência\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Abrir Seleção com Aplicativo Padrão\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Abrir Pasta da Seleção\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Renomear Seleção\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Marcar Tudo\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Desmarcar Tudo\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Inverter Marcação\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Marcar Seleção\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Exportar como HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Exportar como CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Salvar Resultados…\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Executar Comando Personalizado\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Marcar\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Colunas\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Restaurar Padrões\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"Resultados do {}\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Duplicatas\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Valores Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Selecione onde salvar seus resultados\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ignorar arquivos menores que\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"Resultados do %@\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Ação\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Adicionar Pasta…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Avançado\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Buscar atualizações automaticamente\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Básico\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Trazer Todas para Frente\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Buscar Atualizações…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Fechar Janela\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Copiar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Comando personalizado (argumentos: %d (dup), %r (ref)):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Cortar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Detalhes do Arquivo Selecionado\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Painel de Detalhes\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Pastas\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Preferências do dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Resultados do dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Site do dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Editar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Exportar Resultados para CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Exportar Resultados para XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"- resultados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Filtrar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Pressão do filtro:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Filtrar Resultados…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Janela de Seleção de Pasta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Tam. Fonte:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Ocultar dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Ocultar Outros\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ignorar arquivos menores que:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Carregar do arquivo…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Minimizar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Modo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"+ resultados\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Colar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Preferências…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Visualização Rápida\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Encerrar dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Restaurar Padrões\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Restaurar Padrões\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Mostrar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Mostrar Seleção no Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Selecionar Tudo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Mover Marcados para o Lixo…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Serviços\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Mostrar Tudo\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Iniciar Escaneamento de Duplicata\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"O nome ‘%@’ já existe.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Janela\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Ampliar/Reduzir\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Filtros de Exclusão\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Resultados da varredura\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Carregar diretórios...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Salvar diretórios...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Selecione um arquivo de diretórios para carregar\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"Diretórios de dupeGuru (* .dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Selecione um arquivo para salvar seus diretórios\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"Diretórios de dupeGuru (* .dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Adicionar\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Restaurar padrões\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"String de teste\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Digite uma expressão regular python aqui...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Digite um caminho ou nome de arquivo do sistema de arquivos aqui...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Essas expressões regulares Python (diferenciando maiúsculas e minúsculas) filtrarão os arquivos durante as varreduras.<br>Directores também terão seu <strong>estado padrão</strong> definido como \\\"Excluído\\\" na guia Diretórios se seu nome corresponder a uma das expressões regulares selecionadas.<br>Para cada arquivo coletado, dois testes são realizados para determinar se deve ou não ignorá-lo completamente:<br><li>\\n\"\n\"1. Expressões regulares sem separador de caminho serão comparadas apenas ao nome do arquivo.<li>2. Expressões regulares com pelo menos um separador de caminho serão comparadas ao caminho completo para o arquivo.</li><br>\\n\"\n\"Exemplo: se você deseja filtrar arquivos .PNG apenas do diretório \\\"Minhas imagens\\\":<br><code>*Minhas\\\\sImagens\\\\\\\\.*\\\\.png</code><br><br>Você pode testar a expressão regular com o botão \\\"string de teste\\\" após colar um caminho falso no campo de teste:<br><code>C:\\\\\\\\Usuário\\\\Minhas Imagens\\\\test.png</code><br><br>\\n\"\n\"As expressões regulares correspondentes serão destacadas.<br>Se houver pelo menos um destaque, o caminho ou nome do arquivo testado será ignorado durante as varreduras.<br><br>Diretórios e arquivos que começam com um ponto '.' são filtrados por padrão.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Erro de compilação:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Aumentar zoom\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Diminuir zoom\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Tamanho normal\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Melhor ajuste\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Modo de cache de imagem:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Substituir os ícones do tema na barra de ferramentas do visualizador\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Use nossos próprios ícones internos em vez dos fornecidos pelo mecanismo de \"\n\"tema\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Mostrar barras de rolagem em visualizadores de imagens\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Quando a imagem exibida não couber na janela de visualização, mostre as \"\n\"barras de rolagem para abranger a visualização\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Use a posição padrão para a barra de guias (requer reinicialização)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Coloque a barra de guias abaixo do menu principal em vez de ao lado dele\\n\"\n\"No MacOS, a barra de guias preencherá a largura da janela.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Use fonte em negrito para referências\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Cor de primeiro plano de referência:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Cor de fundo de referência:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Cor de primeiro plano do delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Mostra a barra de título e pode ser encaixada\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Enquanto a barra de título está oculta, use a tecla modificadora para \"\n\"arrastar a janela flutuante\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"A barra de título só pode ser desativada enquanto a janela está encaixada\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Barra de título vertical\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Altere a barra de título de horizontal na parte superior para vertical no \"\n\"lado esquerdo\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Mostrar barra de abas\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Essas expressões regulares Python (diferenciando maiúsculas e minúsculas) filtrarão os arquivos durante as varreduras.<br>Directores também terão seu <strong>estado padrão</strong> definido como \\\"Excluído\\\" na guia Diretórios se seu nome corresponder a uma das expressões regulares selecionadas.<br>Para cada arquivo coletado, dois testes são realizados para determinar se deve ou não ignorá-lo completamente:<br><li>\\n\"\n\"1. Expressões regulares sem separador de caminho serão comparadas apenas ao nome do arquivo.<li>2. Expressões regulares com pelo menos um separador de caminho serão comparadas ao caminho completo para o arquivo.</li><br>\\n\"\n\"Exemplo: se você deseja filtrar arquivos .PNG apenas do diretório \\\"Minhas imagens\\\":<br><code>*Minhas\\\\sImagens\\\\\\\\.*\\\\.png</code><br><br>Você pode testar a expressão regular com o botão \\\"string de teste\\\" após colar um caminho falso no campo de teste:<br><code>C:\\\\\\\\Usuário\\\\Minhas Imagens\\\\test.png</code><br><br>\\n\"\n\"As expressões regulares correspondentes serão destacadas.<br>Se houver pelo menos um destaque, o caminho ou nome do arquivo testado será ignorado durante as varreduras.<br><br>Diretórios e arquivos que começam com um ponto '.' são filtrados por padrão.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Resultados\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Interface Geral\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Tabela de Resultados\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Janela de Detalhes\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Geral\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Exibição\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Sobre o {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Versão {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Licenciado sob GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Relatório de Erro\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Algo deu errado. Deseja relatar o erro?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\n\"\n\"O que geralmente ajuda muito é adicionar uma descrição de como o erro ocorreu. Obrigado!\\n\"\n\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Ir para o GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Tcheco\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Alemão\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Grega\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Inglês\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Espanhol\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Francês\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Armênio\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Italiano\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japonês\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Coreano\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malaio\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Holandês\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Polonês\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Português Brasileiro\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Russo\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Turco\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ucraniano\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamita\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Chinês (Simplificado)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Limpar Lista\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Buscar…\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ru/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# AHOHNMYC <lqwh2h2cwa@protonmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: AHOHNMYC <lqwh2h2cwa@protonmail.com>, 2023\\n\"\n\"Language-Team: Russian (https://app.transifex.com/voltaicideas/teams/116153/ru/)\\n\"\n\"Language: ru\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Путь к файлу\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Сообщение об ошибке\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Продолжительность\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Битрейт\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Частота дискретизации\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:94\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Имя файла\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Каталог\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Размер (МБ)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Время\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Частота дискретизации\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Тип\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:165 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Время изменения\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Название\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Исполнитель\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Альбом\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Жанр\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Год\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Номер дорожки\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Комментарий\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Совпадение %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Использованные слова\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Количество дубликатов\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Размеры\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Размер (КБ)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Временная отметка EXIF\"\n\n#: core\\prioritize.py:158\nmsgid \"Size\"\nmsgstr \"Размер\"\n"
  },
  {
    "path": "locale/ru/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n# AHOHNMYC <lqwh2h2cwa@protonmail.com>, 2023\n# Eugene Morozov <transifex@emorozov.net>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Eugene Morozov <transifex@emorozov.net>, 2023\\n\"\n\"Language-Team: Russian (https://app.transifex.com/voltaicideas/teams/116153/ru/)\\n\"\n\"Language: ru\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Дубликаты не отмечены. Нечего выполнять.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Дубликаты не выбраны. Нечего выполнять.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Вы собираетесь открыть много файлов за один раз. В зависимости от того, чем \"\n\"файлы будут открыты, это действие может создать настоящий беспорядок. \"\n\"Продолжать?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Проверка на наличие дубликатов\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Загрузка\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Перемещение\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Копирование\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Перемещение в Корзину\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Предыдущее действие до сих пор выполняется. Вы не можете начать новое. \"\n\"Подождите несколько секунд, затем повторите попытку.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Дубликаты не найдены.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Все отмеченные файлы были скопированы успешно.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Все отмеченные файлы были перемещены успешно.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"Все отмеченные файлы были удалены успешно.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Все отмеченные файлы были успешно отправлены в Корзину.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Не удалось загрузить файл: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' уже присутствует в списке.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' не существует.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Все выбранные %d совпадений будут игнорироваться при всех последующих \"\n\"проверках. Продолжить?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Выберите каталог, в который вы хотите скопировать отмеченные файлы\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Выберите каталог, в который вы хотите переместить отмеченные файлы\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Выберите назначение для экспортируемого \"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Не удалось записать в файл: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"Вы не создали пользовательскую команду. Задайте её в настройках.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Вы собираетесь удалить %d файлов из результата поиска. Продолжить?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} групп дубликатов было изменено при реприоритезации.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Выбранные каталоги не содержат файлов для сканирования.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Сбор файлов для сканирования\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s. (%d отменено)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"Собрано {} файлов для сканирования\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"Собрано {} каталогов для сканирования\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"Найдено %d совпадений из %d групп\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Вы перемещаете {} файлов в Корзину.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Регулярные выражения\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"Вы действительно хотите удалить все элементы %d из списка игнорирования?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Имя файла\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Имя файла - Поля\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Имя файла - Поля (без сортировки)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Теги\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Содержание\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Проанализировано %d из %d изображений\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Проверено %d/%d совпадений\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Подготовка для сравнения\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Проверено %d/%d совпадений\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Прочитана EXIF-информация %d/%d фотографий\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Метка времени EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Ни один\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Заканчивается номером\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Не заканчивается номером\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Самый длинный\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Самый короткий\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Наивысший\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Самый низкий\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Новейший\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Старейший\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) дубликатов отмечено.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"фильтр: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Прочитаны метаданные %d/%d файлов\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Почти готово! Вожусь с результатами...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Папки\"\n"
  },
  {
    "path": "locale/ru/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n# AHOHNMYC <lqwh2h2cwa@protonmail.com>, 2023\n# Captain Quake <elizabeth-keen.gardy@simplelogin.co>, 2023\n# Eugene Morozov <transifex@emorozov.net>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Eugene Morozov <transifex@emorozov.net>, 2023\\n\"\n\"Language-Team: Russian (https://app.transifex.com/voltaicideas/teams/116153/ru/)\\n\"\n\"Language: ru\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Выйти\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Настройки\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Список игнорирования\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Очистить кэш изображений\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Справка dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"О dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Открыть журнал отладки\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Вы действительно хотите удалить все кэшированные данные изображений?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Кэш изображений очищен.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} файл (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Параметры удаления\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Создать ссылку вместо удалённого файла\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"После удаления дубликата создать ссылку на эталонный файл на месте \"\n\"удалённого.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Жёсткая ссылка\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Символическая ссылка\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(не поддерживается)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Удалить файл с диска\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Удалить файлы с диска вместо отправки в Корзину. Используйте если нормальный\"\n\" метод удаления не работает.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Приступить\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Отменить\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Атрибут\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Выбранный\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Эталон\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Загрузка результатов…\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Окно результатов\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Добавить каталог…\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Файл\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Вид\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Справка\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Загрузка последних результатов\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Режим приложения:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Музыка\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Изображения\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Обычный\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Тип поиска:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Больше вариантов\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Выберите каталоги для поиска и нажмите \\\"Сканировать\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Загрузить результаты\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Сканировать\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Несохранённые результаты\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Имеются несохранённые результаты, вы действительно хотите выйти?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Выберите каталог для добавления в список сканирования\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Выберите файл результатов для загрузки\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Все файлы (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Результаты dupeGuru (*. dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Начать новую проверку\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Имеются несохранённые результаты, вы действительно хотите продолжить?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Имя\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Состояние\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Исключён\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Обычный\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Удалить выбранные\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Очистить\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Закрыть\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Детали\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Теги для проверки:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Трек\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Исполнитель\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Альбом\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Название\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Жанр\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Год\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Вес слова\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Совпадение похожих слов\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Можно смешивать типы файлов\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Использовать регулярные выражения для фильтров\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Удалять пустые каталоги при удалении или перемещении\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Игнорировать жёсткие ссылки на тот же файл\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Режим отладки (требуется перезапуск)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Искать дубликаты изображений разных размеров\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Уровень фильтрации:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Дополнительные результаты\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Меньше результатов\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Размер шрифта:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Язык:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Копирование и перемещение:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Прямо в каталог назначения\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Восстановить относительный путь\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Восстановить абсолютный путь\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Пользовательская команда (аргументы: %d для дубликата, %r для эталона):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"Новый язык будет загружен при следующем запуске dupeGuru.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Изменить приоритеты дубликатов\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Добавьте критерий в правое поле и нажмите кнопку OK, чтобы поместить \"\n\"дубликаты, которые соответствуют лучшим из этих критериев, в эталонную \"\n\"позицию соответствующих групп. Прочитайте справку для получения \"\n\"дополнительной информации.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Проблемка!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Были проблемы обработки некоторых (или всех) файлов. Причины этих проблем \"\n\"описаны в таблице ниже. Эти файлы не были удалены из результатов поиска.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Показать выбранное\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Действия\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Показать только дубликаты\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Показать значения разницы\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Переместить отмеченные в Корзину…\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Переместить отмеченные в…\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Скопировать отмеченные в…\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Удалить отмеченные из результатов\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Изменить приоритеты результатов…\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Удалить выбранные из результатов\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Добавить выбранные в список игнорирования\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Сделать выбранные эталоном\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Открыть выбранные в приложении по умолчанию\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Открыть каталог с выбранными\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Переименовать выбранные\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Отметить все\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Снять отметки\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Обратить отметки\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Отметить выбранные\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Экспорт в HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Экспорт в CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Сохранить результаты…\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Выполнить пользовательскую команду\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Отметить\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Колонки\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Восстановить значения по умолчанию\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Результаты\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Только дубликаты\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Значения разницы\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Выберите файл, чтобы сохранить ваши результаты\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Игнорировать файлы меньше чем\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"КБ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Результаты\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Действие\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Добавить новый каталог…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Расширенные\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Автоматически проверять наличие обновлений\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Основной\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Все на передний план\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Проверка обновлений…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Закрыть окно\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Копировать\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\"Пользовательская команда (аргументы: %d для дубликата, %r для эталона):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Вырезать\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Разница\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Информация о выбранном файле\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Панель деталей\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Каталоги\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Настройки dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Результаты dupeGuru \"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Вебсайт dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Редактировать\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Экспорт результатов в CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Экспорт результатов в XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Меньше результатов\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Фильтр\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Уровень фильтрации:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Отфильтровать результаты…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Окно выбора каталога\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Размер шрифта:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Скрыть dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Скрыть остальные\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Пропускать файлы меньше чем:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Загрузить из файла…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Свернуть\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Режим\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Доп. результаты\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"OK\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Вставить\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Настройки…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Быстрый просмотр\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Выйти из dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Восстановить значения по умолчанию\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Восстановить значения по умолчанию\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Показать\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Показать выбранное в Finder-е\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Выбрать все\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Переместить отмеченные в Корзину…\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Сервисы\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Показать все\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Начать поиск дубликатов\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Имя '%@' уже существует.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Окно\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Увеличить\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Фильтры исключения\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Результаты сканирования\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Загрузить каталоги...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Сохранить каталоги...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Выберите файл каталогов для загрузки\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"Каталоги dupeGuru (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Выберите файл для сохранения каталогов\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"Каталоги dupeGuru (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Добавить\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Восстановить значения по умолчанию\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Проверить строку\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Введите здесь регулярное выражение Python...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Введите здесь путь к файловой системе или имя файла...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Эти (чувствительные к регистру) регулярные выражения Python будут отфильтровывать файлы во время сканирования.<br>Если имя каталога подходит под какое-либо из выбранных регулярных выражений, для него на вкладке «Каталоги» будет выставлено <strong>состояние по умолчанию</strong> «Исключено».<br>Для каждого файла выполняется две проверки, чтобы определить, следует ли его полностью игнорировать:<br><li>1. Регулярные выражения без разделителя пути будут сравниваться только с именем файла.</li>\\n\"\n\"<li>2. Регулярные выражения, содержащие хотя бы один разделитель пути, будут сравниваться с полным путем к файлу.</li><br>\\n\"\n\"Пример: отфильтровать .PNG из каталога «Мои изображения»:<br><code>.*Мои\\\\sизображения\\\\\\\\.*\\\\.png</code><br><br>Вы можете проверить регулярное выражение с помощью кнопки «Проверить строку», указав, например, такой путь:<br><code>C:\\\\\\\\Пользователь\\\\Мои изображения\\\\test.png</code><br><br>\\n\"\n\"Сработавшие регулярные выражения будут выделены.<br>Если есть хотя бы одно выделение, проверенный путь или имя файла будет проигнорирован во время сканирования.<br><br>Каталоги и файлы, начинающиеся с точки \\\".\\\" по умолчанию будут отфильтрованы.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Ошибка компиляции:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Увеличить масштаб\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Уменьшить масштаб\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Нормальный размер\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Наиболее подходящий\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Режим кеширования изображений:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Переопределить значки темы на панели инструментов средства просмотра\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"Используйте внутренние значки вместо значков, встроенных в тему\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Показывать полосы прокрутки в средствах просмотра изображений\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"Показывать полосы прокрутки для больших изображений\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Использовать позицию по умолчанию для панели вкладок (требуется перезапуск)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Разместите панель вкладок под главным меню, а не рядом с ним.\\n\"\n\"В MacOS панель вкладок заполнит ширину окна.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Использовать жирный шрифт для ссылок\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Цвет шрифта для эталонных файлов:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Цвет фона для эталонных файлов:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Цвет шрифта для дельты:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Показать строку заголовка и может быть закреплен\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Если строка заголовка скрыта, перемещать плавающее окно с клавишей-\"\n\"модификатором\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Строку заголовка можно отключить, только когда окно закреплено\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Вертикальная строка заголовка\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Измените строку заголовка с горизонтальной сверху на вертикальную слева\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Показать панель вкладок\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Эти (чувствительные к регистру) регулярные выражения Python будут отфильтровывать файлы во время сканирования.<br>Если имя каталога подходит под какое-либо из выбранных регулярных выражений, для него на вкладке «Каталоги» будет выставлено <strong>состояние по умолчанию</strong> «Исключено».<br>Для каждого файла выполняется две проверки, чтобы определить, следует ли его полностью игнорировать:<br><li>1. Регулярные выражения без разделителя пути будут сравниваться только с именем файла.</li>\\n\"\n\"<li>2. Регулярные выражения, содержащие хотя бы один разделитель пути, будут сравниваться с полным путем к файлу.</li><br>\\n\"\n\"Пример: отфильтровать .PNG из каталога «Мои изображения»:<br><code>.*Мои\\\\sизображения\\\\\\\\.*\\\\.png</code><br><br>Вы можете проверить регулярное выражение с помощью кнопки «Проверить строку», указав, например, такой путь:<br><code>C:\\\\\\\\Пользователь\\\\Мои изображения\\\\test.png</code><br><br>\\n\"\n\"Сработавшие регулярные выражения будут выделены.<br>Если есть хотя бы одно выделение, проверенный путь или имя файла будет проигнорирован во время сканирования.<br><br>Каталоги и файлы, начинающиеся с точки \\\".\\\" по умолчанию будут отфильтрованы.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Результаты\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Общий интерфейс\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Таблица с результатами\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Окно с подробностями\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Общие\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Внешний вид\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Частично хешировать большие файлы, размером более\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"МБ\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"Использовать системное окно выбора файлов и папок\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"При выборе файлов или папок для добавления в список сканирования будет \"\n\"использоваться диалоговое окно системы, а не встроенное в dupeGuru. \"\n\"Некоторые системные диалоговые окна имеют ограниченную функциональность.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Игнорировать файлы больше чем\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Очистить кэш\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"Вы действительно хотите очистить кэш? Это удалит все кэшированные хеши \"\n\"файлов и анализ данных изображений.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Кэш очищен \"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Использовать темную тему\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Сохранить профиль сканирования\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\"В папке установленной или портативной программы, есть папка Data в которую \"\n\"сохраняется логи и файл с раширением *.profile для оптимизации.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"Сохранять отчеты в: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Отладка\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"О программе {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Версия {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Проверка обновлений...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Под лицензией GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"У вас самая свежая версия\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"Обнаружена новая {} версия, загружать <a href=\\\"{}\\\">тут</a>.\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Сообщение об ошибке\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Что-то пошло не так. Хотите отправить отчёт об ошибке?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Отчеты об ошибках следует сообщать в виде проблем (issues) на GitHub. Вы можете скопировать приведенный выше отчет об ошибке и сообщить о нем на GitHub.\\n\"\n\"\\n\"\n\"Пожалуйста, предварительно сделайте поиск уже существующих проблем в репозитории программы на GitHub. Также убедитесь, что вы используете самую последнюю версию, доступную из репозитория, поскольку ошибка, с которой вы столкнулись, возможно, уже исправлена.\\n\"\n\"\\n\"\n\"Чтобы быстро и качественно решить проблему, очень поможет пошаговое описание действий пришедших к ошибке. Спасибо!\\n\"\n\"\\n\"\n\"Приложение должно продолжать работу после ошибки, хотя и не стабильно. Рекомендуется перезапустить программу.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Перейти на GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Чешский\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Немецкий\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Греческий\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Английский\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Испанский\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Французский\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Армянский\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Итальянский\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Японский\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Корейский\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Малайский\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Голландский\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Польский\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Бразильский\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Русский\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Турецкий\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Украинский\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Вьетнамский\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Китайский (упрощенный)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Очистить список\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Искать...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\"Опция для опытных пользователей для различных специфичных ситуаций. Если не \"\n\"знаете, что делаете, не трогайте!\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"Включить проверку после завершения сканирования\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"Игнорировать разницу во времени при загрузке кэшированных дайджестов\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"Отменить?\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"Вы уверены, что хотите отменить? Весь прогресс будет потерян.\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/tr/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021\n# Emin Tufan Çetin <etcetin@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>, 2021\\n\"\n\"Language-Team: Turkish (https://www.transifex.com/voltaicideas/teams/116153/tr/)\\n\"\n\"Language: tr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Dosya Konumu\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Hata İletisi\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Süre\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bit Oranı\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Örnek Hızı\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Dosya Adı\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Klasör\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Boyut (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Zaman\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Örnek Hızı\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Tür\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Değişiklik\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Başlık\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Sanatçı\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Albüm\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Tür\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Yıl\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Parça Numarası\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Yorum\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Eşleşme %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Kullanılan Sözcükler\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Kopya Sayısı\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Ölçüler\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Boyut (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Zaman Damgası\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Boyut\"\n"
  },
  {
    "path": "locale/tr/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2021\n# Emin Tufan Çetin <etcetin@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>, 2021\\n\"\n\"Language-Team: Turkish (https://app.transifex.com/voltaicideas/teams/116153/tr/)\\n\"\n\"Language: tr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"İmlenen kopya yok. İşlem yapılmadı.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Seçilen kopya yok. İşlem yapılmadı.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Birden çok dosyayı aynı anda açmaya çalışıyorsunuz. Dosyaların açıldığı \"\n\"programlara bağlı olarak, bu sorun yaratabilir. Sürdürülsün mü?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Kopyalar aranıyor\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Yükleniyor\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Taşınıyor\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Kopyalanıyor\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Çöpe Gönderiliyor\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Önceki işlem hala sürüyor. Yenisini başlatamazsınız. Birkaç saniye bekleyip \"\n\"yeniden deneyin.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Kopya bulunamadı.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"İmlenen tüm dosyalar başarıyla kopyalandı.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"İmlenen tüm dosyalar başarıyla taşındı.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"İmlenen tüm dosyalar başarıyla silindi.\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"İmlenen tüm dosyalar başarıyla çöpe gönderildi.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Dosya yüklenemedi: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' çoktan listede.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' bulunamadı.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Seçilen %d eşleşmenin tümü sonraki taramalarda göz ardı edilecek. Sürdür?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"İmlenen dosyaları kopyalamak için dizin seç\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"İmlenen dosyaları taşımak için dizin seç\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Dışa aktarılan CSV'niz için hedef seçin\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Dosyaya yazılamadı: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"Ayarlı özel komutunuz yok. Tercihlerinizden ayarlayınız.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"%d ögeyi sonuçlardan kaldırıyorsunuz. Sürdür?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} kopya küme, yeniden önceliklendirme tarafından değiştirildi.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Seçili dizinler taranabilir dosya içermiyor.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Taranacak dosyalar toplanıyor\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d göz ardı edilen)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"Taranacak {} dosya toplandı\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"Taranacak {} klasör toplandı\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"%d eşleşme, %d kümeden\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"{} dosyayı Çöp'e gönderiyorsunuz.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Düzenli İfadeler\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"\"\n\"%d ögenin tümünü göz ardı edilenler listesinden kaldırmak istiyor musunuz?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Dosya adı\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Dosya adı - Alanlar\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Dosya Adı - Alanlar (Düzen Yok)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Etiketler\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"İçindekiler\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"%d/%d fotoğraf incelendi\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"%d/%d yığın eşleşmesi gerçekleştirildi\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Eşlemeye hazırlanıyor\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"%d/%d eşleşme doğrulandı\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"%d/%d fotorğafın EXIF'i okundu\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Zaman damgası\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Hiçbiri\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Sayıyla bitenler\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Sayıyla bitmeyenler\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"En uzun\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"En kısa\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"En yüksek\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"En düşük\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"En yeni\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"En eski\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) kopya imlendi.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"süz: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"%d/%d dosyanın üst verisi okundu\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Neredeyse bitti! Sonuçlarla uğraşılıyor...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Dizinler\"\n"
  },
  {
    "path": "locale/tr/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Ahmet Haydar Işık <itsahmthydr@gmail.com>, 2022\n# Emin Tufan Çetin <etcetin@gmail.com>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Turkish (https://app.transifex.com/voltaicideas/teams/116153/tr/)\\n\"\n\"Language: tr\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=2; plural=(n > 1);\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Çık\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Seçenekler\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Göz Ardı Edilenler Listesi\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Fotoğraf Önbelleğini Temizle\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru Yardımı\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"dupeGuru Hakkında\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Hata Ayıklama Günlüğünü Aç\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"Tüm önbelleklenen fotoğraf incelemenizi kaldırmak istediğinize emin misiniz?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Fotoğraf önbelleği temizlendi.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} dosyası (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Silme Seçenekleri\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Silinen dosyaları bağlantıla\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Kopyayı sildikten sonra, silinen dosyayının yerine kaynak dosyayı hedefleyen\"\n\" bağlantı yerleştir.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Katı bağlantı\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Simgesel bağlantı\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(desteklenmiyor)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Doğrudan dosyaları sil\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Dosyaları çöpe göndermek yerine doğrudan sil. Bu seçenek, olağan silme \"\n\"yöntemi çalışmadığında geçici çözüm olarak kullanılır.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Sürdür\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"İptal\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Öznitelik\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Seçili\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Kaynak\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Sonuçları Yükle...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Sonuç Penceresi\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Klasör Ekle...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Dosya\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Görünüm\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Yardım\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Son Sonuçları Yükle\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Uygulama Kipi:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Müzik\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Resim\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Standart\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Tarama Türü:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Daha Çok Seçenek\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Taranacak klasörleri seç ve \\\"Tara\\\"ya bas.\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Sonuçları Yükle\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Tara\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Kaydedilmemiş sonuçlar\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Kaydedilmemiş sonuçlarınız var, çıkmak istediğinizden emin misiniz?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Tarama listesine eklenecek klasör seç\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Yüklenecek sonuç dosyası seç\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Tüm Dosyalar (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru Sonuçları (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Yeni tarama başlat\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"Kaydedilmemiş sonuçlarınız var, sürdürmek istediğinizden emin misiniz?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Ad\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Durum\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Dışlandı\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Olağan\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Seçileni Kaldır\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Temizle\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Kapat\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Ayrıntılar\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Taranacak etiketler:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Parça\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Sanatçı\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Albüm\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Başlık\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Tür\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Yıl\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Sözcük ağırlıklandırması\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Benzer sözcükleri eşle\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Dosya türü karışık olabilir\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Süzerken düzenli ifadeler kullan\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Silme veya taşımada boş klasörleri kaldır\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Aynı dosyaya katı bağlantısı olan kopyaları göz ardı et\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Hata ayıklama kipi (yeniden başlatılmalıdır)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Başka boyutlardaki fotoğrafları eşle\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Süzme Katılığı:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Daha Çok Sonuç\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Daha Az Sonuç\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Yazı tipi boyutu:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Dil:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Kopyala ve Taşı:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Hedefin içine\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Göreceli yolu yeniden yarat\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Mutlak yolu yeniden yarat\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Özel Komut (argümanlar: dupe için %d, ref için %r):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"Dil değişimlerinin gerçekleşmesi için dupeGuru yeniden başlamalıdır.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Kopyaları yeniden önceliklendir\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Sağ kutuya ölçüt ekle ve bu ölçütlere en iyi uyan kopyaları, kendi ilgili \"\n\"kümelerinin kaynak konumuna gönder. Daha çok bilgi için yardım dosyasını \"\n\"okuyun.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Sorunlar!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Bazı (veya tüm) dosyaların işlenmesinde sorun var. Bu sorunların nedeni \"\n\"aşağıdaki tabloda açıklanmıştır. Bu dosyalar sonuçlarınızdan \"\n\"kaldırılmamıştır.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Seçiliyi Göster\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Eylemler\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Yalnızca Kopyaları Göster\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Delta Değerleri Göster\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"İmliyi Geri Dönüşüm Kutusuna Gönder...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"İmliyi Şuraya Taşı...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"İmliyi Şuraya Kopyala...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"İmliyi Sonuçlardan Kaldır\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Sonuçları Yeniden Önceliklendir...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Seçiliyi Sonuçlardan Kaldır\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Seçiliyi Göz Ardı Edilenler Listesine Ekle\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"İmliyi Kaynak Yap\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Seçiliyi Öntanımlı Uygulamayla Aç\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Seçiliyi İçeren Klasörü Aç\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Seçiliyi Yeniden Adlandır\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Tümünü İmle\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Hiçbirini İmleme\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"İmlemeyi Ters Çevir\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Seçiliyi İmle\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"HTML'ye Aktar\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"CSV'ye Aktar\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Sonuçları Kaydet...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Özel Komut Çalıştır\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"İmle\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Sütunlar\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Öntanımlılara Sıfırla\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Sonuçları\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Yalnızca Kopyalar\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Delta Değerler\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Sonuçlarınızın kaydedileceği dosyayı seçin\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Şundan küçük dosyaları göz ardı et:\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Sonuçları\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Eylem\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Yeni Klasör Ekle...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Gelişmiş\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Güncellemeleri kendiliğinden denetle\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Temel\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Tümünü Öne Çıkar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Güncellemeleri denetle...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Pencereyi Kapat\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Kopyala\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Özel komut (argümanlar: dupe için %d , ref için %r )\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Kes\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Seçili Dosyanın Ayrıntıları\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Ayrıntılar Bölmesi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Dizinler\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru Tercihleri\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru Sonuçları\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru Web Sitesi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Düzenle\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Sonuçları CSV'ye Aktar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Sonuçları XHTML'ye Aktar\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Daha az sonuç\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Süz\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Süzme katılığı:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Sonuçları Süz...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Klasör Seçim Penceresi\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Yazı Tipi Boyutu:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"dupeGuru'yu Gizle\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Diğerlerini Gizle\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Şundan küçük dosyaları göz ardı et:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Dosyadan yükle...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Küçült\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Kip\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Daha çok sonuç\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Tamam\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Yapıştır\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Tercihler...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Hızlı Bakış\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"dupeGuru'dan Çık\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Öntanımlıya Sıfırla\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Öntanımlılara Sıfırla\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Göster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Seçiliyi Finder'da Göster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Tümünü Seç\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"İmliyi Çöpe Gönder...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Servisler\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Tümünü Göster\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Kopya Taramayı Başlat\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Halihazırda '%@' adı var.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Pencere\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Yakınlaş\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Dışlama Süzgeçleri\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Tarama Sonuçları\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Dizinleri Yükle...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Dizinleri Kaydet...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Yüklenecek bir dizin dosyası seçin\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Sonuçları  (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Dizinlerinizi kaydetmek için bir dosya seçin\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Dizinleri (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Ekle\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Öntanımlıları geri yükle\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Dizgeyi sına\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Buraya Python düzenli ifadesi yaz...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Buraya dosya sistemi yolu veya dosya adı yaz...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri taramalar sırasında dosyaları süzecektir.<br>Ayrıca dizinlerin adları, seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesinde<strong>öntanımlı durumları</strong>Dışlandı olarak ayarlanır.<br>Toplanan her dosya, tümüyle göz ardı edilip edilmeyeceğiyle ilgili iki kez sınanır:<br><li>1. İçinde yol ayracı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılacaktır.</li>\\n\"\n\"<li>2. İçinde yol ayracı olmayan düzenli ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.</li><br>\\n\"\n\"Örneğin: \\\"Benim Resimlerim\\\" dizininden yalnızca .PNG dosyalarını süzmek istiyorsanız:<br><code>.*Benim\\\\sResimlerim\\\\\\\\.*\\\\.png</code><br><br>Düzenli ifadeyi, dizgeyi sınama özelliğinin içine sahte bir yol yapıştırarak sınayabilirsiniz:<br><code>C:\\\\\\\\Kullanıcı\\\\Benim Resimlerim\\\\sına.png</code><br><br>\\n\"\n\"Eşleşen düzenli ifadeler vurgulanacaktır.<br>En az bir vurgu varsa, sınanan yol taramalar sırasında yok sayılacaktır.<br><br>Nokta '.' ile başlayan dizinler ve dosyalar öntanımlı olarak süzülür.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Derleme hatası:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Yakınlaştırmayı arttır\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Yakınlaştırmayı azalt\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Olağan boyut\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"En uygun\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Resim önbellek kipi:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Görüntüleyici araç çubuğundaki gövde simgelerini geçersiz kıl\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"Gövde motorunca sağlanan yerine kendi iç simgelerimizi kullanın\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Resim görüntüleyicilerde kaydırma çubuklarını göster\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Görüntülenen görüntü görünüm alanına sığmadığında, görünümü etrafa yaymak \"\n\"için kaydırma çubuklarını göster\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Sekme çubuğu için varsayılan konumu kullan (yeniden başlatma gerektirir)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Sekme çubuğunu ana menünün yanına değil altına yerleştirin\\n\"\n\"MacOS'ta sekme çubuğu bunun yerine pencerenin genişliğini dolduracaktır.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Referanslar için kalın yazı tipi kullanın\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Referans ön plan rengi:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Referans arka plan rengi:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Delta ön plan rengi:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Başlık çubuğunu görüntüleyebilir ve sabitleyebilirsiniz.\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Başlık çubuğu gizliyken, kayan pencereyi çevrede sürüklemek için değiştirici\"\n\" tuşu kullan\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\"Başlık çubuğu yalnızca pencere sabitlendiğinde devre dışı bırakılabilir\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Dikey başlık çubuğu\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Başlık çubuğunu üstte yataydan sol tarafta dikey olarak değiştirin\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Sekme çubuğunu göster\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Bu (büyük/küçük harfe duyarlı) python düzenli ifadeleri taramalar sırasında dosyaları süzecektir.<br>Ayrıca dizinlerin adları, seçilen düzenli ifadelerden biriyle eşleşirse, Dizinler sekmesinde <strong>öntanımlı durumları</strong> Dışlandı olarak ayarlanır.<br>Toplanan her dosya için, tümüyle göz ardı edilip edilmeyeceğini belirlemek için iki kez sınanır:<br><li>1. İçinde yol ayracı olmayan düzenli ifadeler yalnızca dosya adıyla karşılaştırılacaktır.</li>\\n\"\n\"<li>2. İçinde en az bir yol ayracı bulunan düzenli ifadeler, dosyanın tam yolu ile karşılaştırılacaktır.</li><br>\\n\"\n\"<br>Örneğin: \\\"Benim Resimlerim\\\" dizininden yalnızca .PNG dosyalarını süzmek istiyorsanız:<code>.*Benim\\\\sResimlerim\\\\\\\\.*\\\\.png</code><br><br>Sınama alanına sahte bir yol yapıştırdıktan sonra düzenli ifadeyi \\\"Dizgeyi sına\\\" düğmesiyle sınayabilirsiniz:<br><code>C:\\\\\\\\Kullanıcı\\\\Benim Resimlerim\\\\sınama.png</code><br><br>\\n\"\n\"Eşleşen düzenli ifadeler vurgulanacaktır.<br>En az bir vurgu varsa, sınanan yol veya dosya adı taramalar sırasında yok sayılacaktır.<br><br>Nokta '.' ile başlayan dizinler ve dosyalar öntanımlı olarak süzülür.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Sonuçlar\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Genel Arayüz\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Sonuç Tablosu\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Ayrıntı Penceresi\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Genel\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Görüntüle\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"Şundan büyük dosyaları kısmen özetle:\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"İşletim sistemi yerel iletişim kutularını kullan\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\"Dosya/klasör seçimi gibi eylemlerde işletim sistemi yerel iletişim kutularını kullan.\\n\"\n\"Bazı yerel iletişim kutuları sınırlı işlevselliktedir.\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"Şundan büyük dosyaları göz ardı et:\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"Önbelleği Temizle\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"Önbelleği gerçekten temizlemek istiyor musunuz? Bu, tüm önbelleklenen dosya \"\n\"özetleri ve resim incelemelerini kaldıracak.\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"Önbellek temizlendi.\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"Karanlık biçem kullan\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"Tarama işlemini profille\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"Tarama işlemini profille ve iyileştirme için günlükleri kaydet.\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"Günlükler şuradadır: <a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"Hata ayıklama\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"{} Hakkında\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Sürüm {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"Güncellemeler denetleniyor...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"GPLv3 altında lisanslanmıştır.\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"Güncelleme yok.\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"Yeni {} sürümü var, <a href=\\\"{}\\\">buradan</a> indirilebilir.\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Hata Raporu\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Bir şeyler yanlış gitti. Hatayı raporlamak ister misin?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Hata raporları GitHub'da sorun (issue) olarak bildirilmelidir. Yukarıdaki hata kaynağını kopyalayabilir ve yeni sorun bildirimine yapıştırabilirsiniz\\n\"\n\"\\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\"\\n\"\n\"Hatayı nasıl aldığınızın açıklamasını eklemeniz gerçekten yardımcı olabilir. Teşekkürler!\\n\"\n\"\\n\"\n\"Bu hatadan sonra uygulama çalışmaya sürdürebilse de kararsız durumda olabilir, bu nedenle uygulamayı yeniden başlatmanız önerilir.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"GitHub'a Git\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Çekçe\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Almanca\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Yunanca\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"İngilizce\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"İspanyolca\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Fransızca\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Ermenice\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"İtalyanca\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Japonca\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Korece\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Malayca\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Felemenkçe\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Lehçe\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Brezilya Portekizcesi\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Rusça\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Türkçe\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Ukraynaca\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Vietnamca\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Çince (Basitleştirilmiş)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Listeyi Temizle\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Ara...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/ui.pot",
    "content": "#\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/uk/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Ukrainian (https://www.transifex.com/voltaicideas/teams/116153/uk/)\\n\"\n\"Language: uk\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Шлях до файлу\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Повідомлення про помилку\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Тривалість\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Якість звуку\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Частота оцифровки\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Ім’я файлу\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Папка\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Розмір (Мб)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Час\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Частота дискретизації\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Тип\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Дата модифікації\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Назва\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Виконавець\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Альбом\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Жанр\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Рік\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Номер доріжки\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Коментар\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Збіг (%)\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Використані слова\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Кількість дублікатів\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Виміри\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Розмір (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"Відмітка часу EXIF\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Розмір\"\n"
  },
  {
    "path": "locale/uk/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Ukrainian (https://app.transifex.com/voltaicideas/teams/116153/uk/)\\n\"\n\"Language: uk\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"Немає позначених дублікатів - нічого робити.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"Немає обраних дублікатів - нічого робити.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"Ви збираєтеся відкрити багато файлів одночасно.\\n\"\n\"Залежно від того, з чим відкриваються ці файли, це може створити неабияку халепу. Продовжити?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Пошук дублікатів\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Завантаження\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Переміщення\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Копіювання\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Відправка до кошику\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"Попередню дію ще не закінчено. Ви покищо не можете розпочаті нову. Зачекайте\"\n\" кілька секунд, потім повторіть спробу.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Не знайдено жодного дублікату.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Усі позначені файли були скопійовані успішно.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Усі позначені файли були переміщені успішно.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"Усі позначені файли були успішно відправлені до кошика.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Не вдалося завантажити файл: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' вже є в списку.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' не існує.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"Усі обрані %d результатів будуть ігноруватися під час усіх наступних \"\n\"пошуків. Продовжити?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"Виберіть каталог для копіювання позначених файлів\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"Виберіть каталог, куди ви хочете перемістити позначені файли\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Виберіть каталог, куди потрібно скопіювати позначені файли\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Не вдалося записати у файл: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"Власна команда не встановлена. Встановіть її у налаштуваннях.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Ви збираєтеся видалити %d файлів з результату пошуку. Продовжити?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"\"\n\"{} повторюваних груп було змінено шляхом повторного встановлення \"\n\"пріоритетів.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Обрані папки не містять файлів придатних для пошуку.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Збір файлів для пошуку\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d відкинуто)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Ви надсилаєте {} файлів у Кошик.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Регулярні вирази\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Ви дійсно хочете видалити всі %d елементів з чорного списку?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Ім'я файлу\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Назва файлу - поля\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Назва файлу - поля (без замовлення)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Теги\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Зміст\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Проаналізовано %d/%d фотографій\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Виконано %d/%d порівнянь шматків\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Підготовка до порівняння\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Перевірено %d/%d результатів\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Прочитано EXIF з %d/%d фотографій\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Відмітка часу EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Жоден\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Закінчується номером\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Не закінчується номером\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Найдовший\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Найкоротший\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Найвища\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Найнижча\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Найновіші\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Найдавніший\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) дублікатів позначено.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \"фільтр: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Прочитано метаданих з %d/%d файлів\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Майже зроблено! Возився з результатами...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Папки\"\n"
  },
  {
    "path": "locale/uk/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Ukrainian (https://app.transifex.com/voltaicideas/teams/116153/uk/)\\n\"\n\"Language: uk\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\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\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Вихід\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Опції\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Чорний список\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Очистити кеш зображень\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Довідка dupeGuru\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Про dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Відкрити журнал налагодження\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"Ви дійсно хочете видалити всі кешовані результати аналізу зображень?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Кеш зображень очищено.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} файл (*. {})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"варіанти делеций\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Посилання на видалені файли\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"Після того, як віддаляється дублікат, розмістити посилання таргетування \"\n\"посилального файлу, щоб замінити віддалений файл.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Hardlink\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Симлінк\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \"(не підтримується)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Безпосередньо видаляйте файли\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"Замість надсилання файлів у кошик, видаліть їх безпосередньо. Цей варіант \"\n\"зазвичай використовується як обхідний спосіб, коли звичайний метод видалення\"\n\" не працює.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Продовжуйте\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Скасувати\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Атрибут\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Обраний\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Посилання\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Завантажити результати ...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Вікно результатів\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Додати папку ...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Файл\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Вид\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Допомога\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Завантажити нещодавні результати\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Режим застосування:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Музика\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"зображення\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Стандартний\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Тип пошуку:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Більше варіантів\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Оберіть папки для пошуку і натисніть \\\"Шукати\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Завантажити результати\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Шукати\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Незбережені результати\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Ви маєте незбережені результати, ви дійсно хочете вийти?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Оберіть папку для додання в список пошуку\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Виберіть файл результатів для завантаження\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Всі файли (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"Результати dupeGuru (*.dupeguru) \"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Почати новий пошук\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"Ви маєте незбережені результати, ви дійсно хочете продовжити?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Ім'я\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Стан\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Виключений\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Нормальний\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Видалити обрані\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Очистити\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Закрити\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Деталі\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Теги для пошуку:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Трек\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Артист\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Альбом\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Назва\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Жанр\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Рік\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Порівнювати за словами\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Вважати схожі слова однаковими\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Можна змішувати типи файлів\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Використовувати регулярних виразів при фільтрації\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Видалити порожні папки під час видалення чи переміщення\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Ігнорувати дублікати, що є жорсткими посиланнями на той самий файл\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Режим налагодження (потрібен перезапуск)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Порівнювати малюнки різних розмірів\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Жорсткість фільтру:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Більше результатів\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Менше результатів\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Розмір шрифта:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Мова:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Копіювання і переміщення:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Прямо у цільову папку\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Відтворити відносний шлях\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Відтворити абсолютний шлях\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Власна команда (аргументи: %d для дублікату, %r для посиланя):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru необхідно перезапустити для застосування зміни мови.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Змінити пріоритети дублікатів\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Додайте критерії в праве поле і натисніть кнопку ОК, щоб відправити \"\n\"дублікати, які найкраще відповідають цим критеріям, до вихідної позиції \"\n\"відповідних груп. Прочитайте файл довідки для отримання додаткової \"\n\"інформації.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Проблеми!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"Виникли проблеми під час обробки деяких (або всіх) файлів. Причини цих \"\n\"проблем описані в таблиці нижче. Ці файли не були видалені з результатів \"\n\"пошуку.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Показати вибрані\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Дії\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Показати тільки дуплікати\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Показати різницю\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Надіслати позначене до кошику...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Перемістити позначене до ...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Скопіювати позначене до ...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Видалити позначене з результатів\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Змінити пріоритети результатів ...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Видалити обране з результатів\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Додати обране в чорний список\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Зробити обраний елемент в довідковий пункт.\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Відкрити обране програмою за умовчанням\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Відкрити папку, що містить обране\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Перейменувати обране\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Позначити всі\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Скинути позначення\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Інвертувати позначення\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Позначити обране\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Експорт в HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Експорт у CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Зберегти результати ...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Викликати власну команду\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Позначити\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Колонки\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Відновити налаштування за замовчуванням\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Результати\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Тільки дублікати\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Різниця\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Оберіть файл у який слід зберегти ваші результати\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Ігнорувати файли менші ніж\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"КБ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Результати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Дія\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Додати нову папку ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"Розширені\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Автоматично перевіряти наявність оновлень\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Основні\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Всі на передній план\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Перевірити оновлення ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Закрити вікно\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Копіювати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Власна комада (аргументи: %d для дублікату, %r для посилання):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Вирізати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Різниця\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Інформація про обраний файл\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Панель інформації\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Папки\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Налаштування dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Результати dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Веб-сайт dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Редагувати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Експортувати результати до CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Експорт результатів в XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Менше результатів\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Фільтр\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Жорсткість фільтру:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Фільтрувати результати ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Вікно вибору папок\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Розмір шрифту:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Приховати dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Сховати інші\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Ігнорувати файли менші ніж:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Завантажити з файлу ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Мінімізувати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Режим\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Більше результатів\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Ok\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Вставити\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Налаштування ...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Швидкий перегляд\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Вийти з dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Відновити налаштування за замовчуванням\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Скинути до значень за замовчуванням\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"показувати\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Показати вибране у Finder\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Виберіть Усі\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Надіслати позначене до кошику...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Послуги\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Показати всі\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Почати пошук дублікатів\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Ім’я '%@' вже існує.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Вікно\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Збільшити\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Фільтри виключення\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Результати сканування\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Завантажити каталоги...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Зберегти каталоги...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Виберіть файл каталогів для завантаження.\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru довідники (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Виберіть файл, до якого зберігатимуться ваші каталоги\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru довідники (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Додати\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Відновити значення за замовчуванням\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Тестовий рядок\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Введіть сюди регулярний вираз python...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Введіть тут шлях до файлової системи або ім’я файлу...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ці регулярні вирази python (чутливі до регістру) фільтруватимуть файли під час сканування.<br>Також для каталогів буде встановлено <strong>статус за замовчуванням</strong> \\\"Виключено\\\" на вкладці \\\"Каталоги\\\", якщо їх ім'я збігається з одним із вибраних регулярних виразів.<br>Для кожного зібраного файлу проводяться два тести, щоб визначити, чи повністю його ігнорувати:<br><li>1. Регулярні вирази, у яких немає роздільника шляху, будуть порівнюватися лише з назвою файлу.</li>\\n\"\n\"<li>2. Регулярні вирази, що містять принаймні один роздільник шляхів, будуть порівняні з повним шляхом до файлу.</li><br>\\n\"\n\"Приклад: якщо ви хочете відфільтрувати файли PNG лише з каталогу \\\"Мої фотографії\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Ви можете перевірити регулярний вираз за допомогою кнопки \\\"тестовий рядок\\\" після вставки підробленого шляху в тестове поле:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Відповідні регулярні вирази будуть виділені. <br>Якщо є хоча б одне виділення, тестований шлях або ім’я файлу буде проігноровано під час сканування.<br><br>Каталоги та файли, що починаються з крапки '.' за замовчуванням відфільтровуються.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Помилка компіляції:\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Збільште масштабування\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Зменште масштабування\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Звичайний розмір\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Найкраще підходить\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Режим кешування зображення:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Замінити значки тем на панелі інструментів засобу перегляду\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"Використовуйте наші власні внутрішні піктограми замість тих, що надаються \"\n\"механізмом теми\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Показувати смуги прокрутки в засобах перегляду зображень\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Коли зображення, що відображається, не відповідає області перегляду, \"\n\"покажіть смуги прокрутки, щоб перемістити подання\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"\"\n\"Використовуйте положення за замовчуванням для панелі вкладок (потрібно \"\n\"перезапуск)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Розташуйте панель вкладок під головним меню, а не поруч з нею.\\n\"\n\"На MacOS, панель вкладок буде заповнити ширину вікна замість цього.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Використовуйте жирний шрифт для посилальних елементів.\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Довідковий колір переднього плану:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Довідковий колір тла:\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Колір переднього плану Delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Показати рядок заголовка і може бути пристикований\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Поки рядок заголовка прихований, за допомогою клавіші модифікатора \"\n\"перетягніть плаваюче вікно навколо\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Рядок заголовка можна вимкнути лише тоді, коли вікно закріплено\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Вертикальний рядок заголовка\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"\"\n\"Змініть рядок заголовка з горизонтального зверху на вертикальний зліва\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Показати панель вкладок\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"Ці регулярні вирази python (чутливі до регістру) фільтруватимуть файли під час сканування.<br>Також для каталогів буде встановлено <strong>статус за замовчуванням</strong> \\\"Виключено\\\" на вкладці \\\"Каталоги\\\", якщо їх ім'я збігається з одним із вибраних регулярних виразів.<br>Для кожного зібраного файлу проводяться два тести, щоб визначити, чи повністю його ігнорувати:<br><li>1. Регулярні вирази, у яких немає роздільника шляху, будуть порівнюватися лише з назвою файлу.</li>\\n\"\n\"<li>2. Регулярні вирази, що містять принаймні один роздільник шляхів, будуть порівняні з повним шляхом до файлу.</li><br>\\n\"\n\"Приклад: якщо ви хочете відфільтрувати файли PNG лише з каталогу \\\"Мої фотографії\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>Ви можете перевірити регулярний вираз за допомогою кнопки \\\"тестовий рядок\\\" після вставки підробленого шляху в тестове поле:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Відповідні регулярні вирази будуть виділені. <br>Якщо є хоча б одне виділення, тестований шлях або ім’я файлу буде проігноровано під час сканування.<br><br>Каталоги та файли, що починаються з крапки '.' за замовчуванням відфільтровуються.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Результати\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Загальний інтерфейс\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Таблиця результатів\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Вікно деталей\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Загальні\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Дисплей\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Про {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Версія {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Ліцензовано згідно з GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Повідомлення про помилки\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Щось пішло не так. Як щодо повідомлення про помилку? \"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"Звіти про помилки слід повідомляти як проблеми GitHub. Ви можете скопіювати помилку відстеження помилки вище та вставити її в нове видання.\\n\"\n\"\\n\"\n\"Будь ласка, не забудьте заздалегідь здійснити пошук уже існуючих проблем. Також не забудьте протестувати найновішу версію, доступну зі сховища, оскільки виправлена помилка, можливо, вже виправлена.\\n\"\n\"\\n\"\n\"Зазвичай справді допомагає, якщо ви додаєте опис того, як ви отримали помилку. Дякую!\\n\"\n\"\\n\"\n\"Незважаючи на те, що програма повинна продовжувати працювати після цієї помилки, вона може бути в нестабільному стані, тому рекомендується перезапустити програму.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Перейдіть до GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Чеська\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Німецька\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Німецька\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Англійська\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Іспанська\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"Французька\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"Вірменська\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Італійська\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"Японський\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Корейська\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Малайська\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Голландська\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Польська\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Бразильська\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Російська\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Турецька\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Українська\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"В'єтнамська\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Китайська (спрощена)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Очистити список\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Шукати...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/vi/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2021\\n\"\n\"Language-Team: Vietnamese (https://www.transifex.com/voltaicideas/teams/116153/vi/)\\n\"\n\"Language: vi\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"Đường dẫn tập tin\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"Thông báo lỗi\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"Độ dài\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"Bitrate\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"Samplerate\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"Tên tập tin\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"Thư mục\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"Kích thước (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"Thời gian\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"Sample Rate\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"Loại\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"Chỉnh sửa\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"Tiêu đề\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"Nghệ sĩ\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"Loại nhạc\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"Năm\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"Số track\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"Bình luận\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"Tỉ lệ khớp %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"Từ được dùng\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"Số lần bị lừa\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"Chiều\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"Kích thước (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF Timestamp\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"Kích thước\"\n"
  },
  {
    "path": "locale/vi/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Fuan <jcfrt@posteo.net>, 2021\\n\"\n\"Language-Team: Vietnamese (https://app.transifex.com/voltaicideas/teams/116153/vi/)\\n\"\n\"Language: vi\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"\"\n\"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.\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"\"\n\"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.\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"\"\n\"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 \"\n\"đượ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?\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"Quét các phần trùng nhau\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"Đang tải\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"Đang di chuyển\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"Đang sao chép\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"Đang gửi vào thùng rác\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"\"\n\"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 \"\n\"phần khác. Hãy đợi trong vài giây, và sau đó thử lại lần nữa.\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"Không tìm thấy thành phần trùng nhau.\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"Tất cả tập tin được đánh dấu đã được sao chép thành công.\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"Tất cả các tập tin được đánh dấu đã được di chuyển thành công.\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"\"\n\"Tất cả các tập tin được đánh dấu đã được gửi đến Thùng Rác thành công.\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"Không thể tải tệp: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' đã tồn tại trong danh sách.\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' không tồn tại.\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"\"\n\"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. \"\n\"Tiếp tục?\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"\"\n\"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\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"\"\n\"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\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"Chọn một điểm xuất dữ liệu dạng CSV\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"Không thể ghi vào tệp: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"\"\n\"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 \"\n\"trong phần tùy biến của bạn.\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"Bạn chuẩn bị loại bỏ %d tập tin từ phần kết quả. Tiếp tục?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} các nhóm trùng nhau đã được thay đổi bởi thứ tự-tái ưu tiên.\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"Các thứ mục được chọn chứa các tập tin không thể quét được.\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"Đang thu thập các tập tin để quét\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d bị bỏ qua)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"Bạn sắp sửa gửi {} (các)tập tin đến Thùng Rác.\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"Biểu thức chính quy\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"Bạn có thực sự muốn loại bỏ tất cả %d đối tượng từ danh sách bỏ qua?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"Tên tệp\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"Tên tệp - Trường\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"Tên tệp - Trường (Không có thứ tự)\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"Tags\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"Nội dung\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"Đã phân tích %d/%d hình ảnh\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"Đã thể thiện %d/%d các phần khớp nhau\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"Đang chuẩn bị phần khớp nhau\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"Đã xác nhận %d/%d phần khớp nhau\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"Đọc thông tin EXIF của %d/%d hình ảnh\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"Dấu thời gian EXIF\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"Không \"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"Tận cùng là số\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"Tận cùng không chứa số\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"Dài nhất\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"Ngắn nhất\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"Cao nhất\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"Thấp nhất\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"Mới nhất\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"Cũ nhất\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"%d / %d (%s / %s) phần trùng nhau đã được đánh dấu.\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" bộ lọc: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"Đọc thông tin chi tiết của %d/%d tập tin\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"Sắp xong! Loay hoay với kết quả...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"Thư mục\"\n"
  },
  {
    "path": "locale/vi/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Vietnamese (https://app.transifex.com/voltaicideas/teams/116153/vi/)\\n\"\n\"Language: vi\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"Thoát\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"Tùy chọn\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"Danh sách bỏ qua\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"Dọn dẹp bộ nhớ đệm của hình ảnh\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"Trợ giúp\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"Dịch bởi Phan Anh\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"Mở nhật trình gỡ rối\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"\"\n\"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 \"\n\"không?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"Đã dọn dẹp bộ nhớ đệm xử lý hình ảnh.\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} tập tin (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"Xóa tùy chọn\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"Liên kết đến các tập tin đã bị xóa\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"\"\n\"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 \"\n\"chiếu đến tập tin để thay thế tập tin đã được xóa.\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"Liên kết cứng\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"Liên kết biểu tượng\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (chưa được hỗ trợ)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"Trực tiếp xóa các tập tin\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"\"\n\"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\" 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 \"\n\"những tập tin theo cách thông thường không được áp dụng.\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"Tiếp tục\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"Hủy bỏ\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"Thuộc tính\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"Được chọn\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"Tham chiếu\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"Tải kết quả...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"Cửa sổ hiển thị kết quả\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"Thêm thư mục...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"Tập tin\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"Xem\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"Trợ giúp\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"Tải các kết quả gần đây\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"Chế độ ứng dụng:\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"Âm nhạc\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"Hình ảnh\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"Tiêu chuẩn\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"Loại thao tác quét:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"Các tùy chọn khác\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"Chọn thư mục để quét và nhấn \\\"Quét\\\".\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"Tải kết quả\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"Quét\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"Các kết quả chưa lưu\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"Bạn vẫn chưa lưu các kết quả, bạn có thực sự muốn thoát?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"Chọn một thư mục để thêm vào danh sách quét\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"Chọn một tập tin kết quả để tải\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"Tất cả tập tin (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"tập tin kết quả của dupeGuru (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"Bắt đầu quá trình quét mới\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"\"\n\"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?\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"Tên\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"Trạng thái\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"Bao gồm\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"Bình thường\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"Loại bỏ phần đã chọn\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"Dọn dẹp\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"Đóng lại\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"Chi tiết\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"Thẻ đánh dấu để quét:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"Track\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"Nghệ sĩ\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"Album\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"Tiêu đề\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"Loại nhạc\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"Năm\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"Độ rộng của từ\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"Các từ tương tự và khớp lẫn nhau\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"Có thể pha trộn loại tập tin\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"Sử dụng các biểu thức thông thường khi lọc dữ liệu\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"Loại bỏ các thư mục rỗng khi xóa hoặc di chuyển\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"Bỏ qua các liên kết cứng đến phần trùng nhau trong cùng một tập tin\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"Chế độ gỡ rối (yêu cầu khởi động lại)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"Chỉ các hình ảnh khớp nhau với các chiều khác nhau\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"Lọc theo nguyên tắc:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"Nhiều kết quả hơn\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"Ít kết quả hơn\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"Kích thước kiểu chữ:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"Ngôn ngữ:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"Sao chép và di chuyển:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"Ngay tại đích đến\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"Tạo lại các đường dẫn liên hệ lẫn nhau\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"Tạo lại đường dẫn xác thực\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Lệnh tùy chọn (đối số: %d cho trùng nhau, %r cho tham số):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"\"\n\"dupeGure phải khởi động lại để việc thay đổi ngôn ngữ giao diện được áp \"\n\"dụng.\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"Tái-Ưu tiên phần trùng nhau\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"Xảy ra vấn đề!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"Biểu hiện các phần được chọn\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"Thao tác\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"Chỉ hiển thị trùng\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"Hiển thị giá trị Delta\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"Gửi các phần đánh dấu vào Thùng Rác...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"Di chuyển phần đánh dấu đến...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"Sao chép phần đánh dấu đến...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"Loại bỏ phần được đánh dấu khỏi kết quả\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"Tái-ưu tiên kết quả...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"Loại bỏ các phần được chọn khỏi kết quả\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"Thêm phần được chọn vào danh sách bỏ qua\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"Đem các phần được đánh dấu vào mục Tham Chiếu\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"Mở phần được chọn với ứng dụng mặc định\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"Mở thư mục chứa phần được lựa chọn\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"Lựa chọn được đổi tên\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"Đánh dấu tất cả\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"Không đánh dấu đối tượng nào\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"Chuyển đổi phần đánh dấu\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"Đánh dấu phần được chọn\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"Xuất sang định dạng HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"Xuất sang định dạng CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"Lưu kết quả...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"Dòng lệnh tùy chọn khẩn cấp\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"Đánh dấu\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"Cột\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"Thiết đặt lại về mặc định\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} Kết quả\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"Chỉ trùng nhau\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Giá trị Delta\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"Chọn một tập tin để lưu kết quả vào\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"Bỏ qua các tập tin có kích thước nhỏ hơn\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ Kết quả\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"Thao tác\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"Thêm thư mục mới...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"nâng cao\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"Tự động kiểm tra cập nhật\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"Cơ bản\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"Đem tất cả lên phía trước\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"Kiểm tra cập nhật...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"Đóng lại cửa sổ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"Sao chép\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"Lệnh tùy chọn (đối số: %d cho phần trùng, %r cho phần đối chiếu):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"Cắt\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"Chi tiết tập tin được chọn\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"Khung cửa sổ chi tiết\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"Thư mục\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"Tùy biến\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"Kết quả\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"Website\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"Chỉnh sửa\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"Xuất kết quả sang dạng CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"Xuất kết quả sang dạng XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"Ít kết quả hơn\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"Lọc dữ liệu\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"Lọc dữ liệu theo quy luật:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"Kết quả dữ liệu được lọc...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"Cửa sổ chọn thư mục\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"Kích thước kiểu chữ:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"Ẩn chương trình\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"Ẩn các phần khác\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"Bỏ qua các tập tin nhỏ hơn:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"Tải từ tập tin...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"Thu nhỏ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"Chế độ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"Thêm các kết quả\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"Đồng ý\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"Dán\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"Tùy biến...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"Xem sơ lược\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"Thoát chương trình\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"Thiết đặt lại về chế độ mặc định\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"Thiết đặt lại về chế độ mặc định\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"Biểu hiện\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"Biểu hiện các phần được chọn trong phần Tìm Kiếm\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"Chọn tất cả\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"Gửi phần được đánh dấu vào Thùng Rác...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"Dịch vụ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"Hiển thị tất cả\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"Bắt đầu quét phần trùng nhau\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"Tên '%@' đã tồn tại.\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"Cửa sổ\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"Phóng to\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"Bộ lọc Loại trừ\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"Quét kết quả\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"Nạp Thư mục...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"Lưu các thư mục...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"Chọn một tập tin thư mục để tải\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru Thư mục (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"Chọn một tệp để lưu các thư mục của bạn vào\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru Thư mục (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"Thêm vào\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"Khôi phục mặc định\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"Thử nghiệm chuỗi ký tự\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"Nhập một biểu thức chính quy python tại đây...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"Nhập đường dẫn hệ thống tệp hoặc tên tệp tại đây...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"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.<br>Các thư mục cũng sẽ có <strong>trạng thái mặc định</strong> đượ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.<br>Đố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:<br><li>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.</li>\\n\"\n\"<li>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.</li><br>\\n\"\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\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Các cụm từ thông dụng phù hợp sẽ được đánh dấu.<br>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.<br><br>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.<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"Lỗi biên dịch\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"Tăng thu phóng\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"Giảm thu phóng\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"Kích thước bình thường\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"Phù hợp nhất\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"Chế độ bộ nhớ cache hình ảnh:\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"Ghi đè các biểu tượng chủ đề trong thanh công cụ của trình xem\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"\"\n\"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 \"\n\"được cung cấp bởi công cụ chủ đề\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"Hiển thị thanh cuộn trong trình xem hình ảnh\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"\"\n\"Khi hình ảnh được hiển thị không vừa với chế độ xem, hãy hiển thị các thanh \"\n\"cuộn để di chuyển chế độ xem xung quanh\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"Sử dụng vị trí mặc định cho thanh tab (yêu cầu khởi động lại)\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"Đặt thanh tab bên dưới menu chính thay vì bên cạnh nó.\\n\"\n\"Trên MacOS, thay vào đó, thanh tab sẽ lấp đầy chiều rộng của cửa sổ.\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"Sử dụng phông chữ đậm cho tài liệu tham khảo\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"Màu nền trước tham khảo:\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"Màu nền tham chiếu\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Màu nền trước Delta:\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"Hiển thị thanh tiêu đề và có thể được cập cảng\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"\"\n\"Trong khi thanh tiêu đề bị ẩn, hãy sử dụng phím bổ trợ để kéo cửa sổ nổi \"\n\"xung quanh\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"Thanh tiêu đề chỉ có thể bị vô hiệu hóa khi cửa sổ được gắn vào đế\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"Thanh tiêu đề dọc\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"Thay đổi thanh tiêu đề từ ngang ở trên, sang dọc ở bên trái\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"Hiển thị thanh tab\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"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.<br>Các thư mục cũng sẽ có <strong>trạng thái mặc định</strong> đượ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.<br>Đố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:<br><li>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.</li>\\n\"\n\"<li>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.</li><br>\\n\"\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\\\":<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>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:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Các cụm từ thông dụng phù hợp sẽ được đánh dấu.<br>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.<br><br>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.<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"Kết quả\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"Giao diện chung\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"Kết quả Bảng\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"Cửa sổ chi tiết\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"Chung\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"Trưng bày\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"Về {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"Phiên bản {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"Được cấp phép theo GPLv3\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"Báo cáo lỗi\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"Đã xảy ra lỗi. Làm thế nào về việc báo cáo lỗi?\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"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\"\\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\"\\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\"\\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.\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"Truy cập GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"Tiếng Séc\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"Tiếng Đức\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"Ngôn ngữ Hy lạp\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"Ngôn ngữ tiếng anh\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"Tiếng Tây Ban Nha\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"ngôn ngữ Pháp\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"ngôn ngữ Armenia\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"Ngôn ngữ Ý\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"tiếng Nhật\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"Ngôn ngữ Hàn Quốc\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"Tiếng Mã lai\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"Tiếng Hà Lan\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"Ngôn ngữ Ba Lan\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"Ngôn ngữ Brazil\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"Ngôn ngữ Nga\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"Tiếng Thổ Nhĩ Kỳ\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"Tiếng Ukraina\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"Ngôn ngữ tiếng Việt\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"Ngôn ngữ Trung Quốc (giản thể)\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"Xóa danh sách\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"Tìm kiếm...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/zh_CN/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Chris Ocelot, 2021\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Chris Ocelot, 2021\\n\"\n\"Language-Team: Chinese (China) (https://www.transifex.com/voltaicideas/teams/116153/zh_CN/)\\n\"\n\"Language: zh_CN\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"文件路径\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"错误信息\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"持续时间\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"比特率\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"采样率\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:92\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"文件名称\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"文件夹\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"大小 (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"时间\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"采样率\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"类型\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:163 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"编辑日期\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"歌曲名\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"作者\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"专辑\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"音乐类型\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"音轨号\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"注释\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"匹配度 %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"使用过的词语\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"重复文件数\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"规格\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"大小 (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 时间戳\"\n\n#: core\\prioritize.py:156\nmsgid \"Size\"\nmsgstr \"大小\"\n"
  },
  {
    "path": "locale/zh_CN/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Andrew Senetar <arsenetar@gmail.com>, 2021\n# Chris Ocelot, 2021\n# Fuan <jcfrt@posteo.net>, 2021\n# YaNing Lu, 2021\n# Mèng yáo, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Mèng yáo, 2023\\n\"\n\"Language-Team: Chinese (China) (https://app.transifex.com/voltaicideas/teams/116153/zh_CN/)\\n\"\n\"Language: zh_CN\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"没有已标记的重复项。无需任何操作。\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"没有已选定的重复项。无需任何操作。\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"您即将一次性打开多个文件。取决于这些文件的默认打开方式，此项操作可能导致非常混乱的状况。是否继续？\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"正在扫描重复内容\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"载入中\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"移动中\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"复制中\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"正在移至回收站\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"前一项操作还在执行，无法启动新操作。请等待几秒钟后再重试一次。\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"没有找到重复文件。\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"所有已标记的文件已复制成功。\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"所有已标记的文件已移动成功。\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"已复制所有标记文件\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"所有已标记的文件已成功移至回收站。\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"无法加载文件：{}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"“{}”已在列表中。\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"“{}”不存在。\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"目前已选的 %d 个匹配项将在后续的扫描中被忽略。是否继续？\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"请选择要将标记文件复制到的目录\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"请选择要将标记文件移动到的目录\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"选择您导出 CSV 的目标文件夹\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"不能写入文件：{}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"您没有设定自定义命令。请在设置中进行设定。\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"您将从结果中移除 %d 个文件。是否继续?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{} 个重复的组已被重新排列。\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"所选目录中不包含可供扫描的文件。\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"收集文件以供扫描\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d 项已丢弃)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"收集要扫描的 {} 个文件\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"收集要扫描的 {} 个文件夹\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"找到 %d 个匹配，来自于 %d 个组中\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"您正在移动 {} 个文件至回收站。\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"正则表达式\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"确定要从忽略列表中移除所有 %d 项吗？\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"文件名\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"分组比较文件名（如：作者-歌曲名）\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"分组比较文件名（不固定顺序，如：作者-歌曲名 或者 歌曲名-作者）\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"标签\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"内容\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"已分析 %d 张图像（共 %d 张）\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"已执行 %d 个区块匹配（共 %d 个）\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"准备进行匹配\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"已验证 %d 个匹配项（共 %d 个）\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"已读取 %d 张图片的 EXIF（共 %d 张）\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 时间戳\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"无\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"以数字结尾\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"不以数字结尾\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"最长\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"最短\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"最高\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"最低\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"最新\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"最旧\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"已标记 %d / %d (%s / %s) 个重复项。\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" 过滤：%s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"已读取 %d 个文件元数据（共 %d 个）\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"即将完成！整理结果中...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"文件夹\"\n"
  },
  {
    "path": "locale/zh_CN/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# 太子 VC <taiziccf@gmail.com>, 2022\n# Chris Ocelot, 2023\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n# Mèng yáo, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Mèng yáo, 2023\\n\"\n\"Language-Team: Chinese (China) (https://app.transifex.com/voltaicideas/teams/116153/zh_CN/)\\n\"\n\"Language: zh_CN\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"退出\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"选项\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"忽略列表\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"清空图片缓存\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru 帮助\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"关于 dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"打开调试记录\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"确定要移除所有缓存的图片分析吗？\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"图片缓存已清空。\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} 文件 (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"删除选项\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"链接已删除的文件\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"在删除重复文件后，以源文件的链接来替代已删除的文件。\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"硬链接\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"符号链接\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (不支持)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"直接删除文件\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"直接将文件删除，而不是将其移至回收站。此选项通常作为常规删除方法不起作用时的替代方案。\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"继续\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"属性\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"已选择\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"源文件\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"载入结果...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"结果窗口\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"添加文件夹...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"文件\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"视图\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"帮助\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"载入最近的结果\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"程序模式：\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"音乐\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"图片\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"标准\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"扫描类型：\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"更多选项\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"请选择要扫描的文件夹，然后点击“扫描”。\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"载入结果\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"扫描\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"未保存的结果\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"您还没有保存扫描结果，确定要退出吗？\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"请选择一个文件夹以加入到扫描列表中\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"选择一个结果文件以载入\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"所有文件 (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru 结果 (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"开始新的扫描\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"您还没有保存扫描结果，确定要继续吗？\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"名称\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"状态\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"不包含\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"正常\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"移除已选择的文件\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"关闭\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"详细信息\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"扫描标签：\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"音轨\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"作者\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"专辑\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"歌曲名\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"音乐类型\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"根据词语长度不同赋予不同权重\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"匹配近似词语\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"允许混合文件类型\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"过滤时使用正则表达式\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"删除或移动时一并移除空文件夹\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"忽略硬链接到相同文件的重复文件\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"调试模式（需要重新启动）\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"匹配不同尺寸的图像\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"过滤强度：\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"较多结果\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"较少结果\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"字体大小：\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"语言：\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"复制并移动：\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"直接至目标文件夹\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"重建相对路径\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"重建绝对路径\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"自定义命令（参数：%d 指重复文件，%r 指源文件）：\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru 需要重新启动以使语言修改生效。\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"重新排列重复文件\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"在右侧的框内添加规则然后点击确定，以将最符合规则的文件置于组内源文件的位置。阅读帮助文件获取更多信息。\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"有问题！\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"在处理部分（或全部）文件时出现问题。产生问题的原因如下。这些文件没有从结果中移除。\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"显示已选择的文件\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"操作\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"仅显示重复文件\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"显示 Delta 值\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"将已标记的文件移至回收站...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"将已标记的文件移动到...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"将已标记的文件复制到...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"从结果中移除已标记的文件\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"重新排列结果...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"从结果中移除所选文件\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"将所选文件添加到忽略列表中\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"将所选文件作为源文件\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"使用默认程序打开所选文件\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"打开所选文件所在的文件夹\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"重命名所选文件\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"全部标记\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"全部取消标记\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"反转文件标记\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"标记所选文件\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"导出为 HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"导出为 CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"保存结果...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"调用自定义命令\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"标记\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"显示列\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"重置为默认值\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} 个结果\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"仅 Dupes\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Delta 值\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"选择一个文件来保存您的结果\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"忽略文件，当其小于\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ 结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"操作\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"添加新文件夹...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"高级\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"自动检查更新\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"基本\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"全部前置\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"检查更新...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"关闭窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"复制\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"自定义命令（参数：%d 指重复文件，%r 指源文件）：\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"剪切\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"所选文件的详细信息\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"详细信息面板\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"目录\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru 设置\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru 结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru 网站\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"编辑\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"导出结果到 CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"导出结果到 XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"较少结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"过滤器\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"过滤强度：\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"过滤结果...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"文件夹选择窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"字体大小：\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"隐藏 dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"隐藏其他\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"忽略文件，当其小于：\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"从文件载入...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"最小化\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"模式\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"更多结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"确定\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"粘贴\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"设置...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"快速查找\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"退出 dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"重置为默认值\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"重置为默认值\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"显示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"在 Finder 中显示所选项\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"全选\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"将已标记的文件移至回收站...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"服务\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"全部显示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"开始重复内容扫描\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"名称“%@”已存在。\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"缩放\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"排除过滤器\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"扫描结果\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"载入目录...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"保存目录...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"选择一个目录文件以载入\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru 结果 (*.dupegurudirs)\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"选择一个文件来保存您的目录\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru 目录 (*.dupegurudirs)\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"添加\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"还原至默认值\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"测试字符串\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"在此输入一个 python 正则表达式...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"在此输入一个系统路径或者文件名...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"这些（大小写敏感）的 python 正则表达式会在扫描过程中筛选文件。<br>如果目录的名称和某一个正则表达式匹配的话，它们的<strong>默认状态</strong>将为被设为排除状态。<br>每一个被采集的文件都会被进行两种不同的测试来决定它是否会被排除掉：<br><li>1. 没有路径分隔符的正则表达式只会和文件名作比较。</li>\\n\"\n\"<li>2. 有路径分隔符的正则表达式，会和文件的完整路径作比较。</li><br>\\n\"\n\"如：假如您想要仅从“我的图片”目录排除掉 .PNG 文件的话：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>您可以使用测试字符串功能来测试正则表达式，只需要在其中粘贴一个假的路径：<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"匹配的正则表达式会被高亮。<br>假如至少有一个高亮的话，在扫描中这个路径将会被忽略。<br><br>以“.”开头的目录和文件默认就会被忽略。<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"编译错误：\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"放大\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"缩小\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"正常尺寸\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"最佳结果\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"图片缓存模式：\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"在图片浏览器的工具栏里，覆盖默认图标设置\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"使用程序自带的图标来替代系统默认图标。\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"在图片浏览器里显示滚动条\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"当图片尺寸大于显示窗口时，显示滚动条来移动图片。\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"在默认位置显示标签栏（需要重启）\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"把标签栏放在主菜单下面而不是旁边\\n\"\n\"在 MacOS 上，标签栏会填充满整个窗口的宽度。\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"源文件使用粗体\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"源文件前景色：\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"源文件背景色：\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Delta 前景色：\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"显示标题栏，并使其可被停靠\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"标题栏隐藏时，使用修饰键来移动浮动窗口。\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"仅当窗口被停靠时，标题栏可被隐藏\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"竖直标题栏\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"把标题栏从顶部横向改为左侧竖直\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"显示标签栏\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"这些 python 正则表达式（区分大小写）将在扫描期间过滤掉文件。<br>如果目录的名称恰好与某个选定的正则表达式匹配，则目录的<strong>默认状态</strong>也将在“目录”选项卡中设置为“已排除”。<br>对于收集的每个文件，将执行以下两个测试以确定是否完全忽略它：<br><li>1、没有路径分隔符的正则表达式将仅与文件名进行比较。</li>\\n\"\n\"<li>2、包含至少一个路径分隔符的正则表达式将与文件的完整路径进行比较。</li><br>\\n\"\n\"示例：如果只想从“我的图片”目录中过滤掉 .PNG 文件：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>那么在测试框中粘贴如下虚拟路径后，可以使用“测试字符串”按钮测试正则表达式：<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"匹配的正则表达式将突出显示。<br>如果至少有一个突出显示，则在扫描过程中将忽略测试的路径或文件名。<br><br>默认情况下，以英文句点“.”开头的目录和文件将被过滤掉。<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"结果\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"通用界面\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"结果表\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"详细信息窗口\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"常规\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"显示\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"只计算部分哈希值，如果文件大于\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"使用操作系统原生对话窗口\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"使用操作系统原生对话窗口选择文件、文件夹。部分系统的原生对话窗口功能可能有限制。\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"忽略文件，如果大于\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"清除缓存\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"\"\n\"确定要清除缓存吗？\\n\"\n\"\\n\"\n\"这将删除所有缓存的文件哈希和图片分析。\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"缓存已清除。\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"使用暗色主题\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"将扫描操作保存为配置\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"将扫描操作保存为配置，并保存日志用于优化。\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"日志位于：<a href=\\\"{}\\\">{}</a>\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"调试\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"关于 {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"版本 {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"正在检查更新...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"本项目基于 GPLv3 开源协议发布\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"没有新版本。\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"新版本 {} 可用，<a href=\\\"{}\\\">点这里</a>下载。\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"错误报告\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"发生错误，是否要报告错误？\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"错误报告应该以GitHub issue的形式进行提交。您可以把错误信息复制粘贴到新的issue中\\n\"\n\"\\n\"\n\"在提交新issue前，请搜索已经存在的issue，以确保没有其他人已经报告了相同的错误。同时请确保使用仓库中的最新版进行测试，因为您所遇到的bug可能已经被最新版修复。\\n\"\n\"\\n\"\n\"如果您能详细描述一下错误发生时的具体情况，将会更好的帮助我们解决问题，谢谢！\\n\"\n\"\\n\"\n\"虽然本程序在此错误后依然会继续运行，但是可能处于不稳定的状态，因此推荐重启本程序。\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"前往GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"捷克语\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"德语\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"希腊语\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"英语\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"西班牙语\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"法语\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"亚美尼亚语\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"意大利语\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"日语\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"韩语\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"马来语\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"荷兰语\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"波兰语\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"巴西葡萄牙语\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"俄语\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"土耳其\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"乌克兰语\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"越南语\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"中文（简体）\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"清空列表\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"搜索...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"这些选项是供高级用户或者特定情况下使用，大多数用户不该修改它们。\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"在扫描结束后检查文件是否存在\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"载入缓存摘要时忽略mtime的不同\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"取消？\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"确定要取消吗？这将会导致所有进展都付诸东流。\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"这些 python 正则表达式（区分大小写）将在扫描期间过滤掉文件。<br>如果目录的名称恰好与某个选定的正则表达式匹配，则目录的<strong>默认状态</strong>也将在“目录”选项卡中设置为“已排除”。<br>对于收集的每个文件，将执行以下两个测试以确定是否完全忽略它：<br><li>1、没有路径分隔符的正则表达式将仅与文件名进行比较。</li>\\n\"\n\"<li>2、包含至少一个路径分隔符的正则表达式将与文件的完整路径进行比较。</li><br>示例：如果只想从“我的图片”目录中过滤掉 .PNG 文件：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>那么在测试框中粘贴如下虚拟路径后，可以使用“测试字符串”按钮测试正则表达式：<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"匹配的正则表达式将突出显示。<br>如果至少有一个突出显示，则在扫描过程中将忽略测试的路径或文件名。<br><br>默认情况下，以英文句点“.”开头的目录和文件将被过滤掉。<br><br>\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/zh_TW/LC_MESSAGES/columns.po",
    "content": "# Translators:\n# Chris Ocelot, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2022\\n\"\n\"Language-Team: Chinese (Taiwan) (https://www.transifex.com/voltaicideas/teams/116153/zh_TW/)\\n\"\n\"Language: zh_TW\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\gui\\ignore_list_table.py:19 core\\gui\\ignore_list_table.py:20\n#: core\\gui\\problem_table.py:18\nmsgid \"File Path\"\nmsgstr \"文件路径\"\n\n#: core\\gui\\problem_table.py:19\nmsgid \"Error Message\"\nmsgstr \"错误信息\"\n\n#: core\\me\\prioritize.py:23\nmsgid \"Duration\"\nmsgstr \"持续时间\"\n\n#: core\\me\\prioritize.py:30 core\\me\\result_table.py:23\nmsgid \"Bitrate\"\nmsgstr \"比特率\"\n\n#: core\\me\\prioritize.py:37\nmsgid \"Samplerate\"\nmsgstr \"采样率\"\n\n#: core\\me\\result_table.py:19 core\\pe\\result_table.py:19 core\\prioritize.py:94\n#: core\\se\\result_table.py:19\nmsgid \"Filename\"\nmsgstr \"文件名\"\n\n#: core\\me\\result_table.py:20 core\\pe\\result_table.py:20 core\\prioritize.py:75\n#: core\\se\\result_table.py:20\nmsgid \"Folder\"\nmsgstr \"文件夹\"\n\n#: core\\me\\result_table.py:21\nmsgid \"Size (MB)\"\nmsgstr \"大小 (MB)\"\n\n#: core\\me\\result_table.py:22\nmsgid \"Time\"\nmsgstr \"时间\"\n\n#: core\\me\\result_table.py:24\nmsgid \"Sample Rate\"\nmsgstr \"采样率\"\n\n#: core\\me\\result_table.py:25 core\\pe\\result_table.py:22 core\\prioritize.py:65\n#: core\\se\\result_table.py:22\nmsgid \"Kind\"\nmsgstr \"类型\"\n\n#: core\\me\\result_table.py:26 core\\pe\\result_table.py:25\n#: core\\prioritize.py:165 core\\se\\result_table.py:23\nmsgid \"Modification\"\nmsgstr \"编辑日期\"\n\n#: core\\me\\result_table.py:27\nmsgid \"Title\"\nmsgstr \"歌曲名\"\n\n#: core\\me\\result_table.py:28\nmsgid \"Artist\"\nmsgstr \"作者\"\n\n#: core\\me\\result_table.py:29\nmsgid \"Album\"\nmsgstr \"专辑\"\n\n#: core\\me\\result_table.py:30\nmsgid \"Genre\"\nmsgstr \"音乐类型\"\n\n#: core\\me\\result_table.py:31\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: core\\me\\result_table.py:32\nmsgid \"Track Number\"\nmsgstr \"音轨号\"\n\n#: core\\me\\result_table.py:33\nmsgid \"Comment\"\nmsgstr \"注释\"\n\n#: core\\me\\result_table.py:34 core\\pe\\result_table.py:26\n#: core\\se\\result_table.py:24\nmsgid \"Match %\"\nmsgstr \"匹配度 %\"\n\n#: core\\me\\result_table.py:35 core\\se\\result_table.py:25\nmsgid \"Words Used\"\nmsgstr \"使用过的词语\"\n\n#: core\\me\\result_table.py:36 core\\pe\\result_table.py:27\n#: core\\se\\result_table.py:26\nmsgid \"Dupe Count\"\nmsgstr \"重复文件数\"\n\n#: core\\pe\\prioritize.py:23 core\\pe\\result_table.py:23\nmsgid \"Dimensions\"\nmsgstr \"规格\"\n\n#: core\\pe\\result_table.py:21 core\\se\\result_table.py:21\nmsgid \"Size (KB)\"\nmsgstr \"大小 (KB)\"\n\n#: core\\pe\\result_table.py:24\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 时间戳\"\n\n#: core\\prioritize.py:158\nmsgid \"Size\"\nmsgstr \"大小\"\n"
  },
  {
    "path": "locale/zh_TW/LC_MESSAGES/core.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# YaNing Lu, 2022\n# Andrew Senetar <arsenetar@gmail.com>, 2022\n# Chris Ocelot, 2022\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Chris Ocelot, 2022\\n\"\n\"Language-Team: Chinese (Taiwan) (https://app.transifex.com/voltaicideas/teams/116153/zh_TW/)\\n\"\n\"Language: zh_TW\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: core\\app.py:44\nmsgid \"There are no marked duplicates. Nothing has been done.\"\nmsgstr \"没有已标记的重复项。无需任何操作。\"\n\n#: core\\app.py:45\nmsgid \"There are no selected duplicates. Nothing has been done.\"\nmsgstr \"没有已选定的重复项。无需任何操作。\"\n\n#: core\\app.py:46\nmsgid \"\"\n\"You're about to open many files at once. Depending on what those files are \"\n\"opened with, doing so can create quite a mess. Continue?\"\nmsgstr \"您即将一次性打开多个文件。取决于这些文件的默认打开方式，此项操作可能导致非常混乱的状况。是否继续？\"\n\n#: core\\app.py:73\nmsgid \"Scanning for duplicates\"\nmsgstr \"正在扫描重复内容\"\n\n#: core\\app.py:74\nmsgid \"Loading\"\nmsgstr \"载入中\"\n\n#: core\\app.py:75\nmsgid \"Moving\"\nmsgstr \"移动中\"\n\n#: core\\app.py:76\nmsgid \"Copying\"\nmsgstr \"复制中\"\n\n#: core\\app.py:77\nmsgid \"Sending to Trash\"\nmsgstr \"正在移至回收站\"\n\n#: core\\app.py:289\nmsgid \"\"\n\"A previous action is still hanging in there. You can't start a new one yet. \"\n\"Wait a few seconds, then try again.\"\nmsgstr \"前一项操作还在执行，无法启动新操作。请等待几秒钟后再重试一次。\"\n\n#: core\\app.py:300\nmsgid \"No duplicates found.\"\nmsgstr \"没有找到重复文件。\"\n\n#: core\\app.py:315\nmsgid \"All marked files were copied successfully.\"\nmsgstr \"所有已标记的文件已复制成功。\"\n\n#: core\\app.py:317\nmsgid \"All marked files were moved successfully.\"\nmsgstr \"所有已标记的文件已移动成功。\"\n\n#: core\\app.py:319\nmsgid \"All marked files were deleted successfully.\"\nmsgstr \"已复制所有标记文件\"\n\n#: core\\app.py:321\nmsgid \"All marked files were successfully sent to Trash.\"\nmsgstr \"所有已标记的文件已成功移至回收站。\"\n\n#: core\\app.py:326\nmsgid \"Could not load file: {}\"\nmsgstr \"无法加载文件: {}\"\n\n#: core\\app.py:382\nmsgid \"'{}' already is in the list.\"\nmsgstr \"'{}' 已在列表中。\"\n\n#: core\\app.py:384\nmsgid \"'{}' does not exist.\"\nmsgstr \"'{}' 不存在。\"\n\n#: core\\app.py:392\nmsgid \"\"\n\"All selected %d matches are going to be ignored in all subsequent scans. \"\n\"Continue?\"\nmsgstr \"目前已选的 %d 个匹配项将在后续的扫描中被忽略。是否继续？\"\n\n#: core\\app.py:469\nmsgid \"Select a directory to copy marked files to\"\nmsgstr \"请选择要将标记文件复制到的目录\"\n\n#: core\\app.py:471\nmsgid \"Select a directory to move marked files to\"\nmsgstr \"请选择要将标记文件移动到的目录\"\n\n#: core\\app.py:510\nmsgid \"Select a destination for your exported CSV\"\nmsgstr \"选择您导出 CSV 的目标文件夹\"\n\n#: core\\app.py:516 core\\app.py:777 core\\app.py:787\nmsgid \"Couldn't write to file: {}\"\nmsgstr \"不能写入文件: {}\"\n\n#: core\\app.py:539\nmsgid \"You have no custom command set up. Set it up in your preferences.\"\nmsgstr \"您没有设定自定义命令。请在设置中进行设定。\"\n\n#: core\\app.py:701 core\\app.py:713\nmsgid \"You are about to remove %d files from results. Continue?\"\nmsgstr \"您将从结果中移除 %d 个文件。是否继续?\"\n\n#: core\\app.py:749\nmsgid \"{} duplicate groups were changed by the re-prioritization.\"\nmsgstr \"{}个重复的组已被重新排列。\"\n\n#: core\\app.py:797\nmsgid \"The selected directories contain no scannable file.\"\nmsgstr \"所选目录中不包含可供扫描的文件。\"\n\n#: core\\app.py:813\nmsgid \"Collecting files to scan\"\nmsgstr \"收集文件以供扫描\"\n\n#: core\\app.py:863\nmsgid \"%s (%d discarded)\"\nmsgstr \"%s (%d 项已丢弃)\"\n\n#: core\\directories.py:191\nmsgid \"Collected {} files to scan\"\nmsgstr \"收集要扫描的{}文件\"\n\n#: core\\directories.py:207\nmsgid \"Collected {} folders to scan\"\nmsgstr \"收集要扫描的{}文件夹\"\n\n#: core\\engine.py:27\nmsgid \"%d matches found from %d groups\"\nmsgstr \"从1%d组中找到1%d个匹配\"\n\n#: core\\gui\\deletion_options.py:71\nmsgid \"You are sending {} file(s) to the Trash.\"\nmsgstr \"您正在移动 {} 个文件至回收站。\"\n\n#: core\\gui\\exclude_list_table.py:14\nmsgid \"Regular Expressions\"\nmsgstr \"正则表达式\"\n\n#: core\\gui\\ignore_list_dialog.py:25\nmsgid \"Do you really want to remove all %d items from the ignore list?\"\nmsgstr \"确定要从忽略列表中移除所有 %d 项吗?\"\n\n#: core\\me\\scanner.py:20 core\\se\\scanner.py:16\nmsgid \"Filename\"\nmsgstr \"文件名\"\n\n#: core\\me\\scanner.py:21\nmsgid \"Filename - Fields\"\nmsgstr \"分组比较文件名（如作者-歌曲名）\"\n\n#: core\\me\\scanner.py:22\nmsgid \"Filename - Fields (No Order)\"\nmsgstr \"分组比较文件名（不固定顺序（如作者-歌曲名或者歌曲名-作者））\"\n\n#: core\\me\\scanner.py:23\nmsgid \"Tags\"\nmsgstr \"标签\"\n\n#: core\\me\\scanner.py:24 core\\pe\\scanner.py:22 core\\se\\scanner.py:17\nmsgid \"Contents\"\nmsgstr \"内容\"\n\n#: core\\pe\\matchblock.py:66\nmsgid \"Analyzed %d/%d pictures\"\nmsgstr \"已分析 %d/%d 图像\"\n\n#: core\\pe\\matchblock.py:183\nmsgid \"Performed %d/%d chunk matches\"\nmsgstr \"已执行 %d/%d 个区块匹配\"\n\n#: core\\pe\\matchblock.py:191\nmsgid \"Preparing for matching\"\nmsgstr \"准备进行匹配\"\n\n#: core\\pe\\matchblock.py:240\nmsgid \"Verified %d/%d matches\"\nmsgstr \"已验证 %d/%d 匹配项\"\n\n#: core\\pe\\matchexif.py:19\nmsgid \"Read EXIF of %d/%d pictures\"\nmsgstr \"已读取 %d/%d 张图片的 EXIF\"\n\n#: core\\pe\\scanner.py:23\nmsgid \"EXIF Timestamp\"\nmsgstr \"EXIF 时间戳\"\n\n#: core\\prioritize.py:70\nmsgid \"None\"\nmsgstr \"无\"\n\n#: core\\prioritize.py:102\nmsgid \"Ends with number\"\nmsgstr \"以数字结尾\"\n\n#: core\\prioritize.py:103\nmsgid \"Doesn't end with number\"\nmsgstr \"不以数字结尾\"\n\n#: core\\prioritize.py:104\nmsgid \"Longest\"\nmsgstr \"最长\"\n\n#: core\\prioritize.py:105\nmsgid \"Shortest\"\nmsgstr \"最短\"\n\n#: core\\prioritize.py:142\nmsgid \"Highest\"\nmsgstr \"最高\"\n\n#: core\\prioritize.py:142\nmsgid \"Lowest\"\nmsgstr \"最低\"\n\n#: core\\prioritize.py:171\nmsgid \"Newest\"\nmsgstr \"最新\"\n\n#: core\\prioritize.py:171\nmsgid \"Oldest\"\nmsgstr \"最旧\"\n\n#: core\\results.py:135\nmsgid \"%d / %d (%s / %s) duplicates marked.\"\nmsgstr \"已标记 %d / %d (%s / %s) 个重复项。\"\n\n#: core\\results.py:142\nmsgid \" filter: %s\"\nmsgstr \" 过滤: %s\"\n\n#: core\\scanner.py:114\nmsgid \"Read metadata of %d/%d files\"\nmsgstr \"已读取 %d/%d 文件元数据\"\n\n#: core\\scanner.py:152\nmsgid \"Almost done! Fiddling with results...\"\nmsgstr \"即将完成！整理结果中...\"\n\n#: core\\se\\scanner.py:18\nmsgid \"Folders\"\nmsgstr \"文件夹\"\n"
  },
  {
    "path": "locale/zh_TW/LC_MESSAGES/ui.po",
    "content": "# Translators:\n# Fuan <jcfrt@posteo.net>, 2022\n# 太子 VC <taiziccf@gmail.com>, 2022\n# Chris Ocelot, 2023\n# Andrew Senetar <arsenetar@gmail.com>, 2023\n#\nmsgid \"\"\nmsgstr \"\"\n\"Last-Translator: Andrew Senetar <arsenetar@gmail.com>, 2023\\n\"\n\"Language-Team: Chinese (Taiwan) (https://app.transifex.com/voltaicideas/teams/116153/zh_TW/)\\n\"\n\"Language: zh_TW\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: utf-8\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: qt/app.py:81\nmsgid \"Quit\"\nmsgstr \"退出\"\n\n#: qt/app.py:82 qt/preferences_dialog.py:116\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Options\"\nmsgstr \"选项\"\n\n#: qt/app.py:83 qt/ignore_list_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore List\"\nmsgstr \"忽略列表\"\n\n#: qt/app.py:84 qt/app.py:179 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear Picture Cache\"\nmsgstr \"清空图片缓存\"\n\n#: qt/app.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Help\"\nmsgstr \"dupeGuru 帮助\"\n\n#: qt/app.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"About dupeGuru\"\nmsgstr \"关于 dupeGuru\"\n\n#: qt/app.py:87\nmsgid \"Open Debug Log\"\nmsgstr \"打开调试记录\"\n\n#: qt/app.py:180 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Do you really want to remove all your cached picture analysis?\"\nmsgstr \"确定要移除所有缓存分析图片?\"\n\n#: qt/app.py:184\nmsgid \"Picture cache cleared.\"\nmsgstr \"图片缓存已清空。\"\n\n#: qt/app.py:251\nmsgid \"{} file (*.{})\"\nmsgstr \"{} 文件 (*.{})\"\n\n#: qt/deletion_options.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Deletion Options\"\nmsgstr \"删除选项\"\n\n#: qt/deletion_options.py:35 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Link deleted files\"\nmsgstr \"链接已删除的文件\"\n\n#: qt/deletion_options.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"After having deleted a duplicate, place a link targeting the reference file \"\n\"to replace the deleted file.\"\nmsgstr \"在删除重复文件后，以源文件的链接来替代已删除的文件。\"\n\n#: qt/deletion_options.py:44\nmsgid \"Hardlink\"\nmsgstr \"硬链接\"\n\n#: qt/deletion_options.py:44\nmsgid \"Symlink\"\nmsgstr \"符号链接\"\n\n#: qt/deletion_options.py:48\nmsgid \" (unsupported)\"\nmsgstr \" (不支持)\"\n\n#: qt/deletion_options.py:49 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directly delete files\"\nmsgstr \"直接删除文件\"\n\n#: qt/deletion_options.py:51 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Instead of sending files to trash, delete them directly. This option is \"\n\"usually used as a workaround when the normal deletion method doesn't work.\"\nmsgstr \"直接将文件删除，而不是将其移至回收站。此选项通常作为普通删除方法不能正常使用时替代方案。\"\n\n#: qt/deletion_options.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Proceed\"\nmsgstr \"继续\"\n\n#: qt/deletion_options.py:60 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cancel\"\nmsgstr \"取消\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Attribute\"\nmsgstr \"属性\"\n\n#: qt/details_table.py:16 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Selected\"\nmsgstr \"已选择\"\n\n#: qt/details_table.py:16 qt/directories_model.py:24\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reference\"\nmsgstr \"源文件\"\n\n#: qt/directories_dialog.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results...\"\nmsgstr \"载入结果...\"\n\n#: qt/directories_dialog.py:65 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Results Window\"\nmsgstr \"结果窗口\"\n\n#: qt/directories_dialog.py:66\nmsgid \"Add Folder...\"\nmsgstr \"添加文件夹...\"\n\n#: qt/directories_dialog.py:74 qt/result_window.py:100\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"File\"\nmsgstr \"文件\"\n\n#: qt/directories_dialog.py:76 qt/result_window.py:108\nmsgid \"View\"\nmsgstr \"视图\"\n\n#: qt/directories_dialog.py:78 qt/result_window.py:110\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Help\"\nmsgstr \"帮助\"\n\n#: qt/directories_dialog.py:80 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Recent Results\"\nmsgstr \"载入最近的结果\"\n\n#: qt/directories_dialog.py:116 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Application Mode:\"\nmsgstr \"程序模式：\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Music\"\nmsgstr \"音乐\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Picture\"\nmsgstr \"图片\"\n\n#: qt/directories_dialog.py:121 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Standard\"\nmsgstr \"标准\"\n\n#: qt/directories_dialog.py:128 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan Type:\"\nmsgstr \"扫描类型:\"\n\n#: qt/directories_dialog.py:135\nmsgid \"More Options\"\nmsgstr \"更多选项\"\n\n#: qt/directories_dialog.py:139 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select folders to scan and press \\\"Scan\\\".\"\nmsgstr \"请选择要扫描的文件夹，然后点击 \\\"扫描\\\"。\"\n\n#: qt/directories_dialog.py:163 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load Results\"\nmsgstr \"载入结果\"\n\n#: qt/directories_dialog.py:166 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Scan\"\nmsgstr \"扫描\"\n\n#: qt/directories_dialog.py:230\nmsgid \"Unsaved results\"\nmsgstr \"未保存的结果\"\n\n#: qt/directories_dialog.py:231 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to quit?\"\nmsgstr \"您还没有保存扫描结果，确定要退出吗?\"\n\n#: qt/directories_dialog.py:239 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a folder to add to the scanning list\"\nmsgstr \"请选择一个文件夹以加入到扫描列表中\"\n\n#: qt/directories_dialog.py:266 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a results file to load\"\nmsgstr \"选择一个结果文件以载入\"\n\n#: qt/directories_dialog.py:267\nmsgid \"All Files (*.*)\"\nmsgstr \"所有文件 (*.*)\"\n\n#: qt/directories_dialog.py:267 qt/result_window.py:311\nmsgid \"dupeGuru Results (*.dupeguru)\"\nmsgstr \"dupeGuru 结果 (*.dupeguru)\"\n\n#: qt/directories_dialog.py:278\nmsgid \"Start a new scan\"\nmsgstr \"开始新的扫描\"\n\n#: qt/directories_dialog.py:279 cocoa/en.lproj/Localizable.strings:0\nmsgid \"You have unsaved results, do you really want to continue?\"\nmsgstr \"您还没有保存扫描结果，确定要继续吗？\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Name\"\nmsgstr \"名称\"\n\n#: qt/directories_model.py:23 cocoa/en.lproj/Localizable.strings:0\nmsgid \"State\"\nmsgstr \"状态\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Excluded\"\nmsgstr \"不包含\"\n\n#: qt/directories_model.py:24 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Normal\"\nmsgstr \"正常\"\n\n#: qt/ignore_list_dialog.py:45 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected\"\nmsgstr \"移除已选择的文件\"\n\n#: qt/ignore_list_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Clear\"\nmsgstr \"清除\"\n\n#: qt/ignore_list_dialog.py:47 qt/problem_dialog.py:61\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close\"\nmsgstr \"关闭\"\n\n#: qt/me/details_dialog.py:18 qt/pe/details_dialog.py:24\n#: qt/result_window.py:56 qt/result_window.py:192 qt/se/details_dialog.py:18\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details\"\nmsgstr \"详细信息\"\n\n#: qt/me/preferences_dialog.py:30 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Tags to scan:\"\nmsgstr \"扫描标签:\"\n\n#: qt/me/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Track\"\nmsgstr \"音轨\"\n\n#: qt/me/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Artist\"\nmsgstr \"作者\"\n\n#: qt/me/preferences_dialog.py:40 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Album\"\nmsgstr \"专辑\"\n\n#: qt/me/preferences_dialog.py:42 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Title\"\nmsgstr \"歌曲名\"\n\n#: qt/me/preferences_dialog.py:44 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Genre\"\nmsgstr \"音乐类型\"\n\n#: qt/me/preferences_dialog.py:46 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Year\"\nmsgstr \"年\"\n\n#: qt/me/preferences_dialog.py:50 qt/se/preferences_dialog.py:30\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Word weighting\"\nmsgstr \"根据词语长度不同赋予不同权重\"\n\n#: qt/me/preferences_dialog.py:52 qt/se/preferences_dialog.py:32\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match similar words\"\nmsgstr \"匹配近似词语\"\n\n#: qt/me/preferences_dialog.py:54 qt/pe/preferences_dialog.py:21\n#: qt/se/preferences_dialog.py:34 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Can mix file kind\"\nmsgstr \"允许混合文件类型\"\n\n#: qt/me/preferences_dialog.py:56 qt/pe/preferences_dialog.py:23\n#: qt/se/preferences_dialog.py:36 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Use regular expressions when filtering\"\nmsgstr \"过滤时使用正则表达式\"\n\n#: qt/me/preferences_dialog.py:58 qt/pe/preferences_dialog.py:25\n#: qt/se/preferences_dialog.py:38 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove empty folders on delete or move\"\nmsgstr \"删除或移动时一并移除空文件夹\"\n\n#: qt/me/preferences_dialog.py:60 qt/pe/preferences_dialog.py:27\n#: qt/se/preferences_dialog.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore duplicates hardlinking to the same file\"\nmsgstr \"忽略硬链接到相同文件的重复文件\"\n\n#: qt/me/preferences_dialog.py:62 qt/pe/preferences_dialog.py:29\n#: qt/se/preferences_dialog.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Debug mode (restart required)\"\nmsgstr \"调试模式 (需要重新启动)\"\n\n#: qt/pe/preferences_dialog.py:19 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Match pictures of different dimensions\"\nmsgstr \"匹配不同尺寸的图像\"\n\n#: qt/preferences_dialog.py:43\nmsgid \"Filter Hardness:\"\nmsgstr \"过滤强度:\"\n\n#: qt/preferences_dialog.py:69\nmsgid \"More Results\"\nmsgstr \"较多结果\"\n\n#: qt/preferences_dialog.py:74\nmsgid \"Fewer Results\"\nmsgstr \"较少结果\"\n\n#: qt/preferences_dialog.py:81\nmsgid \"Font size:\"\nmsgstr \"字体大小:\"\n\n#: qt/preferences_dialog.py:85\nmsgid \"Language:\"\nmsgstr \"语言:\"\n\n#: qt/preferences_dialog.py:91 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy and Move:\"\nmsgstr \"复制并移动:\"\n\n#: qt/preferences_dialog.py:94 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Right in destination\"\nmsgstr \"直接至目标文件夹\"\n\n#: qt/preferences_dialog.py:95 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate relative path\"\nmsgstr \"重建相对路径\"\n\n#: qt/preferences_dialog.py:96 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Recreate absolute path\"\nmsgstr \"重建绝对路径\"\n\n#: qt/preferences_dialog.py:99\nmsgid \"Custom Command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"自定义命令 (参数: %d 指重复文件, %r 指源文件):\"\n\n#: qt/preferences_dialog.py:174\nmsgid \"dupeGuru has to restart for language changes to take effect.\"\nmsgstr \"dupeGuru需要重新启动以使语言修改生效。\"\n\n#: qt/prioritize_dialog.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize duplicates\"\nmsgstr \"重新排列重复文件\"\n\n#: qt/prioritize_dialog.py:79 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"Add criteria to the right box and click OK to send the dupes that correspond\"\n\" the best to these criteria to their respective group's reference position. \"\n\"Read the help file for more information.\"\nmsgstr \"在右侧的框内添加规则然后点击确定，以将最符合规则的文件置于组内源文件的位置。阅读帮助文件获取更多信息。\"\n\n#: qt/problem_dialog.py:33 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Problems!\"\nmsgstr \"有问题!\"\n\n#: qt/problem_dialog.py:37 cocoa/en.lproj/Localizable.strings:0\nmsgid \"\"\n\"There were problems processing some (or all) of the files. The cause of \"\n\"these problems are described in the table below. Those files were not \"\n\"removed from your results.\"\nmsgstr \"在处理部分（或全部）文件时出现问题。产生问题的原因如下。这些文件没有从结果中移除。\"\n\n#: qt/problem_dialog.py:56\nmsgid \"Reveal Selected\"\nmsgstr \"显示已选择的文件\"\n\n#: qt/result_window.py:57 qt/result_window.py:104 qt/result_window.py:167\n#: qt/result_window.py:191 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Actions\"\nmsgstr \"操作\"\n\n#: qt/result_window.py:58 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Dupes Only\"\nmsgstr \"仅显示重复文件\"\n\n#: qt/result_window.py:59 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show Delta Values\"\nmsgstr \"显示 Delta 值\"\n\n#: qt/result_window.py:60\nmsgid \"Send Marked to Recycle Bin...\"\nmsgstr \"将已标记的文件移至回收站...\"\n\n#: qt/result_window.py:61 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Move Marked to...\"\nmsgstr \"将已标记的文件移动到...\"\n\n#: qt/result_window.py:62 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy Marked to...\"\nmsgstr \"将已标记的文件复制到...\"\n\n#: qt/result_window.py:63 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Marked from Results\"\nmsgstr \"从结果中移除已标记的文件\"\n\n#: qt/result_window.py:64 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Re-Prioritize Results...\"\nmsgstr \"重新排列结果...\"\n\n#: qt/result_window.py:67 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Remove Selected from Results\"\nmsgstr \"从结果中移除所选文件\"\n\n#: qt/result_window.py:71 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add Selected to Ignore List\"\nmsgstr \"将所选文件添加到忽略列表中\"\n\n#: qt/result_window.py:75 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Make Selected into Reference\"\nmsgstr \"将所选文件作为源文件\"\n\n#: qt/result_window.py:77 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Open Selected with Default Application\"\nmsgstr \"使用默认程序打开所选文件\"\n\n#: qt/result_window.py:80\nmsgid \"Open Containing Folder of Selected\"\nmsgstr \"打开所选文件所在的文件夹\"\n\n#: qt/result_window.py:82 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Rename Selected\"\nmsgstr \"重命名所选文件\"\n\n#: qt/result_window.py:83 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark All\"\nmsgstr \"全部标记\"\n\n#: qt/result_window.py:84 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark None\"\nmsgstr \"全部取消标记\"\n\n#: qt/result_window.py:85 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invert Marking\"\nmsgstr \"反转文件标记\"\n\n#: qt/result_window.py:86 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mark Selected\"\nmsgstr \"标记所选文件\"\n\n#: qt/result_window.py:87\nmsgid \"Export To HTML\"\nmsgstr \"导出为 HTML\"\n\n#: qt/result_window.py:88\nmsgid \"Export To CSV\"\nmsgstr \"导出为 CSV\"\n\n#: qt/result_window.py:89 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Save Results...\"\nmsgstr \"保存结果...\"\n\n#: qt/result_window.py:90 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Invoke Custom Command\"\nmsgstr \"调用自定义命令\"\n\n#: qt/result_window.py:102\nmsgid \"Mark\"\nmsgstr \"标记\"\n\n#: qt/result_window.py:106 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Columns\"\nmsgstr \"显示列\"\n\n#: qt/result_window.py:163\nmsgid \"Reset to Defaults\"\nmsgstr \"重置为默认值\"\n\n#: qt/result_window.py:185\nmsgid \"{} Results\"\nmsgstr \"{} 个结果\"\n\n#: qt/result_window.py:193 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Dupes Only\"\nmsgstr \"仅 Dupes\"\n\n#: qt/result_window.py:194\nmsgid \"Delta Values\"\nmsgstr \"Delta 值\"\n\n#: qt/result_window.py:310 cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select a file to save your results to\"\nmsgstr \"选择一个文件来保存您的结果\"\n\n#: qt/se/preferences_dialog.py:41\nmsgid \"Ignore files smaller than\"\nmsgstr \"忽略文件当其小于\"\n\n#: qt/se/preferences_dialog.py:52 cocoa/en.lproj/Localizable.strings:0\nmsgid \"KB\"\nmsgstr \"KB\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"%@ Results\"\nmsgstr \"%@ 结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Action\"\nmsgstr \"操作\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Add New Folder...\"\nmsgstr \"添加新文件夹...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Advanced\"\nmsgstr \"高级\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Automatically check for updates\"\nmsgstr \"自动检查更新\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Basic\"\nmsgstr \"基本\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Bring All to Front\"\nmsgstr \"全部前置\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Check for update...\"\nmsgstr \"检查更新...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Close Window\"\nmsgstr \"关闭窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Copy\"\nmsgstr \"复制\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Custom command (arguments: %d for dupe, %r for ref):\"\nmsgstr \"自定义命令 (参数: %d 指重复文件, %r 指源文件):\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Cut\"\nmsgstr \"剪切\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Delta\"\nmsgstr \"Delta\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details of Selected File\"\nmsgstr \"所选文件的详细信息\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Details Panel\"\nmsgstr \"详细信息面板\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Directories\"\nmsgstr \"目录\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru\"\nmsgstr \"dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Preferences\"\nmsgstr \"dupeGuru 设置\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Results\"\nmsgstr \"dupeGuru 结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"dupeGuru Website\"\nmsgstr \"dupeGuru 网站\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Edit\"\nmsgstr \"编辑\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to CSV\"\nmsgstr \"导出结果到 CSV\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Export Results to XHTML\"\nmsgstr \"导出结果到 XHTML\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Fewer results\"\nmsgstr \"较少结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter\"\nmsgstr \"过滤器\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter hardness:\"\nmsgstr \"过滤强度:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Filter Results...\"\nmsgstr \"过滤结果...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Folder Selection Window\"\nmsgstr \"文件夹选择窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Font Size:\"\nmsgstr \"字体大小:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide dupeGuru\"\nmsgstr \"隐藏 dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Hide Others\"\nmsgstr \"隐藏其他\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ignore files smaller than:\"\nmsgstr \"忽略文件当其小于:\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Load from file...\"\nmsgstr \"从文件载入...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Minimize\"\nmsgstr \"最小化\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Mode\"\nmsgstr \"模式\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"More results\"\nmsgstr \"更多结果\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Ok\"\nmsgstr \"确定\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Paste\"\nmsgstr \"粘贴\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Preferences...\"\nmsgstr \"设置...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quick Look\"\nmsgstr \"快速查找\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Quit dupeGuru\"\nmsgstr \"退出 dupeGuru\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset to Default\"\nmsgstr \"重置为默认值\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reset To Defaults\"\nmsgstr \"重置为默认值\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal\"\nmsgstr \"显示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Reveal Selected in Finder\"\nmsgstr \"在 Finder 中显示所选项\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Select All\"\nmsgstr \"全选\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Send Marked to Trash...\"\nmsgstr \"将已标记的文件移至回收站...\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Services\"\nmsgstr \"服务\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Show All\"\nmsgstr \"全部显示\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Start Duplicate Scan\"\nmsgstr \"开始重复内容扫描\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"The name '%@' already exists.\"\nmsgstr \"名称 '%@' 已存在。\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Window\"\nmsgstr \"窗口\"\n\n#: cocoa/en.lproj/Localizable.strings:0\nmsgid \"Zoom\"\nmsgstr \"缩放\"\n\n#: qt\\app.py:158\nmsgid \"Exclusion Filters\"\nmsgstr \"排除过滤器\"\n\n#: qt\\directories_dialog.py:91\nmsgid \"Scan Results\"\nmsgstr \"扫描结果\"\n\n#: qt\\directories_dialog.py:95\nmsgid \"Load Directories...\"\nmsgstr \"载入目录...\"\n\n#: qt\\directories_dialog.py:96\nmsgid \"Save Directories...\"\nmsgstr \"保存目录...\"\n\n#: qt\\directories_dialog.py:337\nmsgid \"Select a directories file to load\"\nmsgstr \"选择一个目录文件以载入\"\n\n#: qt\\directories_dialog.py:338\nmsgid \"dupeGuru Results (*.dupegurudirs)\"\nmsgstr \"dupeGuru 结果 （*.dupegurudirs）\"\n\n#: qt\\directories_dialog.py:347\nmsgid \"Select a file to save your directories to\"\nmsgstr \"选择一个文件来保存您的目录\"\n\n#: qt\\directories_dialog.py:348\nmsgid \"dupeGuru Directories (*.dupegurudirs)\"\nmsgstr \"dupeGuru 目录 （*.dupegurudirs）\"\n\n#: qt\\exclude_list_dialog.py:44\nmsgid \"Add\"\nmsgstr \"添加\"\n\n#: qt\\exclude_list_dialog.py:46\nmsgid \"Restore defaults\"\nmsgstr \"还原至默认值\"\n\n#: qt\\exclude_list_dialog.py:47\nmsgid \"Test string\"\nmsgstr \"测试字符串\"\n\n#: qt\\exclude_list_dialog.py:83\nmsgid \"Type a python regular expression here...\"\nmsgstr \"在此输入一个python正则表达式...\"\n\n#: qt\\exclude_list_dialog.py:85\nmsgid \"Type a file system path or filename here...\"\nmsgstr \"在此输入一个系统路径或者文件名...\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happen to match one of the regular expressions.<br>For each file collected two tests are perfomed on each of them to determine whether or not to filter them out:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with no path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the test string feature by pasting a fake path in it:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"这些（大小写敏感）的python正则表达式会在扫描过程中筛选文件。<br>如果目录的名称和某一个正则表达式匹配的话，它们的<strong>默认状态</strong>将为被设为排除状态。<br>每一个被采集的文件都会被进行两种不同的测试来决定它是否会被排除掉：<br><li>1. 没有路径分隔符的正则表达式只会和文件名作比较。</li>\\n\"\n\"<li>2. 有路径分隔符的正则表达式，会和文件的完整路径作比较。</li><br>\\n\"\n\"如：假如您想要仅从“我的图片”目录排除掉 .PNG 文件的话：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>您可以使用测试字符串功能来测试正则表达式，只需要在其中粘贴一个假的路径：<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"匹配的正则表达式会被高亮。<br>假如至少有一个高亮的话，在扫描中这个路径将会被忽略。<br><br>以“.”开头的目录和文件默认就会被忽略。<br><br>\"\n\n#: qt\\exclude_list_table.py:36\nmsgid \"Compilation error: \"\nmsgstr \"编译错误：\"\n\n#: qt\\pe\\image_viewer.py:56\nmsgid \"Increase zoom\"\nmsgstr \"放大\"\n\n#: qt\\pe\\image_viewer.py:66\nmsgid \"Decrease zoom\"\nmsgstr \"缩小\"\n\n#: qt\\pe\\image_viewer.py:71\nmsgid \"Ctrl+/\"\nmsgstr \"Ctrl+/\"\n\n#: qt\\pe\\image_viewer.py:76\nmsgid \"Normal size\"\nmsgstr \"正常尺寸\"\n\n#: qt\\pe\\image_viewer.py:81\nmsgid \"Ctrl+*\"\nmsgstr \"Ctrl+*\"\n\n#: qt\\pe\\image_viewer.py:86\nmsgid \"Best fit\"\nmsgstr \"最佳结果\"\n\n#: qt\\pe\\preferences_dialog.py:49\nmsgid \"Picture cache mode:\"\nmsgstr \"图片缓存模式：\"\n\n#: qt\\pe\\preferences_dialog.py:56\nmsgid \"Override theme icons in viewer toolbar\"\nmsgstr \"在图片浏览器的工具栏里，覆盖默认图标设置\"\n\n#: qt\\pe\\preferences_dialog.py:58\nmsgid \"\"\n\"Use our own internal icons instead of those provided by the theme engine\"\nmsgstr \"使用程序自带的图标来替代系统默认图标。\"\n\n#: qt\\pe\\preferences_dialog.py:66\nmsgid \"Show scrollbars in image viewers\"\nmsgstr \"在图片浏览器里显示滚动条\"\n\n#: qt\\pe\\preferences_dialog.py:68\nmsgid \"\"\n\"When the image displayed doesn't fit the viewport, show scrollbars to span \"\n\"the view around\"\nmsgstr \"当图片尺寸大于显示窗口时，显示滚动条来移动图片。\"\n\n#: qt\\preferences_dialog.py:156\nmsgid \"Use default position for tab bar (requires restart)\"\nmsgstr \"在默认位置显示Tab Bar（需要重启）\"\n\n#: qt\\preferences_dialog.py:158\nmsgid \"\"\n\"Place the tab bar below the main menu instead of next to it\\n\"\n\"On MacOS, the tab bar will fill up the window's width instead.\"\nmsgstr \"\"\n\"把Tab Bar放在主菜单下面而不是旁边\\n\"\n\"在MacOS上，Tab Bar会填充满整个窗口的宽度。\"\n\n#: qt\\preferences_dialog.py:172\nmsgid \"Use bold font for references\"\nmsgstr \"源文件使用粗体\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Reference foreground color:\"\nmsgstr \"源文件前景色：\"\n\n#: qt\\preferences_dialog.py:179\nmsgid \"Reference background color:\"\nmsgstr \"源文件背景色：\"\n\n#: qt\\preferences_dialog.py:182 qt\\preferences_dialog.py:216\nmsgid \"Delta foreground color:\"\nmsgstr \"Delta 前景色：\"\n\n#: qt\\preferences_dialog.py:195\nmsgid \"Show the title bar and can be docked\"\nmsgstr \"显示标题栏，并使其可被停靠\"\n\n#: qt\\preferences_dialog.py:197\nmsgid \"\"\n\"While the title bar is hidden, use the modifier key to drag the floating \"\n\"window around\"\nmsgstr \"标题栏隐藏时，使用修饰键来移动浮动窗口。\"\n\n#: qt\\preferences_dialog.py:199\nmsgid \"The title bar can only be disabled while the window is docked\"\nmsgstr \"仅当窗口被停靠时，标题栏可被隐藏\"\n\n#: qt\\preferences_dialog.py:202\nmsgid \"Vertical title bar\"\nmsgstr \"竖直标题栏\"\n\n#: qt\\preferences_dialog.py:204\nmsgid \"\"\n\"Change the title bar from horizontal on top, to vertical on the left side\"\nmsgstr \"把标题栏从顶部横向改为左侧竖直\"\n\n#: qt\\tabbed_window.py:44\nmsgid \"Show tab bar\"\nmsgstr \"显示 Tab Bar\"\n\n#: qt\\exclude_list_dialog.py:152\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>\\n\"\n\"Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\"这些（大小写敏感）的python正则表达式会在扫描过程中对结果进行过滤。<br>除非目录名称和正则表达式匹配，它们的<strong>默认状态</strong>会被设成从目录标签排除。<br>每一个文件都会经过两种测试，以确定它是否会被完全忽略：<br><li>1. 没有路径分隔符的正则表达式，仅用于和文件名进行比较。</li>\\n\"\n\"<li>2. 至少有一个路径分隔符的正则表达式，会被用于和文件的完整路径进行比较。</li><br>\\n\"\n\"例如：如果你想要仅在“图片”目录中排除所有.PNG文件：<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>你可以使用“测试字符串”按钮来测试你的正则表达式，只需要将虚拟的路径输入测试框即可：<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"匹配的正则表达式会被高亮。<br>假如有至少一个高亮，测试文件的文件名或者路径就会在扫描中被忽略。<br><br>以“.”开头的目录或文件默认会被忽略。<br><br>\"\n\n#: qt\\app.py:256\nmsgid \"Results\"\nmsgstr \"结果\"\n\n#: qt\\preferences_dialog.py:150\nmsgid \"General Interface\"\nmsgstr \"通用介面\"\n\n#: qt\\preferences_dialog.py:176\nmsgid \"Result Table\"\nmsgstr \"结果表\"\n\n#: qt\\preferences_dialog.py:205\nmsgid \"Details Window\"\nmsgstr \"详细信息窗口\"\n\n#: qt\\preferences_dialog.py:285\nmsgid \"General\"\nmsgstr \"一般\"\n\n#: qt\\preferences_dialog.py:286\nmsgid \"Display\"\nmsgstr \"展示\"\n\n#: qt\\se\\preferences_dialog.py:70\nmsgid \"Partially hash files bigger than\"\nmsgstr \"只哈希部分如果文件大于\"\n\n#: qt\\se\\preferences_dialog.py:80\nmsgid \"MB\"\nmsgstr \"MB\"\n\n#: qt\\preferences_dialog.py:163\nmsgid \"Use native OS dialogs\"\nmsgstr \"使用操作系统原生对话窗口\"\n\n#: qt\\preferences_dialog.py:166\nmsgid \"\"\n\"For actions such as file/folder selection use the OS native dialogs.\\n\"\n\"Some native dialogs have limited functionality.\"\nmsgstr \"使用操作系统原生对话窗口选择文件、文件夹。部分系统的原生对话窗口功能可能有限制。\"\n\n#: qt\\se\\preferences_dialog.py:68\nmsgid \"Ignore files larger than\"\nmsgstr \"忽略文件，如果大于\"\n\n#: qt\\app.py:135 qt\\app.py:293\nmsgid \"Clear Cache\"\nmsgstr \"清除缓存\"\n\n#: qt\\app.py:294\nmsgid \"\"\n\"Do you really want to clear the cache? This will remove all cached file \"\n\"hashes and picture analysis.\"\nmsgstr \"你确定要清除缓存吗？所有缓存的文件hash和图片分析都会被移除。\"\n\n#: qt\\app.py:299\nmsgid \"Cache cleared.\"\nmsgstr \"缓存已清除。\"\n\n#: qt\\preferences_dialog.py:173\nmsgid \"Use dark style\"\nmsgstr \"使用暗色主题\"\n\n#: qt\\preferences_dialog.py:241\nmsgid \"Profile scan operation\"\nmsgstr \"将扫描操作保存为配置\"\n\n#: qt\\preferences_dialog.py:242\nmsgid \"Profile the scan operation and save logs for optimization.\"\nmsgstr \"将扫描操作保存为配置，并保存日志用于优化。\"\n\n#: qt\\preferences_dialog.py:246\nmsgid \"Logs located in: <a href=\\\"{}\\\">{}</a>\"\nmsgstr \"日志位于：<a href=\\\"{}\\\">{}\"\n\n#: qt\\preferences_dialog.py:291\nmsgid \"Debug\"\nmsgstr \"调试\"\n\n#: qt\\about_box.py:31\nmsgid \"About {}\"\nmsgstr \"关于 {}\"\n\n#: qt\\about_box.py:47\nmsgid \"Version {}\"\nmsgstr \"版本 {}\"\n\n#: qt\\about_box.py:49 qt\\about_box.py:75\nmsgid \"Checking for updates...\"\nmsgstr \"检查更新...\"\n\n#: qt\\about_box.py:54\nmsgid \"Licensed under GPLv3\"\nmsgstr \"本项目基于GPLv3开源协议发布\"\n\n#: qt\\about_box.py:68\nmsgid \"No update available.\"\nmsgstr \"没有新版本。\"\n\n#: qt\\about_box.py:71\nmsgid \"New version {} available, download <a href=\\\"{}\\\">here</a>.\"\nmsgstr \"有新版本{}可用，在<a href=\\\"{}\\\">这里</a>下载。\"\n\n#: qt\\error_report_dialog.py:50\nmsgid \"Error Report\"\nmsgstr \"错误报告\"\n\n#: qt\\error_report_dialog.py:54\nmsgid \"Something went wrong. How about reporting the error?\"\nmsgstr \"发生错误，是否要报告错误？\"\n\n#: qt\\error_report_dialog.py:60\nmsgid \"\"\n\"Error reports should be reported as GitHub issues. You can copy the error traceback above and paste it in a new issue.\\n\"\n\"\\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\"\\n\"\n\"What usually really helps is if you add a description of how you got the error. Thanks!\\n\"\n\"\\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.\"\nmsgstr \"\"\n\"错误报告应该以GitHub issue的形式进行提交。您可以把错误信息复制粘贴到新的issue中\\n\"\n\"\\n\"\n\"在提交新issue前，请搜索已经存在的issue，以确保没有其他人已经报告了相同的错误。同时请确保使用仓库中的最新版进行测试，因为您所遇到的bug可能已经被最新版修复。\\n\"\n\"\\n\"\n\"如果您能详细描述一下错误发生时的具体情况，将会更好的帮助我们解决问题，谢谢！\\n\"\n\"\\n\"\n\"虽然本程序在此错误后依然会继续运行，但是可能处于不稳定的状态，因此推荐重启本程序。\"\n\n#: qt\\error_report_dialog.py:80\nmsgid \"Go to GitHub\"\nmsgstr \"前往GitHub\"\n\n#: qt\\preferences.py:24\nmsgid \"Czech\"\nmsgstr \"捷克语\"\n\n#: qt\\preferences.py:25\nmsgid \"German\"\nmsgstr \"德语\"\n\n#: qt\\preferences.py:26\nmsgid \"Greek\"\nmsgstr \"希腊语\"\n\n#: qt\\preferences.py:27\nmsgid \"English\"\nmsgstr \"英语\"\n\n#: qt\\preferences.py:28\nmsgid \"Spanish\"\nmsgstr \"西班牙语\"\n\n#: qt\\preferences.py:29\nmsgid \"French\"\nmsgstr \"法语\"\n\n#: qt\\preferences.py:30\nmsgid \"Armenian\"\nmsgstr \"亚美尼亚语\"\n\n#: qt\\preferences.py:31\nmsgid \"Italian\"\nmsgstr \"意大利语\"\n\n#: qt\\preferences.py:32\nmsgid \"Japanese\"\nmsgstr \"日语\"\n\n#: qt\\preferences.py:33\nmsgid \"Korean\"\nmsgstr \"韩语\"\n\n#: qt\\preferences.py:34\nmsgid \"Malay\"\nmsgstr \"马来语\"\n\n#: qt\\preferences.py:35\nmsgid \"Dutch\"\nmsgstr \"荷兰语\"\n\n#: qt\\preferences.py:36\nmsgid \"Polish\"\nmsgstr \"波兰语\"\n\n#: qt\\preferences.py:37\nmsgid \"Brazilian\"\nmsgstr \"巴西葡萄牙语\"\n\n#: qt\\preferences.py:38\nmsgid \"Russian\"\nmsgstr \"俄语\"\n\n#: qt\\preferences.py:39\nmsgid \"Turkish\"\nmsgstr \"土耳其\"\n\n#: qt\\preferences.py:40\nmsgid \"Ukrainian\"\nmsgstr \"乌克兰语\"\n\n#: qt\\preferences.py:41\nmsgid \"Vietnamese\"\nmsgstr \"越南语\"\n\n#: qt\\preferences.py:42\nmsgid \"Chinese (Simplified)\"\nmsgstr \"中文（简体）\"\n\n#: qt\\recent.py:54\nmsgid \"Clear List\"\nmsgstr \"清空列表\"\n\n#: qt\\search_edit.py:78\nmsgid \"Search...\"\nmsgstr \"搜索...\"\n\n#: qt\\preferences_dialog.py:219\nmsgid \"\"\n\"These options are for advanced users or for very specific situations, most \"\n\"users should not have to modify these.\"\nmsgstr \"这些选项是供高级用户或者特定情况下使用，大多数用户不该修改它们。\"\n\n#: qt\\preferences_dialog.py:225\nmsgid \"Include existence check after scan completion\"\nmsgstr \"在扫描结束后检查文件是否存在\"\n\n#: qt\\preferences_dialog.py:227\nmsgid \"Ignore difference in mtime when loading cached digests\"\nmsgstr \"载入缓存摘要时忽略mtime的不同\"\n\n#: qt\\progress_window.py:64\nmsgid \"Cancel?\"\nmsgstr \"\"\n\n#: qt\\progress_window.py:65\nmsgid \"Are you sure you want to cancel? All progress will be lost.\"\nmsgstr \"\"\n\n#: qt\\exclude_list_dialog.py:161\nmsgid \"\"\n\"These (case sensitive) python regular expressions will filter out files during scans.<br>Directores will also have their <strong>default state</strong> set to Excluded in the Directories tab if their name happens to match one of the selected regular expressions.<br>For each file collected, two tests are performed to determine whether or not to completely ignore it:<br><li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\\n\"\n\"<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li><br>Example: if you want to filter out .PNG files from the \\\"My Pictures\\\" directory only:<br><code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>You can test the regular expression with the \\\"test string\\\" button after pasting a fake path in the test field:<br><code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\\n\"\n\"Matching regular expressions will be highlighted.<br>If there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>Directories and files starting with a period '.' are filtered out by default.<br><br>\"\nmsgstr \"\"\n\n#: qt\\pe\\preferences_dialog.py:24\nmsgid \"Match pictures of different rotations\"\nmsgstr \"\"\n"
  },
  {
    "path": "macos.md",
    "content": "## How to build dupeGuru for macos\nThese instructions are for the Qt version of the UI on macOS.\n\n*Note: The Cocoa UI of dupeGuru is hosted in a separate repo: https://github.com/arsenetar/dupeguru-cocoa and is no longer \"supported\".*\n### Prerequisites\n\n- [Python 3.7+][python]\n- [Xcode 12.3][xcode] or just Xcode command line tools (older versions can be used if not interested in arm macs)\n- [Homebrew][homebrew]\n- [qt5](https://www.qt.io/)\n\n#### Prerequisite setup\n1. Install Xcode if desired\n2. Install [Homebrew][homebrew], if not on the path after install (arm based Macs) create `~/.zshrc`\nwith `export PATH=\"/opt/homebrew/bin:$PATH\"`. Will need to reload terminal or source the file to take\neffect.\n3. Install qt5 with `brew`. If you are using a version of macos without system python 3.7+ then you will\nalso need to install that via brew or with pyenv.\n\n        $ brew install qt5\n\n    NOTE: Using `brew` to install qt5 is to allow pyqt5 to build without a native wheel\n    available.  If you are using an intel based mac you can probably skip this step.\n\n4. May need to launch a new terminal to have everything working.\n\n### With build.py\nOSX comes with a version of python 3 by default in newer versions of OSX.  To produce universal\nbuilds either the 3.8 version shipped in macos or 3.9.1 or newer needs to be used. If needing to\nbuild pyqt5 from source then the first line below is needed, else it may be omitted. (Path shown is\nfor an arm mac.)\n\n    $ export PATH=\"/opt/homebrew/opt/qt/bin:$PATH\"\n    $ cd <dupeGuru directory>\n    $ python3 -m venv ./env\n    $ source ./env/bin/activate\n    $ pip install -r requirements.txt\n    $ python build.py\n    $ python run.py\n\n### Generate OSX Packages\nThe extra requirements need to be installed to run packaging: `pip install -r requirements-extra.txt`.\nRun the following in the respective virtual environment.\n\n    $ python package.py\n\nThis will produce a dupeGuru.app in the dist folder.\n\n### Running tests\nThe complete test suite can be run with tox just like on linux. NOTE: The extra requirements need to\nbe installed to run unit tests: `pip install -r requirements-extra.txt`.\n\n[python]: http://www.python.org/\n[homebrew]: https://brew.sh/\n[xcode]: https://developer.apple.com/xcode/\n"
  },
  {
    "path": "package.py",
    "content": "# Copyright 2017 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport sys\nimport os\nimport os.path as op\nimport compileall\nimport shutil\nimport json\nfrom argparse import ArgumentParser\nimport platform\nimport distro\nimport re\n\nfrom hscommon.build import (\n    print_and_do,\n    copy_packages,\n    build_debian_changelog,\n    get_module_version,\n    filereplace,\n    copy,\n    setup_package_argparser,\n    copy_all,\n)\n\nENTRY_SCRIPT = \"run.py\"\nLOCALE_DIR = \"build/locale\"\nHELP_DIR = \"build/help\"\n\n\ndef parse_args():\n    parser = ArgumentParser()\n    setup_package_argparser(parser)\n    return parser.parse_args()\n\n\ndef check_loc_doc():\n    if not op.exists(LOCALE_DIR):\n        print('Locale files are missing. Have you run \"build.py --loc\"?')\n    # include help files if they are built otherwise exit as they should be included?\n    if not op.exists(HELP_DIR):\n        print('Help files are missing. Have you run \"build.py --doc\"?')\n    return op.exists(LOCALE_DIR) and op.exists(HELP_DIR)\n\n\ndef copy_files_to_package(destpath, packages, with_so):\n    # when with_so is true, we keep .so files in the package, and otherwise, we don't. We need this\n    # flag because when building debian src pkg, we *don't* want .so files (they're compiled later)\n    # and when we're packaging under Arch, we're packaging a binary package, so we want them.\n    if op.exists(destpath):\n        shutil.rmtree(destpath)\n    os.makedirs(destpath)\n    shutil.copy(ENTRY_SCRIPT, op.join(destpath, ENTRY_SCRIPT))\n    extra_ignores = [\"*.so\"] if not with_so else None\n    copy_packages(packages, destpath, extra_ignores=extra_ignores)\n    # include locale files if they are built otherwise exit as it will break\n    # the localization\n    if not check_loc_doc():\n        print(\"Exiting...\")\n        return\n    shutil.copytree(op.join(\"build\", \"help\"), op.join(destpath, \"help\"))\n    shutil.copytree(op.join(\"build\", \"locale\"), op.join(destpath, \"locale\"))\n    compileall.compile_dir(destpath)\n\n\ndef package_debian_distribution(distribution):\n    app_version = get_module_version(\"core\")\n    version = \"{}~{}\".format(app_version, distribution)\n    destpath = op.join(\"build\", \"dupeguru-{}\".format(version))\n    srcpath = op.join(destpath, \"src\")\n    packages = [\"hscommon\", \"core\", \"qt\", \"send2trash\"]\n    copy_files_to_package(srcpath, packages, with_so=False)\n    os.mkdir(op.join(destpath, \"modules\"))\n    copy_all(op.join(\"core\", \"pe\", \"modules\", \"*.*\"), op.join(destpath, \"modules\"))\n    copy(\n        op.join(\"qt\", \"pe\", \"modules\", \"block.c\"),\n        op.join(destpath, \"modules\", \"block_qt.c\"),\n    )\n    copy(\n        op.join(\"pkg\", \"debian\", \"build_pe_modules.py\"),\n        op.join(destpath, \"build_pe_modules.py\"),\n    )\n    debdest = op.join(destpath, \"debian\")\n    debskel = op.join(\"pkg\", \"debian\")\n    os.makedirs(debdest)\n    debopts = json.load(open(op.join(debskel, \"dupeguru.json\")))\n    for fn in [\"compat\", \"copyright\", \"dirs\", \"rules\", \"source\"]:\n        copy(op.join(debskel, fn), op.join(debdest, fn))\n    filereplace(op.join(debskel, \"control\"), op.join(debdest, \"control\"), **debopts)\n    filereplace(op.join(debskel, \"Makefile\"), op.join(destpath, \"Makefile\"), **debopts)\n    filereplace(op.join(debskel, \"dupeguru.desktop\"), op.join(debdest, \"dupeguru.desktop\"), **debopts)\n    changelogpath = op.join(\"help\", \"changelog\")\n    changelog_dest = op.join(debdest, \"changelog\")\n    project_name = debopts[\"pkgname\"]\n    from_version = \"2.9.2\"\n    build_debian_changelog(\n        changelogpath,\n        changelog_dest,\n        project_name,\n        from_version=from_version,\n        distribution=distribution,\n    )\n    shutil.copy(op.join(\"images\", \"dgse_logo_128.png\"), srcpath)\n    os.chdir(destpath)\n    cmd = \"dpkg-buildpackage -F -us -uc\"\n    os.system(cmd)\n    os.chdir(\"../..\")\n\n\ndef package_debian():\n    print(\"Packaging for Debian/Ubuntu\")\n    for distribution in [\"unstable\"]:\n        package_debian_distribution(distribution)\n\n\ndef package_arch():\n    # For now, package_arch() will only copy the source files into build/. It copies less packages\n    # than package_debian because there are more python packages available in Arch (so we don't\n    # need to include them).\n    print(\"Packaging for Arch\")\n    srcpath = op.join(\"build\", \"dupeguru-arch\")\n    packages = [\"hscommon\", \"core\", \"qt\"]\n    copy_files_to_package(srcpath, packages, with_so=True)\n    shutil.copy(op.join(\"images\", \"dgse_logo_128.png\"), srcpath)\n    debopts = json.load(open(op.join(\"pkg\", \"arch\", \"dupeguru.json\")))\n    filereplace(op.join(\"pkg\", \"arch\", \"dupeguru.desktop\"), op.join(srcpath, \"dupeguru.desktop\"), **debopts)\n\n\ndef package_source_txz():\n    print(\"Creating git archive\")\n    app_version = get_module_version(\"core\")\n    name = \"dupeguru-src-{}.tar\".format(app_version)\n    base_path = os.getcwd()\n    build_path = op.join(base_path, \"build\")\n    dest = op.join(build_path, name)\n    print_and_do(\"git archive -o {} HEAD\".format(dest))\n    print_and_do(\"xz {}\".format(dest))\n\n\ndef package_windows():\n    app_version = get_module_version(\"core\")\n    arch = platform.architecture()[0]\n    # Information to pass to pyinstaller and NSIS\n    match = re.search(\"[0-9]+.[0-9]+.[0-9]+\", app_version)\n    version_array = match.group(0).split(\".\")\n    match = re.search(\"[0-9]+\", arch)\n    bits = match.group(0)\n    if bits == \"64\":\n        arch = \"x64\"\n    else:\n        arch = \"x86\"\n    # include locale files if they are built otherwise exit as it will break\n    # the localization\n    if not check_loc_doc():\n        print(\"Exiting...\")\n        return\n    # create version information file from template\n    try:\n        version_template = open(\"win_version_info.temp\", \"r\")\n        version_info = version_template.read()\n        version_template.close()\n        version_info_file = open(\"win_version_info.txt\", \"w\")\n        version_info_file.write(version_info.format(version_array[0], version_array[1], version_array[2], bits))\n        version_info_file.close()\n    except Exception:\n        print(\"Error creating version info file, exiting...\")\n        return\n    # run pyinstaller from here:\n    import PyInstaller.__main__\n\n    # UCRT dlls are included if the system has the windows kit installed\n    PyInstaller.__main__.run(\n        [\n            \"--name=dupeguru-win{0}\".format(bits),\n            \"--windowed\",\n            \"--noconfirm\",\n            \"--icon=images/dgse_logo.ico\",\n            \"--add-data={0};locale\".format(LOCALE_DIR),\n            \"--add-data={0};help\".format(HELP_DIR),\n            \"--version-file=win_version_info.txt\",\n            \"--paths=C:\\\\Program Files (x86)\\\\Windows Kits\\\\10\\\\Redist\\\\ucrt\\\\DLLs\\\\{0}\".format(arch),\n            ENTRY_SCRIPT,\n        ]\n    )\n    # remove version info file\n    os.remove(\"win_version_info.txt\")\n    # Call NSIS (TODO update to not use hardcoded path)\n    cmd = (\n        '\"C:\\\\Program Files (x86)\\\\NSIS\\\\Bin\\\\makensis.exe\" '\n        \"/DVERSIONMAJOR={0} /DVERSIONMINOR={1} /DVERSIONPATCH={2} /DBITS={3} setup.nsi\"\n    )\n    print_and_do(cmd.format(version_array[0], version_array[1], version_array[2], bits))\n\n\ndef package_macos():\n    # include locale files if they are built otherwise exit as it will break\n    # the localization\n    if not check_loc_doc():\n        print(\"Exiting\")\n        return\n    # run pyinstaller from here:\n    import PyInstaller.__main__\n\n    PyInstaller.__main__.run(\n        [\n            \"--name=dupeguru\",\n            \"--windowed\",\n            \"--noconfirm\",\n            \"--icon=images/dupeguru.icns\",\n            \"--osx-bundle-identifier=com.hardcoded-software.dupeguru\",\n            \"--add-data={0}:locale\".format(LOCALE_DIR),\n            \"--add-data={0}:help\".format(HELP_DIR),\n            \"{0}\".format(ENTRY_SCRIPT),\n        ]\n    )\n\n\ndef main():\n    args = parse_args()\n    if args.src_pkg:\n        print(\"Creating source package for dupeGuru\")\n        package_source_txz()\n        return\n    print(\"Packaging dupeGuru with UI qt\")\n    if sys.platform == \"win32\":\n        package_windows()\n    elif sys.platform == \"darwin\":\n        package_macos()\n    else:\n        if not args.arch_pkg:\n            distname = distro.id()\n        else:\n            distname = \"arch\"\n        if distname == \"arch\":\n            package_arch()\n        else:\n            package_debian()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "pkg/arch/dupeguru.desktop",
    "content": "[Desktop Entry]\nName={longname}\nComment=Find duplicate files.\nExec={execname}\nIcon={iconpath}\nTerminal=false\nType=Application\nCategories=Utility;\n"
  },
  {
    "path": "pkg/arch/dupeguru.json",
    "content": "{\n\t\"pkgname\": \"dupeguru\",\n\t\"longname\": \"dupeGuru\",\n\t\"execname\": \"dupeguru\",\n\t\"arch\": \"any\",\n\t\"iconpath\": \"dupeguru\"\n}\n"
  },
  {
    "path": "pkg/debian/Makefile",
    "content": "#!/usr/bin/make -f\n\nall:\n\tdh_prep\n\tdh_installdirs\n\ttouch build_pe_modules.py\n\tpython3 build_pe_modules.py\n\tchmod +x src/run.py\n\tcp -R src/ \"$(CURDIR)/debian/{pkgname}/usr/share/{execname}\"\n\tcp \"$(CURDIR)/debian/{execname}.desktop\" \"$(CURDIR)/debian/{pkgname}/usr/share/applications\"\n\tmkdir -p \"$(CURDIR)/debian/{pkgname}/usr/share/pixmaps\"\n\tln -s \"/usr/share/{execname}/dgse_logo_128.png\" \"$(CURDIR)/debian/{pkgname}/usr/share/pixmaps/{execname}.png\"\n\tln -s \"/usr/share/{execname}/run.py\" \"$(CURDIR)/debian/{pkgname}/usr/bin/{execname}\"\n"
  },
  {
    "path": "pkg/debian/build_pe_modules.py",
    "content": "import sys\nimport os\nimport os.path as op\nimport shutil\nimport importlib\n\nfrom setuptools import setup, Extension\n\nsys.path.insert(1, op.abspath(\"src\"))\n\nfrom hscommon.build import move_all\n\nexts = [\n    Extension(\"_block\", [op.join(\"modules\", \"block.c\"), op.join(\"modules\", \"common.c\")]),\n    Extension(\"_cache\", [op.join(\"modules\", \"cache.c\"), op.join(\"modules\", \"common.c\")]),\n    Extension(\"_block_qt\", [op.join(\"modules\", \"block_qt.c\")]),\n]\nsetup(\n    script_args=[\"build_ext\", \"--inplace\"],\n    ext_modules=exts,\n)\nmove_all(\"_block_qt*\", op.join(\"src\", \"qt\", \"pe\"))\nmove_all(\"_cache*\", op.join(\"src\", \"core/pe\"))\nmove_all(\"_block*\", op.join(\"src\", \"core/pe\"))\n"
  },
  {
    "path": "pkg/debian/changelog",
    "content": "dupeguru (4.0.4-1) unstable; urgency=low\n\n  * Update qt/platform.py to support other Unix style OSes (#444)\n  * Fix font size scaling issue in properties dialog [qt] (#504)\n  * Updates to support Python 3.7\n  * Fix issue with result window appearing partially off-screen [qt] (#521)\n  * Fix translation error for Simplified Chinese\n  * Updates to language files for German (#479)\n  * Fix error with multiple close calls to the progress window [qt] (#460, #449)\n  * Add Travis CI Builds\n  * Un-recurse methods get_files() and get_state() to improve stability (#421)\n  * Updates to language files for Italian (#445, #446, #447, #448)\n  * Fix issue with cache_shelve (#402, #439)\n  * Updated Windows packaging and builds (#438, #456, #461, #491, #474, #490, #565)\n  * Handle OS termination signals (#425)\n  * Make documentation installation optional\n  * Move cocoa UI to dupeguru-cocoa [cocoa]\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Mon, 13 May 2019 00:00:00 +0000\n\ndupeguru (4.0.3-1) unstable; urgency=low\n\n  * Add new picture cache backend: shelve\n  * Make shelve picture cache backend the active one on MacOS to fix #394 more  elegantly. [cocoa]\n  * Remove Sparkle (auto-updates) due to technical limitations. [cocoa]\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 24 Nov 2016 00:00:00 +0000\n\ndupeguru (4.0.2-1) unstable; urgency=low\n\n  * Fix systematic crash in Picture Mode under MacOS Sierra. (#394)\n  * No change for Linux. Just keeping version in sync.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sun, 09 Oct 2016 00:00:00 +0000\n\ndupeguru (4.0.1-1) unstable; urgency=low\n\n  * Add Greek localization, by Gabriel Koutilellis. (#382)\n  * Fix localization base path. [qt] (#378)\n  * Fix broken load results dialog. [qt]\n  * Fix crash on load results. [cocoa] (#380)\n  * Save preferences more predictably. [qt] (#379)\n  * Fix picture mode's fuzzy block scanner threshold. (#387)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 24 Aug 2016 00:00:00 +0000\n\ndupeguru (4.0.0-1) unstable; urgency=low\n\n  * Merge Standard, Music and Picture editions in the same application!\n  * Improve documentation. (#294)\n  * Add Polish, Korean, Spanish and Dutch localizations.\n  * qt: Fix wrong use_regexp option propagation to core. (#295)\n  * qt: Fix progress window mistakenly showing up on startup. (#357)\n  * Bump Python requirement to v3.4.\n  * Bump OS X requirement to 10.8\n  * Drop Windows support, maybe temporarily.  `Details <https://www.hardcoded.net/archive2015#2015-11-01>`_\n  * cocoa: Drop iPhoto, Aperture and iTunes support. Was unmaintained and obsolete.\n  * Drop \"Audio Contents\" scan type. Was confusing and seldom useful.\n  * Change license to GPLv3\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Fri, 01 Jul 2016 00:00:00 +0000\n\ndupeguru (3.9.1-1) unstable; urgency=low\n\n  * Fixed ``AttributeError: 'ComboboxModel' object has no attribute 'reset'``. [Linux, Windows] (#254)\n  * Fixed ``PermissionError`` on saving results. (#266)\n  * Fixed a build problem introduced by Sphinx 1.2.3.\n  * Updated German localisation, by Frank Weber.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Fri, 17 Oct 2014 00:00:00 +0000\n\ndupeguru (3.9.0-1) unstable; urgency=low\n\n  * This is mostly a dependencies upgrade.\n  * Upgraded to Python 3.3.\n  * Upgraded to Qt 5.\n  * Minimum Windows version is now Windows 7 64bit.\n  * Minimum Ubuntu version is now 14.04.\n  * Minimum OS X version is now 10.7 (Lion).\n  * ... But with a couple of little improvements.\n  * Improved documentation.\n  * Overwrite subfolders' state when setting states in folder dialog (#248)\n  * The error report dialog now brings the user to Github issues.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 19 Apr 2014 00:00:00 +0000\n\ndupeguru (3.8.0-1) unstable; urgency=low\n\n  * Disable symlink/hardlink deletion option when not relevant. (#247)\n  * Make Cmd+A select all folders in the Folder Selection dialog. [Mac] (#228)\n  * Make non-numeric delta comparison case insensitive. (#239)\n  * Fix surrogate-related UnicodeEncodeError on CSV export. (#210)\n  * Fixed crash on Dupe Count sorting with Delta + Dupes Only. (#238)\n  * Improved documentation.\n  * Important internal refactorings.\n  * Dropped Ubuntu 12.04 and 12.10 support.\n  * Removed the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 07 Dec 2013 00:00:00 +0000\n\ndupeguru (3.7.1-1) unstable; urgency=low\n\n  * Fixed folder scan type, which was broken in v3.7.0.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Mon, 19 Aug 2013 00:00:00 +0000\n\ndupeguru (3.7.0-1) unstable; urgency=low\n\n  * Improved delta values to support non-numerical values. (#213)\n  * Improved the Re-Prioritize dialog's UI. (#224)\n  * Added hardlink/symlink support on Windows Vista+. (#220)\n  * Dropped 32bit support on Mac OS X.\n  * Added Vietnamese localization by Phan Anh.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 17 Aug 2013 00:00:00 +0000\n\ndupeguru (3.6.1-1) unstable; urgency=low\n\n  * Improved \"Make Selection Reference\" to make it clearer. (#222)\n  * Improved \"Open Selected\" to allow opening more than one file at once. (#142)\n  * Fixed a few typos here and there. (#216 #225)\n  * Tweaked the fairware dialog ([More Info](http://www.hardcoded.net/articles/phasing-out-fairware)).\n  * Added Arch Linux packaging\n  * Added a 64-bit build for Windows.\n  * Improved Russian localization by Kyrill Detinov.\n  * Improved Brazilian localization by Victor Figueiredo.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sun, 28 Apr 2013 00:00:00 +0000\n\ndupeguru (3.6.0-1) unstable; urgency=low\n\n  * Added \"Export to CSV\". (#189)\n  * Added \"Replace with symlinks\" to complement \"Replace with hardlinks\". [Mac, Linux] (#194)\n  * dupeGuru now tells how many duplicates were affected after each re-prioritization operation. (#204)\n  * Added Longest/Shortest filename criteria in the re-prioritize dialog. (#198)\n  * Fixed result table cells which mistakenly became writable in v3.5.0. [Mac] (#203)\n  * Fixed \"Rename Selected\" which was broken since v3.5.0. [Mac] (#202)\n  * Fixed a bug where \"Reset to Defaults\" in the Columns menu wouldn't refresh menu items' marked state.\n  * Added Brazilian localization by Victor Figueiredo.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 08 Aug 2012 00:00:00 +0000\n\ndupeguru (3.5.0-1) unstable; urgency=low\n\n  * Added a Deletion Options panel.\n  * Greatly improved memory usage for big scans.\n  * Added a keybinding for the filter field. (#182) [Mac]\n  * Upgraded minimum requirements for Ubuntu to 12.04.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Fri, 01 Jun 2012 00:00:00 +0000\n\ndupeguru (3.4.1-1) unstable; urgency=low\n\n  * Fixed the \"Folders\" scan type. [Mac]\n  * Fixed localization issues. [Windows, Linux]\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 14 Apr 2012 00:00:00 +0000\n\ndupeguru (3.4.0-1) unstable; urgency=low\n\n  * Improved results window UI. [Windows, Linux]\n  * Added a dialog to edit the Ignore List.\n  * Added the ability to sort results by \"marked\" status.\n  * Fixed \"Open with default application\". (#190)\n  * Fixed a bug where there would be a false reporting of discarded matches. (#195)\n  * Fixed various localization glitches.\n  * Fixed hard crashes on crash reporting. (#196)\n  * Fixed bug where the details panel would show up at inconvenient places in the screen. [Windows, Linux]\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 29 Mar 2012 00:00:00 +0000\n\ndupeguru (3.3.3-1) unstable; urgency=low\n\n  * Fixed crash on adding some folders. [Mac OS X]\n  * Added Ukrainian localization by Yuri Petrashko.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 01 Feb 2012 00:00:00 +0000\n\ndupeguru (3.3.2-1) unstable; urgency=low\n\n  * Fixed random hard crashes (yeah, again). [Mac OS X]\n  * Fixed crash on Export to HTML. [Windows, Linux]\n  * Added Armenian localization by Hrant Ohanyan.\n  * Added Russian localization by Igor Pavlov.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Mon, 16 Jan 2012 00:00:00 +0000\n\ndupeguru (3.3.1-1) unstable; urgency=low\n\n  * Fixed a couple of nasty crashes.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Fri, 02 Dec 2011 00:00:00 +0000\n\ndupeguru (3.3.0-1) unstable; urgency=low\n\n  * Added multiple-selection in folder selection dialog for a more efficient folder removal. (#179)\n  * Fixed a crash in the prioritize dialog. (#178)\n  * Fixed a bug where mass marking with a filter would mark more than filtered duplicates. (#181)\n  * Fixed random hard crashes. [Mac OS X] (#183 #184)\n  * Added Czech localization by Ale Nehyba.\n  * Added Italian localization by Paolo Rossi.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 30 Nov 2011 00:00:00 +0000\n\ndupeguru (3.2.1-1) unstable; urgency=low\n\n  * Fixed a couple of broken action bindings from v3.2.0.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sun, 02 Oct 2011 00:00:00 +0000\n\ndupeguru (3.2.0-1) unstable; urgency=low\n\n  * Added duplicate re-prioritization dialog. (#138)\n  * Added font size preference for duplicate table. (#82)\n  * Added Quicklook support. [Mac OS X] (#21)\n  * Improved behavior of Mark Selected. (#139)\n  * Improved filename sorting. (#169)\n  * Added Chinese (Simplified) localization by Eric Dee.\n  * Tweaked the fairware system.\n  * Upgraded minimum requirements to OS X 10.6 and Ubuntu 11.04.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Tue, 27 Sep 2011 00:00:00 +0000\n\ndupeguru (3.1.2-1) unstable; urgency=low\n\n  * Fixed a bug preventing the Folders scan from working. (#172)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 25 Aug 2011 00:00:00 +0000\n\ndupeguru (3.1.1-1) unstable; urgency=low\n\n  * Added German localization by Gregor Ttzner.\n  * Improved OS X Lion compatibility. [Mac OS X]\n  * Made the file collection phase cancellable. (#168)\n  * Fixed glitch in folder window upon selecting a folder state. [Windows, Linux] (#165)\n  * Fixed a text coloring glitch in the results. (#156)\n  * Fixed glitch in the sorting feature of the Folder column. (#161)\n  * Make sure that saved results have the \".dupeguru\" extension. [Linux] (#157)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 24 Aug 2011 00:00:00 +0000\n\ndupeguru (3.1.0-1) unstable; urgency=low\n\n  * Added the \"Folders\" scan type. (#89)\n  * Fixed a couple of crashes. (#140 #149)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 16 Apr 2011 00:00:00 +0000\n\ndupeguru (3.0.2-1) unstable; urgency=low\n\n  * Fixed crash after removing marked dupes. (#140)\n  * Fixed crash on error handling. [Windows] (#144)\n  * Fixed crash on copy/move. [Windows] (#148)\n  * Fixed crash when launching dupeGuru from a very long folder name. [Mac OS X] (#119)\n  * Fixed a refresh bug in directory panel. (#153)\n  * Improved reliability of the \"Send to Trash\" operation. [Linux]\n  * Tweaked Fairware reminders.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 16 Mar 2011 00:00:00 +0000\n\ndupeguru (3.0.1-1) unstable; urgency=low\n\n  * Restored the context menu which had been broken in 3.0.0. [Mac OS X] (#133)\n  * Fixed a bug where an \"unsaved results\" warning would be issued on quit even with empty results. (#134)\n  * Removed focus from the cancel button in the progress dialog to avoid accidental cancellations. [Mac OS X] (#135)\n  * Folders added through drag and drop are added to the recent folders list. (#136)\n  * Added a debugging mode. (#132)\n  * Fixed french localization glitches.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 27 Jan 2011 00:00:00 +0000\n\ndupeguru (3.0.0-1) unstable; urgency=low\n\n  * Re-designed the UI. (#129)\n  * Internationalized dupeGuru and localized it to french. (#32)\n  * Changed the format of the help file. (#130)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Mon, 24 Jan 2011 00:00:00 +0000\n\ndupeguru (2.12.3-1) unstable; urgency=low\n\n  * Fixed bug causing results to be corrupted after a scan cancellation. (#120)\n  * Fixed crash when fetching Fairware unpaid hours. (#121)\n  * Fixed crash when replacing files with hardlinks. (#122)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sat, 01 Jan 2011 00:00:00 +0000\n\ndupeguru (2.12.2-1) unstable; urgency=low\n\n  * Fixed delta column colors which were broken since 2.12.0.\n  * Fixed column sorting crash. (#108)\n  * Fixed occasional crash during scan. (#106)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Tue, 05 Oct 2010 00:00:00 +0000\n\ndupeguru (2.12.1-1) unstable; urgency=low\n\n  * Re-licensed dupeGuru to BSD and made it [Fairware](http://open.hardcoded.net/about/).\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 30 Sep 2010 00:00:00 +0000\n\ndupeguru (2.12.0-1) unstable; urgency=low\n\n  * Improved UI with a little revamp.\n  * Added the possibility to place hardlinks to references after having deleted duplicates. [Mac OS X, Linux] (#91)\n  * Added an option to ignore duplicates hardlinking to the same file. [Mac OS X, Linux] (#92)\n  * Added multiple selection in the \"Add Directory\" dialog. [Mac OS X] (#105)\n  * Fixed a bug preventing drag & drop from working in the Directories panel. [Windows, Linux]\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Sun, 26 Sep 2010 00:00:00 +0000\n\ndupeguru (2.11.1-1) unstable; urgency=low\n\n  * Fixed HTML exporting which was broken in 2.11.0.\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 26 Aug 2010 00:00:00 +0000\n\ndupeguru (2.11.0-1) unstable; urgency=low\n\n  * Added the ability to save results (and reload them) at arbitrary locations.\n  * Improved the way reference files in dupe groups are chosen. (#15)\n  * Remember size/position of all windows between launches. (#102)\n  * Fixed a bug sometimes preventing dupeGuru from reloading previous results.\n  * Fixed a bug sometimes causing the progress dialog to be stuck there. [Mac OS X] (#103)\n  * Removed the Creation Date column, which wasn't displaying the correct value anyway. (#101)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 18 Aug 2010 00:00:00 +0000\n\ndupeguru (2.10.1-1) unstable; urgency=low\n\n  * Fixed a couple of crashes. (#95, #97, #100)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Thu, 15 Jul 2010 00:00:00 +0000\n\ndupeguru (2.10.0-1) unstable; urgency=low\n\n  * Improved error messages when files can't be sent to trash, moved or copied.\n  * Added a custom command invocation action. (#12)\n  * Filters are now applied on whole paths. (#4)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Tue, 13 Apr 2010 00:00:00 +0000\n\ndupeguru (2.9.2-1) unstable; urgency=low\n\n  * dupeGuru is now 64-bit on Mac OS X!\n  * Fixed a crash upon quitting when support folder is not present. (#83)\n  * Fixed a crash during sorting. (#85)\n  * Fixed selection glitches, especially while renaming. (#93)\n\n -- Virgil Dupras <hsoft@hardcoded.net>  Wed, 10 Feb 2010 00:00:00 +0000\n"
  },
  {
    "path": "pkg/debian/compat",
    "content": "9\n"
  },
  {
    "path": "pkg/debian/control",
    "content": "Source: {pkgname}\nSection: devel\nPriority: extra\nMaintainer: Virgil Dupras <hsoft@hardcoded.net>, Eugene San (eugenesan) <eugenesan@gmail.com>\nBuild-Depends: debhelper (>= 7), python3-dev, python3-setuptools\nStandards-Version: 3.9.2\nHomepage: https://dupeguru.voltaicideas.net\nVcs-Browser: https://github.com/arsenetar/dupeguru\nVcs-Git: https://github.com/arsenetar/dupeguru.git\n\nPackage: {pkgname}\nArchitecture: {arch}\nDepends: ${shlibs:Depends}, python3 (>=3.7), python3 (<<3.12), python3-pyqt5, python3-mutagen, python3-semantic-version\nProvides: dupeguru-se, dupeguru-me, dupeguru-pe\nReplaces: dupeguru-se, dupeguru-me, dupeguru-pe\nConflicts: dupeguru-se, dupeguru-me, dupeguru-pe\nDescription: {longname}\n dupeGuru is a cross-platform (Linux and OS X) GUI tool to find duplicate files in a system.\n It's written mostly in Python 3 and has the peculiarity of using multiple GUI toolkits,\n all using the same core Python code.\n On OS X, the UI layer is written in Objective-C and uses Cocoa.\n On Linux, it's written in Python and uses Qt5.\n"
  },
  {
    "path": "pkg/debian/copyright",
    "content": "Copyright 2014 Hardcoded Software Inc. (http://www.hardcoded.net)\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n    * 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.\n    * 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.\n    * 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.\n\nTHIS 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.\n"
  },
  {
    "path": "pkg/debian/dirs",
    "content": "usr/bin\nusr/share\nusr/share/applications\n"
  },
  {
    "path": "pkg/debian/dupeguru.desktop",
    "content": "[Desktop Entry]\nName={longname}\nComment=Find duplicate files.\nExec={execname}\nIcon={iconpath}\nTerminal=false\nType=Application\nCategories=Utility;\n"
  },
  {
    "path": "pkg/debian/dupeguru.json",
    "content": "{\n\t\"pkgname\": \"dupeguru\",\n\t\"longname\": \"dupeGuru\",\n\t\"execname\": \"dupeguru\",\n\t\"arch\": \"any\",\n\t\"iconpath\": \"dupeguru\"\n}\n"
  },
  {
    "path": "pkg/debian/rules",
    "content": "#!/usr/bin/make -f\n%:\n\tdh $@\n"
  },
  {
    "path": "pkg/debian/source/format",
    "content": "3.0 (native)\n"
  },
  {
    "path": "pkg/debian/source/options",
    "content": "compression = \"xz\"\n"
  },
  {
    "path": "pkg/dupeguru.desktop",
    "content": "[Desktop Entry]\nName=dupeGuru\nComment=Find duplicate files.\nExec=dupeguru\nIcon=dupeguru\nTerminal=false\nType=Application\nCategories=Utility;\nKeywords=file manager;gui;\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n[tool.black]\nline-length = 120\n[tool.isort]\n# make it compatible with black\nprofile = \"black\"\nskip_gitignore = true\n"
  },
  {
    "path": "qt/__init__.py",
    "content": ""
  },
  {
    "path": "qt/about_box.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-05-09\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QCoreApplication, QTimer\nfrom PyQt5.QtGui import QPixmap, QFont\nfrom PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QHBoxLayout, QVBoxLayout, QLabel\n\nfrom core.util import check_for_update\nfrom qt.util import move_to_screen_center\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\n\nclass AboutBox(QDialog):\n    def __init__(self, parent, app, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint\n        super().__init__(parent, flags, **kwargs)\n        self.app = app\n        self._setupUi()\n\n        self.button_box.accepted.connect(self.accept)\n        self.button_box.rejected.connect(self.reject)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"About {}\").format(QCoreApplication.instance().applicationName()))\n        size_policy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n        self.setSizePolicy(size_policy)\n        main_layout = QHBoxLayout(self)\n        logo_label = QLabel()\n        logo_label.setPixmap(QPixmap(\":/%s_big\" % self.app.LOGO_NAME))\n        main_layout.addWidget(logo_label)\n        detail_layout = QVBoxLayout()\n        name_label = QLabel()\n        font = QFont()\n        font.setWeight(75)\n        font.setBold(True)\n        name_label.setFont(font)\n        name_label.setText(QCoreApplication.instance().applicationName())\n        detail_layout.addWidget(name_label)\n        version_label = QLabel()\n        version_label.setText(tr(\"Version {}\").format(QCoreApplication.instance().applicationVersion()))\n        detail_layout.addWidget(version_label)\n        self.update_label = QLabel(tr(\"Checking for updates...\"))\n        self.update_label.setTextInteractionFlags(Qt.TextBrowserInteraction)\n        self.update_label.setOpenExternalLinks(True)\n        detail_layout.addWidget(self.update_label)\n        license_label = QLabel()\n        license_label.setText(tr(\"Licensed under GPLv3\"))\n        detail_layout.addWidget(license_label)\n        spacer_label = QLabel()\n        spacer_label.setFont(font)\n        detail_layout.addWidget(spacer_label)\n        self.button_box = QDialogButtonBox()\n        self.button_box.setOrientation(Qt.Horizontal)\n        self.button_box.setStandardButtons(QDialogButtonBox.Ok)\n        detail_layout.addWidget(self.button_box)\n        main_layout.addLayout(detail_layout)\n\n    def _check_for_update(self):\n        update = check_for_update(QCoreApplication.instance().applicationVersion(), include_prerelease=False)\n        if update is None:\n            self.update_label.setText(tr(\"No update available.\"))\n        else:\n            self.update_label.setText(\n                tr('New version {} available, download <a href=\"{}\">here</a>.').format(update[\"version\"], update[\"url\"])\n            )\n\n    def showEvent(self, event):\n        self.update_label.setText(tr(\"Checking for updates...\"))\n        # have to do this here as the frameGeometry is not correct until shown\n        move_to_screen_center(self)\n        super().showEvent(event)\n        QTimer.singleShot(0, self._check_for_update)\n"
  },
  {
    "path": "qt/app.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport sys\nimport os.path as op\n\nfrom PyQt5.QtCore import QTimer, QObject, QUrl, pyqtSignal, Qt\nfrom PyQt5.QtGui import QColor, QDesktopServices, QPalette\nfrom PyQt5.QtWidgets import QApplication, QFileDialog, QDialog, QMessageBox, QStyleFactory, QToolTip\n\nfrom hscommon.trans import trget\nfrom hscommon import desktop, plat\n\nfrom qt.about_box import AboutBox\nfrom qt.recent import Recent\nfrom qt.util import create_actions\nfrom qt.progress_window import ProgressWindow\n\nfrom core.app import AppMode, DupeGuru as DupeGuruModel\nimport core.pe.photo\nfrom qt import platform\nfrom qt.preferences import Preferences\nfrom qt.result_window import ResultWindow\nfrom qt.directories_dialog import DirectoriesDialog\nfrom qt.problem_dialog import ProblemDialog\nfrom qt.ignore_list_dialog import IgnoreListDialog\nfrom qt.exclude_list_dialog import ExcludeListDialog\nfrom qt.deletion_options import DeletionOptions\nfrom qt.se.details_dialog import DetailsDialog as DetailsDialogStandard\nfrom qt.me.details_dialog import DetailsDialog as DetailsDialogMusic\nfrom qt.pe.details_dialog import DetailsDialog as DetailsDialogPicture\nfrom qt.se.preferences_dialog import PreferencesDialog as PreferencesDialogStandard\nfrom qt.me.preferences_dialog import PreferencesDialog as PreferencesDialogMusic\nfrom qt.pe.preferences_dialog import PreferencesDialog as PreferencesDialogPicture\nfrom qt.pe.photo import File as PlatSpecificPhoto\nfrom qt.tabbed_window import TabBarWindow, TabWindow\n\ntr = trget(\"ui\")\n\n\nclass DupeGuru(QObject):\n    LOGO_NAME = \"logo_se\"\n    NAME = \"dupeGuru\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.prefs = Preferences()\n        self.prefs.load()\n        # Enable tabs instead of separate floating windows for each dialog\n        # Could be passed as an argument to this class if we wanted\n        self.use_tabs = True\n        self.model = DupeGuruModel(view=self, portable=self.prefs.portable)\n        self._setup()\n\n    # --- Private\n    def _setup(self):\n        core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = PlatSpecificPhoto\n        self._setupActions()\n        self.details_dialog = None\n        self._update_options()\n        self.recentResults = Recent(self, \"recentResults\")\n        self.recentResults.mustOpenItem.connect(self.model.load_from)\n        self.resultWindow = None\n        if self.use_tabs:\n            self.main_window = TabBarWindow(self) if not self.prefs.tabs_default_pos else TabWindow(self)\n            parent_window = self.main_window\n            self.directories_dialog = self.main_window.createPage(\"DirectoriesDialog\", app=self)\n            self.main_window.addTab(self.directories_dialog, tr(\"Directories\"), switch=False)\n            self.actionDirectoriesWindow.setEnabled(False)\n        else:  # floating windows only\n            self.main_window = None\n            self.directories_dialog = DirectoriesDialog(self)\n            parent_window = self.directories_dialog\n\n        self.progress_window = ProgressWindow(parent_window, self.model.progress_window)\n        self.problemDialog = ProblemDialog(parent=parent_window, model=self.model.problem_dialog)\n        if self.use_tabs:\n            self.ignoreListDialog = self.main_window.createPage(\n                \"IgnoreListDialog\",\n                parent=self.main_window,\n                model=self.model.ignore_list_dialog,\n            )\n\n            self.excludeListDialog = self.main_window.createPage(\n                \"ExcludeListDialog\",\n                app=self,\n                parent=self.main_window,\n                model=self.model.exclude_list_dialog,\n            )\n        else:\n            self.ignoreListDialog = IgnoreListDialog(parent=parent_window, model=self.model.ignore_list_dialog)\n            self.excludeDialog = ExcludeListDialog(app=self, parent=parent_window, model=self.model.exclude_list_dialog)\n\n        self.deletionOptions = DeletionOptions(parent=parent_window, model=self.model.deletion_options)\n        self.about_box = AboutBox(parent_window, self)\n\n        parent_window.show()\n        self.model.load()\n\n        self.SIGTERM.connect(self.handleSIGTERM)\n\n        # The timer scheme is because if the nag is not shown before the application is\n        # completely initialized, the nag will be shown before the app shows up in the task bar\n        # In some circumstances, the nag is hidden by other window, which may make the user think\n        # that the application haven't launched.\n        QTimer.singleShot(0, self.finishedLaunching)\n\n    def _setupActions(self):\n        # Setup actions that are common to both the directory dialog and the results window.\n        # (name, shortcut, icon, desc, func)\n        ACTIONS = [\n            (\"actionQuit\", \"Ctrl+Q\", \"\", tr(\"Quit\"), self.quitTriggered),\n            (\n                \"actionPreferences\",\n                \"Ctrl+P\",\n                \"\",\n                tr(\"Options\"),\n                self.preferencesTriggered,\n            ),\n            (\"actionIgnoreList\", \"\", \"\", tr(\"Ignore List\"), self.ignoreListTriggered),\n            (\n                \"actionDirectoriesWindow\",\n                \"\",\n                \"\",\n                tr(\"Directories\"),\n                self.showDirectoriesWindow,\n            ),\n            (\n                \"actionClearCache\",\n                \"Ctrl+Shift+P\",\n                \"\",\n                tr(\"Clear Cache\"),\n                self.clearCacheTriggered,\n            ),\n            (\n                \"actionExcludeList\",\n                \"\",\n                \"\",\n                tr(\"Exclusion Filters\"),\n                self.excludeListTriggered,\n            ),\n            (\"actionShowHelp\", \"F1\", \"\", tr(\"dupeGuru Help\"), self.showHelpTriggered),\n            (\"actionAbout\", \"\", \"\", tr(\"About dupeGuru\"), self.showAboutBoxTriggered),\n            (\n                \"actionOpenDebugLog\",\n                \"\",\n                \"\",\n                tr(\"Open Debug Log\"),\n                self.openDebugLogTriggered,\n            ),\n        ]\n        create_actions(ACTIONS, self)\n\n    def _update_options(self):\n        self.model.options[\"mix_file_kind\"] = self.prefs.mix_file_kind\n        self.model.options[\"escape_filter_regexp\"] = not self.prefs.use_regexp\n        self.model.options[\"clean_empty_dirs\"] = self.prefs.remove_empty_folders\n        self.model.options[\"ignore_hardlink_matches\"] = self.prefs.ignore_hardlink_matches\n        self.model.options[\"copymove_dest_type\"] = self.prefs.destination_type\n        self.model.options[\"scan_type\"] = self.prefs.get_scan_type(self.model.app_mode)\n        self.model.options[\"min_match_percentage\"] = self.prefs.filter_hardness\n        self.model.options[\"word_weighting\"] = self.prefs.word_weighting\n        self.model.options[\"match_similar_words\"] = self.prefs.match_similar\n        threshold = self.prefs.small_file_threshold if self.prefs.ignore_small_files else 0\n        self.model.options[\"size_threshold\"] = threshold * 1024  # threshold is in KB. The scanner wants bytes\n        large_threshold = self.prefs.large_file_threshold if self.prefs.ignore_large_files else 0\n        self.model.options[\"large_size_threshold\"] = (\n            large_threshold * 1024 * 1024\n        )  # threshold is in MB. The Scanner wants bytes\n        big_file_size_threshold = self.prefs.big_file_size_threshold if self.prefs.big_file_partial_hashes else 0\n        self.model.options[\"big_file_size_threshold\"] = (\n            big_file_size_threshold\n            * 1024\n            * 1024\n            # threshold is in MiB. The scanner wants bytes\n        )\n        scanned_tags = set()\n        if self.prefs.scan_tag_track:\n            scanned_tags.add(\"track\")\n        if self.prefs.scan_tag_artist:\n            scanned_tags.add(\"artist\")\n        if self.prefs.scan_tag_album:\n            scanned_tags.add(\"album\")\n        if self.prefs.scan_tag_title:\n            scanned_tags.add(\"title\")\n        if self.prefs.scan_tag_genre:\n            scanned_tags.add(\"genre\")\n        if self.prefs.scan_tag_year:\n            scanned_tags.add(\"year\")\n        self.model.options[\"scanned_tags\"] = scanned_tags\n        self.model.options[\"match_scaled\"] = self.prefs.match_scaled\n        self.model.options[\"match_rotated\"] = self.prefs.match_rotated\n        self.model.options[\"include_exists_check\"] = self.prefs.include_exists_check\n        self.model.options[\"rehash_ignore_mtime\"] = self.prefs.rehash_ignore_mtime\n\n        if self.details_dialog:\n            self.details_dialog.update_options()\n\n        self._set_style(\"dark\" if self.prefs.use_dark_style else \"light\")\n\n    # --- Private\n    def _get_details_dialog_class(self):\n        if self.model.app_mode == AppMode.PICTURE:\n            return DetailsDialogPicture\n        elif self.model.app_mode == AppMode.MUSIC:\n            return DetailsDialogMusic\n        else:\n            return DetailsDialogStandard\n\n    def _get_preferences_dialog_class(self):\n        if self.model.app_mode == AppMode.PICTURE:\n            return PreferencesDialogPicture\n        elif self.model.app_mode == AppMode.MUSIC:\n            return PreferencesDialogMusic\n        else:\n            return PreferencesDialogStandard\n\n    def _set_style(self, style=\"light\"):\n        # Only support this feature on windows for now\n        if not plat.ISWINDOWS:\n            return\n        if style == \"dark\":\n            QApplication.setStyle(QStyleFactory.create(\"Fusion\"))\n            palette = QApplication.style().standardPalette()\n            palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53))\n            palette.setColor(QPalette.ColorRole.WindowText, Qt.white)\n            palette.setColor(QPalette.ColorRole.Base, QColor(25, 25, 25))\n            palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53))\n            palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(53, 53, 53))\n            palette.setColor(QPalette.ColorRole.ToolTipText, Qt.white)\n            palette.setColor(QPalette.ColorRole.Text, Qt.white)\n            palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53))\n            palette.setColor(QPalette.ColorRole.ButtonText, Qt.white)\n            palette.setColor(QPalette.ColorRole.BrightText, Qt.red)\n            palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))\n            palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218))\n            palette.setColor(QPalette.ColorRole.HighlightedText, Qt.black)\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor(164, 166, 168))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, QColor(164, 166, 168))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor(164, 166, 168))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.HighlightedText, QColor(164, 166, 168))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, QColor(68, 68, 68))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Window, QColor(68, 68, 68))\n            palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Highlight, QColor(68, 68, 68))\n        else:\n            QApplication.setStyle(QStyleFactory.create(\"windowsvista\" if plat.ISWINDOWS else \"Fusion\"))\n            palette = QApplication.style().standardPalette()\n        QToolTip.setPalette(palette)\n        QApplication.setPalette(palette)\n\n    # --- Public\n    def add_selected_to_ignore_list(self):\n        self.model.add_selected_to_ignore_list()\n\n    def remove_selected(self):\n        self.model.remove_selected(self)\n\n    def confirm(self, title, msg, default_button=QMessageBox.Yes):\n        active = QApplication.activeWindow()\n        buttons = QMessageBox.Yes | QMessageBox.No\n        answer = QMessageBox.question(active, title, msg, buttons, default_button)\n        return answer == QMessageBox.Yes\n\n    def invokeCustomCommand(self):\n        self.model.invoke_custom_command()\n\n    def show_details(self):\n        if self.details_dialog is not None:\n            if not self.details_dialog.isVisible():\n                self.details_dialog.show()\n            else:\n                self.details_dialog.hide()\n\n    def showResultsWindow(self):\n        if self.resultWindow is not None:\n            if self.use_tabs:\n                if self.main_window.indexOfWidget(self.resultWindow) < 0:\n                    self.main_window.addTab(self.resultWindow, tr(\"Results\"), switch=True)\n                    return\n                self.main_window.showTab(self.resultWindow)\n            else:\n                self.resultWindow.show()\n\n    def showDirectoriesWindow(self):\n        if self.directories_dialog is not None:\n            if self.use_tabs:\n                self.main_window.showTab(self.directories_dialog)\n            else:\n                self.directories_dialog.show()\n\n    def shutdown(self):\n        self.willSavePrefs.emit()\n        self.prefs.save()\n        self.model.save()\n        self.model.close()\n        # Workaround for #857, hide() or close().\n        if self.details_dialog is not None:\n            self.details_dialog.close()\n        QApplication.quit()\n\n    # --- Signals\n    willSavePrefs = pyqtSignal()\n    SIGTERM = pyqtSignal()\n\n    # --- Events\n    def finishedLaunching(self):\n        if sys.getfilesystemencoding() == \"ascii\":\n            # No need to localize this, it's a debugging message.\n            msg = (\n                \"Something is wrong with the way your system locale is set. If the files you're \"\n                \"scanning have accented letters, you'll probably get a crash. It is advised that \"\n                \"you set your system locale properly.\"\n            )\n            QMessageBox.warning(\n                self.main_window if self.main_window else self.directories_dialog,\n                \"Wrong Locale\",\n                msg,\n            )\n        # Load results on open if passed a .dupeguru file\n        if len(sys.argv) > 1:\n            results = sys.argv[1]\n            if results.endswith(\".dupeguru\"):\n                self.model.load_from(results)\n                self.recentResults.insertItem(results)\n\n    def clearCacheTriggered(self):\n        title = tr(\"Clear Cache\")\n        msg = tr(\"Do you really want to clear the cache? This will remove all cached file hashes and picture analysis.\")\n        if self.confirm(title, msg, QMessageBox.No):\n            self.model.clear_picture_cache()\n            self.model.clear_hash_cache()\n            active = QApplication.activeWindow()\n            QMessageBox.information(active, title, tr(\"Cache cleared.\"))\n\n    def ignoreListTriggered(self):\n        if self.use_tabs:\n            self.showTriggeredTabbedDialog(self.ignoreListDialog, tr(\"Ignore List\"))\n        else:  # floating windows\n            self.model.ignore_list_dialog.show()\n\n    def excludeListTriggered(self):\n        if self.use_tabs:\n            self.showTriggeredTabbedDialog(self.excludeListDialog, tr(\"Exclusion Filters\"))\n        else:  # floating windows\n            self.model.exclude_list_dialog.show()\n\n    def showTriggeredTabbedDialog(self, dialog, desc_string):\n        \"\"\"Add tab for dialog, name the tab with desc_string, then show it.\"\"\"\n        index = self.main_window.indexOfWidget(dialog)\n        # Create the tab if it doesn't exist already\n        if index < 0:  # or (not dialog.isVisible() and not self.main_window.isTabVisible(index)):\n            index = self.main_window.addTab(dialog, desc_string, switch=True)\n        # Show the tab for that widget\n        self.main_window.setCurrentIndex(index)\n\n    def openDebugLogTriggered(self):\n        debug_log_path = op.join(self.model.appdata, \"debug.log\")\n        desktop.open_path(debug_log_path)\n\n    def preferencesTriggered(self):\n        preferences_dialog = self._get_preferences_dialog_class()(\n            self.main_window if self.main_window else self.directories_dialog, self\n        )\n        preferences_dialog.load()\n        result = preferences_dialog.exec()\n        if result == QDialog.Accepted:\n            preferences_dialog.save()\n            self.prefs.save()\n            self._update_options()\n        preferences_dialog.setParent(None)\n\n    def quitTriggered(self):\n        if self.details_dialog is not None:\n            self.details_dialog.close()\n\n        if self.main_window:\n            self.main_window.close()\n        else:\n            self.directories_dialog.close()\n\n    def showAboutBoxTriggered(self):\n        self.about_box.show()\n\n    def showHelpTriggered(self):\n        base_path = platform.HELP_PATH\n        help_path = op.abspath(op.join(base_path, \"index.html\"))\n        if op.exists(help_path):\n            url = QUrl.fromLocalFile(help_path)\n        else:\n            url = QUrl(\"https://dupeguru.voltaicideas.net/help/en/\")\n        QDesktopServices.openUrl(url)\n\n    def handleSIGTERM(self):\n        self.shutdown()\n\n    # --- model --> view\n    def get_default(self, key):\n        return self.prefs.get_value(key)\n\n    def set_default(self, key, value):\n        self.prefs.set_value(key, value)\n\n    def show_message(self, msg):\n        window = QApplication.activeWindow()\n        QMessageBox.information(window, \"\", msg)\n\n    def ask_yes_no(self, prompt):\n        return self.confirm(\"\", prompt)\n\n    def create_results_window(self):\n        \"\"\"Creates resultWindow and details_dialog depending on the selected ``app_mode``.\"\"\"\n        if self.details_dialog is not None:\n            # The object is not deleted entirely, avoid saving its geometry in the future\n            # self.willSavePrefs.disconnect(self.details_dialog.appWillSavePrefs)\n            # or simply delete it on close which is probably cleaner:\n            self.details_dialog.setAttribute(Qt.WA_DeleteOnClose)\n            self.details_dialog.close()\n            # if we don't do the following, Qt will crash when we recreate the Results dialog\n            self.details_dialog.setParent(None)\n        if self.resultWindow is not None:\n            self.resultWindow.close()\n            # This is better for tabs, as it takes care of duplicate items in menu bar\n            self.resultWindow.deleteLater() if self.use_tabs else self.resultWindow.setParent(None)\n        if self.use_tabs:\n            self.resultWindow = self.main_window.createPage(\"ResultWindow\", parent=self.main_window, app=self)\n        else:  # We don't use a tab widget, regular floating QMainWindow\n            self.resultWindow = ResultWindow(self.directories_dialog, self)\n            self.directories_dialog._updateActionsState()\n        self.details_dialog = self._get_details_dialog_class()(self.resultWindow, self)\n\n    def show_results_window(self):\n        self.showResultsWindow()\n\n    def show_problem_dialog(self):\n        self.problemDialog.show()\n\n    def select_dest_folder(self, prompt):\n        flags = QFileDialog.ShowDirsOnly\n        return QFileDialog.getExistingDirectory(self.resultWindow, prompt, \"\", flags)\n\n    def select_dest_file(self, prompt, extension):\n        files = tr(\"{} file (*.{})\").format(extension.upper(), extension)\n        destination, chosen_filter = QFileDialog.getSaveFileName(self.resultWindow, prompt, \"\", files)\n        if not destination.endswith(f\".{extension}\"):\n            destination = f\"{destination}.{extension}\"\n        return destination\n"
  },
  {
    "path": "qt/column.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-11-25\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtWidgets import QHeaderView\n\n\nclass Column:\n    def __init__(\n        self,\n        attrname,\n        default_width,\n        editor=None,\n        alignment=Qt.AlignLeft,\n        cant_truncate=False,\n        painter=None,\n        resize_to_fit=False,\n    ):\n        self.attrname = attrname\n        self.default_width = default_width\n        self.editor = editor\n        # See moneyguru #15. Painter attribute was added to allow custom painting of amount value and\n        # currency information. Can be used as a pattern for custom painting of any column.\n        self.painter = painter\n        self.alignment = alignment\n        # This is to indicate, during printing, that a column can't have its data truncated.\n        self.cant_truncate = cant_truncate\n        self.resize_to_fit = resize_to_fit\n\n\nclass Columns:\n    def __init__(self, model, columns, header_view):\n        self.model = model\n        self._header_view = header_view\n        self._header_view.setDefaultAlignment(Qt.AlignLeft)\n\n        def setspecs(col, modelcol):\n            modelcol.default_width = col.default_width\n            modelcol.editor = col.editor\n            modelcol.painter = col.painter\n            modelcol.resize_to_fit = col.resize_to_fit\n            modelcol.alignment = col.alignment\n            modelcol.cant_truncate = col.cant_truncate\n\n        if columns:\n            for col in columns:\n                modelcol = self.model.column_by_name(col.attrname)\n                setspecs(col, modelcol)\n        else:\n            col = Column(\"\", 100)\n            for modelcol in self.model.column_list:\n                setspecs(col, modelcol)\n        self.model.view = self\n        self._header_view.sectionMoved.connect(self.header_section_moved)\n        self._header_view.sectionResized.connect(self.header_section_resized)\n\n        # See moneyguru #14 and #15.  This was added in order to allow automatic resizing of columns.\n        for column in self.model.column_list:\n            if column.resize_to_fit:\n                self._header_view.setSectionResizeMode(column.logical_index, QHeaderView.ResizeToContents)\n\n    # --- Public\n    def set_columns_width(self, widths):\n        # `widths` can be None. If it is, then default widths are set.\n        columns = self.model.column_list\n        if not widths:\n            widths = [column.default_width for column in columns]\n        for column, width in zip(columns, widths):\n            if width == 0:  # column was hidden before.\n                width = column.default_width\n            self._header_view.resizeSection(column.logical_index, width)\n\n    def set_columns_order(self, column_indexes):\n        if not column_indexes:\n            return\n        for dest_index, column_index in enumerate(column_indexes):\n            # moveSection takes 2 visual index arguments, so we have to get our visual index first\n            visual_index = self._header_view.visualIndex(column_index)\n            self._header_view.moveSection(visual_index, dest_index)\n\n    # --- Events\n    def header_section_moved(self, logical_index, old_visual_index, new_visual_index):\n        attrname = self.model.column_by_index(logical_index).name\n        self.model.move_column(attrname, new_visual_index)\n\n    def header_section_resized(self, logical_index, old_size, new_size):\n        attrname = self.model.column_by_index(logical_index).name\n        self.model.resize_column(attrname, new_size)\n\n    # --- model --> view\n    def restore_columns(self):\n        columns = self.model.ordered_columns\n        indexes = [col.logical_index for col in columns]\n        self.set_columns_order(indexes)\n        widths = [col.width for col in self.model.column_list]\n        if not any(widths):\n            widths = None\n        self.set_columns_width(widths)\n        for column in self.model.column_list:\n            visible = self.model.column_is_visible(column.name)\n            self._header_view.setSectionHidden(column.logical_index, not visible)\n\n    def set_column_visible(self, colname, visible):\n        column = self.model.column_by_name(colname)\n        self._header_view.setSectionHidden(column.logical_index, not visible)\n"
  },
  {
    "path": "qt/deletion_options.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2012-05-30\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QCheckBox, QDialogButtonBox\n\nfrom hscommon.trans import trget\nfrom qt.radio_box import RadioBox\n\ntr = trget(\"ui\")\n\n\nclass DeletionOptions(QDialog):\n    def __init__(self, parent, model, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self.model = model\n        self._setupUi()\n        self.model.view = self\n\n        self.linkCheckbox.stateChanged.connect(self.linkCheckboxChanged)\n        self.buttonBox.accepted.connect(self.accept)\n        self.buttonBox.rejected.connect(self.reject)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Deletion Options\"))\n        self.resize(400, 270)\n        self.verticalLayout = QVBoxLayout(self)\n        self.msgLabel = QLabel()\n        self.verticalLayout.addWidget(self.msgLabel)\n        self.linkCheckbox = QCheckBox(tr(\"Link deleted files\"))\n        self.verticalLayout.addWidget(self.linkCheckbox)\n        text = tr(\n            \"After having deleted a duplicate, place a link targeting the reference file \"\n            \"to replace the deleted file.\"\n        )\n        self.linkMessageLabel = QLabel(text)\n        self.linkMessageLabel.setWordWrap(True)\n        self.verticalLayout.addWidget(self.linkMessageLabel)\n        self.linkTypeRadio = RadioBox(items=[tr(\"Symlink\"), tr(\"Hardlink\")], spread=False)\n        self.verticalLayout.addWidget(self.linkTypeRadio)\n        if not self.model.supports_links():\n            self.linkCheckbox.setEnabled(False)\n            self.linkCheckbox.setText(self.linkCheckbox.text() + tr(\" (unsupported)\"))\n        self.directCheckbox = QCheckBox(tr(\"Directly delete files\"))\n        self.verticalLayout.addWidget(self.directCheckbox)\n        text = tr(\n            \"Instead of sending files to trash, delete them directly. This option is usually \"\n            \"used as a workaround when the normal deletion method doesn't work.\"\n        )\n        self.directMessageLabel = QLabel(text)\n        self.directMessageLabel.setWordWrap(True)\n        self.verticalLayout.addWidget(self.directMessageLabel)\n        self.buttonBox = QDialogButtonBox()\n        self.buttonBox.addButton(tr(\"Proceed\"), QDialogButtonBox.AcceptRole)\n        self.buttonBox.addButton(tr(\"Cancel\"), QDialogButtonBox.RejectRole)\n        self.verticalLayout.addWidget(self.buttonBox)\n\n    # --- Signals\n    def linkCheckboxChanged(self, changed: int):\n        self.model.link_deleted = bool(changed)\n\n    # --- model --> view\n    def update_msg(self, msg: str):\n        self.msgLabel.setText(msg)\n\n    def show(self):\n        self.linkCheckbox.setChecked(self.model.link_deleted)\n        self.linkTypeRadio.selected_index = 1 if self.model.use_hardlinks else 0\n        self.directCheckbox.setChecked(self.model.direct)\n        result = self.exec()\n        self.model.link_deleted = self.linkCheckbox.isChecked()\n        self.model.use_hardlinks = self.linkTypeRadio.selected_index == 1\n        self.model.direct = self.directCheckbox.isChecked()\n        return result == QDialog.Accepted\n\n    def set_hardlink_option_enabled(self, is_enabled: bool):\n        self.linkTypeRadio.setEnabled(is_enabled)\n"
  },
  {
    "path": "qt/details_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-05\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtWidgets import QDockWidget, QWidget\n\nfrom qt.util import move_to_screen_center\nfrom qt.details_table import DetailsModel\nfrom hscommon.plat import ISLINUX\n\n\nclass DetailsDialog(QDockWidget):\n    def __init__(self, parent, app, **kwargs):\n        super().__init__(parent, Qt.Tool, **kwargs)\n        self.parent = parent\n        self.app = app\n        self.model = app.model.details_panel\n        self.setAllowedAreas(Qt.AllDockWidgetAreas)\n        self._setupUi()\n        # To avoid saving uninitialized geometry on appWillSavePrefs, we track whether our dialog\n        # has been shown. If it has, we know that our geometry should be saved.\n        self._shown_once = False\n        self._wasDocked, area = self.app.prefs.restoreGeometry(\"DetailsWindowRect\", self)\n        self.tableModel = DetailsModel(self.model, app)\n        # tableView is defined in subclasses\n        self.tableView.setModel(self.tableModel)\n        self.model.view = self\n        self.app.willSavePrefs.connect(self.appWillSavePrefs)\n        # self.setAttribute(Qt.WA_DeleteOnClose)\n        parent.addDockWidget(area if self._wasDocked else Qt.BottomDockWidgetArea, self)\n\n    def _setupUi(self):  # Virtual\n        pass\n\n    def show(self):\n        if not self._shown_once and self._wasDocked:\n            self.setFloating(False)\n        self._shown_once = True\n        super().show()\n        self.update_options()\n\n    def update_options(self):\n        # This disables the title bar (if we had not set one before already)\n        # essentially making it a simple floating window, not dockable anymore\n        if not self.app.prefs.details_dialog_titlebar_enabled:\n            if not self.titleBarWidget():  # default title bar\n                self.setTitleBarWidget(QWidget())  # disables title bar\n                # Windows (and MacOS?) users cannot move a floating window which\n                # has no native decoration so we force it to dock for now\n                if not ISLINUX:\n                    self.setFloating(False)\n        elif self.titleBarWidget() is not None:  # title bar is disabled\n            self.setTitleBarWidget(None)  # resets to the default title bar\n        elif not self.titleBarWidget() and not self.app.prefs.details_dialog_titlebar_enabled:\n            self.setTitleBarWidget(QWidget())\n\n        features = self.features()\n        if self.app.prefs.details_dialog_vertical_titlebar:\n            self.setFeatures(features | QDockWidget.DockWidgetVerticalTitleBar)\n        elif features & QDockWidget.DockWidgetVerticalTitleBar:\n            self.setFeatures(features ^ QDockWidget.DockWidgetVerticalTitleBar)\n\n    # --- Events\n    def appWillSavePrefs(self):\n        if self._shown_once:\n            self.app.prefs.saveGeometry(\"DetailsWindowRect\", self)\n\n    # --- model --> view\n    def refresh(self):\n        self.tableModel.beginResetModel()\n        self.tableModel.endResetModel()\n\n    def showEvent(self, event):\n        if self._wasDocked is False:\n            # have to do this here as the frameGeometry is not correct until shown\n            move_to_screen_center(self)\n        super().showEvent(event)\n"
  },
  {
    "path": "qt/details_table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-05-17\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QAbstractTableModel\nfrom PyQt5.QtWidgets import QHeaderView, QTableView\nfrom PyQt5.QtGui import QFont, QBrush\n\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\nHEADER = [tr(\"Selected\"), tr(\"Reference\")]\n\n\nclass DetailsModel(QAbstractTableModel):\n    def __init__(self, model, app, **kwargs):\n        super().__init__(**kwargs)\n        self.model = model\n        self.prefs = app.prefs\n\n    def columnCount(self, parent):\n        return len(HEADER)\n\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n        # Skip first value \"Attribute\"\n        column = index.column() + 1\n        row = index.row()\n\n        ignored_fields = [\"Dupe Count\"]\n        if (\n            self.model.row(row)[0] in ignored_fields\n            or self.model.row(row)[1] == \"---\"\n            or self.model.row(row)[2] == \"---\"\n        ):\n            if role != Qt.DisplayRole:\n                return None\n            return self.model.row(row)[column]\n\n        if role == Qt.DisplayRole:\n            return self.model.row(row)[column]\n        if role == Qt.ForegroundRole and self.model.row(row)[1] != self.model.row(row)[2]:\n            return QBrush(self.prefs.details_table_delta_foreground_color)\n        if role == Qt.FontRole and self.model.row(row)[1] != self.model.row(row)[2]:\n            font = QFont(self.model.view.font())  # or simply QFont()\n            font.setBold(True)\n            return font\n        return None  # QVariant()\n\n    def headerData(self, section, orientation, role):\n        if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):\n            return HEADER[section]\n        elif orientation == Qt.Vertical and role == Qt.DisplayRole and section < self.model.row_count():\n            # Read \"Attribute\" cell for horizontal header\n            return self.model.row(section)[0]\n        return None\n\n    def rowCount(self, parent):\n        return self.model.row_count()\n\n\nclass DetailsTable(QTableView):\n    def __init__(self, *args):\n        QTableView.__init__(self, *args)\n        self.setAlternatingRowColors(True)\n        self.setSelectionBehavior(QTableView.SelectRows)\n        self.setSelectionMode(QTableView.NoSelection)\n        self.setShowGrid(False)\n        self.setWordWrap(False)\n        self.setCornerButtonEnabled(False)\n\n    def setModel(self, model):\n        QTableView.setModel(self, model)\n        # The model needs to be set to set header stuff\n        hheader = self.horizontalHeader()\n        hheader.setHighlightSections(False)\n        hheader.setSectionResizeMode(0, QHeaderView.Stretch)\n        hheader.setSectionResizeMode(1, QHeaderView.Stretch)\n        vheader = self.verticalHeader()\n        vheader.setVisible(True)\n        vheader.setDefaultSectionSize(18)\n        # hardcoded value above is not ideal, perhaps resize to contents first?\n        # vheader.setSectionResizeMode(QHeaderView.ResizeToContents)\n        vheader.setSectionResizeMode(QHeaderView.Fixed)\n        vheader.setSectionsMovable(True)\n"
  },
  {
    "path": "qt/dg.qrc",
    "content": "<!DOCTYPE RCC><RCC version=\"1.0\">\n<qresource>\n    <file alias=\"logo_se\">../images/dgse_logo_32.png</file>\n    <file alias=\"logo_se_big\">../images/dgse_logo_128.png</file>\n    <file alias=\"plus\">../images/plus_8.png</file>\n    <file alias=\"minus\">../images/minus_8.png</file>\n    <file alias=\"search_clear_13\">../images/search_clear_13.png</file>\n    <file alias=\"exchange\">../images/exchange_purple_upscaled.png</file>\n    <file alias=\"zoom_in\">../images/old_zoom_in.png</file>\n    <file alias=\"zoom_out\">../images/old_zoom_out.png</file>\n    <file alias=\"zoom_original\">../images/old_zoom_original.png</file>\n    <file alias=\"zoom_best_fit\">../images/old_zoom_best_fit.png</file>\n    <file alias=\"error\">../images/dialog-error.png</file>\n</qresource>\n</RCC>\n"
  },
  {
    "path": "qt/directories_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QRect, Qt\nfrom PyQt5.QtWidgets import (\n    QListView,\n    QWidget,\n    QFileDialog,\n    QHeaderView,\n    QVBoxLayout,\n    QHBoxLayout,\n    QTreeView,\n    QAbstractItemView,\n    QSpacerItem,\n    QSizePolicy,\n    QPushButton,\n    QMainWindow,\n    QMenuBar,\n    QMenu,\n    QLabel,\n    QComboBox,\n)\nfrom PyQt5.QtGui import QPixmap, QIcon\n\nfrom hscommon.trans import trget\nfrom core.app import AppMode\nfrom qt.radio_box import RadioBox\nfrom qt.recent import Recent\nfrom qt.util import move_to_screen_center, create_actions\n\nfrom qt import platform\nfrom qt.directories_model import DirectoriesModel, DirectoriesDelegate\n\ntr = trget(\"ui\")\n\n\nclass DirectoriesDialog(QMainWindow):\n    def __init__(self, app, **kwargs):\n        super().__init__(None, **kwargs)\n        self.app = app\n        self.specific_actions = set()\n        self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS\n        self.recentFolders = Recent(self.app, \"recentFolders\")\n        self._setupUi()\n        self._updateScanTypeList()\n        self.directoriesModel = DirectoriesModel(self.app.model.directory_tree, view=self.treeView)\n        self.directoriesDelegate = DirectoriesDelegate()\n        self.treeView.setItemDelegate(self.directoriesDelegate)\n        self._setupColumns()\n        self.app.recentResults.addMenu(self.menuLoadRecent)\n        self.app.recentResults.addMenu(self.menuRecentResults)\n        self.recentFolders.addMenu(self.menuRecentFolders)\n        self._updateAddButton()\n        self._updateRemoveButton()\n        self._updateLoadResultsButton()\n        self._updateActionsState()\n        self._setupBindings()\n\n    def _setupBindings(self):\n        self.appModeRadioBox.itemSelected.connect(self.appModeButtonSelected)\n        self.showPreferencesButton.clicked.connect(self.app.actionPreferences.trigger)\n        self.scanButton.clicked.connect(self.scanButtonClicked)\n        self.loadResultsButton.clicked.connect(self.actionLoadResults.trigger)\n        self.addFolderButton.clicked.connect(self.actionAddFolder.trigger)\n        self.removeFolderButton.clicked.connect(self.removeFolderButtonClicked)\n        self.treeView.selectionModel().selectionChanged.connect(self.selectionChanged)\n        self.app.recentResults.itemsChanged.connect(self._updateLoadResultsButton)\n        self.recentFolders.itemsChanged.connect(self._updateAddButton)\n        self.recentFolders.mustOpenItem.connect(self.app.model.add_directory)\n        self.directoriesModel.foldersAdded.connect(self.directoriesModelAddedFolders)\n        self.app.willSavePrefs.connect(self.appWillSavePrefs)\n\n    def _setupActions(self):\n        # (name, shortcut, icon, desc, func)\n        ACTIONS = [\n            (\n                \"actionLoadResults\",\n                \"Ctrl+L\",\n                \"\",\n                tr(\"Load Results...\"),\n                self.loadResultsTriggered,\n            ),\n            (\n                \"actionShowResultsWindow\",\n                \"\",\n                \"\",\n                tr(\"Scan Results\"),\n                self.app.showResultsWindow,\n            ),\n            (\"actionAddFolder\", \"\", \"\", tr(\"Add Folder...\"), self.addFolderTriggered),\n            (\"actionLoadDirectories\", \"\", \"\", tr(\"Load Directories...\"), self.loadDirectoriesTriggered),\n            (\"actionSaveDirectories\", \"\", \"\", tr(\"Save Directories...\"), self.saveDirectoriesTriggered),\n        ]\n        create_actions(ACTIONS, self)\n        if self.app.use_tabs:\n            # Keep track of actions which should only be accessible from this window\n            self.specific_actions.add(self.actionLoadDirectories)\n            self.specific_actions.add(self.actionSaveDirectories)\n\n    def _setupMenu(self):\n        if not self.app.use_tabs:\n            # we are our own QMainWindow, we need our own menu bar\n            self.menubar = QMenuBar(self)\n            self.menubar.setGeometry(QRect(0, 0, 42, 22))\n            self.menuFile = QMenu(self.menubar)\n            self.menuFile.setTitle(tr(\"File\"))\n            self.menuView = QMenu(self.menubar)\n            self.menuView.setTitle(tr(\"View\"))\n            self.menuHelp = QMenu(self.menubar)\n            self.menuHelp.setTitle(tr(\"Help\"))\n            self.setMenuBar(self.menubar)\n            menubar = self.menubar\n        else:\n            # we are part of a tab widget, we populate its window's menubar instead\n            self.menuFile = self.app.main_window.menuFile\n            self.menuView = self.app.main_window.menuView\n            self.menuHelp = self.app.main_window.menuHelp\n            menubar = self.app.main_window.menubar\n\n        self.menuLoadRecent = QMenu(self.menuFile)\n        self.menuLoadRecent.setTitle(tr(\"Load Recent Results\"))\n\n        self.menuFile.addAction(self.actionLoadResults)\n        self.menuFile.addAction(self.menuLoadRecent.menuAction())\n        self.menuFile.addSeparator()\n        self.menuFile.addAction(self.app.actionClearCache)\n        self.menuFile.addSeparator()\n        self.menuFile.addAction(self.actionLoadDirectories)\n        self.menuFile.addAction(self.actionSaveDirectories)\n        self.menuFile.addSeparator()\n        self.menuFile.addAction(self.app.actionQuit)\n\n        self.menuView.addAction(self.app.actionDirectoriesWindow)\n        self.menuView.addAction(self.actionShowResultsWindow)\n        self.menuView.addAction(self.app.actionIgnoreList)\n        self.menuView.addAction(self.app.actionExcludeList)\n        self.menuView.addSeparator()\n        self.menuView.addAction(self.app.actionPreferences)\n\n        self.menuHelp.addAction(self.app.actionShowHelp)\n        self.menuHelp.addAction(self.app.actionOpenDebugLog)\n        self.menuHelp.addAction(self.app.actionAbout)\n\n        menubar.addAction(self.menuFile.menuAction())\n        menubar.addAction(self.menuView.menuAction())\n        menubar.addAction(self.menuHelp.menuAction())\n\n        # Recent folders menu\n        self.menuRecentFolders = QMenu()\n        self.menuRecentFolders.addAction(self.actionAddFolder)\n        self.menuRecentFolders.addSeparator()\n\n        # Recent results menu\n        self.menuRecentResults = QMenu()\n        self.menuRecentResults.addAction(self.actionLoadResults)\n        self.menuRecentResults.addSeparator()\n\n    def _setupUi(self):\n        self.setWindowTitle(self.app.NAME)\n        self.resize(420, 338)\n        self.centralwidget = QWidget(self)\n        self.verticalLayout = QVBoxLayout(self.centralwidget)\n        self.verticalLayout.setContentsMargins(4, 0, 4, 0)\n        self.verticalLayout.setSpacing(0)\n        hl = QHBoxLayout()\n        label = QLabel(tr(\"Application Mode:\"), self)\n        label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n        hl.addWidget(label)\n        self.appModeRadioBox = RadioBox(self, items=[tr(\"Standard\"), tr(\"Music\"), tr(\"Picture\")], spread=False)\n        hl.addWidget(self.appModeRadioBox)\n        self.verticalLayout.addLayout(hl)\n        hl = QHBoxLayout()\n        hl.setAlignment(Qt.AlignLeft)\n        label = QLabel(tr(\"Scan Type:\"), self)\n        label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n        hl.addWidget(label)\n        self.scanTypeComboBox = QComboBox(self)\n        self.scanTypeComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))\n        self.scanTypeComboBox.setMaximumWidth(400)\n        hl.addWidget(self.scanTypeComboBox)\n        self.showPreferencesButton = QPushButton(tr(\"More Options\"), self.centralwidget)\n        self.showPreferencesButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)\n        hl.addWidget(self.showPreferencesButton)\n        self.verticalLayout.addLayout(hl)\n        self.promptLabel = QLabel(tr('Select folders to scan and press \"Scan\".'), self.centralwidget)\n        self.verticalLayout.addWidget(self.promptLabel)\n        self.treeView = QTreeView(self.centralwidget)\n        self.treeView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.treeView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.treeView.setAcceptDrops(True)\n        triggers = (\n            QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked\n        )\n        self.treeView.setEditTriggers(triggers)\n        self.treeView.setDragDropOverwriteMode(True)\n        self.treeView.setDragDropMode(QAbstractItemView.DropOnly)\n        self.treeView.setUniformRowHeights(True)\n        self.verticalLayout.addWidget(self.treeView)\n        self.horizontalLayout = QHBoxLayout()\n        self.removeFolderButton = QPushButton(self.centralwidget)\n        self.removeFolderButton.setIcon(QIcon(QPixmap(\":/minus\")))\n        self.removeFolderButton.setShortcut(\"Del\")\n        self.horizontalLayout.addWidget(self.removeFolderButton)\n        self.addFolderButton = QPushButton(self.centralwidget)\n        self.addFolderButton.setIcon(QIcon(QPixmap(\":/plus\")))\n        self.horizontalLayout.addWidget(self.addFolderButton)\n        spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.horizontalLayout.addItem(spacer_item)\n        self.loadResultsButton = QPushButton(self.centralwidget)\n        self.loadResultsButton.setText(tr(\"Load Results\"))\n        self.horizontalLayout.addWidget(self.loadResultsButton)\n        self.scanButton = QPushButton(self.centralwidget)\n        self.scanButton.setText(tr(\"Scan\"))\n        self.scanButton.setDefault(True)\n        self.horizontalLayout.addWidget(self.scanButton)\n        self.verticalLayout.addLayout(self.horizontalLayout)\n        self.setCentralWidget(self.centralwidget)\n\n        self._setupActions()\n        self._setupMenu()\n\n        if self.app.prefs.directoriesWindowRect is not None:\n            self.setGeometry(self.app.prefs.directoriesWindowRect)\n        else:\n            move_to_screen_center(self)\n\n    def _setupColumns(self):\n        header = self.treeView.header()\n        header.setStretchLastSection(False)\n        header.setSectionResizeMode(0, QHeaderView.Stretch)\n        header.setSectionResizeMode(1, QHeaderView.Fixed)\n        header.resizeSection(1, 100)\n\n    def _updateActionsState(self):\n        self.actionShowResultsWindow.setEnabled(self.app.resultWindow is not None)\n\n    def _updateAddButton(self):\n        if self.recentFolders.isEmpty():\n            self.addFolderButton.setMenu(None)\n        else:\n            self.addFolderButton.setMenu(self.menuRecentFolders)\n\n    def _updateRemoveButton(self):\n        indexes = self.treeView.selectedIndexes()\n        if not indexes:\n            self.removeFolderButton.setEnabled(False)\n            return\n        self.removeFolderButton.setEnabled(True)\n\n    def _updateLoadResultsButton(self):\n        if self.app.recentResults.isEmpty():\n            self.loadResultsButton.setMenu(None)\n        else:\n            self.loadResultsButton.setMenu(self.menuRecentResults)\n\n    def _updateScanTypeList(self):\n        try:\n            self.scanTypeComboBox.currentIndexChanged[int].disconnect(self.scanTypeChanged)\n        except TypeError:\n            # Not connected, ignore\n            pass\n        self.scanTypeComboBox.clear()\n        scan_options = self.app.model.SCANNER_CLASS.get_scan_options()\n        for scan_option in scan_options:\n            self.scanTypeComboBox.addItem(scan_option.label)\n        SCAN_TYPE_ORDER = [so.scan_type for so in scan_options]\n        selected_scan_type = self.app.prefs.get_scan_type(self.app.model.app_mode)\n        scan_type_index = SCAN_TYPE_ORDER.index(selected_scan_type)\n        self.scanTypeComboBox.setCurrentIndex(scan_type_index)\n        self.scanTypeComboBox.currentIndexChanged[int].connect(self.scanTypeChanged)\n        self.app._update_options()\n\n    # --- QWidget overrides\n    def closeEvent(self, event):\n        event.accept()\n        if self.app.model.results.is_modified:\n            title = tr(\"Unsaved results\")\n            msg = tr(\"You have unsaved results, do you really want to quit?\")\n            if not self.app.confirm(title, msg):\n                event.ignore()\n        if event.isAccepted():\n            self.app.shutdown()\n\n    # --- Events\n    def addFolderTriggered(self):\n        no_native = not self.app.prefs.use_native_dialogs\n        title = tr(\"Select a folder to add to the scanning list\")\n        file_dialog = QFileDialog(self, title, self.lastAddedFolder)\n        file_dialog.setFileMode(QFileDialog.DirectoryOnly)\n        file_dialog.setOption(QFileDialog.DontUseNativeDialog, no_native)\n        if no_native:\n            file_view = file_dialog.findChild(QListView, \"listView\")\n            if file_view:\n                file_view.setSelectionMode(QAbstractItemView.MultiSelection)\n            f_tree_view = file_dialog.findChild(QTreeView)\n            if f_tree_view:\n                f_tree_view.setSelectionMode(QAbstractItemView.MultiSelection)\n        if not file_dialog.exec():\n            return\n\n        paths = file_dialog.selectedFiles()\n        self.lastAddedFolder = paths[-1]\n        [self.app.model.add_directory(path) for path in paths]\n        [self.recentFolders.insertItem(path) for path in paths]\n\n    def appModeButtonSelected(self, index):\n        if index == 2:\n            mode = AppMode.PICTURE\n        elif index == 1:\n            mode = AppMode.MUSIC\n        else:\n            mode = AppMode.STANDARD\n        self.app.model.app_mode = mode\n        self._updateScanTypeList()\n\n    def appWillSavePrefs(self):\n        self.app.prefs.directoriesWindowRect = self.geometry()\n\n    def directoriesModelAddedFolders(self, folders):\n        for folder in folders:\n            self.recentFolders.insertItem(folder)\n\n    def loadResultsTriggered(self):\n        title = tr(\"Select a results file to load\")\n        files = \";;\".join([tr(\"dupeGuru Results (*.dupeguru)\"), tr(\"All Files (*.*)\")])\n        destination = QFileDialog.getOpenFileName(self, title, \"\", files)[0]\n        if destination:\n            self.app.model.load_from(destination)\n            self.app.recentResults.insertItem(destination)\n\n    def loadDirectoriesTriggered(self):\n        title = tr(\"Select a directories file to load\")\n        files = \";;\".join([tr(\"dupeGuru Directories (*.dupegurudirs)\"), tr(\"All Files (*.*)\")])\n        destination = QFileDialog.getOpenFileName(self, title, \"\", files)[0]\n        if destination:\n            self.app.model.load_directories(destination)\n\n    def removeFolderButtonClicked(self):\n        self.directoriesModel.model.remove_selected()\n\n    def saveDirectoriesTriggered(self):\n        title = tr(\"Select a file to save your directories to\")\n        files = tr(\"dupeGuru Directories (*.dupegurudirs)\")\n        destination, chosen_filter = QFileDialog.getSaveFileName(self, title, \"\", files)\n        if destination:\n            if not destination.endswith(\".dupegurudirs\"):\n                destination = f\"{destination}.dupegurudirs\"\n            self.app.model.save_directories_as(destination)\n\n    def scanButtonClicked(self):\n        if self.app.model.results.is_modified:\n            title = tr(\"Start a new scan\")\n            msg = tr(\"You have unsaved results, do you really want to continue?\")\n            if not self.app.confirm(title, msg):\n                return\n        self.app.model.start_scanning(self.app.prefs.profile_scan)\n\n    def scanTypeChanged(self, index):\n        scan_options = self.app.model.SCANNER_CLASS.get_scan_options()\n        self.app.prefs.set_scan_type(self.app.model.app_mode, scan_options[index].scan_type)\n        self.app._update_options()\n\n    def selectionChanged(self, selected, deselected):\n        self._updateRemoveButton()\n"
  },
  {
    "path": "qt/directories_model.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-04-25\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import pyqtSignal, Qt, QRect, QUrl, QModelIndex, QItemSelection\nfrom PyQt5.QtWidgets import (\n    QComboBox,\n    QStyledItemDelegate,\n    QStyle,\n    QStyleOptionComboBox,\n    QStyleOptionViewItem,\n    QApplication,\n)\nfrom PyQt5.QtGui import QBrush\n\nfrom hscommon.trans import trget\nfrom qt.tree_model import RefNode, TreeModel\n\ntr = trget(\"ui\")\n\nHEADERS = [tr(\"Name\"), tr(\"State\")]\nSTATES = [tr(\"Normal\"), tr(\"Reference\"), tr(\"Excluded\")]\n\n\nclass DirectoriesDelegate(QStyledItemDelegate):\n    def createEditor(self, parent, option, index):\n        editor = QComboBox(parent)\n        editor.addItems(STATES)\n        return editor\n\n    def paint(self, painter, option, index):\n        self.initStyleOption(option, index)\n        # No idea why, but this cast is required if we want to have access to the V4 valuess\n        option = QStyleOptionViewItem(option)\n        if (index.column() == 1) and (option.state & QStyle.State_Selected):\n            cboption = QStyleOptionComboBox()\n            cboption.rect = option.rect\n            # On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to\n            # fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.\n            cboption.state |= QStyle.State_Enabled\n            QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter)\n            painter.setBrush(option.palette.text())\n            rect = QRect(option.rect)\n            rect.setLeft(rect.left() + 4)\n            painter.drawText(rect, Qt.AlignLeft, option.text)\n        else:\n            super().paint(painter, option, index)\n\n    def setEditorData(self, editor, index):\n        value = index.model().data(index, Qt.EditRole)\n        editor.setCurrentIndex(value)\n        editor.showPopup()\n\n    def setModelData(self, editor, model, index):\n        value = editor.currentIndex()\n        model.setData(index, value, Qt.EditRole)\n\n    def updateEditorGeometry(self, editor, option, index):\n        editor.setGeometry(option.rect)\n\n\nclass DirectoriesModel(TreeModel):\n    MIME_TYPE_FORMAT = \"text/uri-list\"\n\n    def __init__(self, model, view, **kwargs):\n        super().__init__(**kwargs)\n        self.model = model\n        self.model.view = self\n        self.view = view\n        self.view.setModel(self)\n\n        self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)\n\n    def _create_node(self, ref, row):\n        return RefNode(self, None, ref, row)\n\n    def _get_children(self):\n        return list(self.model)\n\n    def columnCount(self, parent=QModelIndex()):\n        return 2\n\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n        node = index.internalPointer()\n        ref = node.ref\n        if role == Qt.DisplayRole:\n            if index.column() == 0:\n                return ref.name\n            else:\n                return STATES[ref.state]\n        elif role == Qt.EditRole and index.column() == 1:\n            return ref.state\n        elif role == Qt.ForegroundRole:\n            state = ref.state\n            if state == 1:\n                return QBrush(Qt.blue)\n            elif state == 2:\n                return QBrush(Qt.red)\n        return None\n\n    def dropMimeData(self, mime_data, action, row, column, parent_index):\n        # the data in mimeData is urlencoded **in utf-8**\n        if not mime_data.hasFormat(self.MIME_TYPE_FORMAT):\n            return False\n        data = bytes(mime_data.data(self.MIME_TYPE_FORMAT)).decode(\"ascii\")\n        urls = data.split(\"\\r\\n\")\n        paths = [QUrl(url).toLocalFile() for url in urls if url]\n        for path in paths:\n            self.model.add_directory(path)\n        self.foldersAdded.emit(paths)\n        self.reset()\n        return True\n\n    def flags(self, index):\n        if not index.isValid():\n            return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled\n        result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled\n        if index.column() == 1:\n            result |= Qt.ItemIsEditable\n        return result\n\n    def headerData(self, section, orientation, role):\n        if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADERS):\n            return HEADERS[section]\n        return None\n\n    def mimeTypes(self):\n        return [self.MIME_TYPE_FORMAT]\n\n    def setData(self, index, value, role):\n        if not index.isValid() or role != Qt.EditRole or index.column() != 1:\n            return False\n        node = index.internalPointer()\n        ref = node.ref\n        ref.state = value\n        return True\n\n    def supportedDropActions(self):\n        # Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't\n        # work with ActionMove either. So screw that, and accept anything.\n        return Qt.ActionMask\n\n    # --- Events\n    def selectionChanged(self, selected, deselected):\n        new_nodes = [modelIndex.internalPointer().ref for modelIndex in self.view.selectionModel().selectedRows()]\n        self.model.selected_nodes = new_nodes\n\n    # --- Signals\n    foldersAdded = pyqtSignal(list)\n\n    # --- model --> view\n    def refresh(self):\n        self.reset()\n\n    def refresh_states(self):\n        self.refreshData()\n"
  },
  {
    "path": "qt/error_report_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-05-23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport traceback\nimport sys\nimport os\nimport platform\n\nfrom PyQt5.QtCore import Qt, QCoreApplication, QSize\nfrom PyQt5.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QHBoxLayout,\n    QLabel,\n    QPlainTextEdit,\n    QPushButton,\n)\n\nfrom hscommon.trans import trget\nfrom hscommon.desktop import open_url\nfrom qt.util import horizontal_spacer\n\ntr = trget(\"ui\")\n\n\nclass ErrorReportDialog(QDialog):\n    def __init__(self, parent, github_url, error, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self._setupUi()\n        name = QCoreApplication.applicationName()\n        version = QCoreApplication.applicationVersion()\n        error_text = \"Application Name: {}\\nVersion: {}\\nPython: {}\\nOperating System: {}\\n\\n{}\".format(\n            name, version, platform.python_version(), platform.platform(), error\n        )\n        # Under windows, we end up with an error report without linesep if we don't mangle it\n        error_text = error_text.replace(\"\\n\", os.linesep)\n        self.errorTextEdit.setPlainText(error_text)\n        self.github_url = github_url\n\n        self.sendButton.clicked.connect(self.goToGitHub)\n        self.dontSendButton.clicked.connect(self.reject)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Error Report\"))\n        self.resize(553, 349)\n        self.verticalLayout = QVBoxLayout(self)\n        self.label = QLabel(self)\n        self.label.setText(tr(\"Something went wrong. How about reporting the error?\"))\n        self.label.setWordWrap(True)\n        self.verticalLayout.addWidget(self.label)\n        self.errorTextEdit = QPlainTextEdit(self)\n        self.errorTextEdit.setReadOnly(True)\n        self.verticalLayout.addWidget(self.errorTextEdit)\n        msg = tr(\n            \"Error reports should be reported as GitHub issues. You can copy the error traceback \"\n            \"above and paste it in a new issue.\\n\\nPlease make sure to run a search for any already \"\n            \"existing issues beforehand. Also make sure to test the very latest version available from the repository, \"\n            \"since the bug you are experiencing might have already been patched.\\n\\n\"\n            \"What usually really helps is if you add a description of how you got the error. Thanks!\"\n            \"\\n\\n\"\n            \"Although the application should continue to run after this error, it may be in an \"\n            \"unstable state, so it is recommended that you restart the application.\"\n        )\n        self.label2 = QLabel(msg)\n        self.label2.setWordWrap(True)\n        self.verticalLayout.addWidget(self.label2)\n        self.horizontalLayout = QHBoxLayout()\n        self.horizontalLayout.addItem(horizontal_spacer())\n        self.dontSendButton = QPushButton(self)\n        self.dontSendButton.setText(tr(\"Close\"))\n        self.dontSendButton.setMinimumSize(QSize(110, 0))\n        self.horizontalLayout.addWidget(self.dontSendButton)\n        self.sendButton = QPushButton(self)\n        self.sendButton.setText(tr(\"Go to GitHub\"))\n        self.sendButton.setMinimumSize(QSize(110, 0))\n        self.sendButton.setDefault(True)\n        self.horizontalLayout.addWidget(self.sendButton)\n        self.verticalLayout.addLayout(self.horizontalLayout)\n\n    def goToGitHub(self):\n        open_url(self.github_url)\n\n\ndef install_excepthook(github_url):\n    def my_excepthook(exctype, value, tb):\n        s = \"\".join(traceback.format_exception(exctype, value, tb))\n        dialog = ErrorReportDialog(None, github_url, s)\n        dialog.exec_()\n\n    sys.excepthook = my_excepthook\n"
  },
  {
    "path": "qt/exclude_list_dialog.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport re\nfrom PyQt5.QtCore import Qt, pyqtSlot\nfrom PyQt5.QtWidgets import (\n    QPushButton,\n    QLineEdit,\n    QVBoxLayout,\n    QGridLayout,\n    QDialog,\n    QTableView,\n    QAbstractItemView,\n    QSpacerItem,\n    QSizePolicy,\n    QHeaderView,\n)\nfrom qt.exclude_list_table import ExcludeListTable\n\nfrom core.exclude import AlreadyThereException\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\n\nclass ExcludeListDialog(QDialog):\n    def __init__(self, app, parent, model, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self.app = app\n        self.specific_actions = frozenset()\n        self._setupUI()\n        self.model = model  # ExcludeListDialogCore\n        self.model.view = self\n        self.table = ExcludeListTable(app, view=self.tableView)  # Qt ExcludeListTable\n        self._row_matched = False  # test if at least one row matched our test string\n        self._input_styled = False\n\n        self.buttonAdd.clicked.connect(self.addStringFromLineEdit)\n        self.buttonRemove.clicked.connect(self.removeSelected)\n        self.buttonRestore.clicked.connect(self.restoreDefaults)\n        self.buttonClose.clicked.connect(self.accept)\n        self.buttonHelp.clicked.connect(self.display_help_message)\n        self.buttonTestString.clicked.connect(self.onTestStringButtonClicked)\n        self.inputLine.textEdited.connect(self.reset_input_style)\n        self.testLine.textEdited.connect(self.reset_input_style)\n        self.testLine.textEdited.connect(self.reset_table_style)\n\n    def _setupUI(self):\n        layout = QVBoxLayout(self)\n        gridlayout = QGridLayout()\n        self.buttonAdd = QPushButton(tr(\"Add\"))\n        self.buttonRemove = QPushButton(tr(\"Remove Selected\"))\n        self.buttonRestore = QPushButton(tr(\"Restore defaults\"))\n        self.buttonTestString = QPushButton(tr(\"Test string\"))\n        self.buttonClose = QPushButton(tr(\"Close\"))\n        self.buttonHelp = QPushButton(tr(\"Help\"))\n        self.inputLine = QLineEdit()\n        self.testLine = QLineEdit()\n        self.tableView = QTableView()\n        triggers = (\n            QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.SelectedClicked\n        )\n        self.tableView.setEditTriggers(triggers)\n        self.tableView.setSelectionMode(QTableView.ExtendedSelection)\n        self.tableView.setSelectionBehavior(QTableView.SelectRows)\n        self.tableView.setShowGrid(False)\n        vheader = self.tableView.verticalHeader()\n        vheader.setSectionsMovable(True)\n        vheader.setVisible(False)\n        hheader = self.tableView.horizontalHeader()\n        hheader.setSectionsMovable(False)\n        hheader.setSectionResizeMode(QHeaderView.Fixed)\n        hheader.setStretchLastSection(True)\n        hheader.setHighlightSections(False)\n        hheader.setVisible(True)\n        gridlayout.addWidget(self.inputLine, 0, 0)\n        gridlayout.addWidget(self.buttonAdd, 0, 1, Qt.AlignLeft)\n        gridlayout.addWidget(self.buttonRemove, 1, 1, Qt.AlignLeft)\n        gridlayout.addWidget(self.buttonRestore, 2, 1, Qt.AlignLeft)\n        gridlayout.addWidget(self.buttonHelp, 3, 1, Qt.AlignLeft)\n        gridlayout.addWidget(self.buttonClose, 4, 1)\n        gridlayout.addWidget(self.tableView, 1, 0, 6, 1)\n        gridlayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding), 4, 1)\n        gridlayout.addWidget(self.buttonTestString, 6, 1)\n        gridlayout.addWidget(self.testLine, 6, 0)\n\n        layout.addLayout(gridlayout)\n        self.inputLine.setPlaceholderText(tr(\"Type a python regular expression here...\"))\n        self.inputLine.setFocus()\n        self.testLine.setPlaceholderText(tr(\"Type a file system path or filename here...\"))\n        self.testLine.setClearButtonEnabled(True)\n\n    # --- model --> view\n    def show(self):\n        super().show()\n        self.inputLine.setFocus()\n\n    @pyqtSlot()\n    def addStringFromLineEdit(self):\n        text = self.inputLine.text()\n        if not text:\n            return\n        try:\n            self.model.add(text)\n        except AlreadyThereException:\n            self.app.show_message(\"Expression already in the list.\")\n            return\n        except Exception as e:\n            self.app.show_message(f\"Expression is invalid: {e}\")\n            return\n        self.inputLine.clear()\n\n    def removeSelected(self):\n        self.model.remove_selected()\n\n    def restoreDefaults(self):\n        self.model.restore_defaults()\n\n    def onTestStringButtonClicked(self):\n        input_text = self.testLine.text()\n        if not input_text:\n            self.reset_input_style()\n            return\n        # If at least one row matched, we know whether table is highlighted or not\n        self._row_matched = self.model.test_string(input_text)\n        self.table.refresh()\n\n        # Test the string currently in the input text box as well\n        input_regex = self.inputLine.text()\n        if not input_regex:\n            self.reset_input_style()\n            return\n        compiled = None\n        try:\n            compiled = re.compile(input_regex)\n        except re.error:\n            self.reset_input_style()\n            return\n        if self.model.is_match(input_text, compiled):\n            self.inputLine.setStyleSheet(\"background-color: rgb(10, 200, 10);\")\n            self._input_styled = True\n        else:\n            self.reset_input_style()\n\n    def reset_input_style(self):\n        \"\"\"Reset regex input line background\"\"\"\n        if self._input_styled:\n            self.inputLine.setStyleSheet(self.styleSheet())\n            self._input_styled = False\n\n    def reset_table_style(self):\n        if self._row_matched:\n            self._row_matched = False\n            self.model.reset_rows_highlight()\n        self.table.refresh()\n\n    def display_help_message(self):\n        self.app.show_message(\n            tr(\n                \"\"\"\\\nThese (case sensitive) python regular expressions will filter out files during scans.<br>\\\nDirectores will also have their <strong>default state</strong> set to Excluded \\\nin the Directories tab if their name happens to match one of the selected regular expressions.<br>\\\nFor each file collected, two tests are performed to determine whether or not to completely ignore it:<br>\\\n<li>1. Regular expressions with no path separator in them will be compared to the file name only.</li>\n<li>2. Regular expressions with at least one path separator in them will be compared to the full path to the file.</li>\\\n<br>Example: if you want to filter out .PNG files from the \"My Pictures\" directory only:<br>\\\n<code>.*My\\\\sPictures\\\\\\\\.*\\\\.png</code><br><br>\\\nYou can test the regular expression with the \"test string\" button after pasting a fake path in the test field:<br>\\\n<code>C:\\\\\\\\User\\\\My Pictures\\\\test.png</code><br><br>\nMatching regular expressions will be highlighted.<br>\\\nIf there is at least one highlight, the path or filename tested will be ignored during scans.<br><br>\\\nDirectories and files starting with a period '.' are filtered out by default.<br><br>\"\"\"\n            )\n        )\n"
  },
  {
    "path": "qt/exclude_list_table.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtGui import QFont, QFontMetrics, QIcon, QColor\n\nfrom qt.column import Column\nfrom qt.table import Table\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\n\nclass ExcludeListTable(Table):\n    \"\"\"Model for exclude list\"\"\"\n\n    COLUMNS = [Column(\"marked\", default_width=15), Column(\"regex\", default_width=230)]\n\n    def __init__(self, app, view, **kwargs):\n        model = app.model.exclude_list_dialog.exclude_list_table  # pointer to GUITable\n        super().__init__(model, view, **kwargs)\n        font = view.font()\n        font.setPointSize(app.prefs.tableFontSize)\n        view.setFont(font)\n        fm = QFontMetrics(font)\n        view.verticalHeader().setDefaultSectionSize(fm.height() + 2)\n\n    def _getData(self, row, column, role):\n        if column.name == \"marked\":\n            if role == Qt.CheckStateRole and row.markable:\n                return Qt.Checked if row.marked else Qt.Unchecked\n            if role == Qt.ToolTipRole and not row.markable:\n                return tr(\"Compilation error: \") + row.get_cell_value(\"error\")\n            if role == Qt.DecorationRole and not row.markable:\n                return QIcon.fromTheme(\"dialog-error\", QIcon(\":/error\"))\n            return None\n        if role == Qt.DisplayRole:\n            return row.data[column.name]\n        elif role == Qt.FontRole:\n            return QFont(self.view.font())\n        elif role == Qt.BackgroundRole and column.name == \"regex\":\n            if row.highlight:\n                return QColor(10, 200, 10)  # green\n        elif role == Qt.EditRole and column.name == \"regex\":\n            return row.data[column.name]\n        return None\n\n    def _getFlags(self, row, column):\n        flags = Qt.ItemIsEnabled\n        if column.name == \"marked\":\n            if row.markable:\n                flags |= Qt.ItemIsUserCheckable\n        elif column.name == \"regex\":\n            flags |= Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled\n        return flags\n\n    def _setData(self, row, column, value, role):\n        if role == Qt.CheckStateRole:\n            if column.name == \"marked\":\n                row.marked = bool(value)\n                return True\n        elif role == Qt.EditRole and column.name == \"regex\":\n            return self.model.rename_selected(value)\n        return False\n"
  },
  {
    "path": "qt/ignore_list_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2012-03-13\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QPushButton,\n    QTableView,\n    QAbstractItemView,\n)\n\nfrom hscommon.trans import trget\nfrom qt.util import horizontal_wrap\nfrom qt.ignore_list_table import IgnoreListTable\n\ntr = trget(\"ui\")\n\n\nclass IgnoreListDialog(QDialog):\n    def __init__(self, parent, model, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self.specific_actions = frozenset()\n        self._setupUi()\n        self.model = model\n        self.model.view = self\n        self.table = IgnoreListTable(self.model.ignore_list_table, view=self.tableView)\n\n        self.removeSelectedButton.clicked.connect(self.model.remove_selected)\n        self.clearButton.clicked.connect(self.model.clear)\n        self.closeButton.clicked.connect(self.accept)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Ignore List\"))\n        self.resize(540, 330)\n        self.verticalLayout = QVBoxLayout(self)\n        self.verticalLayout.setContentsMargins(0, 0, 0, 0)\n        self.tableView = QTableView()\n        self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)\n        self.tableView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.tableView.setShowGrid(False)\n        self.tableView.horizontalHeader().setStretchLastSection(True)\n        self.tableView.verticalHeader().setDefaultSectionSize(18)\n        self.tableView.verticalHeader().setHighlightSections(False)\n        self.tableView.verticalHeader().setVisible(False)\n        self.tableView.setWordWrap(False)\n        self.verticalLayout.addWidget(self.tableView)\n        self.removeSelectedButton = QPushButton(tr(\"Remove Selected\"))\n        self.clearButton = QPushButton(tr(\"Clear\"))\n        self.closeButton = QPushButton(tr(\"Close\"))\n        self.verticalLayout.addLayout(\n            horizontal_wrap([self.removeSelectedButton, self.clearButton, None, self.closeButton])\n        )\n\n    # --- model --> view\n    def show(self):\n        super().show()\n"
  },
  {
    "path": "qt/ignore_list_table.py",
    "content": "# Created On: 2012-03-13\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.column import Column\nfrom qt.table import Table\n\n\nclass IgnoreListTable(Table):\n    \"\"\"Ignore list model\"\"\"\n\n    COLUMNS = [\n        Column(\"path1\", default_width=230),\n        Column(\"path2\", default_width=230),\n    ]\n"
  },
  {
    "path": "qt/me/__init__.py",
    "content": ""
  },
  {
    "path": "qt/me/details_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QSize\nfrom PyQt5.QtWidgets import QAbstractItemView\n\nfrom hscommon.trans import trget\nfrom qt.details_dialog import DetailsDialog as DetailsDialogBase\nfrom qt.details_table import DetailsTable\n\ntr = trget(\"ui\")\n\n\nclass DetailsDialog(DetailsDialogBase):\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Details\"))\n        self.resize(502, 295)\n        self.setMinimumSize(QSize(250, 250))\n        self.tableView = DetailsTable(self)\n        self.tableView.setAlternatingRowColors(True)\n        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.tableView.setShowGrid(False)\n        self.setWidget(self.tableView)\n"
  },
  {
    "path": "qt/me/preferences_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QSize\nfrom PyQt5.QtWidgets import (\n    QVBoxLayout,\n    QHBoxLayout,\n    QLabel,\n    QSizePolicy,\n    QSpacerItem,\n    QWidget,\n)\n\nfrom hscommon.trans import trget\nfrom core.app import AppMode\nfrom core.scanner import ScanType\n\nfrom qt.preferences_dialog import PreferencesDialogBase\n\ntr = trget(\"ui\")\n\n\nclass PreferencesDialog(PreferencesDialogBase):\n    def _setupPreferenceWidgets(self):\n        self._setupFilterHardnessBox()\n        self.widgetsVLayout.addLayout(self.filterHardnessHLayout)\n        self.widget = QWidget(self)\n        self.widget.setMinimumSize(QSize(0, 40))\n        self.verticalLayout_4 = QVBoxLayout(self.widget)\n        self.verticalLayout_4.setSpacing(0)\n        self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)\n        self.label_6 = QLabel(self.widget)\n        self.label_6.setText(tr(\"Tags to scan:\"))\n        self.verticalLayout_4.addWidget(self.label_6)\n        self.horizontalLayout_2 = QHBoxLayout()\n        self.horizontalLayout_2.setSpacing(0)\n        spacer_item = QSpacerItem(15, 20, QSizePolicy.Fixed, QSizePolicy.Minimum)\n        self.horizontalLayout_2.addItem(spacer_item)\n        self._setupAddCheckbox(\"tagTrackBox\", tr(\"Track\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagTrackBox)\n        self._setupAddCheckbox(\"tagArtistBox\", tr(\"Artist\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagArtistBox)\n        self._setupAddCheckbox(\"tagAlbumBox\", tr(\"Album\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagAlbumBox)\n        self._setupAddCheckbox(\"tagTitleBox\", tr(\"Title\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagTitleBox)\n        self._setupAddCheckbox(\"tagGenreBox\", tr(\"Genre\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagGenreBox)\n        self._setupAddCheckbox(\"tagYearBox\", tr(\"Year\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.tagYearBox)\n        self.verticalLayout_4.addLayout(self.horizontalLayout_2)\n        self.widgetsVLayout.addWidget(self.widget)\n        self._setupAddCheckbox(\"wordWeightingBox\", tr(\"Word weighting\"))\n        self.widgetsVLayout.addWidget(self.wordWeightingBox)\n        self._setupAddCheckbox(\"matchSimilarBox\", tr(\"Match similar words\"))\n        self.widgetsVLayout.addWidget(self.matchSimilarBox)\n        self._setupAddCheckbox(\"mixFileKindBox\", tr(\"Can mix file kind\"))\n        self.widgetsVLayout.addWidget(self.mixFileKindBox)\n        self._setupAddCheckbox(\"useRegexpBox\", tr(\"Use regular expressions when filtering\"))\n        self.widgetsVLayout.addWidget(self.useRegexpBox)\n        self._setupAddCheckbox(\"removeEmptyFoldersBox\", tr(\"Remove empty folders on delete or move\"))\n        self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)\n        self._setupAddCheckbox(\n            \"ignoreHardlinkMatches\",\n            tr(\"Ignore duplicates hardlinking to the same file\"),\n        )\n        self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)\n        self._setupBottomPart()\n\n    def _load(self, prefs, setchecked, section):\n        setchecked(self.tagTrackBox, prefs.scan_tag_track)\n        setchecked(self.tagArtistBox, prefs.scan_tag_artist)\n        setchecked(self.tagAlbumBox, prefs.scan_tag_album)\n        setchecked(self.tagTitleBox, prefs.scan_tag_title)\n        setchecked(self.tagGenreBox, prefs.scan_tag_genre)\n        setchecked(self.tagYearBox, prefs.scan_tag_year)\n        setchecked(self.matchSimilarBox, prefs.match_similar)\n        setchecked(self.wordWeightingBox, prefs.word_weighting)\n\n        # Update UI state based on selected scan type\n        scan_type = prefs.get_scan_type(AppMode.MUSIC)\n        word_based = scan_type in (\n            ScanType.FILENAME,\n            ScanType.FIELDS,\n            ScanType.FIELDSNOORDER,\n            ScanType.TAG,\n        )\n        tag_based = scan_type == ScanType.TAG\n        self.filterHardnessSlider.setEnabled(word_based)\n        self.matchSimilarBox.setEnabled(word_based)\n        self.wordWeightingBox.setEnabled(word_based)\n        self.tagTrackBox.setEnabled(tag_based)\n        self.tagArtistBox.setEnabled(tag_based)\n        self.tagAlbumBox.setEnabled(tag_based)\n        self.tagTitleBox.setEnabled(tag_based)\n        self.tagGenreBox.setEnabled(tag_based)\n        self.tagYearBox.setEnabled(tag_based)\n\n    def _save(self, prefs, ischecked):\n        prefs.scan_tag_track = ischecked(self.tagTrackBox)\n        prefs.scan_tag_artist = ischecked(self.tagArtistBox)\n        prefs.scan_tag_album = ischecked(self.tagAlbumBox)\n        prefs.scan_tag_title = ischecked(self.tagTitleBox)\n        prefs.scan_tag_genre = ischecked(self.tagGenreBox)\n        prefs.scan_tag_year = ischecked(self.tagYearBox)\n        prefs.match_similar = ischecked(self.matchSimilarBox)\n        prefs.word_weighting = ischecked(self.wordWeightingBox)\n"
  },
  {
    "path": "qt/me/results_model.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.column import Column\nfrom qt.results_model import ResultsModel as ResultsModelBase\n\n\nclass ResultsModel(ResultsModelBase):\n    COLUMNS = [\n        Column(\"marked\", default_width=30),\n        Column(\"name\", default_width=200),\n        Column(\"folder_path\", default_width=180),\n        Column(\"size\", default_width=60),\n        Column(\"duration\", default_width=60),\n        Column(\"bitrate\", default_width=50),\n        Column(\"samplerate\", default_width=60),\n        Column(\"extension\", default_width=40),\n        Column(\"mtime\", default_width=120),\n        Column(\"title\", default_width=120),\n        Column(\"artist\", default_width=120),\n        Column(\"album\", default_width=120),\n        Column(\"genre\", default_width=80),\n        Column(\"year\", default_width=40),\n        Column(\"track\", default_width=40),\n        Column(\"comment\", default_width=120),\n        Column(\"percentage\", default_width=60),\n        Column(\"words\", default_width=120),\n        Column(\"dupe_count\", default_width=80),\n    ]\n"
  },
  {
    "path": "qt/pe/__init__.py",
    "content": ""
  },
  {
    "path": "qt/pe/block.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-05-10\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.pe._block_qt import getblocks  # NOQA\n\n# Converted to C\n# def getblock(image):\n#     width = image.width()\n#     height = image.height()\n#     if width:\n#         pixel_count = width * height\n#         red = green = blue = 0\n#         s = image.bits().asstring(image.numBytes())\n#         for i in xrange(pixel_count):\n#             offset = i * 3\n#             red += ord(s[offset])\n#             green += ord(s[offset + 1])\n#             blue += ord(s[offset + 2])\n#         return (red // pixel_count, green // pixel_count, blue // pixel_count)\n#     else:\n#         return (0, 0, 0)\n#\n# def getblocks(image, block_count_per_side):\n#     width = image.width()\n#     height = image.height()\n#     if not width:\n#         return []\n#     block_width = max(width // block_count_per_side, 1)\n#     block_height = max(height // block_count_per_side, 1)\n#     result = []\n#     for ih in xrange(block_count_per_side):\n#         top = min(ih * block_height, height - block_height)\n#         for iw in range(block_count_per_side):\n#             left = min(iw * block_width, width - block_width)\n#             crop = image.copy(left, top, block_width, block_height)\n#             result.append(getblock(crop))\n#     return result\n"
  },
  {
    "path": "qt/pe/block.pyi",
    "content": "from typing import Tuple, List, Union\nfrom PyQt5.QtGui import QImage\n\n_block = Tuple[int, int, int]\n\ndef getblock(image: QImage) -> _block: ...  # noqa: E302\ndef getblocks(image: QImage, block_count_per_side: int) -> Union[List[_block], None]: ...\n"
  },
  {
    "path": "qt/pe/details_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot\nfrom PyQt5.QtWidgets import QAbstractItemView, QSizePolicy, QGridLayout, QSplitter, QFrame\nfrom PyQt5.QtGui import QResizeEvent\nfrom hscommon.trans import trget\nfrom qt.details_dialog import DetailsDialog as DetailsDialogBase\nfrom qt.details_table import DetailsTable\nfrom qt.pe.image_viewer import ViewerToolBar, ScrollAreaImageViewer, ScrollAreaController\n\ntr = trget(\"ui\")\n\n\nclass DetailsDialog(DetailsDialogBase):\n    def __init__(self, parent, app):\n        self.vController = None\n        super().__init__(parent, app)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Details\"))\n        self.resize(502, 502)\n        self.setMinimumSize(QSize(250, 250))\n        self.splitter = QSplitter(Qt.Vertical)\n        self.topFrame = EmittingFrame()\n        self.topFrame.setFrameShape(QFrame.StyledPanel)\n        self.horizontalLayout = QGridLayout()\n        # Minimum width for the toolbar in the middle:\n        self.horizontalLayout.setColumnMinimumWidth(1, 10)\n        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)\n        self.horizontalLayout.setColumnStretch(0, 32)\n        # Smaller value for the toolbar in the middle to avoid excessive resize\n        self.horizontalLayout.setColumnStretch(1, 2)\n        self.horizontalLayout.setColumnStretch(2, 32)\n        # This avoids toolbar getting incorrectly partially hidden when window resizes\n        self.horizontalLayout.setRowStretch(0, 1)\n        self.horizontalLayout.setRowStretch(1, 24)\n        self.horizontalLayout.setRowStretch(2, 1)\n        self.horizontalLayout.setSpacing(1)  # probably not important\n\n        self.selectedImageViewer = ScrollAreaImageViewer(self, \"selectedImage\")\n        self.horizontalLayout.addWidget(self.selectedImageViewer, 0, 0, 3, 1)\n        # Use a specific type of controller depending on the underlying viewer type\n        self.vController = ScrollAreaController(self)\n\n        self.verticalToolBar = ViewerToolBar(self, self.vController)\n        self.verticalToolBar.setOrientation(Qt.Orientation(Qt.Vertical))\n        self.horizontalLayout.addWidget(self.verticalToolBar, 1, 1, 1, 1, Qt.AlignCenter)\n\n        self.referenceImageViewer = ScrollAreaImageViewer(self, \"referenceImage\")\n        self.horizontalLayout.addWidget(self.referenceImageViewer, 0, 2, 3, 1)\n        self.topFrame.setLayout(self.horizontalLayout)\n        self.splitter.addWidget(self.topFrame)\n        self.splitter.setStretchFactor(0, 8)\n\n        self.tableView = DetailsTable(self)\n        size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)\n        size_policy.setHorizontalStretch(0)\n        size_policy.setVerticalStretch(0)\n        self.tableView.setSizePolicy(size_policy)\n        self.tableView.setAlternatingRowColors(True)\n        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.tableView.setShowGrid(False)\n        self.splitter.addWidget(self.tableView)\n        self.splitter.setStretchFactor(1, 1)\n        # Late population needed here for connections to the toolbar\n        self.vController.setupViewers(self.selectedImageViewer, self.referenceImageViewer)\n        # self.setCentralWidget(self.splitter)  # only as QMainWindow\n        self.setWidget(self.splitter)  # only as QDockWidget\n\n        self.topFrame.resized.connect(self.resizeEvent)\n\n    def _update(self):\n        if self.vController is None:  # Not yet constructed!\n            return\n        if not self.app.model.selected_dupes:\n            # No item from the model, disable and clear everything.\n            self.vController.resetViewersState()\n            return\n        dupe = self.app.model.selected_dupes[0]\n        group = self.app.model.results.get_group_of_duplicate(dupe)\n        ref = group.ref\n\n        self.vController.updateView(ref, dupe, group)\n\n    # --- Override\n    @pyqtSlot(QResizeEvent)\n    def resizeEvent(self, event):\n        self.ensure_same_sizes()\n        if self.vController is None or not self.vController.bestFit:\n            return\n        # Only update the scaled down pixmaps\n        self.vController.updateBothImages()\n\n    def show(self):\n        # Give the splitter a maximum height to reach. This is assuming that\n        # all rows below their headers have the same height\n        self.tableView.setMaximumHeight(\n            self.tableView.rowHeight(1) * self.tableModel.model.row_count()\n            + self.tableView.verticalHeader().sectionSize(0)\n            # looks like the handle is taken into account by the splitter\n            + self.splitter.handle(1).size().height()\n        )\n        DetailsDialogBase.show(self)\n        self.ensure_same_sizes()\n        self._update()\n\n    def ensure_same_sizes(self):\n        # HACK This ensures same size while shrinking.\n        # ReferenceViewer might be 1 pixel shorter in width\n        # due to the toolbar in the middle keeping the same width,\n        # so resizing in the GridLayout's engine leads to not enough space\n        # left for the panel on the right.\n        # This work as a QMainWindow, but doesn't work as a QDockWidget:\n        # resize can only grow. Might need some custom sizeHint somewhere...\n        # self.horizontalLayout.setColumnMinimumWidth(\n        #     0, self.selectedImageViewer.size().width())\n        # self.horizontalLayout.setColumnMinimumWidth(\n        #     2, self.selectedImageViewer.size().width())\n\n        # This works when expanding but it's ugly:\n        if self.selectedImageViewer.size().width() > self.referenceImageViewer.size().width():\n            self.selectedImageViewer.resize(self.referenceImageViewer.size())\n\n    # model --> view\n    def refresh(self):\n        DetailsDialogBase.refresh(self)\n        if self.isVisible():\n            self._update()\n\n\nclass EmittingFrame(QFrame):\n    \"\"\"Emits a signal whenever is resized\"\"\"\n\n    resized = pyqtSignal(QResizeEvent)\n\n    def resizeEvent(self, event):\n        self.resized.emit(event)\n"
  },
  {
    "path": "qt/pe/image_viewer.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QObject, Qt, QSize, QRectF, QPointF, QPoint, pyqtSlot, pyqtSignal, QEvent\nfrom PyQt5.QtGui import QPixmap, QPainter, QPalette, QCursor, QIcon, QKeySequence\nfrom PyQt5.QtWidgets import (\n    QGraphicsView,\n    QGraphicsScene,\n    QGraphicsPixmapItem,\n    QToolBar,\n    QToolButton,\n    QAction,\n    QWidget,\n    QScrollArea,\n    QApplication,\n    QAbstractScrollArea,\n    QStyle,\n)\nfrom hscommon.trans import trget\nfrom hscommon.plat import ISLINUX\n\ntr = trget(\"ui\")\n\nMAX_SCALE = 12.0\nMIN_SCALE = 0.1\n\n\ndef create_actions(actions, target):\n    # actions are list of (name, shortcut, icon, desc, func)\n    for name, shortcut, icon, desc, func in actions:\n        action = QAction(target)\n        if icon:\n            action.setIcon(icon)\n        if shortcut:\n            action.setShortcut(shortcut)\n        action.setText(desc)\n        action.triggered.connect(func)\n        setattr(target, name, action)\n\n\nclass ViewerToolBar(QToolBar):\n    def __init__(self, parent, controller):\n        super().__init__(parent)\n        self.parent = parent\n        self.controller = controller\n        self.setupActions(controller)\n        self.createButtons()\n        self.buttonImgSwap.setEnabled(False)\n        self.buttonZoomIn.setEnabled(False)\n        self.buttonZoomOut.setEnabled(False)\n        self.buttonNormalSize.setEnabled(False)\n        self.buttonBestFit.setEnabled(False)\n\n    def setupActions(self, controller):\n        # actions are list of (name, shortcut, icon, desc, func)\n        ACTIONS = [\n            (\n                \"actionZoomIn\",\n                QKeySequence.ZoomIn,\n                (\n                    QIcon.fromTheme(\"zoom-in\")\n                    if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons\n                    else QIcon(QPixmap(\":/\" + \"zoom_in\"))\n                ),\n                tr(\"Increase zoom\"),\n                controller.zoomIn,\n            ),\n            (\n                \"actionZoomOut\",\n                QKeySequence.ZoomOut,\n                (\n                    QIcon.fromTheme(\"zoom-out\")\n                    if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons\n                    else QIcon(QPixmap(\":/\" + \"zoom_out\"))\n                ),\n                tr(\"Decrease zoom\"),\n                controller.zoomOut,\n            ),\n            (\n                \"actionNormalSize\",\n                tr(\"Ctrl+/\"),\n                (\n                    QIcon.fromTheme(\"zoom-original\")\n                    if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons\n                    else QIcon(QPixmap(\":/\" + \"zoom_original\"))\n                ),\n                tr(\"Normal size\"),\n                controller.zoomNormalSize,\n            ),\n            (\n                \"actionBestFit\",\n                tr(\"Ctrl+*\"),\n                (\n                    QIcon.fromTheme(\"zoom-best-fit\")\n                    if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons\n                    else QIcon(QPixmap(\":/\" + \"zoom_best_fit\"))\n                ),\n                tr(\"Best fit\"),\n                controller.zoomBestFit,\n            ),\n        ]\n        # TODO try with QWidgetAction() instead in order to have\n        # the popup menu work in the toolbar (if resized below minimum height)\n        create_actions(ACTIONS, self)\n\n    def createButtons(self):\n        self.buttonImgSwap = QToolButton(self)\n        self.buttonImgSwap.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.buttonImgSwap.setIcon(\n            QIcon.fromTheme(\"view-refresh\", self.style().standardIcon(QStyle.SP_BrowserReload))\n            if ISLINUX and not self.parent.app.prefs.details_dialog_override_theme_icons\n            else QIcon(QPixmap(\":/\" + \"exchange\"))\n        )\n        self.buttonImgSwap.setText(\"Swap images\")\n        self.buttonImgSwap.setToolTip(\"Swap images\")\n        self.buttonImgSwap.pressed.connect(self.controller.swapImages)\n        self.buttonImgSwap.released.connect(self.controller.swapImages)\n\n        self.buttonZoomIn = QToolButton(self)\n        self.buttonZoomIn.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.buttonZoomIn.setDefaultAction(self.actionZoomIn)\n        self.buttonZoomIn.setEnabled(False)\n\n        self.buttonZoomOut = QToolButton(self)\n        self.buttonZoomOut.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.buttonZoomOut.setDefaultAction(self.actionZoomOut)\n        self.buttonZoomOut.setEnabled(False)\n\n        self.buttonNormalSize = QToolButton(self)\n        self.buttonNormalSize.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.buttonNormalSize.setDefaultAction(self.actionNormalSize)\n        self.buttonNormalSize.setEnabled(True)\n\n        self.buttonBestFit = QToolButton(self)\n        self.buttonBestFit.setToolButtonStyle(Qt.ToolButtonIconOnly)\n        self.buttonBestFit.setDefaultAction(self.actionBestFit)\n        self.buttonBestFit.setEnabled(False)\n\n        self.addWidget(self.buttonImgSwap)\n        self.addWidget(self.buttonZoomIn)\n        self.addWidget(self.buttonZoomOut)\n        self.addWidget(self.buttonNormalSize)\n        self.addWidget(self.buttonBestFit)\n\n\nclass BaseController(QObject):\n    \"\"\"Abstract Base class. Singleton.\n    Base proxy interface to keep image viewers synchronized.\n    Relays function calls, keep tracks of things.\"\"\"\n\n    def __init__(self, parent):\n        super().__init__()\n        self.selectedViewer = None\n        self.referenceViewer = None\n        # cached pixmaps\n        self.selectedPixmap = QPixmap()\n        self.referencePixmap = QPixmap()\n        self.scaledSelectedPixmap = QPixmap()\n        self.scaledReferencePixmap = QPixmap()\n        self.current_scale = 1.0\n        self.bestFit = True\n        self.parent = parent  # To change buttons' states\n        self.cached_group = None\n        self.same_dimensions = True\n\n    def setupViewers(self, selected_viewer, reference_viewer):\n        self.selectedViewer = selected_viewer\n        self.referenceViewer = reference_viewer\n        self.selectedViewer.controller = self\n        self.referenceViewer.controller = self\n        self._setupConnections()\n\n    def _setupConnections(self):\n        self.selectedViewer.connectMouseSignals()\n        self.referenceViewer.connectMouseSignals()\n\n    def updateView(self, ref, dupe, group):\n        # To keep current scale accross dupes from the same group\n        previous_same_dimensions = self.same_dimensions\n        self.same_dimensions = True\n        same_group = True\n        if group != self.cached_group:\n            same_group = False\n            self.resetState()\n        self.cached_group = group\n\n        self.selectedPixmap = QPixmap(str(dupe.path))\n        if ref is dupe:  # currently selected file is the actual reference file\n            self.referencePixmap = QPixmap()\n            self.scaledReferencePixmap = QPixmap()\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n            self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        else:\n            self.referencePixmap = QPixmap(str(ref.path))\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)\n            if ref.dimensions != dupe.dimensions:\n                self.same_dimensions = False\n            self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        self.updateButtonsAsPerDimensions(previous_same_dimensions)\n        self.updateBothImages(same_group)\n        self.centerViews(same_group and self.referencePixmap.isNull())\n\n    def updateBothImages(self, same_group=False):\n        # WARNING this is called on every resize event,\n        ignore_update = self.referencePixmap.isNull()\n        if ignore_update:\n            self.selectedViewer.ignore_signal = True\n        # the SelectedImageViewer widget sometimes ends up being bigger\n        # than the ReferenceImageViewer by one pixel, which distorts the\n        # scaled down pixmap for the reference, hence we'll reuse its size here.\n        self._updateImage(self.selectedPixmap, self.selectedViewer, same_group)\n        self._updateImage(self.referencePixmap, self.referenceViewer, same_group)\n        if ignore_update:\n            self.selectedViewer.ignore_signal = False\n\n    def _updateImage(self, pixmap, viewer, same_group=False):\n        # WARNING this is called on every resize event, might need to split\n        # into a separate function depending on the implementation used\n        if pixmap.isNull():\n            # This should disable the blank widget\n            viewer.setImage(pixmap)\n            return\n        target_size = viewer.size()\n        if not viewer.bestFit:\n            if same_group:\n                viewer.setImage(pixmap)\n                return target_size\n            # zoomed in state, expand\n            # only if not same_group, we need full update\n            scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatioByExpanding, Qt.FastTransformation)\n        else:\n            # best fit, keep ratio always\n            scaledpixmap = pixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.FastTransformation)\n        viewer.setImage(scaledpixmap)\n        return target_size\n\n    def resetState(self):\n        \"\"\"Only called when the group of dupes has changed. We reset our\n        controller internal state and buttons, center view on viewers.\"\"\"\n        self.selectedPixmap = QPixmap()\n        self.scaledSelectedPixmap = QPixmap()\n        self.referencePixmap = QPixmap()\n        self.scaledReferencePixmap = QPixmap()\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.current_scale = 1.0\n        self.referenceViewer.current_scale = 1.0\n        self.selectedViewer.resetCenter()\n        self.referenceViewer.resetCenter()\n        self.selectedViewer.scaleAt(1.0)\n        self.referenceViewer.scaleAt(1.0)\n        self.centerViews()\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)  # active mode by default\n\n    def resetViewersState(self):\n        \"\"\"No item from the model, disable and clear everything.\"\"\"\n        # only called by the details dialog\n        self.selectedPixmap = QPixmap()\n        self.scaledSelectedPixmap = QPixmap()\n        self.referencePixmap = QPixmap()\n        self.scaledReferencePixmap = QPixmap()\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.current_scale = 1.0\n        self.referenceViewer.current_scale = 1.0\n        self.selectedViewer.resetCenter()\n        self.referenceViewer.resetCenter()\n        self.selectedViewer.scaleAt(1.0)\n        self.referenceViewer.scaleAt(1.0)\n        self.centerViews()\n\n        self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)  # active mode by default\n\n        self.selectedViewer.setImage(self.selectedPixmap)  # null\n        self.selectedViewer.setEnabled(False)\n        self.referenceViewer.setImage(self.referencePixmap)  # null\n        self.referenceViewer.setEnabled(False)\n\n    @pyqtSlot()\n    def zoomIn(self):\n        self.scaleImagesBy(1.25)\n\n    @pyqtSlot()\n    def zoomOut(self):\n        self.scaleImagesBy(0.8)\n\n    @pyqtSlot(float)\n    def scaleImagesBy(self, factor):\n        \"\"\"Compute new scale from factor and scale.\"\"\"\n        self.current_scale *= factor\n        self.selectedViewer.scaleBy(factor)\n        self.referenceViewer.scaleBy(factor)\n        self.updateButtons()\n\n    @pyqtSlot(float)\n    def scaleImagesAt(self, scale):\n        \"\"\"Scale at a pre-computed scale.\"\"\"\n        self.current_scale = scale\n        self.selectedViewer.scaleAt(scale)\n        self.referenceViewer.scaleAt(scale)\n        self.updateButtons()\n\n    def updateButtons(self):\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(self.current_scale < MAX_SCALE)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(self.current_scale > MIN_SCALE)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(round(self.current_scale, 1) != 1.0)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(self.bestFit is False)\n\n    def updateButtonsAsPerDimensions(self, previous_same_dimensions):\n        if not self.same_dimensions:\n            self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n            self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n            if not self.bestFit:\n                self.zoomBestFit()\n                self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n                if not self.referencePixmap.isNull():\n                    self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)\n            return\n        if not self.bestFit and not previous_same_dimensions:\n            self.zoomBestFit()\n            self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n            if self.referencePixmap.isNull():\n                self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n\n    @pyqtSlot()\n    def zoomBestFit(self):\n        \"\"\"Setup before scaling to bestfit\"\"\"\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.current_scale = 1.0\n        self.referenceViewer.current_scale = 1.0\n\n        self.selectedViewer.scaleAt(1.0)\n        self.referenceViewer.scaleAt(1.0)\n\n        self.selectedViewer.resetCenter()\n        self.referenceViewer.resetCenter()\n\n        self._updateImage(self.selectedPixmap, self.selectedViewer, True)\n        self._updateImage(self.referencePixmap, self.referenceViewer, True)\n        self.centerViews()\n\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)\n        self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)\n\n    def setBestFit(self, value):\n        self.bestFit = value\n        self.selectedViewer.bestFit = value\n        self.referenceViewer.bestFit = value\n\n    @pyqtSlot()\n    def zoomNormalSize(self):\n        self.setBestFit(False)\n        self.current_scale = 1.0\n\n        self.selectedViewer.setImage(self.selectedPixmap)\n        self.referenceViewer.setImage(self.referencePixmap)\n\n        self.centerViews()\n\n        self.selectedViewer.scaleToNormalSize()\n        self.referenceViewer.scaleToNormalSize()\n\n        if self.same_dimensions:\n            self.parent.verticalToolBar.buttonZoomIn.setEnabled(True)\n            self.parent.verticalToolBar.buttonZoomOut.setEnabled(True)\n        else:\n            # we can't allow swapping pixmaps of different dimensions\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(True)\n\n    def centerViews(self, only_selected=False):\n        self.selectedViewer.centerViewAndUpdate()\n        if only_selected:\n            return\n        self.referenceViewer.centerViewAndUpdate()\n\n    @pyqtSlot()\n    def swapImages(self):\n        # swap the columns in the details table as well\n        self.parent.tableView.horizontalHeader().swapSections(0, 1)\n\n\nclass QWidgetController(BaseController):\n    \"\"\"Specialized version for QWidget-based viewers.\"\"\"\n\n    def __init__(self, parent):\n        super().__init__(parent)\n\n    def _updateImage(self, *args):\n        ret = super()._updateImage(*args)\n        # Fix alignment when resizing window\n        self.centerViews()\n        return ret\n\n    @pyqtSlot(QPointF)\n    def onDraggedMouse(self, delta):\n        if not self.same_dimensions:\n            return\n        if self.sender() is self.referenceViewer:\n            self.selectedViewer.onDraggedMouse(delta)\n        else:\n            self.referenceViewer.onDraggedMouse(delta)\n\n    @pyqtSlot()\n    def swapImages(self):\n        self.selectedViewer._pixmap.swap(self.referenceViewer._pixmap)\n        self.selectedViewer.centerViewAndUpdate()\n        self.referenceViewer.centerViewAndUpdate()\n        super().swapImages()\n\n\nclass ScrollAreaController(BaseController):\n    \"\"\"Specialized version fro QLabel-based viewers.\"\"\"\n\n    def __init__(self, parent):\n        super().__init__(parent)\n\n    def _setupConnections(self):\n        super()._setupConnections()\n        self.selectedViewer.connectScrollBars()\n        self.referenceViewer.connectScrollBars()\n\n    def updateBothImages(self, same_group=False):\n        super().updateBothImages(same_group)\n        if not self.referenceViewer.isEnabled():\n            return\n        self.referenceViewer._horizontalScrollBar.setValue(self.selectedViewer._horizontalScrollBar.value())\n        self.referenceViewer._verticalScrollBar.setValue(self.selectedViewer._verticalScrollBar.value())\n\n    @pyqtSlot(QPoint)\n    def onDraggedMouse(self, delta):\n        self.selectedViewer.ignore_signal = True\n        self.referenceViewer.ignore_signal = True\n\n        if self.same_dimensions:\n            self.selectedViewer.onDraggedMouse(delta)\n            self.referenceViewer.onDraggedMouse(delta)\n        else:\n            if self.sender() is self.selectedViewer:\n                self.selectedViewer.onDraggedMouse(delta)\n            else:\n                self.referenceViewer.onDraggedMouse(delta)\n\n        self.selectedViewer.ignore_signal = False\n        self.referenceViewer.ignore_signal = False\n\n    @pyqtSlot()\n    def swapImages(self):\n        self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)\n        self.referenceViewer.setCachedPixmap()\n        self.selectedViewer.setCachedPixmap()\n        super().swapImages()\n\n    @pyqtSlot(float, QPointF)\n    def onMouseWheel(self, scale, delta):\n        self.scaleImagesAt(scale)\n        self.selectedViewer.adjustScrollBarsScaled(delta)\n        # Signal from scrollbars will automatically change the other:\n        # self.referenceViewer.adjustScrollBarsScaled(delta)\n\n    @pyqtSlot(int)\n    def onVScrollBarChanged(self, value):\n        if not self.same_dimensions:\n            return\n        if self.sender() is self.referenceViewer._verticalScrollBar:\n            if not self.selectedViewer.ignore_signal:\n                self.selectedViewer._verticalScrollBar.setValue(value)\n        else:\n            if not self.referenceViewer.ignore_signal:\n                self.referenceViewer._verticalScrollBar.setValue(value)\n\n    @pyqtSlot(int)\n    def onHScrollBarChanged(self, value):\n        if not self.same_dimensions:\n            return\n        if self.sender() is self.referenceViewer._horizontalScrollBar:\n            if not self.selectedViewer.ignore_signal:\n                self.selectedViewer._horizontalScrollBar.setValue(value)\n        else:\n            if not self.referenceViewer.ignore_signal:\n                self.referenceViewer._horizontalScrollBar.setValue(value)\n\n    @pyqtSlot(float)\n    def scaleImagesBy(self, factor):\n        super().scaleImagesBy(factor)\n        # The other is automatically updated via sigals\n        self.selectedViewer.adjustScrollBarsFactor(factor)\n\n    @pyqtSlot()\n    def zoomBestFit(self):\n        # Disable scrollbars to avoid GridLayout size rounding glitch\n        super().zoomBestFit()\n        if self.referencePixmap.isNull():\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n        self.selectedViewer.toggleScrollBars()\n        self.referenceViewer.toggleScrollBars()\n\n\nclass GraphicsViewController(BaseController):\n    \"\"\"Specialized version fro QGraphicsView-based viewers.\"\"\"\n\n    def __init__(self, parent):\n        super().__init__(parent)\n\n    def _setupConnections(self):\n        super()._setupConnections()\n        self.selectedViewer.connectScrollBars()\n        self.referenceViewer.connectScrollBars()\n        # Special case for mouse wheel event conflicting with scrollbar adjustments\n        self.selectedViewer.other_viewer = self.referenceViewer\n        self.referenceViewer.other_viewer = self.selectedViewer\n\n    @pyqtSlot()\n    def syncCenters(self):\n        if self.sender() is self.referenceViewer:\n            self.selectedViewer.setCenter(self.referenceViewer._centerPoint)\n        else:\n            self.referenceViewer.setCenter(self.selectedViewer._centerPoint)\n\n    @pyqtSlot(float, QPointF)\n    def onMouseWheel(self, factor, new_center):\n        self.current_scale *= factor\n        if self.sender() is self.referenceViewer:\n            self.selectedViewer.scaleBy(factor)\n            self.selectedViewer.setCenter(new_center)\n        else:\n            self.referenceViewer.scaleBy(factor)\n            self.referenceViewer.setCenter(new_center)\n\n    @pyqtSlot(int)\n    def onVScrollBarChanged(self, value):\n        if not self.same_dimensions:\n            return\n        if self.sender() is self.referenceViewer._verticalScrollBar:\n            if not self.selectedViewer.ignore_signal:\n                self.selectedViewer._verticalScrollBar.setValue(value)\n        else:\n            if not self.referenceViewer.ignore_signal:\n                self.referenceViewer._verticalScrollBar.setValue(value)\n\n    @pyqtSlot(int)\n    def onHScrollBarChanged(self, value):\n        if not self.same_dimensions:\n            return\n        if self.sender() is self.referenceViewer._horizontalScrollBar:\n            if not self.selectedViewer.ignore_signal:\n                self.selectedViewer._horizontalScrollBar.setValue(value)\n        else:\n            if not self.referenceViewer.ignore_signal:\n                self.referenceViewer._horizontalScrollBar.setValue(value)\n\n    @pyqtSlot()\n    def swapImages(self):\n        self.referenceViewer._pixmap.swap(self.selectedViewer._pixmap)\n        self.referenceViewer.setCachedPixmap()\n        self.selectedViewer.setCachedPixmap()\n        super().swapImages()\n\n    @pyqtSlot()\n    def zoomBestFit(self):\n        \"\"\"Setup before scaling to bestfit\"\"\"\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.fitScale()\n        self.referenceViewer.fitScale()\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        if not self.referencePixmap.isNull():\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)\n        # else:\n        #     self.referenceViewer.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        #     self.referenceViewer.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n\n    def updateView(self, ref, dupe, group):\n        # Keep current scale accross dupes from the same group\n        previous_same_dimensions = self.same_dimensions\n        self.same_dimensions = True\n        same_group = True\n        if group != self.cached_group:\n            same_group = False\n            self.resetState()\n        self.cached_group = group\n\n        self.selectedPixmap = QPixmap(str(dupe.path))\n        if ref is dupe:  # currently selected file is the actual reference file\n            self.same_dimensions = False\n            self.referencePixmap = QPixmap()\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n            self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        else:\n            self.referencePixmap = QPixmap(str(ref.path))\n            self.parent.verticalToolBar.buttonImgSwap.setEnabled(True)\n            if ref.dimensions != dupe.dimensions:\n                self.same_dimensions = False\n            self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n        self.updateButtonsAsPerDimensions(previous_same_dimensions)\n        self.updateBothImages(same_group)\n\n    def updateBothImages(self, same_group=False):\n        \"\"\"This is called only during resize events and while bestFit.\"\"\"\n        ignore_update = self.referencePixmap.isNull()\n        if ignore_update:\n            self.selectedViewer.ignore_signal = True\n\n        self._updateFitImage(self.selectedPixmap, self.selectedViewer)\n        self._updateFitImage(self.referencePixmap, self.referenceViewer)\n\n        if ignore_update:\n            self.selectedViewer.ignore_signal = False\n\n    def _updateFitImage(self, pixmap, viewer):\n        # If not same_group, we need full update\"\"\"\n        viewer.setImage(pixmap)\n        if pixmap.isNull():\n            return\n        if viewer.bestFit:\n            viewer.fitScale()\n\n    def resetState(self):\n        \"\"\"Only called when the group of dupes has changed. We reset our\n        controller internal state and buttons, center view on viewers.\"\"\"\n        self.selectedPixmap = QPixmap()\n        self.referencePixmap = QPixmap()\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.current_scale = 1.0\n        self.referenceViewer.current_scale = 1.0\n\n        self.selectedViewer.resetCenter()\n        self.referenceViewer.resetCenter()\n\n        self.selectedViewer.fitScale()\n        self.referenceViewer.fitScale()\n        # self.centerViews()\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(True)\n\n    def resetViewersState(self):\n        \"\"\"No item from the model, disable and clear everything.\"\"\"\n        # only called by the details dialog\n        self.selectedPixmap = QPixmap()\n        self.scaledSelectedPixmap = QPixmap()\n        self.referencePixmap = QPixmap()\n        self.scaledReferencePixmap = QPixmap()\n        self.setBestFit(True)\n        self.current_scale = 1.0\n        self.selectedViewer.current_scale = 1.0\n        self.referenceViewer.current_scale = 1.0\n        self.selectedViewer.resetCenter()\n        self.referenceViewer.resetCenter()\n        # self.centerViews()\n        self.parent.verticalToolBar.buttonZoomIn.setEnabled(False)\n        self.parent.verticalToolBar.buttonZoomOut.setEnabled(False)\n        self.parent.verticalToolBar.buttonBestFit.setEnabled(False)\n        self.parent.verticalToolBar.buttonImgSwap.setEnabled(False)\n        self.parent.verticalToolBar.buttonNormalSize.setEnabled(False)\n\n        self.selectedViewer.setImage(self.selectedPixmap)  # null\n        self.selectedViewer.setEnabled(False)\n        self.referenceViewer.setImage(self.referencePixmap)  # null\n        self.referenceViewer.setEnabled(False)\n\n    @pyqtSlot(float)\n    def scaleImagesBy(self, factor):\n        self.selectedViewer.updateCenterPoint()\n        self.referenceViewer.updateCenterPoint()\n        super().scaleImagesBy(factor)\n        self.selectedViewer.centerOn(self.selectedViewer._centerPoint)\n        # Scrollbars sync themselves here\n\n\nclass QWidgetImageViewer(QWidget):\n    \"\"\"Use a QPixmap, but no scrollbars and no keyboard key sequence for navigation.\"\"\"\n\n    # FIXME: panning while zoomed-in is broken (due to delta not interpolated right?\n    mouseDragged = pyqtSignal(QPointF)\n    mouseWheeled = pyqtSignal(float)\n\n    def __init__(self, parent, name=\"\"):\n        super().__init__(parent)\n        self._app = QApplication\n        self._pixmap = QPixmap()\n        self._rect = QRectF()\n        self._lastMouseClickPoint = QPointF()\n        self._mousePanningDelta = QPointF()\n        self.current_scale = 1.0\n        self._drag = False\n        self._dragConnection = None\n        self._wheelConnection = None\n        self._instance_name = name\n        self.bestFit = True\n        self.controller = None\n        self.setMouseTracking(False)\n\n    def __repr__(self):\n        return f\"{self._instance_name}\"\n\n    def connectMouseSignals(self):\n        if not self._dragConnection:\n            self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)\n        if not self._wheelConnection:\n            self._wheelConnection = self.mouseWheeled.connect(self.controller.scaleImagesBy)\n\n    def disconnectMouseSignals(self):\n        if self._dragConnection:\n            self.mouseDragged.disconnect()\n            self._dragConnection = None\n        if self._wheelConnection:\n            self.mouseWheeled.disconnect()\n            self._wheelConnection = None\n\n    def paintEvent(self, event):\n        painter = QPainter(self)\n        painter.translate(self.rect().center())\n        painter.scale(self.current_scale, self.current_scale)\n        painter.translate(self._mousePanningDelta)\n        painter.drawPixmap(self._rect.topLeft(), self._pixmap)\n\n    def resetCenter(self):\n        \"\"\"Resets origin\"\"\"\n        # Make sure we are not still panning around\n        self._mousePanningDelta = QPointF()\n        self.update()\n\n    def changeEvent(self, event):\n        if event.type() == QEvent.EnabledChange:\n            if self.isEnabled():\n                self.connectMouseSignals()\n                return\n            self.disconnectMouseSignals()\n\n    def contextMenuEvent(self, event):\n        \"\"\"Block parent's (main window) context menu on right click.\"\"\"\n        event.accept()\n\n    def mousePressEvent(self, event):\n        if self.bestFit or not self.isEnabled():\n            event.ignore()\n            return\n        if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):\n            self._drag = True\n        else:\n            self._drag = False\n            event.ignore()\n            return\n\n        self._lastMouseClickPoint = event.pos()\n        self._app.setOverrideCursor(Qt.ClosedHandCursor)\n        self.setMouseTracking(True)\n        event.accept()\n\n    def mouseMoveEvent(self, event):\n        if self.bestFit or not self.isEnabled():\n            event.ignore()\n            return\n\n        self._mousePanningDelta += (event.pos() - self._lastMouseClickPoint) * 1.0 / self.current_scale\n        self._lastMouseClickPoint = event.pos()\n        if self._drag:\n            self.mouseDragged.emit(self._mousePanningDelta)\n            self.update()\n\n    def mouseReleaseEvent(self, event):\n        if self.bestFit or not self.isEnabled():\n            event.ignore()\n            return\n        # if event.button() == Qt.LeftButton:\n        self._drag = False\n\n        self._app.restoreOverrideCursor()\n        self.setMouseTracking(False)\n\n    def wheelEvent(self, event):\n        if self.bestFit or not self.controller.same_dimensions or not self.isEnabled():\n            event.ignore()\n            return\n\n        if event.angleDelta().y() > 0:\n            if self.current_scale > MAX_SCALE:\n                return\n            self.mouseWheeled.emit(1.25)  # zoom-in\n        else:\n            if self.current_scale < MIN_SCALE:\n                return\n            self.mouseWheeled.emit(0.8)  # zoom-out\n\n    def setImage(self, pixmap):\n        if pixmap.isNull():\n            if not self._pixmap.isNull():\n                self._pixmap = pixmap\n            self.disconnectMouseSignals()\n            self.setEnabled(False)\n            self.update()\n            return\n        elif not self.isEnabled():\n            self.setEnabled(True)\n            self.connectMouseSignals()\n        self._pixmap = pixmap\n\n    def centerViewAndUpdate(self):\n        self._rect = self._pixmap.rect()\n        self._rect.translate(-self._rect.center())\n        self.update()\n\n    def shouldBeActive(self):\n        return True if not self.pixmap.isNull() else False\n\n    def scaleBy(self, factor):\n        self.current_scale *= factor\n        self.update()\n\n    def scaleAt(self, scale):\n        self.current_scale = scale\n        self.update()\n\n    def sizeHint(self):\n        return QSize(400, 400)\n\n    @pyqtSlot()\n    def scaleToNormalSize(self):\n        \"\"\"Called when the pixmap is set back to original size.\"\"\"\n        self.current_scale = 1.0\n        self.update()\n\n    @pyqtSlot(QPointF)\n    def onDraggedMouse(self, delta):\n        self._mousePanningDelta = delta\n        self.update()\n\n\nclass ScalablePixmap(QWidget):\n    \"\"\"Container for a pixmap that scales up very fast, used in ScrollAreaImageViewer.\"\"\"\n\n    def __init__(self, parent):\n        super().__init__(parent)\n        self._pixmap = QPixmap()\n        self.current_scale = 1.0\n\n    def paintEvent(self, event):\n        painter = QPainter(self)\n        painter.scale(self.current_scale, self.current_scale)\n        # painter.drawPixmap(self.rect().topLeft(), self._pixmap)\n        # should be the same as:\n        painter.drawPixmap(0, 0, self._pixmap)\n\n    def sizeHint(self):\n        return self._pixmap.size() * self.current_scale\n\n    def minimumSizeHint(self):\n        return self.sizeHint()\n\n\nclass ScrollAreaImageViewer(QScrollArea):\n    \"\"\"Implementation using a pixmap container in a simple scroll area.\"\"\"\n\n    mouseDragged = pyqtSignal(QPoint)\n    mouseWheeled = pyqtSignal(float, QPointF)\n\n    def __init__(self, parent, name=\"\"):\n        super().__init__(parent)\n        self._parent = parent\n        self._app = QApplication\n        self._pixmap = QPixmap()\n        self._scaledpixmap = None\n        self._rect = QRectF()\n        self._lastMouseClickPoint = QPointF()\n        self._mousePanningDelta = QPoint()\n        self.current_scale = 1.0\n        self._drag = False\n        self._dragConnection = None\n        self._wheelConnection = None\n        self._instance_name = name\n        self.prefs = parent.app.prefs\n        self.bestFit = True\n        self.controller = None\n        self.label = ScalablePixmap(self)\n        # This is to avoid sending signals twice on scrollbar updates\n        self.ignore_signal = False\n        self.setBackgroundRole(QPalette.Dark)\n        self.setWidgetResizable(False)\n        self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)\n        self.setAlignment(Qt.AlignCenter)\n        self._verticalScrollBar = self.verticalScrollBar()\n        self._horizontalScrollBar = self.horizontalScrollBar()\n\n        if self.prefs.details_dialog_viewers_show_scrollbars:\n            self.toggleScrollBars()\n        else:\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n\n        self.setWidget(self.label)\n        self.setVisible(True)\n\n    def __repr__(self):\n        return f\"{self._instance_name}\"\n\n    def toggleScrollBars(self, force_on=False):\n        if not self.prefs.details_dialog_viewers_show_scrollbars:\n            return\n        # Ensure that it's off on the first run\n        if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded:\n            if force_on:\n                return\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        else:\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n\n    def connectMouseSignals(self):\n        if not self._dragConnection:\n            self._dragConnection = self.mouseDragged.connect(self.controller.onDraggedMouse)\n        if not self._wheelConnection:\n            self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)\n\n    def disconnectMouseSignals(self):\n        if self._dragConnection:\n            self.mouseDragged.disconnect()\n            self._dragConnection = None\n        if self._wheelConnection:\n            self.mouseWheeled.disconnect()\n            self._wheelConnection = None\n\n    def connectScrollBars(self):\n        \"\"\"Only call once controller is connected.\"\"\"\n        # Cyclic connections are handled by Qt\n        self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)\n        self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)\n\n    def contextMenuEvent(self, event):\n        \"\"\"Block parent's (main window) context menu on right click.\"\"\"\n        # Even though we don't have a context menu right now, and the default\n        # contextMenuPolicy is DefaultContextMenu, we leverage that handler to\n        # avoid raising the Result window's Actions menu\n        event.accept()\n\n    def mousePressEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):\n            self._drag = True\n        else:\n            self._drag = False\n            event.ignore()\n            return\n        self._lastMouseClickPoint = event.pos()\n        self._app.setOverrideCursor(Qt.ClosedHandCursor)\n        self.setMouseTracking(True)\n        super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        if self._drag:\n            delta = event.pos() - self._lastMouseClickPoint\n            self._lastMouseClickPoint = event.pos()\n            self.mouseDragged.emit(delta)\n        super().mouseMoveEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        self._drag = False\n        self._app.restoreOverrideCursor()\n        self.setMouseTracking(False)\n        super().mouseReleaseEvent(event)\n\n    def wheelEvent(self, event):\n        if self.bestFit or not self.controller.same_dimensions:\n            event.ignore()\n            return\n        old_scale = self.current_scale\n        if event.angleDelta().y() > 0:  # zoom-in\n            if old_scale < MAX_SCALE:\n                self.current_scale *= 1.25\n        else:\n            if old_scale > MIN_SCALE:  # zoom-out\n                self.current_scale *= 0.8\n        if old_scale == self.current_scale:\n            return\n\n        delta_to_pos = (event.position() / old_scale) - (self.label.pos() / old_scale)\n        delta = (delta_to_pos * self.current_scale) - (delta_to_pos * old_scale)\n        self.mouseWheeled.emit(self.current_scale, delta)\n\n    def setImage(self, pixmap):\n        self._pixmap = pixmap\n        self.label._pixmap = pixmap\n        self.label.update()\n        self.label.adjustSize()\n        if pixmap.isNull():\n            self.setEnabled(False)\n            self.disconnectMouseSignals()\n        elif not self.isEnabled():\n            self.setEnabled(True)\n            self.connectMouseSignals()\n\n    def centerViewAndUpdate(self):\n        self._rect = self.label.rect()\n        self.label.rect().translate(-self._rect.center())\n        self.label.current_scale = self.current_scale\n        self.label.update()\n        # self.viewport().update()\n\n    def setCachedPixmap(self):\n        \"\"\"In case we have changed the cached pixmap, reset it.\"\"\"\n        self.label._pixmap = self._pixmap\n        self.label.update()\n\n    def shouldBeActive(self):\n        return True if not self.pixmap.isNull() else False\n\n    def scaleBy(self, factor):\n        self.current_scale *= factor\n        # factor has to be either 1.25 or 0.8 here\n        self.label.resize(self.label.size().__imul__(factor))\n        self.label.current_scale = self.current_scale\n        self.label.update()\n\n    def scaleAt(self, scale):\n        self.current_scale = scale\n        self.label.resize(self._pixmap.size().__imul__(scale))\n        self.label.current_scale = scale\n        self.label.update()\n        # self.label.adjustSize()\n\n    def adjustScrollBarsFactor(self, factor):\n        \"\"\"After scaling, no mouse position, default to center.\"\"\"\n        # scrollBar.setMaximum(scrollBar.maximum() - scrollBar.minimum() + scrollBar.pageStep())\n        self._horizontalScrollBar.setValue(\n            int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))\n        )\n        self._verticalScrollBar.setValue(\n            int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))\n        )\n\n    def adjustScrollBarsScaled(self, delta):\n        \"\"\"After scaling with the mouse, update relative to mouse position.\"\"\"\n        self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.value() + delta.x()))\n        self._verticalScrollBar.setValue(int(self._verticalScrollBar.value() + delta.y()))\n\n    def adjustScrollBarsAuto(self):\n        \"\"\"After panning, update accordingly.\"\"\"\n        self.horizontalScrollBar().setValue(int(self.horizontalScrollBar().value() - self._mousePanningDelta.x()))\n        self.verticalScrollBar().setValue(int(self.verticalScrollBar().value() - self._mousePanningDelta.y()))\n\n    def adjustScrollBarCentered(self):\n        \"\"\"Just center in the middle.\"\"\"\n        self._horizontalScrollBar.setValue(int(self._horizontalScrollBar.maximum() / 2))\n        self._verticalScrollBar.setValue(int(self._verticalScrollBar.maximum() / 2))\n\n    def resetCenter(self):\n        \"\"\"Resets origin\"\"\"\n        self._mousePanningDelta = QPoint()\n        self.current_scale = 1.0\n        # self.scaleAt(1.0)\n\n    def setCenter(self, point):\n        self._lastMouseClickPoint = point\n\n    def sizeHint(self):\n        return self.viewport().rect().size()\n\n    @pyqtSlot()\n    def scaleToNormalSize(self):\n        \"\"\"Called when the pixmap is set back to original size.\"\"\"\n        self.scaleAt(1.0)\n        self.ensureWidgetVisible(self.label)  # needed for centering\n        self.toggleScrollBars(True)\n\n    @pyqtSlot(QPoint)\n    def onDraggedMouse(self, delta):\n        \"\"\"Update position from mouse delta sent by the other panel.\"\"\"\n        self._mousePanningDelta = delta\n        # Signal from scrollbars had already synced the values here\n        self.adjustScrollBarsAuto()\n\n\nclass GraphicsViewViewer(QGraphicsView):\n    \"\"\"Re-Implementation a full-fledged GraphicsView but is a bit buggy.\"\"\"\n\n    mouseDragged = pyqtSignal()\n    mouseWheeled = pyqtSignal(float, QPointF)\n\n    def __init__(self, parent, name=\"\"):\n        super().__init__(parent)\n        self._parent = parent\n        self._app = QApplication\n        self._pixmap = QPixmap()\n        self._scaledpixmap = None\n        self._rect = QRectF()\n        self._lastMouseClickPoint = QPointF()\n        self._mousePanningDelta = QPointF()\n        self._scaleFactor = 1.3\n        self.zoomInFactor = self._scaleFactor\n        self.zoomOutFactor = 1.0 / self._scaleFactor\n        self.current_scale = 1.0\n        self._drag = False\n        self._dragConnection = None\n        self._wheelConnection = None\n        self._instance_name = name\n        self.prefs = parent.app.prefs\n        self.bestFit = True\n        self.controller = None\n        self._centerPoint = QPointF()\n        self.centerOn(self._centerPoint)\n        self.other_viewer = None\n        # specific to this class\n        self._scene = QGraphicsScene()\n        self._scene.setBackgroundBrush(Qt.black)\n        self._item = QGraphicsPixmapItem()\n        self.setScene(self._scene)\n        self._scene.addItem(self._item)\n        self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)\n        self._horizontalScrollBar = self.horizontalScrollBar()\n        self._verticalScrollBar = self.verticalScrollBar()\n        self.ignore_signal = False\n\n        if self.prefs.details_dialog_viewers_show_scrollbars:\n            self.toggleScrollBars()\n        else:\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n\n        self.setResizeAnchor(QGraphicsView.AnchorViewCenter)\n        self.setAlignment(Qt.AlignCenter)\n        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)\n        self.setMouseTracking(True)\n\n    def connectMouseSignals(self):\n        if not self._dragConnection:\n            self._dragConnection = self.mouseDragged.connect(self.controller.syncCenters)\n        if not self._wheelConnection:\n            self._wheelConnection = self.mouseWheeled.connect(self.controller.onMouseWheel)\n\n    def disconnectMouseSignals(self):\n        if self._dragConnection:\n            self.mouseDragged.disconnect()\n            self._dragConnection = None\n        if self._wheelConnection:\n            self.mouseWheeled.disconnect()\n            self._wheelConnection = None\n\n    def connectScrollBars(self):\n        \"\"\"Only call once controller is connected.\"\"\"\n        # Cyclic connections are handled by Qt\n        self._verticalScrollBar.valueChanged.connect(self.controller.onVScrollBarChanged, Qt.UniqueConnection)\n        self._horizontalScrollBar.valueChanged.connect(self.controller.onHScrollBarChanged, Qt.UniqueConnection)\n\n    def toggleScrollBars(self, force_on=False):\n        if not self.prefs.details_dialog_viewers_show_scrollbars:\n            return\n        # Ensure that it's off on the first run\n        if self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded:\n            if force_on:\n                return\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)\n        else:\n            self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)\n\n    def contextMenuEvent(self, event):\n        \"\"\"Block parent's (main window) context menu on right click.\"\"\"\n        event.accept()\n\n    def mousePressEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        if event.button() & (Qt.LeftButton | Qt.MidButton | Qt.RightButton):\n            self._drag = True\n        else:\n            self._drag = False\n            event.ignore()\n            return\n        self._lastMouseClickPoint = event.pos()\n        self._app.setOverrideCursor(Qt.ClosedHandCursor)\n        self.setMouseTracking(True)\n        # We need to propagate to scrollbars, so we send back up\n        super().mousePressEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        self._drag = False\n        self._app.restoreOverrideCursor()\n        self.setMouseTracking(False)\n        self.updateCenterPoint()\n        super().mouseReleaseEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self.bestFit:\n            event.ignore()\n            return\n        if self._drag:\n            self._lastMouseClickPoint = event.pos()\n            # We can simply rely on the scrollbar updating each other here\n            # self.mouseDragged.emit()\n            self.updateCenterPoint()\n            super().mouseMoveEvent(event)\n\n    def updateCenterPoint(self):\n        self._centerPoint = self.mapToScene(self.rect().center())\n\n    def wheelEvent(self, event):\n        if self.bestFit or MIN_SCALE > self.current_scale > MAX_SCALE or not self.controller.same_dimensions:\n            event.ignore()\n            return\n        point_before_scale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))\n        # Get the original screen centerpoint\n        screen_center = QPointF(self.mapToScene(self.rect().center()))\n        if event.angleDelta().y() > 0:\n            factor = self.zoomInFactor\n        else:\n            factor = self.zoomOutFactor\n        # Avoid scrollbars conflict:\n        self.other_viewer.ignore_signal = True\n        self.scaleBy(factor)\n        point_after_scale = QPointF(self.mapToScene(self.mapFromGlobal(QCursor.pos())))\n        # Get the offset of how the screen moved\n        offset = point_before_scale - point_after_scale\n        # Adjust to the new center for correct zooming\n        new_center = screen_center + offset\n        self.setCenter(new_center)\n        self.mouseWheeled.emit(factor, new_center)\n        self.other_viewer.ignore_signal = False\n\n    def setImage(self, pixmap):\n        if pixmap.isNull():\n            self.ignore_signal = True\n        elif self.ignore_signal:\n            self.ignore_signal = False\n        self._pixmap = pixmap\n        self._item.setPixmap(pixmap)\n        self.translate(1, 1)\n\n    def centerViewAndUpdate(self):\n        # Called from the base controller for Normal Size\n        pass\n\n    def setCenter(self, point):\n        self._centerPoint = point\n        self.centerOn(self._centerPoint)\n\n    def resetCenter(self):\n        \"\"\"Resets origin\"\"\"\n        self._mousePanningDelta = QPointF()\n        self.current_scale = 1.0\n\n    def setNewCenter(self, position):\n        self._centerPoint = position\n        self.centerOn(self._centerPoint)\n\n    def setCachedPixmap(self):\n        \"\"\"In case we have changed the cached pixmap, reset it.\"\"\"\n        self._item.setPixmap(self._pixmap)\n        self._item.update()\n\n    def scaleAt(self, scale):\n        if scale == 1.0:\n            self.resetScale()\n        # self.setTransform( QTransform() )\n        self.scale(scale, scale)\n\n    def getScale(self):\n        return self.transform().m22()\n\n    def scaleBy(self, factor):\n        self.current_scale *= factor\n        super().scale(factor, factor)\n\n    def resetScale(self):\n        # self.setTransform( QTransform() )\n        self.resetTransform()  # probably same as above\n        self.setCenter(self.scene().sceneRect().center())\n\n    def fitScale(self):\n        self.bestFit = True\n        super().fitInView(self._scene.sceneRect(), Qt.KeepAspectRatio)\n        self.setNewCenter(self._scene.sceneRect().center())\n\n    @pyqtSlot()\n    def scaleToNormalSize(self):\n        \"\"\"Called when the pixmap is set back to original size.\"\"\"\n        self.bestFit = False\n        self.scaleAt(1.0)\n        self.toggleScrollBars(True)\n        self.update()\n\n    def adjustScrollBarsScaled(self, delta):\n        \"\"\"After scaling with the mouse, update relative to mouse position.\"\"\"\n        self._horizontalScrollBar.setValue(self._horizontalScrollBar.value() + delta.x())\n        self._verticalScrollBar.setValue(self._verticalScrollBar.value() + delta.y())\n\n    def sizeHint(self):\n        return self.viewport().rect().size()\n\n    def adjustScrollBarsFactor(self, factor):\n        \"\"\"After scaling, no mouse position, default to center.\"\"\"\n        self._horizontalScrollBar.setValue(\n            int(factor * self._horizontalScrollBar.value() + ((factor - 1) * self._horizontalScrollBar.pageStep() / 2))\n        )\n        self._verticalScrollBar.setValue(\n            int(factor * self._verticalScrollBar.value() + ((factor - 1) * self._verticalScrollBar.pageStep() / 2))\n        )\n\n    def adjustScrollBarsAuto(self):\n        \"\"\"After panning, update accordingly.\"\"\"\n        self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - self._mousePanningDelta.x())\n        self.verticalScrollBar().setValue(self.verticalScrollBar().value() - self._mousePanningDelta.y())\n"
  },
  {
    "path": "qt/pe/modules/block.c",
    "content": "/* Created By: Virgil Dupras\n * Created On: 2010-01-31\n * Copyright 2014 Hardcoded Software (http://www.hardcoded.net)\n *\n * This software is licensed under the \"BSD\" License as described in the\n *\"LICENSE\" file, which should be included with this package. The terms are also\n *available at http://www.hardcoded.net/licenses/bsd_license\n **/\n\n#define PY_SSIZE_T_CLEAN\n#include \"Python.h\"\n\n/* It seems like MS VC defines min/max already */\n#ifndef _MSC_VER\nstatic int max(int a, int b) { return b > a ? b : a; }\n\nstatic int min(int a, int b) { return b < a ? b : a; }\n#endif\n\nstatic PyObject *getblock(PyObject *image, int width, int height) {\n  int pixel_count, red, green, blue, bytes_per_line;\n  PyObject *pred, *pgreen, *pblue;\n  PyObject *result;\n\n  red = green = blue = 0;\n  pixel_count = width * height;\n  if (pixel_count) {\n    PyObject *sipptr, *bits_capsule, *pi;\n    char *s;\n    int i;\n\n    pi = PyObject_CallMethod(image, \"bytesPerLine\", NULL);\n    bytes_per_line = PyLong_AsLong(pi);\n    Py_DECREF(pi);\n\n    sipptr = PyObject_CallMethod(image, \"bits\", NULL);\n    bits_capsule = PyObject_CallMethod(sipptr, \"ascapsule\", NULL);\n    Py_DECREF(sipptr);\n    s = (char *)PyCapsule_GetPointer(bits_capsule, NULL);\n    Py_DECREF(bits_capsule);\n    /* Qt aligns all its lines on 32bit, which means that if the number of bytes\n     *per line for image is not divisible by 4, there's going to be crap\n     *inserted in \"s\" We have to take this into account when calculating offsets\n     **/\n    for (i = 0; i < height; i++) {\n      int j;\n      for (j = 0; j < width; j++) {\n        int offset;\n        unsigned char r, g, b;\n\n        offset = i * bytes_per_line + j * 3;\n        r = s[offset];\n        g = s[offset + 1];\n        b = s[offset + 2];\n        red += r;\n        green += g;\n        blue += b;\n      }\n    }\n\n    red /= pixel_count;\n    green /= pixel_count;\n    blue /= pixel_count;\n  }\n\n  pred = PyLong_FromLong(red);\n  pgreen = PyLong_FromLong(green);\n  pblue = PyLong_FromLong(blue);\n  result = PyTuple_Pack(3, pred, pgreen, pblue);\n  Py_DECREF(pred);\n  Py_DECREF(pgreen);\n  Py_DECREF(pblue);\n\n  return result;\n}\n\n/* block_getblocks(QImage image, int block_count_per_side) -> [(int r, int g,\n *int b), ...]\n *\n * Compute blocks out of `image`. Note the use of min/max when compes the time\n *of computing widths and heights and positions. This is to cover the case where\n *the width or height of the image is smaller than `block_count_per_side`. In\n *these cases, blocks will be, of course, 1 pixel big. But also, because all\n *compared block lists are required to be of the same size, any block that has\n * no pixel to be assigned to will simply be assigned the last pixel. This is\n *why we have min(..., height-block_height-1) and stuff like that.\n **/\nstatic PyObject *block_getblocks(PyObject *self, PyObject *args) {\n  int block_count_per_side, width, height, block_width, block_height, ih;\n  PyObject *image;\n  PyObject *pi;\n  PyObject *result;\n\n  if (!PyArg_ParseTuple(args, \"Oi\", &image, &block_count_per_side)) {\n    return NULL;\n  }\n\n  pi = PyObject_CallMethod(image, \"width\", NULL);\n  width = PyLong_AsLong(pi);\n  Py_DECREF(pi);\n  pi = PyObject_CallMethod(image, \"height\", NULL);\n  height = PyLong_AsLong(pi);\n  Py_DECREF(pi);\n\n  if (!(width && height)) {\n    return PyList_New(0);\n  }\n\n  block_width = max(width / block_count_per_side, 1);\n  block_height = max(height / block_count_per_side, 1);\n\n  result = PyList_New((Py_ssize_t)block_count_per_side * block_count_per_side);\n  if (result == NULL) {\n    return NULL;\n  }\n\n  for (ih = 0; ih < block_count_per_side; ih++) {\n    int top, iw;\n    top = min(ih * block_height, height - block_height - 1);\n    for (iw = 0; iw < block_count_per_side; iw++) {\n      int left;\n      PyObject *pcrop;\n      PyObject *pblock;\n\n      left = min(iw * block_width, width - block_width - 1);\n      pcrop = PyObject_CallMethod(image, \"copy\", \"iiii\", left, top, block_width,\n                                  block_height);\n      if (pcrop == NULL) {\n        Py_DECREF(result);\n        return NULL;\n      }\n      pblock = getblock(pcrop, block_width, block_height);\n      Py_DECREF(pcrop);\n      if (pblock == NULL) {\n        Py_DECREF(result);\n        return NULL;\n      }\n      PyList_SET_ITEM(result, ih * block_count_per_side + iw, pblock);\n    }\n  }\n\n  return result;\n}\n\nstatic PyMethodDef BlockMethods[] = {\n    {\"getblocks\", block_getblocks, METH_VARARGS, \"\"},\n    {NULL, NULL, 0, NULL} /* Sentinel */\n};\n\nstatic struct PyModuleDef BlockDef = {PyModuleDef_HEAD_INIT,\n                                      \"_block_qt\",\n                                      NULL,\n                                      -1,\n                                      BlockMethods,\n                                      NULL,\n                                      NULL,\n                                      NULL,\n                                      NULL};\n\nPyObject *PyInit__block_qt(void) {\n  PyObject *m = PyModule_Create(&BlockDef);\n  if (m == NULL) {\n    return NULL;\n  }\n  return m;\n}\n"
  },
  {
    "path": "qt/pe/photo.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\n\nfrom PyQt5.QtGui import QImage, QImageReader, QTransform\n\nfrom core.pe.photo import Photo as PhotoBase\n\nfrom qt.pe.block import getblocks\n\n\nclass File(PhotoBase):\n    def _plat_get_dimensions(self):\n        try:\n            ir = QImageReader(str(self.path))\n            size = ir.size()\n            if size.isValid():\n                return (size.width(), size.height())\n            else:\n                return (0, 0)\n        except OSError:\n            logging.warning(\"Could not read image '%s'\", str(self.path))\n            return (0, 0)\n\n    def _plat_get_blocks(self, block_count_per_side, orientation):\n        image = QImage(str(self.path))\n        image = image.convertToFormat(QImage.Format_RGB888)\n        if not isinstance(orientation, int):\n            logging.warning(\n                \"Orientation for file '%s' was a %s '%s', not an int.\",\n                str(self.path),\n                type(orientation),\n                orientation,\n            )\n            try:\n                orientation = int(orientation)\n            except Exception as e:\n                logging.exception(\n                    \"Skipping transformation because could not convert %s to int. %s\",\n                    type(orientation),\n                    e,\n                )\n                return getblocks(image, block_count_per_side)\n        # MYSTERY TO SOLVE: For reasons I cannot explain, orientations 5 and 7 don't work for\n        # duplicate scanning. The transforms seems to work fine (if I try to save the image after\n        # the transform, we see that the image has been correctly flipped and rotated), but the\n        # analysis part yields wrong blocks. I spent enought time with this feature, so I'll leave\n        # like that for now. (by the way, orientations 5 and 7 work fine under Cocoa)\n        if 2 <= orientation <= 8:\n            t = QTransform()\n            if orientation == 2:\n                t.scale(-1, 1)\n            elif orientation == 3:\n                t.rotate(180)\n            elif orientation == 4:\n                t.scale(1, -1)\n            elif orientation == 5:\n                t.scale(-1, 1)\n                t.rotate(90)\n            elif orientation == 6:\n                t.rotate(90)\n            elif orientation == 7:\n                t.scale(-1, 1)\n                t.rotate(270)\n            elif orientation == 8:\n                t.rotate(270)\n            image = image.transformed(t)\n        return getblocks(image, block_count_per_side)\n"
  },
  {
    "path": "qt/pe/preferences_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\nfrom hscommon.trans import trget\nfrom hscommon.plat import ISLINUX\nfrom core.scanner import ScanType\nfrom core.app import AppMode\n\nfrom qt.preferences_dialog import PreferencesDialogBase\n\ntr = trget(\"ui\")\n\n\nclass PreferencesDialog(PreferencesDialogBase):\n    def _setupPreferenceWidgets(self):\n        self._setupFilterHardnessBox()\n        self.widgetsVLayout.addLayout(self.filterHardnessHLayout)\n        self._setupAddCheckbox(\"matchScaledBox\", tr(\"Match pictures of different dimensions\"))\n        self.widgetsVLayout.addWidget(self.matchScaledBox)\n        self._setupAddCheckbox(\"matchRotatedBox\", tr(\"Match pictures of different rotations\"))\n        self.widgetsVLayout.addWidget(self.matchRotatedBox)\n        self._setupAddCheckbox(\"mixFileKindBox\", tr(\"Can mix file kind\"))\n        self.widgetsVLayout.addWidget(self.mixFileKindBox)\n        self._setupAddCheckbox(\"useRegexpBox\", tr(\"Use regular expressions when filtering\"))\n        self.widgetsVLayout.addWidget(self.useRegexpBox)\n        self._setupAddCheckbox(\"removeEmptyFoldersBox\", tr(\"Remove empty folders on delete or move\"))\n        self.widgetsVLayout.addWidget(self.removeEmptyFoldersBox)\n        self._setupAddCheckbox(\n            \"ignoreHardlinkMatches\",\n            tr(\"Ignore duplicates hardlinking to the same file\"),\n        )\n        self.widgetsVLayout.addWidget(self.ignoreHardlinkMatches)\n\n        self._setupBottomPart()\n\n    def _setupDisplayPage(self):\n        super()._setupDisplayPage()\n        self._setupAddCheckbox(\"details_dialog_override_theme_icons\", tr(\"Override theme icons in viewer toolbar\"))\n        self.details_dialog_override_theme_icons.setToolTip(\n            tr(\"Use our own internal icons instead of those provided by the theme engine\")\n        )\n        # Prevent changing this on platforms where themes are unpredictable\n        self.details_dialog_override_theme_icons.setEnabled(False if not ISLINUX else True)\n        # Insert this right after the vertical title bar option\n        index = self.details_groupbox_layout.indexOf(self.details_dialog_vertical_titlebar)\n        self.details_groupbox_layout.insertWidget(index + 1, self.details_dialog_override_theme_icons)\n        self._setupAddCheckbox(\"details_dialog_viewers_show_scrollbars\", tr(\"Show scrollbars in image viewers\"))\n        self.details_dialog_viewers_show_scrollbars.setToolTip(\n            tr(\n                \"When the image displayed doesn't fit the viewport, \\\nshow scrollbars to span the view around\"\n            )\n        )\n        self.details_groupbox_layout.insertWidget(index + 2, self.details_dialog_viewers_show_scrollbars)\n\n    def _load(self, prefs, setchecked, section):\n        setchecked(self.matchScaledBox, prefs.match_scaled)\n        setchecked(self.matchRotatedBox, prefs.match_rotated)\n\n        # Update UI state based on selected scan type\n        scan_type = prefs.get_scan_type(AppMode.PICTURE)\n        fuzzy_scan = scan_type == ScanType.FUZZYBLOCK\n        self.filterHardnessSlider.setEnabled(fuzzy_scan)\n        setchecked(self.details_dialog_override_theme_icons, prefs.details_dialog_override_theme_icons)\n        setchecked(self.details_dialog_viewers_show_scrollbars, prefs.details_dialog_viewers_show_scrollbars)\n\n    def _save(self, prefs, ischecked):\n        prefs.match_scaled = ischecked(self.matchScaledBox)\n        prefs.match_rotated = ischecked(self.matchRotatedBox)\n        prefs.details_dialog_override_theme_icons = ischecked(self.details_dialog_override_theme_icons)\n        prefs.details_dialog_viewers_show_scrollbars = ischecked(self.details_dialog_viewers_show_scrollbars)\n"
  },
  {
    "path": "qt/pe/results_model.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.column import Column\nfrom qt.results_model import ResultsModel as ResultsModelBase\n\n\nclass ResultsModel(ResultsModelBase):\n    COLUMNS = [\n        Column(\"marked\", default_width=30),\n        Column(\"name\", default_width=200),\n        Column(\"folder_path\", default_width=180),\n        Column(\"size\", default_width=60),\n        Column(\"extension\", default_width=40),\n        Column(\"dimensions\", default_width=100),\n        Column(\"exif_timestamp\", default_width=120),\n        Column(\"mtime\", default_width=120),\n        Column(\"percentage\", default_width=60),\n        Column(\"dupe_count\", default_width=80),\n    ]\n"
  },
  {
    "path": "qt/platform.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport os.path as op\nfrom hscommon.plat import ISWINDOWS, ISOSX, ISLINUX\n\nif op.exists(__file__):\n    # We want to get the absolute path or our root folder. We know that in that folder we're\n    # inside qt/, so we just go back one level.\n    BASE_PATH = op.abspath(op.join(op.dirname(__file__), \"..\"))\nelse:\n    # Should be a frozen environment\n    if ISOSX:\n        BASE_PATH = op.abspath(op.join(op.dirname(__file__), \"..\", \"..\", \"Resources\"))\n    else:\n        # For others our base path is ''.\n        BASE_PATH = \"\"\nHELP_PATH = op.join(BASE_PATH, \"help\", \"en\")\n\nif ISWINDOWS:\n    INITIAL_FOLDER_IN_DIALOGS = \"C:\\\\\"\nelif ISOSX:\n    INITIAL_FOLDER_IN_DIALOGS = \"/\"\nelif ISLINUX:\n    INITIAL_FOLDER_IN_DIALOGS = \"/\"\nelse:\n    # unsupported platform, however '/' is a good guess for a path which is available\n    INITIAL_FOLDER_IN_DIALOGS = \"/\"\n"
  },
  {
    "path": "qt/preferences.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtWidgets import QApplication, QDockWidget\nfrom PyQt5.QtCore import Qt, QRect, QObject, pyqtSignal\nfrom PyQt5.QtGui import QColor\n\nfrom hscommon import trans\nfrom hscommon.plat import ISLINUX\nfrom core.app import AppMode\nfrom core.scanner import ScanType\nfrom hscommon.util import tryint\nfrom qt.util import create_qsettings\n\n\ndef get_langnames():\n    tr = trans.trget(\"ui\")\n    return {\n        \"cs\": tr(\"Czech\"),\n        \"de\": tr(\"German\"),\n        \"el\": tr(\"Greek\"),\n        \"en\": tr(\"English\"),\n        \"es\": tr(\"Spanish\"),\n        \"fr\": tr(\"French\"),\n        \"hy\": tr(\"Armenian\"),\n        \"it\": tr(\"Italian\"),\n        \"ja\": tr(\"Japanese\"),\n        \"ko\": tr(\"Korean\"),\n        \"ms\": tr(\"Malay\"),\n        \"nl\": tr(\"Dutch\"),\n        \"pl_PL\": tr(\"Polish\"),\n        \"pt_BR\": tr(\"Brazilian\"),\n        \"ru\": tr(\"Russian\"),\n        \"tr\": tr(\"Turkish\"),\n        \"uk\": tr(\"Ukrainian\"),\n        \"vi\": tr(\"Vietnamese\"),\n        \"zh_CN\": tr(\"Chinese (Simplified)\"),\n    }\n\n\ndef _normalize_for_serialization(v):\n    # QSettings doesn't consider set/tuple as \"native\" typs for serialization, so if we don't\n    # change them into a list, we get a weird serialized QVariant value which isn't a very\n    # \"portable\" value.\n    if isinstance(v, (set, tuple)):\n        v = list(v)\n    if isinstance(v, list):\n        v = [_normalize_for_serialization(item) for item in v]\n    return v\n\n\ndef _adjust_after_deserialization(v):\n    # In some cases, when reading from prefs, we end up with strings that are supposed to be\n    # bool or int. Convert these.\n    if isinstance(v, list):\n        return [_adjust_after_deserialization(sub) for sub in v]\n    if isinstance(v, str):\n        # might be bool or int, try them\n        if v == \"true\":\n            return True\n        elif v == \"false\":\n            return False\n        else:\n            return tryint(v, v)\n    return v\n\n\nclass PreferencesBase(QObject):\n    prefsChanged = pyqtSignal()\n\n    def __init__(self):\n        QObject.__init__(self)\n        self.reset()\n        self._settings = create_qsettings()\n\n    def _load_values(self, settings):\n        # Implemented in subclasses\n        pass\n\n    def get_rect(self, name, default=None):\n        r = self.get_value(name, default)\n        if r is not None:\n            return QRect(*r)\n        else:\n            return None\n\n    def get_value(self, name, default=None):\n        if self._settings.contains(name):\n            result = _adjust_after_deserialization(self._settings.value(name))\n            if result is not None:\n                return result\n            else:\n                # If result is None, but still present in self._settings, it usually means a value\n                # like \"@Invalid\".\n                return default\n        else:\n            return default\n\n    def load(self):\n        self.reset()\n        self._load_values(self._settings)\n\n    def reset(self):\n        # Implemented in subclasses\n        pass\n\n    def _save_values(self, settings):\n        # Implemented in subclasses\n        pass\n\n    def save(self):\n        self._save_values(self._settings)\n        self._settings.sync()\n\n    def set_rect(self, name, r):\n        # About QRect conversion:\n        # I think Qt supports putting basic structures like QRect directly in QSettings, but I prefer not\n        # to rely on it and stay with generic structures.\n        if isinstance(r, QRect):\n            rect_as_list = [r.x(), r.y(), r.width(), r.height()]\n            self.set_value(name, rect_as_list)\n\n    def set_value(self, name, value):\n        self._settings.setValue(name, _normalize_for_serialization(value))\n\n    def saveGeometry(self, name, widget):\n        # We save geometry under a 7-sized int array: first item is a flag\n        # for whether the widget is maximized, second item is a flag for whether\n        # the widget is docked, third item is a Qt::DockWidgetArea enum value,\n        # and the other 4 are (x, y, w, h).\n        m = 1 if widget.isMaximized() else 0\n        d = 1 if isinstance(widget, QDockWidget) and not widget.isFloating() else 0\n        area = widget.parent.dockWidgetArea(widget) if d else 0\n        r = widget.geometry()\n        rect_as_list = [r.x(), r.y(), r.width(), r.height()]\n        self.set_value(name, [m, d, area] + rect_as_list)\n\n    def restoreGeometry(self, name, widget):\n        geometry = self.get_value(name)\n        if geometry and len(geometry) == 7:\n            m, d, area, x, y, w, h = geometry\n            if m:\n                widget.setWindowState(Qt.WindowMaximized)\n            else:\n                r = QRect(x, y, w, h)\n                widget.setGeometry(r)\n                if isinstance(widget, QDockWidget):\n                    # Inform of the previous dock state and the area used\n                    return bool(d), area\n        return False, 0\n\n\nclass Preferences(PreferencesBase):\n    def _load_values(self, settings):\n        get = self.get_value\n        self.filter_hardness = get(\"FilterHardness\", self.filter_hardness)\n        self.mix_file_kind = get(\"MixFileKind\", self.mix_file_kind)\n        self.ignore_hardlink_matches = get(\"IgnoreHardlinkMatches\", self.ignore_hardlink_matches)\n        self.use_regexp = get(\"UseRegexp\", self.use_regexp)\n        self.remove_empty_folders = get(\"RemoveEmptyFolders\", self.remove_empty_folders)\n        self.rehash_ignore_mtime = get(\"RehashIgnoreMTime\", self.rehash_ignore_mtime)\n        self.include_exists_check = get(\"IncludeExistsCheck\", self.include_exists_check)\n        self.debug_mode = get(\"DebugMode\", self.debug_mode)\n        self.profile_scan = get(\"ProfileScan\", self.profile_scan)\n        self.destination_type = get(\"DestinationType\", self.destination_type)\n        self.custom_command = get(\"CustomCommand\", self.custom_command)\n        self.language = get(\"Language\", self.language)\n        if not self.language and trans.installed_lang:\n            self.language = trans.installed_lang\n        self.portable = get(\"Portable\", False)\n        self.use_dark_style = get(\"UseDarkStyle\", False)\n        self.use_native_dialogs = get(\"UseNativeDialogs\", True)\n\n        self.tableFontSize = get(\"TableFontSize\", self.tableFontSize)\n        self.reference_bold_font = get(\"ReferenceBoldFont\", self.reference_bold_font)\n        self.details_dialog_titlebar_enabled = get(\"DetailsDialogTitleBarEnabled\", self.details_dialog_titlebar_enabled)\n        self.details_dialog_vertical_titlebar = get(\n            \"DetailsDialogVerticalTitleBar\", self.details_dialog_vertical_titlebar\n        )\n        # On Windows and MacOS, use internal icons by default\n        self.details_dialog_override_theme_icons = (\n            get(\"DetailsDialogOverrideThemeIcons\", self.details_dialog_override_theme_icons) if ISLINUX else True\n        )\n        self.details_table_delta_foreground_color = get(\n            \"DetailsTableDeltaForegroundColor\", self.details_table_delta_foreground_color\n        )\n        self.details_dialog_viewers_show_scrollbars = get(\n            \"DetailsDialogViewersShowScrollbars\", self.details_dialog_viewers_show_scrollbars\n        )\n\n        self.result_table_ref_foreground_color = get(\n            \"ResultTableRefForegroundColor\", self.result_table_ref_foreground_color\n        )\n        self.result_table_ref_background_color = get(\n            \"ResultTableRefBackgroundColor\", self.result_table_ref_background_color\n        )\n        self.result_table_delta_foreground_color = get(\n            \"ResultTableDeltaForegroundColor\", self.result_table_delta_foreground_color\n        )\n\n        self.resultWindowIsMaximized = get(\"ResultWindowIsMaximized\", self.resultWindowIsMaximized)\n        self.resultWindowRect = self.get_rect(\"ResultWindowRect\", self.resultWindowRect)\n        self.mainWindowIsMaximized = get(\"MainWindowIsMaximized\", self.mainWindowIsMaximized)\n        self.mainWindowRect = self.get_rect(\"MainWindowRect\", self.mainWindowRect)\n        self.directoriesWindowRect = self.get_rect(\"DirectoriesWindowRect\", self.directoriesWindowRect)\n\n        self.recentResults = get(\"RecentResults\", self.recentResults)\n        self.recentFolders = get(\"RecentFolders\", self.recentFolders)\n        self.tabs_default_pos = get(\"TabsDefaultPosition\", self.tabs_default_pos)\n        self.word_weighting = get(\"WordWeighting\", self.word_weighting)\n        self.match_similar = get(\"MatchSimilar\", self.match_similar)\n        self.ignore_small_files = get(\"IgnoreSmallFiles\", self.ignore_small_files)\n        self.small_file_threshold = get(\"SmallFileThreshold\", self.small_file_threshold)\n        self.ignore_large_files = get(\"IgnoreLargeFiles\", self.ignore_large_files)\n        self.large_file_threshold = get(\"LargeFileThreshold\", self.large_file_threshold)\n        self.big_file_partial_hashes = get(\"BigFilePartialHashes\", self.big_file_partial_hashes)\n        self.big_file_size_threshold = get(\"BigFileSizeThreshold\", self.big_file_size_threshold)\n        self.scan_tag_track = get(\"ScanTagTrack\", self.scan_tag_track)\n        self.scan_tag_artist = get(\"ScanTagArtist\", self.scan_tag_artist)\n        self.scan_tag_album = get(\"ScanTagAlbum\", self.scan_tag_album)\n        self.scan_tag_title = get(\"ScanTagTitle\", self.scan_tag_title)\n        self.scan_tag_genre = get(\"ScanTagGenre\", self.scan_tag_genre)\n        self.scan_tag_year = get(\"ScanTagYear\", self.scan_tag_year)\n        self.match_scaled = get(\"MatchScaled\", self.match_scaled)\n        self.match_rotated = get(\"MatchRotated\", self.match_rotated)\n\n    def reset(self):\n        self.filter_hardness = 95\n        self.mix_file_kind = True\n        self.use_regexp = False\n        self.ignore_hardlink_matches = False\n        self.remove_empty_folders = False\n        self.rehash_ignore_mtime = False\n        self.include_exists_check = True\n        self.debug_mode = False\n        self.profile_scan = False\n        self.destination_type = 1\n        self.custom_command = \"\"\n        self.language = trans.installed_lang if trans.installed_lang else \"\"\n        self.use_dark_style = False\n        self.use_native_dialogs = True\n\n        self.tableFontSize = QApplication.font().pointSize()\n        self.reference_bold_font = True\n        self.details_dialog_titlebar_enabled = True\n        self.details_dialog_vertical_titlebar = True\n        self.details_table_delta_foreground_color = QColor(250, 20, 20)  # red\n        # By default use internal icons on platforms other than Linux for now\n        self.details_dialog_override_theme_icons = False if not ISLINUX else True\n        self.details_dialog_viewers_show_scrollbars = True\n        self.result_table_ref_foreground_color = QColor(Qt.blue)\n        self.result_table_ref_background_color = QColor(Qt.lightGray)\n        self.result_table_delta_foreground_color = QColor(255, 142, 40)  # orange\n        self.resultWindowIsMaximized = False\n        self.resultWindowRect = None\n        self.directoriesWindowRect = None\n        self.mainWindowRect = None\n        self.mainWindowIsMaximized = False\n        self.recentResults = []\n        self.recentFolders = []\n\n        self.tabs_default_pos = True\n        self.word_weighting = True\n        self.match_similar = False\n        self.ignore_small_files = True\n        self.small_file_threshold = 10  # KB\n        self.ignore_large_files = False\n        self.large_file_threshold = 1000  # MB\n        self.big_file_partial_hashes = False\n        self.big_file_size_threshold = 100  # MB\n        self.scan_tag_track = False\n        self.scan_tag_artist = True\n        self.scan_tag_album = True\n        self.scan_tag_title = True\n        self.scan_tag_genre = False\n        self.scan_tag_year = False\n        self.match_scaled = False\n        self.match_rotated = False\n\n    def _save_values(self, settings):\n        set_ = self.set_value\n        set_(\"FilterHardness\", self.filter_hardness)\n        set_(\"MixFileKind\", self.mix_file_kind)\n        set_(\"IgnoreHardlinkMatches\", self.ignore_hardlink_matches)\n        set_(\"UseRegexp\", self.use_regexp)\n        set_(\"RemoveEmptyFolders\", self.remove_empty_folders)\n        set_(\"RehashIgnoreMTime\", self.rehash_ignore_mtime)\n        set_(\"IncludeExistsCheck\", self.include_exists_check)\n        set_(\"DebugMode\", self.debug_mode)\n        set_(\"ProfileScan\", self.profile_scan)\n        set_(\"DestinationType\", self.destination_type)\n        set_(\"CustomCommand\", self.custom_command)\n        set_(\"Language\", self.language)\n        set_(\"Portable\", self.portable)\n        set_(\"UseDarkStyle\", self.use_dark_style)\n        set_(\"UseNativeDialogs\", self.use_native_dialogs)\n\n        set_(\"TableFontSize\", self.tableFontSize)\n        set_(\"ReferenceBoldFont\", self.reference_bold_font)\n        set_(\"DetailsDialogTitleBarEnabled\", self.details_dialog_titlebar_enabled)\n        set_(\"DetailsDialogVerticalTitleBar\", self.details_dialog_vertical_titlebar)\n        set_(\"DetailsDialogOverrideThemeIcons\", self.details_dialog_override_theme_icons)\n        set_(\"DetailsDialogViewersShowScrollbars\", self.details_dialog_viewers_show_scrollbars)\n        set_(\"DetailsTableDeltaForegroundColor\", self.details_table_delta_foreground_color)\n        set_(\"ResultTableRefForegroundColor\", self.result_table_ref_foreground_color)\n        set_(\"ResultTableRefBackgroundColor\", self.result_table_ref_background_color)\n        set_(\"ResultTableDeltaForegroundColor\", self.result_table_delta_foreground_color)\n        set_(\"ResultWindowIsMaximized\", self.resultWindowIsMaximized)\n        set_(\"MainWindowIsMaximized\", self.mainWindowIsMaximized)\n        self.set_rect(\"ResultWindowRect\", self.resultWindowRect)\n        self.set_rect(\"MainWindowRect\", self.mainWindowRect)\n        self.set_rect(\"DirectoriesWindowRect\", self.directoriesWindowRect)\n        set_(\"RecentResults\", self.recentResults)\n        set_(\"RecentFolders\", self.recentFolders)\n\n        set_(\"TabsDefaultPosition\", self.tabs_default_pos)\n        set_(\"WordWeighting\", self.word_weighting)\n        set_(\"MatchSimilar\", self.match_similar)\n        set_(\"IgnoreSmallFiles\", self.ignore_small_files)\n        set_(\"SmallFileThreshold\", self.small_file_threshold)\n        set_(\"IgnoreLargeFiles\", self.ignore_large_files)\n        set_(\"LargeFileThreshold\", self.large_file_threshold)\n        set_(\"BigFilePartialHashes\", self.big_file_partial_hashes)\n        set_(\"BigFileSizeThreshold\", self.big_file_size_threshold)\n        set_(\"ScanTagTrack\", self.scan_tag_track)\n        set_(\"ScanTagArtist\", self.scan_tag_artist)\n        set_(\"ScanTagAlbum\", self.scan_tag_album)\n        set_(\"ScanTagTitle\", self.scan_tag_title)\n        set_(\"ScanTagGenre\", self.scan_tag_genre)\n        set_(\"ScanTagYear\", self.scan_tag_year)\n        set_(\"MatchScaled\", self.match_scaled)\n        set_(\"MatchRotated\", self.match_rotated)\n\n    # scan_type is special because we save it immediately when we set it.\n    def get_scan_type(self, app_mode):\n        if app_mode == AppMode.PICTURE:\n            return self.get_value(\"ScanTypePicture\", ScanType.FUZZYBLOCK)\n        elif app_mode == AppMode.MUSIC:\n            return self.get_value(\"ScanTypeMusic\", ScanType.TAG)\n        else:\n            return self.get_value(\"ScanTypeStandard\", ScanType.CONTENTS)\n\n    def set_scan_type(self, app_mode, value):\n        if app_mode == AppMode.PICTURE:\n            self.set_value(\"ScanTypePicture\", value)\n        elif app_mode == AppMode.MUSIC:\n            self.set_value(\"ScanTypeMusic\", value)\n        else:\n            self.set_value(\"ScanTypeStandard\", value)\n"
  },
  {
    "path": "qt/preferences_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QSize, pyqtSlot\nfrom PyQt5.QtWidgets import (\n    QDialog,\n    QDialogButtonBox,\n    QVBoxLayout,\n    QHBoxLayout,\n    QGridLayout,\n    QLabel,\n    QComboBox,\n    QSlider,\n    QSizePolicy,\n    QSpacerItem,\n    QCheckBox,\n    QLineEdit,\n    QMessageBox,\n    QSpinBox,\n    QLayout,\n    QTabWidget,\n    QWidget,\n    QColorDialog,\n    QPushButton,\n    QGroupBox,\n    QFormLayout,\n)\nfrom PyQt5.QtGui import QPixmap, QIcon\nfrom hscommon import desktop, plat\n\nfrom hscommon.trans import trget\nfrom hscommon.plat import ISLINUX\nfrom qt.util import horizontal_wrap, move_to_screen_center\nfrom qt.preferences import get_langnames\nfrom enum import Flag, auto\n\nfrom qt.preferences import Preferences\n\ntr = trget(\"ui\")\n\n\nclass Sections(Flag):\n    \"\"\"Filter blocks of preferences when reset or loaded\"\"\"\n\n    GENERAL = auto()\n    DISPLAY = auto()\n    ADVANCED = auto()\n    DEBUG = auto()\n    ALL = GENERAL | DISPLAY | ADVANCED | DEBUG\n\n\nclass PreferencesDialogBase(QDialog):\n    def __init__(self, parent, app, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self.app = app\n        self.supportedLanguages = dict(sorted(get_langnames().items(), key=lambda item: item[1]))\n        self._setupUi()\n\n        self.filterHardnessSlider.valueChanged[\"int\"].connect(self.filterHardnessLabel.setNum)\n        self.debug_location_label.linkActivated.connect(desktop.open_path)\n        self.buttonBox.clicked.connect(self.buttonClicked)\n        self.buttonBox.accepted.connect(self.accept)\n        self.buttonBox.rejected.connect(self.reject)\n\n    def _setupFilterHardnessBox(self):\n        self.filterHardnessHLayout = QHBoxLayout()\n        self.filterHardnessLabel = QLabel(self)\n        self.filterHardnessLabel.setText(tr(\"Filter Hardness:\"))\n        self.filterHardnessLabel.setMinimumSize(QSize(0, 0))\n        self.filterHardnessHLayout.addWidget(self.filterHardnessLabel)\n        self.filterHardnessVLayout = QVBoxLayout()\n        self.filterHardnessVLayout.setSpacing(0)\n        self.filterHardnessHLayoutSub1 = QHBoxLayout()\n        self.filterHardnessHLayoutSub1.setSpacing(12)\n        self.filterHardnessSlider = QSlider(self)\n        size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)\n        size_policy.setHorizontalStretch(0)\n        size_policy.setVerticalStretch(0)\n        size_policy.setHeightForWidth(self.filterHardnessSlider.sizePolicy().hasHeightForWidth())\n        self.filterHardnessSlider.setSizePolicy(size_policy)\n        self.filterHardnessSlider.setMinimum(1)\n        self.filterHardnessSlider.setMaximum(100)\n        self.filterHardnessSlider.setTracking(True)\n        self.filterHardnessSlider.setOrientation(Qt.Horizontal)\n        self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessSlider)\n        self.filterHardnessLabel = QLabel(self)\n        self.filterHardnessLabel.setText(\"100\")\n        self.filterHardnessLabel.setMinimumSize(QSize(21, 0))\n        self.filterHardnessHLayoutSub1.addWidget(self.filterHardnessLabel)\n        self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub1)\n        self.filterHardnessHLayoutSub2 = QHBoxLayout()\n        self.filterHardnessHLayoutSub2.setContentsMargins(-1, 0, -1, -1)\n        self.moreResultsLabel = QLabel(self)\n        self.moreResultsLabel.setText(tr(\"More Results\"))\n        self.filterHardnessHLayoutSub2.addWidget(self.moreResultsLabel)\n        spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.filterHardnessHLayoutSub2.addItem(spacer_item)\n        self.fewerResultsLabel = QLabel(self)\n        self.fewerResultsLabel.setText(tr(\"Fewer Results\"))\n        self.filterHardnessHLayoutSub2.addWidget(self.fewerResultsLabel)\n        self.filterHardnessVLayout.addLayout(self.filterHardnessHLayoutSub2)\n        self.filterHardnessHLayout.addLayout(self.filterHardnessVLayout)\n\n    def _setupBottomPart(self):\n        # The bottom part of the pref panel is always the same in all editions.\n        self.copyMoveLabel = QLabel(self)\n        self.copyMoveLabel.setText(tr(\"Copy and Move:\"))\n        self.widgetsVLayout.addWidget(self.copyMoveLabel)\n        self.copyMoveDestinationComboBox = QComboBox(self)\n        self.copyMoveDestinationComboBox.addItem(tr(\"Right in destination\"))\n        self.copyMoveDestinationComboBox.addItem(tr(\"Recreate relative path\"))\n        self.copyMoveDestinationComboBox.addItem(tr(\"Recreate absolute path\"))\n        self.widgetsVLayout.addWidget(self.copyMoveDestinationComboBox)\n        self.customCommandLabel = QLabel(self)\n        self.customCommandLabel.setText(tr(\"Custom Command (arguments: %d for dupe, %r for ref):\"))\n        self.widgetsVLayout.addWidget(self.customCommandLabel)\n        self.customCommandEdit = QLineEdit(self)\n        self.widgetsVLayout.addWidget(self.customCommandEdit)\n\n    def _setupDisplayPage(self):\n        self.ui_groupbox = QGroupBox(\"&\" + tr(\"General Interface\"))\n        layout = QVBoxLayout()\n        self.languageLabel = QLabel(tr(\"Language:\"), self)\n        self.languageComboBox = QComboBox(self)\n        for lang_code, lang_str in self.supportedLanguages.items():\n            self.languageComboBox.addItem(lang_str, userData=lang_code)\n        layout.addLayout(horizontal_wrap([self.languageLabel, self.languageComboBox, None]))\n        self._setupAddCheckbox(\n            \"tabs_default_pos\",\n            tr(\"Use default position for tab bar (requires restart)\"),\n        )\n        self.tabs_default_pos.setToolTip(\n            tr(\n                \"Place the tab bar below the main menu instead of next to it\\n\\\nOn MacOS, the tab bar will fill up the window's width instead.\"\n            )\n        )\n        layout.addWidget(self.tabs_default_pos)\n        self._setupAddCheckbox(\n            \"use_native_dialogs\",\n            tr(\"Use native OS dialogs\"),\n        )\n        self.use_native_dialogs.setToolTip(\n            tr(\n                \"For actions such as file/folder selection use the OS native dialogs.\\n\\\nSome native dialogs have limited functionality.\"\n            )\n        )\n        layout.addWidget(self.use_native_dialogs)\n        if plat.ISWINDOWS:\n            self._setupAddCheckbox(\"use_dark_style\", tr(\"Use dark style\"))\n            layout.addWidget(self.use_dark_style)\n        self.ui_groupbox.setLayout(layout)\n        self.displayVLayout.addWidget(self.ui_groupbox)\n\n        gridlayout = QGridLayout()\n        gridlayout.setColumnStretch(2, 2)\n        formlayout = QFormLayout()\n        result_groupbox = QGroupBox(\"&\" + tr(\"Result Table\"))\n        self.fontSizeSpinBox = QSpinBox()\n        self.fontSizeSpinBox.setMinimum(5)\n        formlayout.addRow(tr(\"Font size:\"), self.fontSizeSpinBox)\n        self._setupAddCheckbox(\"reference_bold_font\", tr(\"Use bold font for references\"))\n        formlayout.addRow(self.reference_bold_font)\n\n        self.result_table_ref_foreground_color = ColorPickerButton(self)\n        formlayout.addRow(tr(\"Reference foreground color:\"), self.result_table_ref_foreground_color)\n        self.result_table_ref_background_color = ColorPickerButton(self)\n        formlayout.addRow(tr(\"Reference background color:\"), self.result_table_ref_background_color)\n        self.result_table_delta_foreground_color = ColorPickerButton(self)\n        formlayout.addRow(tr(\"Delta foreground color:\"), self.result_table_delta_foreground_color)\n        formlayout.setLabelAlignment(Qt.AlignLeft)\n\n        # Keep same vertical spacing as parent layout for consistency\n        formlayout.setVerticalSpacing(self.displayVLayout.spacing())\n        gridlayout.addLayout(formlayout, 0, 0)\n        result_groupbox.setLayout(gridlayout)\n        self.displayVLayout.addWidget(result_groupbox)\n\n        details_groupbox = QGroupBox(\"&\" + tr(\"Details Window\"))\n        self.details_groupbox_layout = QVBoxLayout()\n        self._setupAddCheckbox(\n            \"details_dialog_titlebar_enabled\",\n            tr(\"Show the title bar and can be docked\"),\n        )\n        self.details_dialog_titlebar_enabled.setToolTip(\n            tr(\n                \"While the title bar is hidden, \\\nuse the modifier key to drag the floating window around\"\n            )\n            if ISLINUX\n            else tr(\"The title bar can only be disabled while the window is docked\")\n        )\n        self.details_groupbox_layout.addWidget(self.details_dialog_titlebar_enabled)\n        self._setupAddCheckbox(\"details_dialog_vertical_titlebar\", tr(\"Vertical title bar\"))\n        self.details_dialog_vertical_titlebar.setToolTip(\n            tr(\"Change the title bar from horizontal on top, to vertical on the left side\")\n        )\n        self.details_groupbox_layout.addWidget(self.details_dialog_vertical_titlebar)\n        self.details_dialog_vertical_titlebar.setEnabled(self.details_dialog_titlebar_enabled.isChecked())\n        self.details_dialog_titlebar_enabled.stateChanged.connect(self.details_dialog_vertical_titlebar.setEnabled)\n        gridlayout = QGridLayout()\n        formlayout = QFormLayout()\n        self.details_table_delta_foreground_color = ColorPickerButton(self)\n        # Padding on the right side and space between label and widget to keep it somewhat consistent across themes\n        gridlayout.setColumnStretch(1, 1)\n        formlayout.setHorizontalSpacing(50)\n        formlayout.addRow(tr(\"Delta foreground color:\"), self.details_table_delta_foreground_color)\n        gridlayout.addLayout(formlayout, 0, 0)\n        self.details_groupbox_layout.addLayout(gridlayout)\n        details_groupbox.setLayout(self.details_groupbox_layout)\n        self.displayVLayout.addWidget(details_groupbox)\n\n    def _setup_advanced_page(self):\n        tab_label = QLabel(\n            tr(\n                \"These options are for advanced users or for very specific situations, \\\nmost users should not have to modify these.\"\n            ),\n            wordWrap=True,\n        )\n        self.advanced_vlayout.addWidget(tab_label)\n        self._setupAddCheckbox(\"include_exists_check_box\", tr(\"Include existence check after scan completion\"))\n        self.advanced_vlayout.addWidget(self.include_exists_check_box)\n        self._setupAddCheckbox(\"rehash_ignore_mtime_box\", tr(\"Ignore difference in mtime when loading cached digests\"))\n        self.advanced_vlayout.addWidget(self.rehash_ignore_mtime_box)\n\n    def _setupDebugPage(self):\n        self._setupAddCheckbox(\"debugModeBox\", tr(\"Debug mode (restart required)\"))\n        self._setupAddCheckbox(\"profile_scan_box\", tr(\"Profile scan operation\"))\n        self.profile_scan_box.setToolTip(tr(\"Profile the scan operation and save logs for optimization.\"))\n        self.debugVLayout.addWidget(self.debugModeBox)\n        self.debugVLayout.addWidget(self.profile_scan_box)\n        self.debug_location_label = QLabel(\n            tr('Logs located in: <a href=\"{}\">{}</a>').format(self.app.model.appdata, self.app.model.appdata),\n            wordWrap=True,\n        )\n        self.debugVLayout.addWidget(self.debug_location_label)\n\n    def _setupAddCheckbox(self, name, label, parent=None):\n        if parent is None:\n            parent = self\n        cb = QCheckBox(parent)\n        cb.setText(label)\n        setattr(self, name, cb)\n\n    def _setupPreferenceWidgets(self):\n        # Edition-specific\n        pass\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Options\"))\n        self.setSizeGripEnabled(False)\n        self.setModal(True)\n        self.mainVLayout = QVBoxLayout(self)\n        self.tabwidget = QTabWidget()\n        self.page_general = QWidget()\n        self.page_display = QWidget()\n        self.page_advanced = QWidget()\n        self.page_debug = QWidget()\n        self.widgetsVLayout = QVBoxLayout()\n        self.page_general.setLayout(self.widgetsVLayout)\n        self.displayVLayout = QVBoxLayout()\n        self.displayVLayout.setSpacing(5)  # arbitrary value, might conflict with style\n        self.page_display.setLayout(self.displayVLayout)\n        self.advanced_vlayout = QVBoxLayout()\n        self.page_advanced.setLayout(self.advanced_vlayout)\n        self.debugVLayout = QVBoxLayout()\n        self.page_debug.setLayout(self.debugVLayout)\n        self._setupPreferenceWidgets()\n        self._setupDisplayPage()\n        self._setup_advanced_page()\n        self._setupDebugPage()\n        # self.mainVLayout.addLayout(self.widgetsVLayout)\n        self.buttonBox = QDialogButtonBox(self)\n        self.buttonBox.setStandardButtons(\n            QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.RestoreDefaults\n        )\n        self.mainVLayout.addWidget(self.tabwidget)\n        self.mainVLayout.addWidget(self.buttonBox)\n        self.layout().setSizeConstraint(QLayout.SetFixedSize)\n        self.tabwidget.addTab(self.page_general, tr(\"General\"))\n        self.tabwidget.addTab(self.page_display, tr(\"Display\"))\n        self.tabwidget.addTab(self.page_advanced, tr(\"Advanced\"))\n        self.tabwidget.addTab(self.page_debug, tr(\"Debug\"))\n        self.displayVLayout.addStretch(0)\n        self.widgetsVLayout.addStretch(0)\n        self.advanced_vlayout.addStretch(0)\n        self.debugVLayout.addStretch(0)\n\n    def _load(self, prefs, setchecked, section):\n        # Edition-specific\n        pass\n\n    def _save(self, prefs, ischecked):\n        # Edition-specific\n        pass\n\n    def load(self, prefs=None, section=Sections.ALL):\n        if prefs is None:\n            prefs = self.app.prefs\n\n        def setchecked(cb, b):\n            cb.setCheckState(Qt.Checked if b else Qt.Unchecked)\n\n        if section & Sections.GENERAL:\n            self.filterHardnessSlider.setValue(prefs.filter_hardness)\n            self.filterHardnessLabel.setNum(prefs.filter_hardness)\n            setchecked(self.mixFileKindBox, prefs.mix_file_kind)\n            setchecked(self.useRegexpBox, prefs.use_regexp)\n            setchecked(self.removeEmptyFoldersBox, prefs.remove_empty_folders)\n            setchecked(self.ignoreHardlinkMatches, prefs.ignore_hardlink_matches)\n            self.copyMoveDestinationComboBox.setCurrentIndex(prefs.destination_type)\n            self.customCommandEdit.setText(prefs.custom_command)\n        if section & Sections.DISPLAY:\n            setchecked(self.reference_bold_font, prefs.reference_bold_font)\n            setchecked(self.tabs_default_pos, prefs.tabs_default_pos)\n            setchecked(self.use_native_dialogs, prefs.use_native_dialogs)\n            if plat.ISWINDOWS:\n                setchecked(self.use_dark_style, prefs.use_dark_style)\n            setchecked(\n                self.details_dialog_titlebar_enabled,\n                prefs.details_dialog_titlebar_enabled,\n            )\n            setchecked(\n                self.details_dialog_vertical_titlebar,\n                prefs.details_dialog_vertical_titlebar,\n            )\n            self.fontSizeSpinBox.setValue(prefs.tableFontSize)\n            self.details_table_delta_foreground_color.setColor(prefs.details_table_delta_foreground_color)\n            self.result_table_ref_foreground_color.setColor(prefs.result_table_ref_foreground_color)\n            self.result_table_ref_background_color.setColor(prefs.result_table_ref_background_color)\n            self.result_table_delta_foreground_color.setColor(prefs.result_table_delta_foreground_color)\n            try:\n                selected_lang = self.supportedLanguages[self.app.prefs.language]\n            except KeyError:\n                selected_lang = self.supportedLanguages[\"en\"]\n            self.languageComboBox.setCurrentText(selected_lang)\n        if section & Sections.ADVANCED:\n            setchecked(self.rehash_ignore_mtime_box, prefs.rehash_ignore_mtime)\n            setchecked(self.include_exists_check_box, prefs.include_exists_check)\n        if section & Sections.DEBUG:\n            setchecked(self.debugModeBox, prefs.debug_mode)\n            setchecked(self.profile_scan_box, prefs.profile_scan)\n        self._load(prefs, setchecked, section)\n\n    def save(self):\n        prefs = self.app.prefs\n        prefs.filter_hardness = self.filterHardnessSlider.value()\n\n        def ischecked(cb):\n            return cb.checkState() == Qt.Checked\n\n        prefs.mix_file_kind = ischecked(self.mixFileKindBox)\n        prefs.use_regexp = ischecked(self.useRegexpBox)\n        prefs.remove_empty_folders = ischecked(self.removeEmptyFoldersBox)\n        prefs.ignore_hardlink_matches = ischecked(self.ignoreHardlinkMatches)\n        prefs.rehash_ignore_mtime = ischecked(self.rehash_ignore_mtime_box)\n        prefs.include_exists_check = ischecked(self.include_exists_check_box)\n        prefs.debug_mode = ischecked(self.debugModeBox)\n        prefs.profile_scan = ischecked(self.profile_scan_box)\n        prefs.reference_bold_font = ischecked(self.reference_bold_font)\n        prefs.details_dialog_titlebar_enabled = ischecked(self.details_dialog_titlebar_enabled)\n        prefs.details_dialog_vertical_titlebar = ischecked(self.details_dialog_vertical_titlebar)\n        prefs.details_table_delta_foreground_color = self.details_table_delta_foreground_color.color\n        prefs.result_table_ref_foreground_color = self.result_table_ref_foreground_color.color\n        prefs.result_table_ref_background_color = self.result_table_ref_background_color.color\n        prefs.result_table_delta_foreground_color = self.result_table_delta_foreground_color.color\n        prefs.destination_type = self.copyMoveDestinationComboBox.currentIndex()\n        prefs.custom_command = str(self.customCommandEdit.text())\n        prefs.tableFontSize = self.fontSizeSpinBox.value()\n        prefs.tabs_default_pos = ischecked(self.tabs_default_pos)\n        prefs.use_native_dialogs = ischecked(self.use_native_dialogs)\n        if plat.ISWINDOWS:\n            prefs.use_dark_style = ischecked(self.use_dark_style)\n        lang_code = self.languageComboBox.currentData()\n        old_lang_code = self.app.prefs.language\n        if old_lang_code not in self.supportedLanguages.keys():\n            old_lang_code = \"en\"\n        if lang_code != old_lang_code:\n            QMessageBox.information(\n                self,\n                \"\",\n                tr(\"dupeGuru has to restart for language changes to take effect.\"),\n            )\n        self.app.prefs.language = lang_code\n        self._save(prefs, ischecked)\n\n    def resetToDefaults(self, section_to_update):\n        self.load(Preferences(), section_to_update)\n\n    # --- Events\n    def buttonClicked(self, button):\n        role = self.buttonBox.buttonRole(button)\n        if role == QDialogButtonBox.ResetRole:\n            current_tab = self.tabwidget.currentWidget()\n            section_to_update = Sections.ALL\n            if current_tab is self.page_general:\n                section_to_update = Sections.GENERAL\n            if current_tab is self.page_display:\n                section_to_update = Sections.DISPLAY\n            if current_tab is self.page_debug:\n                section_to_update = Sections.DEBUG\n            self.resetToDefaults(section_to_update)\n\n    def showEvent(self, event):\n        # have to do this here as the frameGeometry is not correct until shown\n        move_to_screen_center(self)\n        super().showEvent(event)\n\n\nclass ColorPickerButton(QPushButton):\n    def __init__(self, parent):\n        super().__init__(parent)\n        self.parent = parent\n        self.color = None\n        self.clicked.connect(self.onClicked)\n\n    @pyqtSlot()\n    def onClicked(self):\n        color = QColorDialog.getColor(self.color if self.color is not None else Qt.white, self.parent)\n        self.setColor(color)\n\n    def setColor(self, color):\n        size = QSize(16, 16)\n        px = QPixmap(size)\n        if color is None:\n            size.width = 0\n            size.height = 0\n        elif not color.isValid():\n            return\n        else:\n            self.color = color\n            px.fill(color)\n        self.setIcon(QIcon(px))\n"
  },
  {
    "path": "qt/prioritize_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-09-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QMimeData, QByteArray\nfrom PyQt5.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QHBoxLayout,\n    QPushButton,\n    QComboBox,\n    QListView,\n    QDialogButtonBox,\n    QAbstractItemView,\n    QLabel,\n    QStyle,\n    QSplitter,\n    QWidget,\n    QSizePolicy,\n)\n\nfrom hscommon.trans import trget\nfrom qt.selectable_list import ComboboxModel, ListviewModel\nfrom qt.util import vertical_spacer\nfrom core.gui.prioritize_dialog import PrioritizeDialog as PrioritizeDialogModel\n\ntr = trget(\"ui\")\n\nMIME_INDEXES = \"application/dupeguru.rowindexes\"\n\n\nclass PrioritizationList(ListviewModel):\n    def flags(self, index):\n        if not index.isValid():\n            return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled\n        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled\n\n    # --- Drag & Drop\n    def dropMimeData(self, mime_data, action, row, column, parent_index):\n        if not mime_data.hasFormat(MIME_INDEXES):\n            return False\n        # Since we only drop in between items, parentIndex must be invalid, and we use the row arg\n        # to know where the drop took place.\n        if parent_index.isValid():\n            return False\n        # \"When row and column are -1 it means that the dropped data should be considered as\n        # dropped directly on parent.\"\n        # Moving items to row -1 would put them before the last item. Fix the row to drop the\n        # dragged items after the last item.\n        if row < 0:\n            row = len(self.model) - 1\n        str_mime_data = bytes(mime_data.data(MIME_INDEXES)).decode()\n        indexes = list(map(int, str_mime_data.split(\",\")))\n        self.model.move_indexes(indexes, row)\n        self.view.selectionModel().clearSelection()\n        return True\n\n    def mimeData(self, indexes):\n        rows = {str(index.row()) for index in indexes}\n        data = \",\".join(rows)\n        mime_data = QMimeData()\n        mime_data.setData(MIME_INDEXES, QByteArray(data.encode()))\n        return mime_data\n\n    def mimeTypes(self):\n        return [MIME_INDEXES]\n\n    def supportedDropActions(self):\n        return Qt.MoveAction\n\n\nclass PrioritizeDialog(QDialog):\n    def __init__(self, parent, app, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self._setupUi()\n        self.model = PrioritizeDialogModel(app=app.model)\n        self.categoryList = ComboboxModel(model=self.model.category_list, view=self.categoryCombobox)\n        self.criteriaList = ListviewModel(model=self.model.criteria_list, view=self.criteriaListView)\n        self.prioritizationList = PrioritizationList(\n            model=self.model.prioritization_list, view=self.prioritizationListView\n        )\n        self.model.view = self\n\n        self.addCriteriaButton.clicked.connect(self.model.add_selected)\n        self.criteriaListView.doubleClicked.connect(self.model.add_selected)\n        self.removeCriteriaButton.clicked.connect(self.model.remove_selected)\n        self.prioritizationListView.doubleClicked.connect(self.model.remove_selected)\n        self.buttonBox.accepted.connect(self.accept)\n        self.buttonBox.rejected.connect(self.reject)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Re-Prioritize duplicates\"))\n        self.resize(700, 400)\n\n        # widgets\n        msg = tr(\n            \"Add criteria to the right box and click OK to send the dupes that correspond the \"\n            \"best to these criteria to their respective group's \"\n            \"reference position. Read the help file for more information.\"\n        )\n        self.promptLabel = QLabel(msg)\n        self.promptLabel.setWordWrap(True)\n        self.categoryCombobox = QComboBox()\n        self.criteriaListView = QListView()\n        self.criteriaListView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.addCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowRight), \"\")\n        self.removeCriteriaButton = QPushButton(self.style().standardIcon(QStyle.SP_ArrowLeft), \"\")\n        self.prioritizationListView = QListView()\n        self.prioritizationListView.setAcceptDrops(True)\n        self.prioritizationListView.setDragEnabled(True)\n        self.prioritizationListView.setDragDropMode(QAbstractItemView.InternalMove)\n        self.prioritizationListView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.prioritizationListView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.buttonBox = QDialogButtonBox()\n        self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)\n\n        # layout\n        self.mainLayout = QVBoxLayout(self)\n        self.mainLayout.addWidget(self.promptLabel)\n        self.splitter = QSplitter()\n        sp = self.splitter.sizePolicy()\n        sp.setVerticalPolicy(QSizePolicy.Expanding)\n        self.splitter.setSizePolicy(sp)\n        self.leftSide = QWidget()\n        self.leftWidgetsLayout = QVBoxLayout()\n        self.leftWidgetsLayout.addWidget(self.categoryCombobox)\n        self.leftWidgetsLayout.addWidget(self.criteriaListView)\n        self.leftSide.setLayout(self.leftWidgetsLayout)\n        self.splitter.addWidget(self.leftSide)\n        self.rightSide = QWidget()\n        self.rightWidgetsLayout = QHBoxLayout()\n        self.addRemoveButtonsLayout = QVBoxLayout()\n        self.addRemoveButtonsLayout.addItem(vertical_spacer())\n        self.addRemoveButtonsLayout.addWidget(self.addCriteriaButton)\n        self.addRemoveButtonsLayout.addWidget(self.removeCriteriaButton)\n        self.addRemoveButtonsLayout.addItem(vertical_spacer())\n        self.rightWidgetsLayout.addLayout(self.addRemoveButtonsLayout)\n        self.rightWidgetsLayout.addWidget(self.prioritizationListView)\n        self.rightSide.setLayout(self.rightWidgetsLayout)\n        self.splitter.addWidget(self.rightSide)\n        self.mainLayout.addWidget(self.splitter)\n        self.mainLayout.addWidget(self.buttonBox)\n"
  },
  {
    "path": "qt/problem_dialog.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-04-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt\nfrom PyQt5.QtWidgets import (\n    QDialog,\n    QVBoxLayout,\n    QHBoxLayout,\n    QPushButton,\n    QSpacerItem,\n    QSizePolicy,\n    QLabel,\n    QTableView,\n    QAbstractItemView,\n)\n\nfrom qt.util import move_to_screen_center\nfrom hscommon.trans import trget\nfrom qt.problem_table import ProblemTable\n\ntr = trget(\"ui\")\n\n\nclass ProblemDialog(QDialog):\n    def __init__(self, parent, model, **kwargs):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        super().__init__(parent, flags, **kwargs)\n        self._setupUi()\n        self.model = model\n        self.model.view = self\n        self.table = ProblemTable(self.model.problem_table, view=self.tableView)\n\n        self.revealButton.clicked.connect(self.model.reveal_selected_dupe)\n        self.closeButton.clicked.connect(self.accept)\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Problems!\"))\n        self.resize(413, 323)\n        self.verticalLayout = QVBoxLayout(self)\n        self.label = QLabel(self)\n        msg = tr(\n            \"There were problems processing some (or all) of the files. The cause of \"\n            \"these problems are described in the table below. Those files were not \"\n            \"removed from your results.\"\n        )\n        self.label.setText(msg)\n        self.label.setWordWrap(True)\n        self.verticalLayout.addWidget(self.label)\n        self.tableView = QTableView(self)\n        self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)\n        self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)\n        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.tableView.setShowGrid(False)\n        self.tableView.horizontalHeader().setStretchLastSection(True)\n        self.tableView.verticalHeader().setDefaultSectionSize(18)\n        self.tableView.verticalHeader().setHighlightSections(False)\n        self.verticalLayout.addWidget(self.tableView)\n        self.horizontalLayout = QHBoxLayout()\n        self.revealButton = QPushButton(self)\n        self.revealButton.setText(tr(\"Reveal Selected\"))\n        self.horizontalLayout.addWidget(self.revealButton)\n        spacer_item = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.horizontalLayout.addItem(spacer_item)\n        self.closeButton = QPushButton(self)\n        self.closeButton.setText(tr(\"Close\"))\n        self.closeButton.setDefault(True)\n        self.horizontalLayout.addWidget(self.closeButton)\n        self.verticalLayout.addLayout(self.horizontalLayout)\n\n    def showEvent(self, event):\n        # have to do this here as the frameGeometry is not correct until shown\n        move_to_screen_center(self)\n        super().showEvent(event)\n"
  },
  {
    "path": "qt/problem_table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-04-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.column import Column\nfrom qt.table import Table\n\n\nclass ProblemTable(Table):\n    COLUMNS = [\n        Column(\"path\", default_width=150),\n        Column(\"msg\", default_width=150),\n    ]\n\n    def __init__(self, model, view, **kwargs):\n        super().__init__(model, view, **kwargs)\n        # we have to prevent Return from initiating editing.\n        # self.view.editSelected = lambda: None\n"
  },
  {
    "path": "qt/progress_window.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QTimer\nfrom PyQt5.QtWidgets import QDialog, QMessageBox, QVBoxLayout, QLabel, QProgressBar, QPushButton\n\nfrom hscommon.trans import tr\n\n\nclass ProgressWindow:\n    def __init__(self, parent, model):\n        self._window = None\n        self.parent = parent\n        self.model = model\n        model.view = self\n        # We don't have access to QProgressDialog's labels directly, so we se the model label's view\n        # to self and we'll refresh them together.\n        self.model.jobdesc_textfield.view = self\n        self.model.progressdesc_textfield.view = self\n\n    # --- Callbacks\n    def refresh(self):  # Labels\n        if self._window is not None:\n            self._window.setWindowTitle(self.model.jobdesc_textfield.text)\n            self._label.setText(self.model.progressdesc_textfield.text)\n\n    def set_progress(self, last_progress):\n        if self._window is not None:\n            if last_progress < 0:\n                self._progress_bar.setRange(0, 0)\n            else:\n                self._progress_bar.setRange(0, 100)\n            self._progress_bar.setValue(last_progress)\n\n    def show(self):\n        flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint\n        self._window = QDialog(self.parent, flags)\n        self._setup_ui()\n        self._window.setModal(True)\n        self._timer = QTimer(self._window)\n        self._timer.timeout.connect(self.model.pulse)\n        self._window.show()\n        self._timer.start(500)\n\n    def _setup_ui(self):\n        self._window.setWindowTitle(tr(\"Cancel\"))\n        vertical_layout = QVBoxLayout(self._window)\n        self._label = QLabel(\"\", self._window)\n        vertical_layout.addWidget(self._label)\n        self._progress_bar = QProgressBar(self._window)\n        self._progress_bar.setRange(0, 100)\n        vertical_layout.addWidget(self._progress_bar)\n        self._cancel_button = QPushButton(tr(\"Cancel\"), self._window)\n        self._cancel_button.clicked.connect(self.cancel)\n        vertical_layout.addWidget(self._cancel_button)\n\n    def cancel(self):\n        if self._window is not None:\n            confirm_dialog = QMessageBox(\n                QMessageBox.Icon.Question,\n                tr(\"Cancel?\"),\n                tr(\"Are you sure you want to cancel? All progress will be lost.\"),\n                QMessageBox.StandardButton.No | QMessageBox.StandardButton.Yes,\n                self._window,\n            )\n            confirm_dialog.setDefaultButton(QMessageBox.StandardButton.No)\n            result = confirm_dialog.exec_()\n            if result != QMessageBox.StandardButton.Yes:\n                return\n        self.close()\n\n    def close(self):\n        # it seems it is possible for close to be called without a corresponding\n        # show, only perform a close if there is a window to close\n        if self._window is not None:\n            self._timer.stop()\n            del self._timer\n            self._window.close()\n            self._window.setParent(None)\n            self._window = None\n            self.model.cancel()\n"
  },
  {
    "path": "qt/radio_box.py",
    "content": "# Created On: 2010-06-02\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import pyqtSignal\nfrom PyQt5.QtWidgets import QWidget, QHBoxLayout, QRadioButton\n\nfrom qt.util import horizontal_spacer\n\n\nclass RadioBox(QWidget):\n    def __init__(self, parent=None, items=None, spread=True, **kwargs):\n        # If spread is False, insert a spacer in the layout so that the items don't use all the\n        # space they're given but rather align left.\n        if items is None:\n            items = []\n        super().__init__(parent, **kwargs)\n        self._buttons = []\n        self._labels = items\n        self._selected_index = 0\n        self._spacer = horizontal_spacer() if not spread else None\n        self._layout = QHBoxLayout(self)\n        self._update_buttons()\n\n    # --- Private\n    def _update_buttons(self):\n        if self._spacer is not None:\n            self._layout.removeItem(self._spacer)\n        to_remove = self._buttons[len(self._labels) :]\n        for button in to_remove:\n            self._layout.removeWidget(button)\n            button.setParent(None)\n        del self._buttons[len(self._labels) :]\n        to_add = self._labels[len(self._buttons) :]\n        for _ in to_add:\n            button = QRadioButton(self)\n            self._buttons.append(button)\n            self._layout.addWidget(button)\n            button.toggled.connect(self.buttonToggled)\n        if self._spacer is not None:\n            self._layout.addItem(self._spacer)\n        if not self._buttons:\n            return\n        for button, label in zip(self._buttons, self._labels):\n            button.setText(label)\n        self._update_selection()\n\n    def _update_selection(self):\n        self._selected_index = max(0, min(self._selected_index, len(self._buttons) - 1))\n        selected = self._buttons[self._selected_index]\n        selected.setChecked(True)\n\n    # --- Event Handlers\n    def buttonToggled(self):\n        for i, button in enumerate(self._buttons):\n            if button.isChecked():\n                self._selected_index = i\n                self.itemSelected.emit(i)\n                break\n\n    # --- Signals\n    itemSelected = pyqtSignal(int)\n\n    # --- Properties\n    @property\n    def buttons(self):\n        return self._buttons[:]\n\n    @property\n    def items(self):\n        return self._labels[:]\n\n    @items.setter\n    def items(self, value):\n        self._labels = value\n        self._update_buttons()\n\n    @property\n    def selected_index(self):\n        return self._selected_index\n\n    @selected_index.setter\n    def selected_index(self, value):\n        self._selected_index = value\n        self._update_selection()\n"
  },
  {
    "path": "qt/recent.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-11-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom collections import namedtuple\n\nfrom PyQt5.QtCore import pyqtSignal, QObject\nfrom PyQt5.QtWidgets import QAction\n\nfrom hscommon.trans import trget\nfrom hscommon.util import dedupe\n\ntr = trget(\"ui\")\n\nMenuEntry = namedtuple(\"MenuEntry\", \"menu fixedItemCount\")\n\n\nclass Recent(QObject):\n    def __init__(self, app, pref_name, max_item_count=10, **kwargs):\n        super().__init__(**kwargs)\n        self._app = app\n        self._menuEntries = []\n        self._prefName = pref_name\n        self._maxItemCount = max_item_count\n        self._items = []\n        self._loadFromPrefs()\n\n        self._app.willSavePrefs.connect(self._saveToPrefs)\n\n    # --- Private\n    def _loadFromPrefs(self):\n        items = getattr(self._app.prefs, self._prefName)\n        if not isinstance(items, list):\n            items = []\n        self._items = items\n\n    def _insertItem(self, item):\n        self._items = dedupe([item] + self._items)[: self._maxItemCount]\n\n    def _refreshMenu(self, menu_entry):\n        menu, fixed_item_count = menu_entry\n        for action in menu.actions()[fixed_item_count:]:\n            menu.removeAction(action)\n        for item in self._items:\n            action = QAction(item, menu)\n            action.setData(item)\n            action.triggered.connect(self.menuItemWasClicked)\n            menu.addAction(action)\n        menu.addSeparator()\n        action = QAction(tr(\"Clear List\"), menu)\n        action.triggered.connect(self.clear)\n        menu.addAction(action)\n\n    def _refreshAllMenus(self):\n        for menu_entry in self._menuEntries:\n            self._refreshMenu(menu_entry)\n\n    def _saveToPrefs(self):\n        setattr(self._app.prefs, self._prefName, self._items)\n\n    # --- Public\n    def addMenu(self, menu):\n        menu_entry = MenuEntry(menu, len(menu.actions()))\n        self._menuEntries.append(menu_entry)\n        self._refreshMenu(menu_entry)\n\n    def clear(self):\n        self._items = []\n        self._refreshAllMenus()\n        self.itemsChanged.emit()\n\n    def insertItem(self, item):\n        self._insertItem(str(item))\n        self._refreshAllMenus()\n        self.itemsChanged.emit()\n\n    def isEmpty(self):\n        return not bool(self._items)\n\n    # --- Event Handlers\n    def menuItemWasClicked(self):\n        action = self.sender()\n        if action is not None:\n            item = action.data()\n            self.mustOpenItem.emit(item)\n            self._refreshAllMenus()\n\n    # --- Signals\n    mustOpenItem = pyqtSignal(str)\n    itemsChanged = pyqtSignal()\n"
  },
  {
    "path": "qt/result_window.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-04-25\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QRect\nfrom PyQt5.QtWidgets import (\n    QMainWindow,\n    QMenu,\n    QLabel,\n    QFileDialog,\n    QMenuBar,\n    QWidget,\n    QVBoxLayout,\n    QAbstractItemView,\n    QStatusBar,\n    QDialog,\n    QPushButton,\n    QCheckBox,\n    QDesktopWidget,\n)\n\nfrom hscommon.trans import trget\nfrom qt.util import move_to_screen_center, horizontal_wrap, create_actions\nfrom qt.search_edit import SearchEdit\n\nfrom core.app import AppMode\nfrom qt.results_model import ResultsView\nfrom qt.stats_label import StatsLabel\nfrom qt.prioritize_dialog import PrioritizeDialog\nfrom qt.se.results_model import ResultsModel as ResultsModelStandard\nfrom qt.me.results_model import ResultsModel as ResultsModelMusic\nfrom qt.pe.results_model import ResultsModel as ResultsModelPicture\n\ntr = trget(\"ui\")\n\n\nclass ResultWindow(QMainWindow):\n    def __init__(self, parent, app, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.app = app\n        self.specific_actions = set()\n        self._setupUi()\n        if app.model.app_mode == AppMode.PICTURE:\n            MODEL_CLASS = ResultsModelPicture\n        elif app.model.app_mode == AppMode.MUSIC:\n            MODEL_CLASS = ResultsModelMusic\n        else:\n            MODEL_CLASS = ResultsModelStandard\n        self.resultsModel = MODEL_CLASS(self.app, self.resultsView)\n        self.stats = StatsLabel(app.model.stats_label, self.statusLabel)\n        self._update_column_actions_status()\n\n        self.menuColumns.triggered.connect(self.columnToggled)\n        self.resultsView.doubleClicked.connect(self.resultsDoubleClicked)\n        self.resultsView.spacePressed.connect(self.resultsSpacePressed)\n        self.detailsButton.clicked.connect(self.actionDetails.triggered)\n        self.dupesOnlyCheckBox.stateChanged.connect(self.powerMarkerTriggered)\n        self.deltaValuesCheckBox.stateChanged.connect(self.deltaTriggered)\n        self.searchEdit.searchChanged.connect(self.searchChanged)\n        self.app.willSavePrefs.connect(self.appWillSavePrefs)\n\n    def _setupActions(self):\n        # (name, shortcut, icon, desc, func)\n        ACTIONS = [\n            (\"actionDetails\", \"Ctrl+I\", \"\", tr(\"Details\"), self.detailsTriggered),\n            (\"actionActions\", \"\", \"\", tr(\"Actions\"), self.actionsTriggered),\n            (\n                \"actionPowerMarker\",\n                \"Ctrl+1\",\n                \"\",\n                tr(\"Show Dupes Only\"),\n                self.powerMarkerTriggered,\n            ),\n            (\"actionDelta\", \"Ctrl+2\", \"\", tr(\"Show Delta Values\"), self.deltaTriggered),\n            (\n                \"actionDeleteMarked\",\n                \"Ctrl+D\",\n                \"\",\n                tr(\"Send Marked to Recycle Bin...\"),\n                self.deleteTriggered,\n            ),\n            (\n                \"actionMoveMarked\",\n                \"Ctrl+M\",\n                \"\",\n                tr(\"Move Marked to...\"),\n                self.moveTriggered,\n            ),\n            (\n                \"actionCopyMarked\",\n                \"Ctrl+Shift+M\",\n                \"\",\n                tr(\"Copy Marked to...\"),\n                self.copyTriggered,\n            ),\n            (\n                \"actionRemoveMarked\",\n                \"Ctrl+R\",\n                \"\",\n                tr(\"Remove Marked from Results\"),\n                self.removeMarkedTriggered,\n            ),\n            (\n                \"actionReprioritize\",\n                \"\",\n                \"\",\n                tr(\"Re-Prioritize Results...\"),\n                self.reprioritizeTriggered,\n            ),\n            (\n                \"actionRemoveSelected\",\n                \"Ctrl+Del\",\n                \"\",\n                tr(\"Remove Selected from Results\"),\n                self.removeSelectedTriggered,\n            ),\n            (\n                \"actionIgnoreSelected\",\n                \"Ctrl+Shift+Del\",\n                \"\",\n                tr(\"Add Selected to Ignore List\"),\n                self.addToIgnoreListTriggered,\n            ),\n            (\n                \"actionMakeSelectedReference\",\n                \"Ctrl+Space\",\n                \"\",\n                tr(\"Make Selected into Reference\"),\n                self.app.model.make_selected_reference,\n            ),\n            (\n                \"actionOpenSelected\",\n                \"Ctrl+O\",\n                \"\",\n                tr(\"Open Selected with Default Application\"),\n                self.openTriggered,\n            ),\n            (\n                \"actionRevealSelected\",\n                \"Ctrl+Shift+O\",\n                \"\",\n                tr(\"Open Containing Folder of Selected\"),\n                self.revealTriggered,\n            ),\n            (\n                \"actionRenameSelected\",\n                \"F2\",\n                \"\",\n                tr(\"Rename Selected\"),\n                self.renameTriggered,\n            ),\n            (\"actionMarkAll\", \"Ctrl+A\", \"\", tr(\"Mark All\"), self.markAllTriggered),\n            (\n                \"actionMarkNone\",\n                \"Ctrl+Shift+A\",\n                \"\",\n                tr(\"Mark None\"),\n                self.markNoneTriggered,\n            ),\n            (\n                \"actionInvertMarking\",\n                \"Ctrl+Alt+A\",\n                \"\",\n                tr(\"Invert Marking\"),\n                self.markInvertTriggered,\n            ),\n            (\n                \"actionMarkSelected\",\n                Qt.Key_Space,\n                \"\",\n                tr(\"Mark Selected\"),\n                self.markSelectedTriggered,\n            ),\n            (\n                \"actionExportToHTML\",\n                \"\",\n                \"\",\n                tr(\"Export To HTML\"),\n                self.app.model.export_to_xhtml,\n            ),\n            (\n                \"actionExportToCSV\",\n                \"\",\n                \"\",\n                tr(\"Export To CSV\"),\n                self.app.model.export_to_csv,\n            ),\n            (\n                \"actionSaveResults\",\n                \"Ctrl+S\",\n                \"\",\n                tr(\"Save Results...\"),\n                self.saveResultsTriggered,\n            ),\n            (\n                \"actionInvokeCustomCommand\",\n                \"Ctrl+Alt+I\",\n                \"\",\n                tr(\"Invoke Custom Command\"),\n                self.app.invokeCustomCommand,\n            ),\n        ]\n        create_actions(ACTIONS, self)\n        self.actionDelta.setCheckable(True)\n        self.actionPowerMarker.setCheckable(True)\n\n        if self.app.main_window:  # We use tab widgets in this case\n            # Keep track of actions which should only be accessible from this class\n            for action, _, _, _, _ in ACTIONS:\n                self.specific_actions.add(getattr(self, action))\n\n    def _setupMenu(self):\n        if not self.app.use_tabs:\n            # we are our own QMainWindow, we need our own menu bar\n            self.menubar = QMenuBar()  # self.menuBar() works as well here\n            self.menubar.setGeometry(QRect(0, 0, 630, 22))\n            self.menuFile = QMenu(self.menubar)\n            self.menuFile.setTitle(tr(\"File\"))\n            self.menuMark = QMenu(self.menubar)\n            self.menuMark.setTitle(tr(\"Mark\"))\n            self.menuActions = QMenu(self.menubar)\n            self.menuActions.setTitle(tr(\"Actions\"))\n            self.menuColumns = QMenu(self.menubar)\n            self.menuColumns.setTitle(tr(\"Columns\"))\n            self.menuView = QMenu(self.menubar)\n            self.menuView.setTitle(tr(\"View\"))\n            self.menuHelp = QMenu(self.menubar)\n            self.menuHelp.setTitle(tr(\"Help\"))\n            self.setMenuBar(self.menubar)\n            menubar = self.menubar\n        else:\n            # we are part of a tab widget, we populate its window's menubar instead\n            self.menuFile = self.app.main_window.menuFile\n            self.menuMark = self.app.main_window.menuMark\n            self.menuActions = self.app.main_window.menuActions\n            self.menuColumns = self.app.main_window.menuColumns\n            self.menuView = self.app.main_window.menuView\n            self.menuHelp = self.app.main_window.menuHelp\n            menubar = self.app.main_window.menubar\n\n        self.menuActions.addAction(self.actionDeleteMarked)\n        self.menuActions.addAction(self.actionMoveMarked)\n        self.menuActions.addAction(self.actionCopyMarked)\n        self.menuActions.addAction(self.actionRemoveMarked)\n        self.menuActions.addAction(self.actionReprioritize)\n        self.menuActions.addSeparator()\n        self.menuActions.addAction(self.actionRemoveSelected)\n        self.menuActions.addAction(self.actionIgnoreSelected)\n        self.menuActions.addAction(self.actionMakeSelectedReference)\n        self.menuActions.addSeparator()\n        self.menuActions.addAction(self.actionOpenSelected)\n        self.menuActions.addAction(self.actionRevealSelected)\n        self.menuActions.addAction(self.actionInvokeCustomCommand)\n        self.menuActions.addAction(self.actionRenameSelected)\n        self.menuMark.addAction(self.actionMarkAll)\n        self.menuMark.addAction(self.actionMarkNone)\n        self.menuMark.addAction(self.actionInvertMarking)\n        self.menuMark.addAction(self.actionMarkSelected)\n\n        self.menuView.addAction(self.actionDetails)\n        self.menuView.addSeparator()\n        self.menuView.addAction(self.actionPowerMarker)\n        self.menuView.addAction(self.actionDelta)\n        self.menuView.addSeparator()\n        if not self.app.use_tabs:\n            self.menuView.addAction(self.app.actionIgnoreList)\n        # This also pushes back the options entry to the bottom of the menu\n        self.menuView.addSeparator()\n        self.menuView.addAction(self.app.actionPreferences)\n\n        self.menuHelp.addAction(self.app.actionShowHelp)\n        self.menuHelp.addAction(self.app.actionOpenDebugLog)\n        self.menuHelp.addAction(self.app.actionAbout)\n        self.menuFile.addAction(self.actionSaveResults)\n        self.menuFile.addAction(self.actionExportToHTML)\n        self.menuFile.addAction(self.actionExportToCSV)\n        self.menuFile.addSeparator()\n        self.menuFile.addAction(self.app.actionQuit)\n\n        menubar.addAction(self.menuFile.menuAction())\n        menubar.addAction(self.menuMark.menuAction())\n        menubar.addAction(self.menuActions.menuAction())\n        menubar.addAction(self.menuColumns.menuAction())\n        menubar.addAction(self.menuView.menuAction())\n        menubar.addAction(self.menuHelp.menuAction())\n\n        # Columns menu\n        menu = self.menuColumns\n        # Avoid adding duplicate actions in tab widget menu in case we recreated\n        # the Result Window instance.\n        if menu.actions():\n            menu.clear()\n        self._column_actions = []\n        for index, (display, visible) in enumerate(self.app.model.result_table._columns.menu_items()):\n            action = menu.addAction(display)\n            action.setCheckable(True)\n            action.setChecked(visible)\n            action.item_index = index\n            self._column_actions.append(action)\n        menu.addSeparator()\n        action = menu.addAction(tr(\"Reset to Defaults\"))\n        action.item_index = -1\n\n        # Action menu\n        action_menu = QMenu(tr(\"Actions\"), menubar)\n        action_menu.addAction(self.actionDeleteMarked)\n        action_menu.addAction(self.actionMoveMarked)\n        action_menu.addAction(self.actionCopyMarked)\n        action_menu.addAction(self.actionRemoveMarked)\n        action_menu.addSeparator()\n        action_menu.addAction(self.actionRemoveSelected)\n        action_menu.addAction(self.actionIgnoreSelected)\n        action_menu.addAction(self.actionMakeSelectedReference)\n        action_menu.addSeparator()\n        action_menu.addAction(self.actionOpenSelected)\n        action_menu.addAction(self.actionRevealSelected)\n        action_menu.addAction(self.actionInvokeCustomCommand)\n        action_menu.addAction(self.actionRenameSelected)\n        self.actionActions.setMenu(action_menu)\n        self.actionsButton.setMenu(self.actionActions.menu())\n\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"{} Results\").format(self.app.NAME))\n        self.resize(630, 514)\n        self.centralwidget = QWidget(self)\n        self.verticalLayout = QVBoxLayout(self.centralwidget)\n        self.verticalLayout.setContentsMargins(0, 0, 0, 0)\n        self.verticalLayout.setSpacing(0)\n        self.actionsButton = QPushButton(tr(\"Actions\"))\n        self.detailsButton = QPushButton(tr(\"Details\"))\n        self.dupesOnlyCheckBox = QCheckBox(tr(\"Dupes Only\"))\n        self.deltaValuesCheckBox = QCheckBox(tr(\"Delta Values\"))\n        self.searchEdit = SearchEdit()\n        self.searchEdit.setMaximumWidth(300)\n        self.horizontalLayout = horizontal_wrap(\n            [\n                self.actionsButton,\n                self.detailsButton,\n                self.dupesOnlyCheckBox,\n                self.deltaValuesCheckBox,\n                None,\n                self.searchEdit,\n                8,\n            ]\n        )\n        self.horizontalLayout.setSpacing(8)\n        self.verticalLayout.addLayout(self.horizontalLayout)\n        self.resultsView = ResultsView(self.centralwidget)\n        self.resultsView.setSelectionMode(QAbstractItemView.ExtendedSelection)\n        self.resultsView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.resultsView.setSortingEnabled(True)\n        self.resultsView.setWordWrap(False)\n        self.resultsView.verticalHeader().setVisible(False)\n        h = self.resultsView.horizontalHeader()\n        h.setHighlightSections(False)\n        h.setSectionsMovable(True)\n        h.setStretchLastSection(False)\n        h.setDefaultAlignment(Qt.AlignLeft)\n        self.verticalLayout.addWidget(self.resultsView)\n        self.setCentralWidget(self.centralwidget)\n        self._setupActions()\n        self._setupMenu()\n        self.statusbar = QStatusBar(self)\n        self.statusbar.setSizeGripEnabled(True)\n        self.setStatusBar(self.statusbar)\n        self.statusLabel = QLabel(self)\n        self.statusbar.addPermanentWidget(self.statusLabel, 1)\n\n        if self.app.prefs.resultWindowIsMaximized:\n            self.setWindowState(self.windowState() | Qt.WindowMaximized)\n        else:\n            if self.app.prefs.resultWindowRect is not None:\n                self.setGeometry(self.app.prefs.resultWindowRect)\n                # if not on any screen move to center of default screen\n                # moves to center of closest screen if partially off screen\n                frame = self.frameGeometry()\n                if QDesktopWidget().screenNumber(self) == -1:\n                    move_to_screen_center(self)\n                elif QDesktopWidget().availableGeometry(self).contains(frame) is False:\n                    frame.moveCenter(QDesktopWidget().availableGeometry(self).center())\n                    self.move(frame.topLeft())\n            else:\n                move_to_screen_center(self)\n\n    # --- Private\n    def _update_column_actions_status(self):\n        # Update menu checked state\n        menu_items = self.app.model.result_table._columns.menu_items()\n        for action, (display, visible) in zip(self._column_actions, menu_items):\n            action.setChecked(visible)\n\n    # --- Actions\n    def actionsTriggered(self):\n        self.actionsButton.showMenu()\n\n    def addToIgnoreListTriggered(self):\n        self.app.model.add_selected_to_ignore_list()\n\n    def copyTriggered(self):\n        self.app.model.copy_or_move_marked(True)\n\n    def deleteTriggered(self):\n        self.app.model.delete_marked()\n\n    def deltaTriggered(self, state=None):\n        # The sender can be either the action or the checkbox, but both have a isChecked() method.\n        self.resultsModel.delta_values = self.sender().isChecked()\n        self.actionDelta.setChecked(self.resultsModel.delta_values)\n        self.deltaValuesCheckBox.setChecked(self.resultsModel.delta_values)\n\n    def detailsTriggered(self):\n        self.app.show_details()\n\n    def markAllTriggered(self):\n        self.app.model.mark_all()\n\n    def markInvertTriggered(self):\n        self.app.model.mark_invert()\n\n    def markNoneTriggered(self):\n        self.app.model.mark_none()\n\n    def markSelectedTriggered(self):\n        self.app.model.toggle_selected_mark_state()\n\n    def moveTriggered(self):\n        self.app.model.copy_or_move_marked(False)\n\n    def openTriggered(self):\n        self.app.model.open_selected()\n\n    def powerMarkerTriggered(self, state=None):\n        # see deltaTriggered\n        self.resultsModel.power_marker = self.sender().isChecked()\n        self.actionPowerMarker.setChecked(self.resultsModel.power_marker)\n        self.dupesOnlyCheckBox.setChecked(self.resultsModel.power_marker)\n\n    def preferencesTriggered(self):\n        self.app.show_preferences()\n\n    def removeMarkedTriggered(self):\n        self.app.model.remove_marked()\n\n    def removeSelectedTriggered(self):\n        self.app.model.remove_selected()\n\n    def renameTriggered(self):\n        index = self.resultsView.selectionModel().currentIndex()\n        # Our index is the current row, with column set to 0. Our filename column is 1 and that's\n        # what we want.\n        index = index.sibling(index.row(), 1)\n        self.resultsView.edit(index)\n\n    def reprioritizeTriggered(self):\n        dlg = PrioritizeDialog(self, self.app)\n        result = dlg.exec()\n        if result == QDialog.Accepted:\n            dlg.model.perform_reprioritization()\n\n    def revealTriggered(self):\n        self.app.model.reveal_selected()\n\n    def saveResultsTriggered(self):\n        title = tr(\"Select a file to save your results to\")\n        files = tr(\"dupeGuru Results (*.dupeguru)\")\n        destination, chosen_filter = QFileDialog.getSaveFileName(self, title, \"\", files)\n        if destination:\n            if not destination.endswith(\".dupeguru\"):\n                destination = f\"{destination}.dupeguru\"\n            self.app.model.save_as(destination)\n            self.app.recentResults.insertItem(destination)\n\n    # --- Events\n    def appWillSavePrefs(self):\n        prefs = self.app.prefs\n        prefs.resultWindowIsMaximized = self.isMaximized()\n        prefs.resultWindowRect = self.geometry()\n\n    def columnToggled(self, action):\n        index = action.item_index\n        if index == -1:\n            self.app.model.result_table._columns.reset_to_defaults()\n            self._update_column_actions_status()\n        else:\n            visible = self.app.model.result_table._columns.toggle_menu_item(index)\n            action.setChecked(visible)\n\n    def contextMenuEvent(self, event):\n        self.actionActions.menu().exec_(event.globalPos())\n\n    def resultsDoubleClicked(self, model_index):\n        self.app.model.open_selected()\n\n    def resultsSpacePressed(self):\n        self.app.model.toggle_selected_mark_state()\n\n    def searchChanged(self):\n        self.app.model.apply_filter(self.searchEdit.text())\n\n    def closeEvent(self, event):\n        # this saves the location of the results window when it is closed\n        self.appWillSavePrefs()\n"
  },
  {
    "path": "qt/results_model.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-04-23\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, pyqtSignal, QModelIndex\nfrom PyQt5.QtGui import QBrush, QFont, QFontMetrics\nfrom PyQt5.QtWidgets import QTableView\n\nfrom qt.table import Table\n\n\nclass ResultsModel(Table):\n    def __init__(self, app, view, **kwargs):\n        model = app.model.result_table\n        super().__init__(model, view, **kwargs)\n        view.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)\n        font = view.font()\n        font.setPointSize(app.prefs.tableFontSize)\n        view.setFont(font)\n        fm = QFontMetrics(font)\n        view.verticalHeader().setDefaultSectionSize(fm.height() + 2)\n\n        app.willSavePrefs.connect(self.appWillSavePrefs)\n        self.prefs = app.prefs\n\n    def _getData(self, row, column, role):\n        if column.name == \"marked\":\n            if role == Qt.BackgroundRole and row.isref:\n                return QBrush(self.prefs.result_table_ref_background_color)\n            if role == Qt.CheckStateRole and row.markable:\n                return Qt.Checked if row.marked else Qt.Unchecked\n            return None\n        if role == Qt.DisplayRole:\n            data = row.data_delta if self.model.delta_values else row.data\n            return data[column.name]\n        elif role == Qt.ForegroundRole:\n            if row.isref:\n                return QBrush(self.prefs.result_table_ref_foreground_color)\n            elif row.is_cell_delta(column.name):\n                return QBrush(self.prefs.result_table_delta_foreground_color)\n        elif role == Qt.BackgroundRole:\n            if row.isref:\n                return QBrush(self.prefs.result_table_ref_background_color)\n        elif role == Qt.FontRole:\n            font = QFont(self.view.font())\n            if self.prefs.reference_bold_font:\n                font.setBold(row.isref)\n            return font\n        elif role == Qt.EditRole and column.name == \"name\":\n            return row.data[column.name]\n        return None\n\n    def _getFlags(self, row, column):\n        flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable\n        if column.name == \"marked\":\n            if row.markable:\n                flags |= Qt.ItemIsUserCheckable\n        elif column.name == \"name\":\n            flags |= Qt.ItemIsEditable\n        return flags\n\n    def _setData(self, row, column, value, role):\n        if role == Qt.CheckStateRole:\n            if column.name == \"marked\":\n                row.marked = bool(value)\n                return True\n        elif role == Qt.EditRole and column.name == \"name\":\n            return self.model.rename_selected(value)\n        return False\n\n    def sort(self, column, order):\n        column = self.model.COLUMNS[column]\n        self.model.sort(column.name, order == Qt.AscendingOrder)\n\n    # --- Properties\n    @property\n    def power_marker(self):\n        return self.model.power_marker\n\n    @power_marker.setter\n    def power_marker(self, value):\n        self.model.power_marker = value\n\n    @property\n    def delta_values(self):\n        return self.model.delta_values\n\n    @delta_values.setter\n    def delta_values(self, value):\n        self.model.delta_values = value\n\n    # --- Events\n    def appWillSavePrefs(self):\n        self.model._columns.save_columns()\n\n    # --- model --> view\n    def invalidate_markings(self):\n        # redraw view\n        # HACK. this is the only way I found to update the widget without reseting everything\n        self.view.scroll(0, 1)\n        self.view.scroll(0, -1)\n\n\nclass ResultsView(QTableView):\n    # --- Override\n    def keyPressEvent(self, event):\n        if event.text() == \" \":\n            self.spacePressed.emit()\n            return\n        super().keyPressEvent(event)\n\n    def mouseDoubleClickEvent(self, event):\n        self.doubleClicked.emit(QModelIndex())\n        # We don't call the superclass' method because the default behavior is to rename the cell.\n\n    # --- Signals\n    spacePressed = pyqtSignal()\n"
  },
  {
    "path": "qt/se/__init__.py",
    "content": ""
  },
  {
    "path": "qt/se/details_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QSize\nfrom PyQt5.QtWidgets import QAbstractItemView\n\nfrom hscommon.trans import trget\nfrom qt.details_dialog import DetailsDialog as DetailsDialogBase\nfrom qt.details_table import DetailsTable\n\ntr = trget(\"ui\")\n\n\nclass DetailsDialog(DetailsDialogBase):\n    def _setupUi(self):\n        self.setWindowTitle(tr(\"Details\"))\n        self.resize(502, 186)\n        self.setMinimumSize(QSize(200, 0))\n        self.tableView = DetailsTable(self)\n        self.tableView.setAlternatingRowColors(True)\n        self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows)\n        self.tableView.setShowGrid(False)\n        self.setWidget(self.tableView)\n"
  },
  {
    "path": "qt/se/preferences_dialog.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QSize\nfrom PyQt5.QtWidgets import (\n    QSpinBox,\n    QVBoxLayout,\n    QHBoxLayout,\n    QLabel,\n    QSizePolicy,\n    QSpacerItem,\n    QWidget,\n)\n\nfrom hscommon.trans import trget\n\nfrom core.app import AppMode\nfrom core.scanner import ScanType\n\nfrom qt.preferences_dialog import PreferencesDialogBase\n\ntr = trget(\"ui\")\n\n\nclass PreferencesDialog(PreferencesDialogBase):\n    def _setupPreferenceWidgets(self):\n        self._setupFilterHardnessBox()\n        self.widgetsVLayout.addLayout(self.filterHardnessHLayout)\n        self.widget = QWidget(self)\n        self.widget.setMinimumSize(QSize(0, 136))\n        self.verticalLayout_4 = QVBoxLayout(self.widget)\n        self._setupAddCheckbox(\"wordWeightingBox\", tr(\"Word weighting\"), self.widget)\n        self.verticalLayout_4.addWidget(self.wordWeightingBox)\n        self._setupAddCheckbox(\"matchSimilarBox\", tr(\"Match similar words\"), self.widget)\n        self.verticalLayout_4.addWidget(self.matchSimilarBox)\n        self._setupAddCheckbox(\"mixFileKindBox\", tr(\"Can mix file kind\"), self.widget)\n        self.verticalLayout_4.addWidget(self.mixFileKindBox)\n        self._setupAddCheckbox(\"useRegexpBox\", tr(\"Use regular expressions when filtering\"), self.widget)\n        self.verticalLayout_4.addWidget(self.useRegexpBox)\n        self._setupAddCheckbox(\n            \"removeEmptyFoldersBox\",\n            tr(\"Remove empty folders on delete or move\"),\n            self.widget,\n        )\n        self.verticalLayout_4.addWidget(self.removeEmptyFoldersBox)\n        self.horizontalLayout_2 = QHBoxLayout()\n        self._setupAddCheckbox(\"ignoreSmallFilesBox\", tr(\"Ignore files smaller than\"), self.widget)\n        self.horizontalLayout_2.addWidget(self.ignoreSmallFilesBox)\n        self.sizeThresholdSpinBox = QSpinBox(self.widget)\n        size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)\n        size_policy.setHorizontalStretch(0)\n        size_policy.setVerticalStretch(0)\n        size_policy.setHeightForWidth(self.sizeThresholdSpinBox.sizePolicy().hasHeightForWidth())\n        self.sizeThresholdSpinBox.setSizePolicy(size_policy)\n        self.sizeThresholdSpinBox.setMaximumSize(QSize(300, 16777215))\n        self.sizeThresholdSpinBox.setRange(0, 1000000)\n        self.horizontalLayout_2.addWidget(self.sizeThresholdSpinBox)\n        self.label_6 = QLabel(self.widget)\n        self.label_6.setText(tr(\"KB\"))\n        self.horizontalLayout_2.addWidget(self.label_6)\n        spacer_item1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.horizontalLayout_2.addItem(spacer_item1)\n        self.verticalLayout_4.addLayout(self.horizontalLayout_2)\n        self.horizontalLayout_2a = QHBoxLayout()\n        self._setupAddCheckbox(\"ignoreLargeFilesBox\", tr(\"Ignore files larger than\"), self.widget)\n        self.horizontalLayout_2a.addWidget(self.ignoreLargeFilesBox)\n        self.sizeSaturationSpinBox = QSpinBox(self.widget)\n        size_policy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)\n        self.sizeSaturationSpinBox.setSizePolicy(size_policy)\n        self.sizeSaturationSpinBox.setMaximumSize(QSize(300, 16777215))\n        self.sizeSaturationSpinBox.setRange(0, 1000000)\n        self.horizontalLayout_2a.addWidget(self.sizeSaturationSpinBox)\n        self.label_6a = QLabel(self.widget)\n        self.label_6a.setText(tr(\"MB\"))\n        self.horizontalLayout_2a.addWidget(self.label_6a)\n        spacer_item3 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.horizontalLayout_2a.addItem(spacer_item3)\n        self.verticalLayout_4.addLayout(self.horizontalLayout_2a)\n        self.horizontalLayout_2b = QHBoxLayout()\n        self._setupAddCheckbox(\n            \"bigFilePartialHashesBox\",\n            tr(\"Partially hash files bigger than\"),\n            self.widget,\n        )\n        self.horizontalLayout_2b.addWidget(self.bigFilePartialHashesBox)\n        self.bigSizeThresholdSpinBox = QSpinBox(self.widget)\n        self.bigSizeThresholdSpinBox.setSizePolicy(size_policy)\n        self.bigSizeThresholdSpinBox.setMaximumSize(QSize(300, 16777215))\n        self.bigSizeThresholdSpinBox.setRange(0, 1000000)\n        self.horizontalLayout_2b.addWidget(self.bigSizeThresholdSpinBox)\n        self.label_6b = QLabel(self.widget)\n        self.label_6b.setText(tr(\"MB\"))\n        self.horizontalLayout_2b.addWidget(self.label_6b)\n        spacer_item2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)\n        self.horizontalLayout_2b.addItem(spacer_item2)\n        self.verticalLayout_4.addLayout(self.horizontalLayout_2b)\n        self._setupAddCheckbox(\n            \"ignoreHardlinkMatches\",\n            tr(\"Ignore duplicates hardlinking to the same file\"),\n            self.widget,\n        )\n        self.verticalLayout_4.addWidget(self.ignoreHardlinkMatches)\n        self.widgetsVLayout.addWidget(self.widget)\n        self._setupBottomPart()\n\n    def _load(self, prefs, setchecked, section):\n        setchecked(self.matchSimilarBox, prefs.match_similar)\n        setchecked(self.wordWeightingBox, prefs.word_weighting)\n        setchecked(self.ignoreSmallFilesBox, prefs.ignore_small_files)\n        self.sizeThresholdSpinBox.setValue(prefs.small_file_threshold)\n        setchecked(self.ignoreLargeFilesBox, prefs.ignore_large_files)\n        self.sizeSaturationSpinBox.setValue(prefs.large_file_threshold)\n        setchecked(self.bigFilePartialHashesBox, prefs.big_file_partial_hashes)\n        self.bigSizeThresholdSpinBox.setValue(prefs.big_file_size_threshold)\n\n        # Update UI state based on selected scan type\n        scan_type = prefs.get_scan_type(AppMode.STANDARD)\n        word_based = scan_type == ScanType.FILENAME\n        self.filterHardnessSlider.setEnabled(word_based)\n        self.matchSimilarBox.setEnabled(word_based)\n        self.wordWeightingBox.setEnabled(word_based)\n\n    def _save(self, prefs, ischecked):\n        prefs.match_similar = ischecked(self.matchSimilarBox)\n        prefs.word_weighting = ischecked(self.wordWeightingBox)\n        prefs.ignore_small_files = ischecked(self.ignoreSmallFilesBox)\n        prefs.small_file_threshold = self.sizeThresholdSpinBox.value()\n        prefs.ignore_large_files = ischecked(self.ignoreLargeFilesBox)\n        prefs.large_file_threshold = self.sizeSaturationSpinBox.value()\n        prefs.big_file_partial_hashes = ischecked(self.bigFilePartialHashesBox)\n        prefs.big_file_size_threshold = self.bigSizeThresholdSpinBox.value()\n"
  },
  {
    "path": "qt/se/results_model.py",
    "content": "# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom qt.column import Column\nfrom qt.results_model import ResultsModel as ResultsModelBase\n\n\nclass ResultsModel(ResultsModelBase):\n    COLUMNS = [\n        Column(\"marked\", default_width=30),\n        Column(\"name\", default_width=200),\n        Column(\"folder_path\", default_width=180),\n        Column(\"size\", default_width=60),\n        Column(\"extension\", default_width=40),\n        Column(\"mtime\", default_width=120),\n        Column(\"percentage\", default_width=60),\n        Column(\"words\", default_width=120),\n        Column(\"dupe_count\", default_width=80),\n    ]\n"
  },
  {
    "path": "qt/search_edit.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-12-10\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import pyqtSignal, Qt\nfrom PyQt5.QtGui import QIcon, QPixmap, QPainter, QPalette\nfrom PyQt5.QtWidgets import QToolButton, QLineEdit, QStyle, QStyleOptionFrame\n\nfrom hscommon.trans import trget\n\ntr = trget(\"ui\")\n\n# IMPORTANT: For this widget to work propertly, you have to add \"search_clear_13\" from the\n# \"images\" folder in your resources.\n\n\nclass LineEditButton(QToolButton):\n    def __init__(self, parent, **kwargs):\n        super().__init__(parent, **kwargs)\n        pixmap = QPixmap(\":/search_clear_13\")\n        self.setIcon(QIcon(pixmap))\n        self.setIconSize(pixmap.size())\n        self.setCursor(Qt.ArrowCursor)\n        self.setPopupMode(QToolButton.InstantPopup)\n        stylesheet = \"QToolButton { border: none; padding: 0px; }\"\n        self.setStyleSheet(stylesheet)\n\n\nclass ClearableEdit(QLineEdit):\n    def __init__(self, parent=None, is_clearable=True, **kwargs):\n        super().__init__(parent, **kwargs)\n        self._is_clearable = is_clearable\n        if is_clearable:\n            self._clearButton = LineEditButton(self)\n            frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)\n            padding_right = self._clearButton.sizeHint().width() + frame_width + 1\n            stylesheet = f\"QLineEdit {{ padding-right:{padding_right}px; }}\"\n            self.setStyleSheet(stylesheet)\n            self._updateClearButton()\n\n            self._clearButton.clicked.connect(self._clearSearch)\n        self.textChanged.connect(self._textChanged)\n\n    # --- Private\n    def _clearSearch(self):\n        self.clear()\n\n    def _updateClearButton(self):\n        self._clearButton.setVisible(self._hasClearableContent())\n\n    def _hasClearableContent(self):\n        return bool(self.text())\n\n    # --- QLineEdit overrides\n    def resizeEvent(self, event):\n        if self._is_clearable:\n            frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)\n            rect = self.rect()\n            right_hint = self._clearButton.sizeHint()\n            right_x = rect.right() - frame_width - right_hint.width()\n            right_y = (rect.bottom() - right_hint.height()) // 2\n            self._clearButton.move(right_x, right_y)\n\n    # --- Event Handlers\n    def _textChanged(self, text):\n        if self._is_clearable:\n            self._updateClearButton()\n\n\nclass SearchEdit(ClearableEdit):\n    def __init__(self, parent=None, immediate=False):\n        # immediate: send searchChanged signals at each keystroke.\n        ClearableEdit.__init__(self, parent, is_clearable=True)\n        self.inactiveText = tr(\"Search...\")\n        self.immediate = immediate\n\n        self.returnPressed.connect(self._returnPressed)\n\n    # --- Overrides\n    def _clearSearch(self):\n        ClearableEdit._clearSearch(self)\n        self.searchChanged.emit()\n\n    def _textChanged(self, text):\n        ClearableEdit._textChanged(self, text)\n        if self.immediate:\n            self.searchChanged.emit()\n\n    def keyPressEvent(self, event):\n        key = event.key()\n        if key == Qt.Key_Escape:\n            self._clearSearch()\n        else:\n            ClearableEdit.keyPressEvent(self, event)\n\n    def paintEvent(self, event):\n        ClearableEdit.paintEvent(self, event)\n        if not bool(self.text()) and self.inactiveText and not self.hasFocus():\n            panel = QStyleOptionFrame()\n            self.initStyleOption(panel)\n            text_rect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)\n            left_margin = 2\n            right_margin = self._clearButton.iconSize().width()\n            text_rect.adjust(left_margin, 0, -right_margin, 0)\n            painter = QPainter(self)\n            disabled_color = self.palette().brush(QPalette.Disabled, QPalette.Text).color()\n            painter.setPen(disabled_color)\n            painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, self.inactiveText)\n\n    # --- Event Handlers\n    def _returnPressed(self):\n        if not self.immediate:\n            self.searchChanged.emit()\n\n    # --- Signals\n    searchChanged = pyqtSignal()  # Emitted when return is pressed or when the test is cleared\n"
  },
  {
    "path": "qt/selectable_list.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-09-06\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import Qt, QAbstractListModel, QItemSelection, QItemSelectionModel\n\n\nclass SelectableList(QAbstractListModel):\n    def __init__(self, model, view, **kwargs):\n        super().__init__(**kwargs)\n        self._updating = False\n        self.view = view\n        self.model = model\n        self.view.setModel(self)\n        self.model.view = self\n\n    # --- Override\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n        # We need EditRole for QComboBoxes with setEditable(True)\n        if role in {Qt.DisplayRole, Qt.EditRole}:\n            return self.model[index.row()]\n        return None\n\n    def rowCount(self, index):\n        if index.isValid():\n            return 0\n        return len(self.model)\n\n    # --- Virtual\n    def _updateSelection(self):\n        raise NotImplementedError()\n\n    def _restoreSelection(self):\n        raise NotImplementedError()\n\n    # --- model --> view\n    def refresh(self):\n        self._updating = True\n        self.beginResetModel()\n        self.endResetModel()\n        self._updating = False\n        self._restoreSelection()\n\n    def update_selection(self):\n        self._restoreSelection()\n\n\nclass ComboboxModel(SelectableList):\n    def __init__(self, model, view, **kwargs):\n        super().__init__(model, view, **kwargs)\n        self.view.currentIndexChanged[int].connect(self.selectionChanged)\n\n    # --- Override\n    def _updateSelection(self):\n        index = self.view.currentIndex()\n        if index != self.model.selected_index:\n            self.model.select(index)\n\n    def _restoreSelection(self):\n        index = self.model.selected_index\n        if index is not None:\n            self.view.setCurrentIndex(index)\n\n    # --- Events\n    def selectionChanged(self, index):\n        if not self._updating:\n            self._updateSelection()\n\n\nclass ListviewModel(SelectableList):\n    def __init__(self, model, view, **kwargs):\n        super().__init__(model, view, **kwargs)\n        self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)\n\n    # --- Override\n    def _updateSelection(self):\n        new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]\n        if new_indexes != self.model.selected_indexes:\n            self.model.select(new_indexes)\n\n    def _restoreSelection(self):\n        new_selection = QItemSelection()\n        for index in self.model.selected_indexes:\n            new_selection.select(self.createIndex(index, 0), self.createIndex(index, 0))\n        self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect)\n        if len(new_selection.indexes()):\n            current_index = new_selection.indexes()[0]\n            self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current)\n            self.view.scrollTo(current_index)\n\n    # --- Events\n    def selectionChanged(self, index):\n        if not self._updating:\n            self._updateSelection()\n"
  },
  {
    "path": "qt/stats_label.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2010-02-12\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\n\nclass StatsLabel:\n    def __init__(self, model, view):\n        self.view = view\n        self.model = model\n        self.model.view = self\n\n    def refresh(self):\n        self.view.setText(self.model.display)\n"
  },
  {
    "path": "qt/tabbed_window.py",
    "content": "# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nfrom PyQt5.QtCore import QRect, pyqtSlot, Qt, QEvent\nfrom PyQt5.QtWidgets import (\n    QWidget,\n    QVBoxLayout,\n    QHBoxLayout,\n    QMainWindow,\n    QTabWidget,\n    QMenu,\n    QTabBar,\n    QStackedWidget,\n)\nfrom hscommon.trans import trget\nfrom qt.util import move_to_screen_center, create_actions\nfrom qt.directories_dialog import DirectoriesDialog\nfrom qt.result_window import ResultWindow\nfrom qt.ignore_list_dialog import IgnoreListDialog\nfrom qt.exclude_list_dialog import ExcludeListDialog\n\ntr = trget(\"ui\")\n\n\nclass TabWindow(QMainWindow):\n    def __init__(self, app, **kwargs):\n        super().__init__(None, **kwargs)\n        self.app = app\n        self.pages = {}  # This is currently not used anywhere\n        self.menubar = None\n        self.menuList = set()\n        self.last_index = -1\n        self.previous_widget_actions = set()\n        self._setupUi()\n        self.app.willSavePrefs.connect(self.appWillSavePrefs)\n\n    def _setupActions(self):\n        # (name, shortcut, icon, desc, func)\n        ACTIONS = [\n            (\n                \"actionToggleTabs\",\n                \"\",\n                \"\",\n                tr(\"Show tab bar\"),\n                self.toggleTabBar,\n            ),\n        ]\n        create_actions(ACTIONS, self)\n        self.actionToggleTabs.setCheckable(True)\n        self.actionToggleTabs.setChecked(True)\n\n    def _setupUi(self):\n        self.setWindowTitle(self.app.NAME)\n        self.resize(640, 480)\n        self.tabWidget = QTabWidget()\n        # self.tabWidget.setTabPosition(QTabWidget.South)\n        self.tabWidget.setContentsMargins(0, 0, 0, 0)\n        # self.tabWidget.setTabBarAutoHide(True)\n        # This gets rid of the annoying margin around the TabWidget:\n        self.tabWidget.setDocumentMode(True)\n\n        self._setupActions()\n        self._setupMenu()\n        # This should be the same as self.centralWidget.setLayout(self.verticalLayout)\n        self.verticalLayout = QVBoxLayout(self.tabWidget)\n        # self.verticalLayout.addWidget(self.tabWidget)\n        self.verticalLayout.setContentsMargins(0, 0, 0, 0)\n        self.tabWidget.setTabsClosable(True)\n        self.setCentralWidget(self.tabWidget)  # only for QMainWindow\n\n        self.tabWidget.currentChanged.connect(self.updateMenuBar)\n        self.tabWidget.tabCloseRequested.connect(self.onTabCloseRequested)\n        self.updateMenuBar(self.tabWidget.currentIndex())\n        self.restoreGeometry()\n\n    def restoreGeometry(self):\n        if self.app.prefs.mainWindowRect is not None:\n            self.setGeometry(self.app.prefs.mainWindowRect)\n        if self.app.prefs.mainWindowIsMaximized:\n            self.showMaximized()\n\n    def _setupMenu(self):\n        \"\"\"Setup the menubar boiler plates which will be filled by the underlying\n        tab's widgets whenever they are instantiated.\"\"\"\n        self.menubar = self.menuBar()  # QMainWindow, similar to just QMenuBar() here\n        # self.setMenuBar(self.menubar)  # already set if QMainWindow class\n        self.menubar.setGeometry(QRect(0, 0, 100, 22))\n        self.menuFile = QMenu(self.menubar)\n        self.menuFile.setTitle(tr(\"File\"))\n        self.menuMark = QMenu(self.menubar)\n        self.menuMark.setTitle(tr(\"Mark\"))\n        self.menuActions = QMenu(self.menubar)\n        self.menuActions.setTitle(tr(\"Actions\"))\n        self.menuColumns = QMenu(self.menubar)\n        self.menuColumns.setTitle(tr(\"Columns\"))\n        self.menuView = QMenu(self.menubar)\n        self.menuView.setTitle(tr(\"View\"))\n        self.menuHelp = QMenu(self.menubar)\n        self.menuHelp.setTitle(tr(\"Help\"))\n\n        self.menuView.addAction(self.actionToggleTabs)\n        self.menuView.addSeparator()\n\n        self.menuList.add(self.menuFile)\n        self.menuList.add(self.menuMark)\n        self.menuList.add(self.menuActions)\n        self.menuList.add(self.menuColumns)\n        self.menuList.add(self.menuView)\n        self.menuList.add(self.menuHelp)\n\n    @pyqtSlot(int)\n    def updateMenuBar(self, page_index=-1):\n        if page_index < 0:\n            return\n        current_index = self.getCurrentIndex()\n        active_widget = self.getWidgetAtIndex(current_index)\n        if self.last_index < 0:\n            self.last_index = current_index\n            self.previous_widget_actions = active_widget.specific_actions\n            return\n\n        page_type = type(active_widget).__name__\n        for menu in self.menuList:\n            if menu is self.menuColumns or menu is self.menuActions or menu is self.menuMark:\n                if not isinstance(active_widget, ResultWindow):\n                    menu.setEnabled(False)\n                    continue\n                else:\n                    menu.setEnabled(True)\n            for action in menu.actions():\n                if action not in active_widget.specific_actions:\n                    if action in self.previous_widget_actions:\n                        action.setEnabled(False)\n                    continue\n                action.setEnabled(True)\n\n        self.app.directories_dialog.actionShowResultsWindow.setEnabled(\n            False if page_type == \"ResultWindow\" else self.app.resultWindow is not None\n        )\n        self.app.actionIgnoreList.setEnabled(\n            True if self.app.ignoreListDialog is not None and not page_type == \"IgnoreListDialog\" else False\n        )\n        self.app.actionDirectoriesWindow.setEnabled(False if page_type == \"DirectoriesDialog\" else True)\n        self.app.actionExcludeList.setEnabled(\n            True if self.app.excludeListDialog is not None and not page_type == \"ExcludeListDialog\" else False\n        )\n\n        self.previous_widget_actions = active_widget.specific_actions\n        self.last_index = current_index\n\n    def createPage(self, cls, **kwargs):\n        app = kwargs.get(\"app\", self.app)\n        page = None\n        if cls == \"DirectoriesDialog\":\n            page = DirectoriesDialog(app)\n        elif cls == \"ResultWindow\":\n            parent = kwargs.get(\"parent\", self)\n            page = ResultWindow(parent, app)\n        elif cls == \"IgnoreListDialog\":\n            parent = kwargs.get(\"parent\", self)\n            model = kwargs.get(\"model\")\n            page = IgnoreListDialog(parent, model)\n            page.accepted.connect(self.onDialogAccepted)\n        elif cls == \"ExcludeListDialog\":\n            app = kwargs.get(\"app\", app)\n            parent = kwargs.get(\"parent\", self)\n            model = kwargs.get(\"model\")\n            page = ExcludeListDialog(app, parent, model)\n            page.accepted.connect(self.onDialogAccepted)\n        self.pages[cls] = page  # Not used, might remove\n        return page\n\n    def addTab(self, page, title, switch=False):\n        # Warning: this supposedly takes ownership of the page\n        index = self.tabWidget.addTab(page, title)\n        if isinstance(page, DirectoriesDialog):\n            self.tabWidget.tabBar().setTabButton(index, QTabBar.RightSide, None)\n        if switch:\n            self.setCurrentIndex(index)\n        return index\n\n    def showTab(self, page):\n        index = self.indexOfWidget(page)\n        self.setCurrentIndex(index)\n\n    def indexOfWidget(self, widget):\n        return self.tabWidget.indexOf(widget)\n\n    def setCurrentIndex(self, index):\n        return self.tabWidget.setCurrentIndex(index)\n\n    def removeTab(self, index):\n        return self.tabWidget.removeTab(index)\n\n    def isTabVisible(self, index):\n        return self.tabWidget.isTabVisible(index)\n\n    def getCurrentIndex(self):\n        return self.tabWidget.currentIndex()\n\n    def getWidgetAtIndex(self, index):\n        return self.tabWidget.widget(index)\n\n    def getCount(self):\n        return self.tabWidget.count()\n\n    # --- Events\n    def appWillSavePrefs(self):\n        # Right now this is useless since the first spawned dialog inside the\n        # QTabWidget will assign its geometry after restoring it\n        prefs = self.app.prefs\n        prefs.mainWindowIsMaximized = self.isMaximized()\n        if not self.isMaximized():\n            prefs.mainWindowRect = self.geometry()\n\n    def showEvent(self, event):\n        if not self.isMaximized():\n            # have to do this here as the frameGeometry is not correct until shown\n            move_to_screen_center(self)\n        super().showEvent(event)\n\n    def changeEvent(self, event):\n        if event.type() == QEvent.WindowStateChange and not self.isMaximized():\n            move_to_screen_center(self)\n        super().changeEvent(event)\n\n    def closeEvent(self, close_event):\n        # Force closing of our tabbed widgets in reverse order so that the\n        # directories dialog (which usually is at index 0) will be called last\n        for index in range(self.getCount() - 1, -1, -1):\n            self.getWidgetAtIndex(index).closeEvent(close_event)\n        self.appWillSavePrefs()\n\n    @pyqtSlot(int)\n    def onTabCloseRequested(self, index):\n        current_widget = self.getWidgetAtIndex(index)\n        if isinstance(current_widget, DirectoriesDialog):\n            # if we close this one, the application quits. Force user to use the\n            # menu or shortcut. But this is useless if we don't have a button\n            # set up to make a close request anyway. This check could be removed.\n            return\n        self.removeTab(index)\n\n    @pyqtSlot()\n    def onDialogAccepted(self):\n        \"\"\"Remove tabbed dialog when Accepted/Done (close button clicked).\"\"\"\n        widget = self.sender()\n        index = self.indexOfWidget(widget)\n        if index > -1:\n            self.removeTab(index)\n\n    @pyqtSlot()\n    def toggleTabBar(self):\n        value = self.sender().isChecked()\n        self.actionToggleTabs.setChecked(value)\n        self.tabWidget.tabBar().setVisible(value)\n\n\nclass TabBarWindow(TabWindow):\n    \"\"\"Implementation which uses a separate QTabBar and QStackedWidget.\n    The Tab bar is placed next to the menu bar to save real estate.\"\"\"\n\n    def __init__(self, app, **kwargs):\n        super().__init__(app, **kwargs)\n\n    def _setupUi(self):\n        self.setWindowTitle(self.app.NAME)\n        self.resize(640, 480)\n        self.tabBar = QTabBar()\n        self.verticalLayout = QVBoxLayout()\n        self.verticalLayout.setContentsMargins(0, 0, 0, 0)\n        self._setupActions()\n        self._setupMenu()\n\n        self.centralWidget = QWidget(self)\n        self.setCentralWidget(self.centralWidget)\n        self.stackedWidget = QStackedWidget()\n        self.centralWidget.setLayout(self.verticalLayout)\n        self.horizontalLayout = QHBoxLayout()\n        self.horizontalLayout.addWidget(self.menubar, 0, Qt.AlignTop)\n        self.horizontalLayout.addWidget(self.tabBar, 0, Qt.AlignTop)\n        self.verticalLayout.addLayout(self.horizontalLayout)\n        self.verticalLayout.addWidget(self.stackedWidget)\n\n        self.tabBar.currentChanged.connect(self.showTabIndex)\n        self.tabBar.tabCloseRequested.connect(self.onTabCloseRequested)\n\n        self.stackedWidget.currentChanged.connect(self.updateMenuBar)\n        self.stackedWidget.widgetRemoved.connect(self.onRemovedWidget)\n\n        self.tabBar.setTabsClosable(True)\n        self.restoreGeometry()\n\n    def addTab(self, page, title, switch=True):\n        stack_index = self.stackedWidget.addWidget(page)\n        self.tabBar.insertTab(stack_index, title)\n\n        if isinstance(page, DirectoriesDialog):\n            self.tabBar.setTabButton(stack_index, QTabBar.RightSide, None)\n        if switch:  # switch to the added tab immediately upon creation\n            self.setTabIndex(stack_index)\n        return stack_index\n\n    @pyqtSlot(int)\n    def showTabIndex(self, index):\n        # The tab bar's indices should be aligned with the stackwidget's\n        if index >= 0 and index <= self.stackedWidget.count():\n            self.stackedWidget.setCurrentIndex(index)\n\n    def indexOfWidget(self, widget):\n        # Warning: this may return -1 if widget is not a child of stackedwidget\n        return self.stackedWidget.indexOf(widget)\n\n    def setCurrentIndex(self, tab_index):\n        self.setTabIndex(tab_index)\n        # The signal will handle switching the stackwidget's widget\n        # self.stackedWidget.setCurrentWidget(self.stackedWidget.widget(tab_index))\n\n    def setCurrentWidget(self, widget):\n        \"\"\"Sets the current Tab on TabBar for this widget.\"\"\"\n        self.tabBar.setCurrentIndex(self.indexOfWidget(widget))\n\n    @pyqtSlot(int)\n    def setTabIndex(self, index):\n        if index is None:\n            return\n        self.tabBar.setCurrentIndex(index)\n\n    @pyqtSlot(int)\n    def onRemovedWidget(self, index):\n        self.removeTab(index)\n\n    @pyqtSlot(int)\n    def removeTab(self, index):\n        \"\"\"Remove the tab, but not the widget (it should already be removed)\"\"\"\n        return self.tabBar.removeTab(index)\n\n    @pyqtSlot(int)\n    def removeWidget(self, widget):\n        return self.stackedWidget.removeWidget(widget)\n\n    def isTabVisible(self, index):\n        return self.tabBar.isTabVisible(index)\n\n    def getCurrentIndex(self):\n        return self.stackedWidget.currentIndex()\n\n    def getWidgetAtIndex(self, index):\n        return self.stackedWidget.widget(index)\n\n    def getCount(self):\n        return self.stackedWidget.count()\n\n    @pyqtSlot()\n    def toggleTabBar(self):\n        value = self.sender().isChecked()\n        self.actionToggleTabs.setChecked(value)\n        self.tabBar.setVisible(value)\n\n    @pyqtSlot(int)\n    def onTabCloseRequested(self, index):\n        target_widget = self.getWidgetAtIndex(index)\n        if isinstance(target_widget, DirectoriesDialog):\n            # On MacOS, the tab has a close button even though we explicitely\n            # set it to None in order to hide it. This should prevent\n            # the \"Directories\" tab from closing by mistake.\n            return\n        # target_widget.close()  # seems unnecessary\n        # Removing the widget should trigger tab removal via the signal\n        self.removeWidget(self.getWidgetAtIndex(index))\n\n    @pyqtSlot()\n    def onDialogAccepted(self):\n        \"\"\"Remove tabbed dialog when Accepted/Done (close button clicked).\"\"\"\n        widget = self.sender()\n        self.removeWidget(widget)\n"
  },
  {
    "path": "qt/table.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-11-01\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport typing\nfrom PyQt5.QtCore import (\n    Qt,\n    QAbstractTableModel,\n    QModelIndex,\n    QItemSelectionModel,\n    QItemSelection,\n)\n\nfrom qt.column import Columns, Column\n\n\nclass Table(QAbstractTableModel):\n    # Flags you want when index.isValid() is False. In those cases, _getFlags() is never called.\n    INVALID_INDEX_FLAGS = Qt.ItemFlag.ItemIsEnabled\n    COLUMNS: typing.List[Column] = []\n\n    def __init__(self, model, view, **kwargs):\n        super().__init__(**kwargs)\n        self.model = model\n        self.view = view\n        self.view.setModel(self)\n        self.model.view = self\n        if hasattr(self.model, \"_columns\"):\n            self._columns = Columns(self.model._columns, self.COLUMNS, view.horizontalHeader())\n\n        self.view.selectionModel().selectionChanged[(QItemSelection, QItemSelection)].connect(self.selectionChanged)\n\n    def _updateModelSelection(self):\n        # Takes the selection on the view's side and update the model with it.\n        # an _updateViewSelection() call will normally result in an _updateModelSelection() call.\n        # to avoid infinite loops, we check that the selection will actually change before calling\n        # model.select()\n        new_indexes = [modelIndex.row() for modelIndex in self.view.selectionModel().selectedRows()]\n        if new_indexes != self.model.selected_indexes:\n            self.model.select(new_indexes)\n\n    def _updateViewSelection(self):\n        # Takes the selection on the model's side and update the view with it.\n        new_selection = QItemSelection()\n        column_count = self.columnCount(QModelIndex())\n        for index in self.model.selected_indexes:\n            new_selection.select(self.createIndex(index, 0), self.createIndex(index, column_count - 1))\n        self.view.selectionModel().select(new_selection, QItemSelectionModel.ClearAndSelect)\n        if len(new_selection.indexes()):\n            current_index = new_selection.indexes()[0]\n            self.view.selectionModel().setCurrentIndex(current_index, QItemSelectionModel.Current)\n            self.view.scrollTo(current_index)\n\n    # --- Data Model methods\n    # Virtual\n    def _getData(self, row, column, role):\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            attrname = column.name\n            return row.get_cell_value(attrname)\n        elif role == Qt.TextAlignmentRole:\n            return column.alignment\n        return None\n\n    # Virtual\n    def _getFlags(self, row, column):\n        flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable\n        if row.can_edit_cell(column.name):\n            flags |= Qt.ItemIsEditable\n        return flags\n\n    # Virtual\n    def _setData(self, row, column, value, role):\n        if role == Qt.EditRole:\n            attrname = column.name\n            if attrname == \"from\":\n                attrname = \"from_\"\n            setattr(row, attrname, value)\n            return True\n        return False\n\n    def columnCount(self, index):\n        return self.model._columns.columns_count()\n\n    def data(self, index, role):\n        if not index.isValid():\n            return None\n        row = self.model[index.row()]\n        column = self.model._columns.column_by_index(index.column())\n        return self._getData(row, column, role)\n\n    def flags(self, index):\n        if not index.isValid():\n            return self.INVALID_INDEX_FLAGS\n        row = self.model[index.row()]\n        column = self.model._columns.column_by_index(index.column())\n        return self._getFlags(row, column)\n\n    def headerData(self, section, orientation, role):\n        if orientation != Qt.Horizontal:\n            return None\n        if section >= self.model._columns.columns_count():\n            return None\n        column = self.model._columns.column_by_index(section)\n        if role == Qt.DisplayRole:\n            return column.display\n        elif role == Qt.TextAlignmentRole:\n            return column.alignment\n        else:\n            return None\n\n    def revert(self):\n        self.model.cancel_edits()\n\n    def rowCount(self, index):\n        if index.isValid():\n            return 0\n        return len(self.model)\n\n    def setData(self, index, value, role):\n        if not index.isValid():\n            return False\n        row = self.model[index.row()]\n        column = self.model._columns.column_by_index(index.column())\n        return self._setData(row, column, value, role)\n\n    def sort(self, section, order):\n        column = self.model._columns.column_by_index(section)\n        attrname = column.name\n        self.model.sort_by(attrname, desc=order == Qt.DescendingOrder)\n\n    def submit(self):\n        self.model.save_edits()\n        return True\n\n    # --- Events\n    def selectionChanged(self, selected, deselected):\n        self._updateModelSelection()\n\n    # --- model --> view\n    def refresh(self):\n        self.beginResetModel()\n        self.endResetModel()\n        self._updateViewSelection()\n\n    def show_selected_row(self):\n        if self.model.selected_index is not None:\n            self.view.showRow(self.model.selected_index)\n\n    def start_editing(self):\n        self.view.editSelected()\n\n    def stop_editing(self):\n        self.view.setFocus()  # enough to stop editing\n\n    def update_selection(self):\n        self._updateViewSelection()\n"
  },
  {
    "path": "qt/tree_model.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2009-09-14\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport logging\n\nfrom PyQt5.QtCore import QAbstractItemModel, QModelIndex\n\n\nclass NodeContainer:\n    def __init__(self):\n        self._subnodes = None\n        self._ref2node = {}\n\n    # --- Protected\n    def _create_node(self, ref, row):\n        # This returns a TreeNode instance from ref\n        raise NotImplementedError()\n\n    def _get_children(self):\n        # This returns a list of ref instances, not TreeNode instances\n        raise NotImplementedError()\n\n    # --- Public\n    def invalidate(self):\n        # Invalidates cached data and list of subnodes without resetting ref2node.\n        self._subnodes = None\n\n    # --- Properties\n    @property\n    def subnodes(self):\n        if self._subnodes is None:\n            children = self._get_children()\n            self._subnodes = []\n            for index, child in enumerate(children):\n                if child in self._ref2node:\n                    node = self._ref2node[child]\n                    node.row = index\n                else:\n                    node = self._create_node(child, index)\n                    self._ref2node[child] = node\n                self._subnodes.append(node)\n        return self._subnodes\n\n\nclass TreeNode(NodeContainer):\n    def __init__(self, model, parent, row):\n        NodeContainer.__init__(self)\n        self.model = model\n        self.parent = parent\n        self.row = row\n\n    @property\n    def index(self):\n        return self.model.createIndex(self.row, 0, self)\n\n\nclass RefNode(TreeNode):\n    \"\"\"Node pointing to a reference node.\n\n    Use this if your Qt model wraps around a tree model that has iterable nodes.\n    \"\"\"\n\n    def __init__(self, model, parent, ref, row):\n        TreeNode.__init__(self, model, parent, row)\n        self.ref = ref\n\n    def _create_node(self, ref, row):\n        return RefNode(self.model, self, ref, row)\n\n    def _get_children(self):\n        return list(self.ref)\n\n\n# We use a specific TreeNode subclass to easily spot dummy nodes, especially in exception tracebacks.\nclass DummyNode(TreeNode):\n    pass\n\n\nclass TreeModel(QAbstractItemModel, NodeContainer):\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self._dummy_nodes = set()  # dummy nodes' reference have to be kept to avoid segfault\n\n    # --- Private\n    def _create_dummy_node(self, parent, row):\n        # In some cases (drag & drop row removal, to be precise), there's a temporary discrepancy\n        # between a node's subnodes and what the model think it has. This leads to invalid indexes\n        # being queried. Rather than going through complicated row removal crap, it's simpler to\n        # just have rows with empty data replacing removed rows for the millisecond that the drag &\n        # drop lasts. Override this to return a node of the correct type.\n        return DummyNode(self, parent, row)\n\n    def _last_index(self):\n        \"\"\"Index of the very last item in the tree.\"\"\"\n        current_index = QModelIndex()\n        row_count = self.rowCount(current_index)\n        while row_count > 0:\n            current_index = self.index(row_count - 1, 0, current_index)\n            row_count = self.rowCount(current_index)\n        return current_index\n\n    # --- Overrides\n    def index(self, row, column, parent):\n        if not self.subnodes:\n            return QModelIndex()\n        node = parent.internalPointer() if parent.isValid() else self\n        try:\n            return self.createIndex(row, column, node.subnodes[row])\n        except IndexError:\n            logging.debug(\n                \"Wrong tree index called (%r, %r, %r). Returning DummyNode\",\n                row,\n                column,\n                node,\n            )\n            parent_node = parent.internalPointer() if parent.isValid() else None\n            dummy = self._create_dummy_node(parent_node, row)\n            self._dummy_nodes.add(dummy)\n            return self.createIndex(row, column, dummy)\n\n    def parent(self, index):\n        if not index.isValid():\n            return QModelIndex()\n        node = index.internalPointer()\n        if node.parent is None:\n            return QModelIndex()\n        else:\n            return self.createIndex(node.parent.row, 0, node.parent)\n\n    def reset(self):\n        super().beginResetModel()\n        self.invalidate()\n        self._ref2node = {}\n        self._dummy_nodes = set()\n        super().endResetModel()\n\n    def rowCount(self, parent=QModelIndex()):\n        node = parent.internalPointer() if parent.isValid() else self\n        return len(node.subnodes)\n\n    # --- Public\n    def findIndex(self, row_path):\n        \"\"\"Returns the QModelIndex at `row_path`\n\n        `row_path` is a sequence of node rows. For example, [1, 2, 1] is the 2nd child of the\n        3rd child of the 2nd child of the root.\n        \"\"\"\n        result = QModelIndex()\n        for row in row_path:\n            result = self.index(row, 0, result)\n        return result\n\n    @staticmethod\n    def pathForIndex(index):\n        reversed_path = []\n        while index.isValid():\n            reversed_path.append(index.row())\n            index = index.parent()\n        return list(reversed(reversed_path))\n\n    def refreshData(self):\n        \"\"\"Updates the data on all nodes, but without having to perform a full reset.\n\n        A full reset on a tree makes us lose selection and expansion states. When all we ant to do\n        is to refresh the data on the nodes without adding or removing a node, a call on\n        dataChanged() is better. But of course, Qt makes our life complicated by asking us topLeft\n        and bottomRight indexes. This is a convenience method refreshing the whole tree.\n        \"\"\"\n        column_count = self.columnCount()\n        top_left = self.index(0, 0, QModelIndex())\n        bottom_left = self._last_index()\n        bottom_right = self.sibling(bottom_left.row(), column_count - 1, bottom_left)\n        self.dataChanged.emit(top_left, bottom_right)\n"
  },
  {
    "path": "qt/util.py",
    "content": "# Created By: Virgil Dupras\n# Created On: 2011-02-01\n# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport sys\nimport io\nimport os.path as op\nimport os\nimport logging\n\nfrom core.util import executable_folder\nfrom hscommon.util import first\nfrom hscommon.plat import ISWINDOWS\n\nfrom PyQt5.QtCore import QStandardPaths, QSettings\nfrom PyQt5.QtGui import QPixmap, QIcon, QGuiApplication\nfrom PyQt5.QtWidgets import (\n    QSpacerItem,\n    QSizePolicy,\n    QAction,\n    QHBoxLayout,\n)\n\n\ndef move_to_screen_center(widget):\n    frame = widget.frameGeometry()\n    if QGuiApplication.screenAt(frame.center()) is None:\n        # if center not on any screen use default screen\n        screen = QGuiApplication.screens()[0].availableGeometry()\n    else:\n        screen = QGuiApplication.screenAt(frame.center()).availableGeometry()\n    # moves to center of screen if partially off screen\n    if screen.contains(frame) is False:\n        # make sure the frame is not larger than screen\n        # resize does not seem to take frame size into account (move does)\n        widget.resize(frame.size().boundedTo(screen.size() - (frame.size() - widget.size())))\n        frame = widget.frameGeometry()\n        frame.moveCenter(screen.center())\n        widget.move(frame.topLeft())\n\n\ndef vertical_spacer(size=None):\n    if size:\n        return QSpacerItem(1, size, QSizePolicy.Fixed, QSizePolicy.Fixed)\n    else:\n        return QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)\n\n\ndef horizontal_spacer(size=None):\n    if size:\n        return QSpacerItem(size, 1, QSizePolicy.Fixed, QSizePolicy.Fixed)\n    else:\n        return QSpacerItem(1, 1, QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)\n\n\ndef horizontal_wrap(widgets):\n    \"\"\"Wrap all widgets in `widgets` in a horizontal layout.\n\n    If, instead of placing a widget in your list, you place an int or None, an horizontal spacer\n    with the width corresponding to the int will be placed (0 or None means an expanding spacer).\n    \"\"\"\n    layout = QHBoxLayout()\n    for widget in widgets:\n        if widget is None or isinstance(widget, int):\n            layout.addItem(horizontal_spacer(size=widget))\n        else:\n            layout.addWidget(widget)\n    return layout\n\n\ndef create_actions(actions, target):\n    # actions are list of (name, shortcut, icon, desc, func)\n    for name, shortcut, icon, desc, func in actions:\n        action = QAction(target)\n        if icon:\n            action.setIcon(QIcon(QPixmap(\":/\" + icon)))\n        if shortcut:\n            action.setShortcut(shortcut)\n        action.setText(desc)\n        action.triggered.connect(func)\n        setattr(target, name, action)\n\n\ndef set_accel_keys(menu):\n    actions = menu.actions()\n    titles = [a.text() for a in actions]\n    available_characters = {c.lower() for s in titles for c in s if c.isalpha()}\n    for action in actions:\n        text = action.text()\n        c = first(c for c in text if c.lower() in available_characters)\n        if c is None:\n            continue\n        i = text.index(c)\n        newtext = text[:i] + \"&\" + text[i:]\n        available_characters.remove(c.lower())\n        action.setText(newtext)\n\n\ndef get_appdata(portable=False):\n    if portable:\n        return op.join(executable_folder(), \"data\")\n    else:\n        return QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)[0]\n\n\nclass SysWrapper(io.IOBase):\n    def write(self, s):\n        if s.strip():  # don't log empty stuff\n            logging.warning(s)\n\n\ndef setup_qt_logging(level=logging.WARNING, log_to_stdout=False):\n    # Under Qt, we log in \"debug.log\" in appdata. Moreover, when under cx_freeze, we have a\n    # problem because sys.stdout and sys.stderr are None, so we need to replace them with a\n    # wrapper that logs with the logging module.\n    appdata = get_appdata()\n    if not op.exists(appdata):\n        os.makedirs(appdata)\n    # Setup logging\n    # Have to use full configuration over basicConfig as FileHandler encoding was not being set.\n    filename = op.join(appdata, \"debug.log\") if not log_to_stdout else None\n    log = logging.getLogger()\n    handler = logging.FileHandler(filename, \"a\", \"utf-8\")\n    formatter = logging.Formatter(\"%(asctime)s - %(levelname)s - %(message)s\")\n    handler.setFormatter(formatter)\n    log.addHandler(handler)\n    if sys.stderr is None:  # happens under a cx_freeze environment\n        sys.stderr = SysWrapper()\n    if sys.stdout is None:\n        sys.stdout = SysWrapper()\n\n\ndef escape_amp(s):\n    # Returns `s` with escaped ampersand (& --> &&). QAction text needs to have & escaped because\n    # that character is used to define \"accel keys\".\n    return s.replace(\"&\", \"&&\")\n\n\ndef create_qsettings():\n    # Create a QSettings instance with the correct arguments.\n    config_location = op.join(executable_folder(), \"settings.ini\")\n    if op.isfile(config_location):\n        settings = QSettings(config_location, QSettings.IniFormat)\n        settings.setValue(\"Portable\", True)\n    elif ISWINDOWS:\n        # On windows use an ini file in the AppDataLocation instead of registry if possible as it\n        # makes it easier for a user to clear it out when there are issues.\n        locations = QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)\n        if locations:\n            settings = QSettings(op.join(locations[0], \"settings.ini\"), QSettings.IniFormat)\n        else:\n            settings = QSettings()\n        settings.setValue(\"Portable\", False)\n    else:\n        settings = QSettings()\n        settings.setValue(\"Portable\", False)\n    return settings\n"
  },
  {
    "path": "requirements-extra.txt",
    "content": "pytest>=7,<8\nflake8\nblack\npyinstaller>=5.6,<6.0; sys_platform != 'linux'\n"
  },
  {
    "path": "requirements.txt",
    "content": "distro>=1.8.0,<2.0.0\nmutagen>=1.46.0,<2.0.0\npolib>=1.1.0,<2.0.0\nPyQt5 >=5.15.0,<6.0; sys_platform != 'linux'\npywin32>=304; sys_platform == 'win32'\nsemantic-version>=2.0.0,<3.0.0\nSend2Trash>=1.8.2,<2.0.0\nsphinx>=5.3.0,<8.0.0\nxxhash>=3.0.0,<4.0.0\n"
  },
  {
    "path": "run.py",
    "content": "#!/usr/bin/python3\n# Copyright 2017 Virgil Dupras\n#\n# This software is licensed under the \"GPLv3\" License as described in the \"LICENSE\" file,\n# which should be included with this package. The terms are also available at\n# http://www.gnu.org/licenses/gpl-3.0.html\n\nimport sys\nimport os.path as op\nimport gc\n\nfrom PyQt5.QtCore import QCoreApplication\nfrom PyQt5.QtGui import QIcon, QPixmap\nfrom PyQt5.QtWidgets import QApplication\n\nfrom hscommon.trans import install_gettext_trans_under_qt\nfrom qt.error_report_dialog import install_excepthook\nfrom qt.util import setup_qt_logging, create_qsettings\nfrom qt import dg_rc  # noqa: F401\nfrom qt.platform import BASE_PATH\nfrom core import __version__, __appname__\n\n# SIGQUIT is not defined on Windows\nif sys.platform == \"win32\":\n    from signal import signal, SIGINT, SIGTERM\n\n    SIGQUIT = SIGTERM\nelse:\n    from signal import signal, SIGINT, SIGTERM, SIGQUIT\n\nglobal dgapp\ndgapp = None\n\n\ndef signal_handler(sig, frame):\n    global dgapp\n    if dgapp is None:\n        return\n    if sig in (SIGINT, SIGTERM, SIGQUIT):\n        dgapp.SIGTERM.emit()\n\n\ndef setup_signals():\n    signal(SIGINT, signal_handler)\n    signal(SIGTERM, signal_handler)\n    signal(SIGQUIT, signal_handler)\n\n\ndef main():\n    app = QApplication(sys.argv)\n    QCoreApplication.setOrganizationName(\"Hardcoded Software\")\n    QCoreApplication.setApplicationName(__appname__)\n    QCoreApplication.setApplicationVersion(__version__)\n    setup_qt_logging()\n    settings = create_qsettings()\n    lang = settings.value(\"Language\")\n    locale_folder = op.join(BASE_PATH, \"locale\")\n    install_gettext_trans_under_qt(locale_folder, lang)\n    # Handle OS signals\n    setup_signals()\n    # Let the Python interpreter runs every 500ms to handle signals.  This is\n    # required because Python cannot handle signals while the Qt event loop is\n    # running.\n    from PyQt5.QtCore import QTimer\n\n    timer = QTimer()\n    timer.start(500)\n    timer.timeout.connect(lambda: None)\n    # Many strings are translated at import time, so this is why we only import after the translator\n    # has been installed\n    from qt.app import DupeGuru\n\n    app.setWindowIcon(QIcon(QPixmap(f\":/{DupeGuru.LOGO_NAME}\")))\n    global dgapp\n    dgapp = DupeGuru()\n    install_excepthook(\"https://github.com/arsenetar/dupeguru/issues\")\n    result = app.exec()\n    # I was getting weird crashes when quitting under Windows, and manually deleting main app\n    # references with gc.collect() in between seems to fix the problem.\n    del dgapp\n    gc.collect()\n    del app\n    gc.collect()\n    return result\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\nname = dupeGuru\nversion = attr: core.__version__\nurl = https://github.com/arsenetar/dupeguru\nproject_urls =\n    Bug Reports = https://github.com/arsenetar/dupeguru/issues\nauthor = Andrew Senetar\nauthor_email = arsenetar@voltaicideas.net\nlicense = GPLv3\nlicense_files = license\ndescription = dupeGuru is a tool to find duplicate files on your computer.\nlong_description = file:README.md\nlong_description_content_type = text/markdown\nclassifiers =\n    Development Status :: 5 - Production/Stable\n    Intended Audience :: End Users/Desktop\n    License :: OSI Approved :: GNU General Public License v3 (GPLv3)\n    Operating System :: MacOS :: MacOS X\n    Operating System :: Microsoft :: Windows\n    Operating System :: POSIX\n    Programming Language :: Python :: 3.7\n    Programming Language :: Python :: 3.8\n    Programming Language :: Python :: 3.9\n    Programming Language :: Python :: 3.10\n    Programming Language :: Python :: 3 :: Only\n    Topic :: Desktop Environment :: File Managers\n\n[options]\npackages = find:\npython_requires = >=3.7\ninstall_requires =\n    Send2Trash>=1.8.2,<2.0.0\n    mutagen>=1.46.0,<2.0.0\n    distro>=1.8.0,<2.0.0\n    PyQt5 >=5.15.0,<6.0; sys_platform != 'linux'\n    pywin32>=228; sys_platform == 'win32'\n    semantic-version>=2.0.0,<3.0.0\n    xxhash>=3.0.0,<4.0.0\nsetup_requires =\n    sphinx>=3.0.0\n    polib>=1.1.0\ntests_require =\n    pytest >=6,<7\ninclude_package_data = true\n\n[options.entry_points]\nconsole_scripts =\n    dupeguru = run.py\n"
  },
  {
    "path": "setup.nsi",
    "content": ";==============================================================================\n; dupeGuru Installer Script for Windows via NSIS\n;\n; When calling makensis use the following:\n; makensis /DVERSIONMAJOR=x /DVERSIONMINOR=x /DVERSIONPATCH=x /DBITS=x \\\n;   /DSOURCEPATH=x\n; NOTE:\n; If SOURCEPATH is not set it will default to build (uses subdir based on app).\n;==============================================================================\nUnicode true\n; Compression Setting\nSetCompressor /SOLID lzma\n; General Headers\n!include \"FileFunc.nsh\"\n!include \"WinVer.nsh\"\n!include \"LogicLib.nsh\"\n\n;==============================================================================\n; Configuration Defines\n;==============================================================================\n\n; Environment Defines\n!verbose push\n!verbose 4\n!ifndef VERSIONMAJOR\n  !echo \"VERSIONMAJOR is NOT defined\"\n!endif\n!ifndef VERSIONMINOR\n  !echo \"VERSIONMINOR is NOT defined\"\n!endif\n!ifndef VERSIONPATCH\n  !echo \"VERSIONPATCH is NOT defined\"\n!endif\n!ifndef BITS\n  !echo \"BITS is NOT defined\"\n!endif\n!ifndef SOURCEPATH\n  !echo \"SOURCEPATH is NOT defined\"\n  !define SOURCEPATH \"dist\"\n!endif\n!ifndef VERSIONMAJOR | VERSIONMINOR | VERSIONPATCH | BITS | SOURCEPATH\n  !error \"Command line Defines missing use /DDEFINE=VALUE to define before script\"\n!endif\n!verbose pop\n\n; Application Specific Defines\n!define APPNAME \"dupeGuru\"\n!define COMPANYNAME \"Hardcoded Software\"\n!define DESCRIPTION \"dupeGuru is a tool to find duplicate files on your computer.\"\n!define APPLICENSE \"LICENSE\"           ; License is not in build directory\n!define APPICON \"images\\dgse_logo.ico\" ; nor is the icon\n!define DISTDIR \"dist\"\n!define HELPURL \"https://github.com/arsenetar/dupeguru/issues\"\n!define UPDATEURL \"https://dupeguru.voltaicideas.net/\"\n!define ABOUTURL \"https://dupeguru.voltaicideas.net/\"\n\n; Static Defines\n!define UNINSTALLREGBASE \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\"\n\n; Derived Defines\n!define BASEREGKEY \"Software\\${COMPANYNAME}\\${APPNAME}\" ;without root key\n!define VENDORREGKEY \"Software\\${COMPANYNAME}\" ;without root key\n!define UNINSTALLREG \"${UNINSTALLREGBASE}\\${APPNAME}\" ;without root key\n!define INSTPATH \"${COMPANYNAME}\\${APPNAME}\" ;without programs / appdata\n\n; Global vars\nvar StartMenuFolder\nvar InstallSize\n\n;==============================================================================\n; Plugin Setup\n;==============================================================================\n\n; MultiUser Plugin - Allow single user or all install based on execution level\n!define MULTIUSER_EXECUTIONLEVEL Highest\n!define MULTIUSER_MUI\n!define MULTIUSER_INSTALLMODE_COMMANDLINE\n!define MULTIUSER_INSTALLMODE_INSTDIR \"${INSTPATH}\" ; without programs /appdata\n; allow for next run of installer to automatically find install path and type\n!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY \"${BASEREGKEY}\"\n!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME \"InstallPath\"\n!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY \"${BASEREGKEY}\"\n!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME \"InstallType\"\n!if ${BITS} == \"64\"\n  !define MULTIUSER_USE_PROGRAMFILES64\n!endif\n!include MultiUser.nsh\n\n; Modern UI 2\n!include MUI2.nsh\n!define MUI_ICON \"${APPICON}\"\n!define MUI_ABORTWARNING\n!define MUI_UNABORTWARNING\n\n;==============================================================================\n; NSIS Variables\n;==============================================================================\n\nName \"${APPNAME}\"\n!system 'mkdir \"${DISTDIR}\"'\nOutFile \"${DISTDIR}\\${APPNAME}_win${BITS}_${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}.exe\"\nIcon \"${APPICON}\"\n\n;==============================================================================\n; Pages\n;==============================================================================\n\n!insertmacro MUI_PAGE_WELCOME\n!insertmacro MUI_PAGE_LICENSE \"${APPLICENSE}\"\n!insertmacro MULTIUSER_PAGE_INSTALLMODE\n!insertmacro MUI_PAGE_DIRECTORY\n\n; values for start menu page\n!define MUI_STARTMENUPAGE_REGISTRY_ROOT \"SHCTX\" ; uses shell context\n!define MUI_STARTMENUPAGE_REGISTRY_KEY \"${BASEREGKEY}\"\n!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME \"Start Menu Folder\"\n!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder\n\n!insertmacro MUI_PAGE_INSTFILES\n!insertmacro MUI_PAGE_FINISH\n\n; uninstaller pages\n!insertmacro MUI_UNPAGE_CONFIRM\n!insertmacro MUI_UNPAGE_INSTFILES\n\n;==============================================================================\n; Languages\n;==============================================================================\n\n!insertmacro MUI_LANGUAGE \"English\" ;first language is the default language\n!insertmacro MUI_LANGUAGE \"French\"\n!insertmacro MUI_LANGUAGE \"German\"\n!insertmacro MUI_LANGUAGE \"Greek\"\n!insertmacro MUI_LANGUAGE \"Italian\"\n!insertmacro MUI_LANGUAGE \"Korean\"\n!insertmacro MUI_LANGUAGE \"Polish\"\n!insertmacro MUI_LANGUAGE \"Russian\"\n!insertmacro MUI_LANGUAGE \"Spanish\"\n!insertmacro MUI_LANGUAGE \"Ukrainian\"\n!insertmacro MUI_LANGUAGE \"Vietnamese\"\n!insertmacro MUI_LANGUAGE \"Dutch\"\n!insertmacro MUI_LANGUAGE \"Czech\"\n;!insertmacro MUI_LANGUAGE \"Chinese\" ; no NSIS builtin support\n;!insertmacro MUI_LANGUAGE \"Brazilian\" ; no NSIS builtin support\n;!insertmacro MUI_LANGUAGE \"Armenian\" ; requires UNICODE\n\n;==============================================================================\n; Reserve Files\n;==============================================================================\n\n; If you are using solid compression, files that are required before\n; the actual installation should be stored first in the data block,\n; because this will make your installer start faster.\n\n!insertmacro MUI_RESERVEFILE_LANGDLL\nReserveFile /nonfatal \"${NSISDIR}\\Plugins\\*.dll\" ;reserve if needed\n\n;==============================================================================\n; Installer Sections\n;==============================================================================\n\nSection \"!Application\" AppSec\n  SetOutPath \"$INSTDIR\" ; set from result of installer pages\n\n  ; Files to install\n  File /r \"${SOURCEPATH}\\${APPNAME}-win${BITS}\\*\"\n\n  ; Create Start Menu Items\n  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application\n    CreateDirectory \"$SMPROGRAMS\\$StartMenuFolder\"\n    CreateShortcut \"$SMPROGRAMS\\$StartMenuFolder\\${APPNAME}.lnk\" \"$INSTDIR\\${APPNAME}-win${BITS}.exe\"\n    CreateShortcut \"$SMPROGRAMS\\$StartMenuFolder\\Uninstall.lnk\" \"$INSTDIR\\Uninstall.exe\"\n  !insertmacro MUI_STARTMENU_WRITE_END\n\n  ; Store installation folder\n  WriteRegStr SHCTX \"${BASEREGKEY}\" \"InstallPath\" $INSTDIR\n  WriteRegStr SHCTX \"${BASEREGKEY}\" \"InstallType\" $MultiUser.InstallMode\n\n  ; get installed size\n  Push $R0\n  Push $R1\n  Push $R2\n  ${GetSize} \"$INSTDIR\" \"/S=0K\" $R0 $R1 $R2 ; look into locate\n  IntFmt $InstallSize \"0x%08X\" $R0\n  Pop $R2\n  Pop $R1\n  Pop $R0\n\n  ; Set file association\n  ReadRegStr $1 HKCR \".dupeguru\" \"\"\n  StrCmp $1 \"\" NoBackup  ; is it empty\n  StrCmp $1 \"${APPNAME}.File\" NoBackup  ; is it our own\n  WriteRegStr HKCR \".dupeguru\" \"backup_val\" \"$1\"  ; backup current value\nNoBackup:\n  WriteRegStr HKCR \".dupeguru\" \"\" \"${APPNAME}.File\"  ; set our file association\n\n  ReadRegStr $0 HKCR \"${APPNAME}.File\" \"\"\n  StrCmp $0 \"\" 0 Skip\n    WriteRegStr HKCR \"${APPNAME}.File\" \"\" \"${APPNAME} File\"\n    WriteRegStr HKCR \"${APPNAME}.File\\shell\" \"\" \"open\"\n    WriteRegStr HKCR \"${APPNAME}.File\\DefaultIcon\" \"\" \"$INSTDIR\\${APPNAME}-win${BITS}.exe,0\"\nSkip:\n  WriteRegStr HKCR \"${APPNAME}.File\\shell\\open\\command\" \"\" '\"$INSTDIR\\${APPNAME}-win${BITS}.exe\" \"%1\"'\n  WriteRegStr HKCR \"${APPNAME}.File\\shell\\edit\" \"\" \"Edit ${APPNAME} File\"\n  WriteRegStr HKCR \"${APPNAME}.File\\shell\\edit\\command\" \"\" '\"$INSTDIR\\${APPNAME}-win${BITS}.exe\" \"%1\"'\n\n  ; Uninstall Entry\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"DisplayName\" \"${APPNAME} ${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"DisplayVersion\" \"${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONPATCH}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"DisplayIcon\" \"$INSTDIR\\${APPNAME}.exe\"\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"VersionMajor\" ${VERSIONMAJOR}\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"VersionMinor\" ${VERSIONMINOR}\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"VersionPatch\" ${VERSIONPATCH}\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"Comments\" \"${APPNAME} installer\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"InstallLocation\" \"$INSTDIR\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"Publisher\" \"${COMPANYNAME}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"Contact\" \"${HELPURL}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"HelpLink\" \"${HELPURL}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"URLUpdateInfo\" \"${UPDATEURL}\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"URLInfoAbout\" \"${ABOUTURL}\"\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"NoModify\" 1\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"NoRepair\" 1\n  WriteRegDWORD SHCTX \"${UNINSTALLREG}\" \"EstimatedSize\" $InstallSize\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"UninstallString\" \"$\\\"$INSTDIR\\uninstall.exe$\\\" /$MultiUser.InstallMode\"\n  WriteRegStr SHCTX \"${UNINSTALLREG}\" \"QuietUninstallString\" \"$\\\"$INSTDIR\\uninstall.exe$\\\" /$MultiUser.InstallMode /S\"\n\n  ; Create uninstaller\n  WriteUninstaller \"$INSTDIR\\Uninstall.exe\"\nSectionEnd\n\n;==============================================================================\n; Descriptions\n;==============================================================================\n; Add descriptions as needed\n\n;==============================================================================\n; Uninstaller Sections\n;==============================================================================\n\nSection \"Uninstall\"\n  ; Remove Start Menu Folder\n  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder\n  RMDir /r \"$SMPROGRAMS\\$StartMenuFolder\"\n\n  ; Remove Files & Folders in Install Folder\n  RMDir /r \"$INSTDIR\\core\"\n  RMDir /r \"$INSTDIR\\help\"\n  RMDir /r \"$INSTDIR\\PyQt5\"\n  RMDir /r \"$INSTDIR\\qt\"\n  RMDir /r \"$INSTDIR\\locale\"\n  Delete \"$INSTDIR\\*.exe\"\n  Delete \"$INSTDIR\\*.dll\"\n  Delete \"$INSTDIR\\*.pyd\"\n  Delete \"$INSTDIR\\*.zip\"\n  Delete \"$INSTDIR\\*.manifest\"\n\n  ; Remove Install Folder if empty\n  RMDir \"$INSTDIR\"\n\n ReadRegStr $1 HKCR \".dupeguru\" \"\"\n  StrCmp $1 \"${APPNAME}.File\" 0 NotOwn ; only do this if we own it\n  ReadRegStr $1 HKCR \".dupeguru\" \"backup_val\"\n  StrCmp $1 \"\" 0 Restore ; if backup=\"\" then delete the whole key\n  DeleteRegKey HKCR \".dupeGuru\"\n  Goto NotOwn\n\nRestore:\n  WriteRegStr HKCR \".dupeguru\" \"\" $1\n  DeleteRegValue HKCR \".dupeguru\" \"backup_val\"\nNotOwn:\n  DeleteRegKey HKCR \"${APPNAME}.File\" ;Delete key with association name settings\n\n  ; Remove registry keys and vendor keys (if empty)\n  DeleteRegKey  SHCTX \"${BASEREGKEY}\"\n  DeleteRegKey /ifempty SHCTX \"${VENDORREGKEY}\"\n  DeleteRegKey SHCTX \"${UNINSTALLREG}\"\nSectionEnd\n\n;==============================================================================\n; Functions\n;==============================================================================\n\nFunction .onInit\n  ${IfNot} ${AtLeastWin7}\n    MessageBox MB_OK \"Windows 7 and above required\"\n    Quit\n  ${EndIf}\n  !if ${BITS} == \"64\"\n    SetRegView 64\n  !else\n    SetRegView 32\n  !endif\n  !insertmacro MULTIUSER_INIT\n  ; it appears that the languages shown may not always be filtered correctly\n  !define MUI_LANGDLL_ALLLANGUAGES\n  !insertmacro MUI_LANGDLL_DISPLAY\nFunctionEnd\n\nFunction un.onInit\n  !if ${BITS} == \"64\"\n    SetRegView 64\n  !else\n    SetRegView 32\n  !endif\n  !insertmacro MULTIUSER_UNINIT\n  !insertmacro MUI_UNGETLANGUAGE\nFunctionEnd\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, Extension\nfrom pathlib import Path\n\nexts = [\n    Extension(\n        \"core.pe._block\",\n        [\n            str(Path(\"core\", \"pe\", \"modules\", \"block.c\")),\n            str(Path(\"core\", \"pe\", \"modules\", \"common.c\")),\n        ],\n        include_dirs=[str(Path(\"core\", \"pe\", \"modules\"))],\n    ),\n    Extension(\n        \"core.pe._cache\",\n        [\n            str(Path(\"core\", \"pe\", \"modules\", \"cache.c\")),\n            str(Path(\"core\", \"pe\", \"modules\", \"common.c\")),\n        ],\n        include_dirs=[str(Path(\"core\", \"pe\", \"modules\"))],\n    ),\n    Extension(\"qt.pe._block_qt\", [str(Path(\"qt\", \"pe\", \"modules\", \"block.c\"))]),\n]\n\nheaders = [str(Path(\"core\", \"pe\", \"modules\", \"common.h\"))]\n\nsetup(ext_modules=exts, headers=headers)\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py37,py38,py39,py310,py311\nskipsdist = True\nskip_missing_interpreters = True\n\n[testenv]\nsetenv =\n    PYTHON=\"{envpython}\"\ncommands =\n    python build.py --modules\n    flake8\n    black --check .\n    {posargs:py.test core hscommon}\ndeps =\n    -r{toxinidir}/requirements.txt\n    -r{toxinidir}/requirements-extra.txt\n\n[flake8]\nexclude = .tox,env*,build,help,qt/dg_rc.py,pkg\nmax-line-length = 120\nselect = C,E,F,W,B,B950\nextend-ignore = E203,W503\n"
  },
  {
    "path": "win_version_info.temp",
    "content": "# UTF-8\n#\n# For more details about fixed file info 'ffi' see:\n# http://msdn.microsoft.com/en-us/library/ms646997.aspx\nVSVersionInfo(\n  ffi=FixedFileInfo(\n    # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)\n    # Set not needed items to zero 0.\n    filevers=({0}, {1}, {2}, 0),\n    prodvers=({0}, {1}, {2}, 0),\n    # Contains a bitmask that specifies the valid bits 'flags'r\n    mask=0x3f,\n    # Contains a bitmask that specifies the Boolean attributes of the file.\n    flags=0x0,\n    # The operating system for which this file was designed.\n    # 0x4 - NT and there is no need to change it.\n    OS=0x40004,\n    # The general type of file.\n    # 0x1 - the file is an application.\n    fileType=0x1,\n    # The function of the file.\n    # 0x0 - the function is not defined for this fileType\n    subtype=0x0,\n    # Creation date and time stamp.\n    date=(0, 0)\n    ),\n  kids=[\n    StringFileInfo(\n      [\n      StringTable(\n        u'040904B0',\n        [StringStruct(u'CompanyName', u'Hardcoded Software'),\n        StringStruct(u'FileDescription', u'dupeGuru'),\n        StringStruct(u'FileVersion', u'{0}.{1}.{2}.0'),\n        StringStruct(u'InternalName', u'dupeGuru'),\n        StringStruct(u'LegalCopyright', u'© Hardcoded Software'),\n        StringStruct(u'OriginalFilename', u'dupeguru-win{3}.exe'),\n        StringStruct(u'ProductName', u'dupeGuru'),\n        StringStruct(u'ProductVersion', u'{0}.{1}.{2}.0')])\n      ]),\n    VarFileInfo([VarStruct(u'Translation', [1033, 1200])])\n  ]\n)\n"
  }
]