├── 7-Zip ├── 7-zip.chm ├── 7-zip.dll ├── 7-zip32.dll ├── 7z.dll ├── 7z.exe ├── 7z.sfx ├── 7zCon.sfx ├── 7zFM.exe ├── 7zG.exe ├── History.txt ├── License.txt ├── Uninstall.exe ├── descript.ion └── readme.txt ├── README.md ├── constant.py ├── icon ├── app.ico ├── archive.png ├── ask_path.png ├── clear.png ├── disconnect.png ├── drop.png ├── error.png ├── extract.png ├── extract_gif.gif ├── finish.png ├── history.png ├── homepage.png ├── main_default.png ├── main_path.png ├── open_folder.png ├── password.png ├── setting.png ├── skip.png ├── stop.png ├── test.png ├── test_gif.gif └── warning.png ├── main.py ├── module ├── function_7zip.py ├── function_archive.py ├── function_config.py ├── function_normal.py └── function_password.py ├── nuitka.txt ├── requirements.txt ├── thread └── thread_7zip.py └── ui ├── OnlyUnzip.py ├── label_drop.py ├── src ├── ui_main.py ├── ui_main.ui ├── ui_widget_page_homepage.py ├── ui_widget_page_homepage.ui ├── ui_widget_page_password.py ├── ui_widget_page_password.ui ├── ui_widget_page_setting.py └── ui_widget_page_setting.ui ├── widget_page_history.py ├── widget_page_homepage.py ├── widget_page_password.py └── widget_page_setting.py /7-Zip/7-zip.chm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7-zip.chm -------------------------------------------------------------------------------- /7-Zip/7-zip.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7-zip.dll -------------------------------------------------------------------------------- /7-Zip/7-zip32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7-zip32.dll -------------------------------------------------------------------------------- /7-Zip/7z.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7z.dll -------------------------------------------------------------------------------- /7-Zip/7z.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7z.exe -------------------------------------------------------------------------------- /7-Zip/7z.sfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7z.sfx -------------------------------------------------------------------------------- /7-Zip/7zCon.sfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7zCon.sfx -------------------------------------------------------------------------------- /7-Zip/7zFM.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7zFM.exe -------------------------------------------------------------------------------- /7-Zip/7zG.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/7zG.exe -------------------------------------------------------------------------------- /7-Zip/History.txt: -------------------------------------------------------------------------------- 1 | HISTORY of the 7-Zip 2 | -------------------- 3 | 4 | This file contains information about changes for latest versions of 7-Zip. 5 | The full changelog file can be downloaded here: 6 | https://7-zip.org/history.txt 7 | 8 | 9 | 24.08 2024-08-11 10 | ------------------------- 11 | - The bug in 7-Zip 24.00-24.07 was fixed: 12 | For creating a zip archive: 7-Zip could write extra zero bytes after the end of the archive, 13 | if a file included to archive cannot be compressed to a size smaller than original. 14 | The created zip archive is correct except for the useless zero bytes after the end of the archive. 15 | When unpacking such a zip archive, 7-Zip displays a warning: 16 | "WARNING: There are data after the end of archive". 17 | - The bug was fixed: there was a leak of GDI objects (internal resources in Windows) 18 | in "Confirm File Replace" window, causing problems after 1600 displays of "Confirm File Replace" 19 | window from same running 7-Zip process. 20 | - Some optimizations for displaying file icons in 7-Zip File Manager and in "Confirm File Replace" window. 21 | - Some bugs were fixed. 22 | 23 | 24 | 24.07 2024-06-19 25 | ------------------------- 26 | - The bug was fixed: 7-Zip could crash for some incorrect ZSTD archives. 27 | 28 | 29 | 24.06 2024-05-26 30 | ------------------------- 31 | - The bug was fixed: 7-Zip could not unpack some ZSTD archives. 32 | 33 | 34 | 24.05 2024-05-14 35 | ------------------------- 36 | - New switch -myv={MMNN} to set decoder compatibility version for 7z archive creating. 37 | {MMNN} is 4-digit number that represents the version of 7-Zip without a dot. 38 | If -myv={MMNN} switch is specified, 7-Zip will only use compression methods that can 39 | be decoded by the specified version {MMNN} of 7-Zip and newer versions. 40 | If -myv={MMNN} switch is not specified, -myv=2300 is used, and 7-Zip will only 41 | use compression methods that can be decoded by 7-Zip 23.00 and newer versions. 42 | - New switch -myfa={FilterID} to allow 7-Zip to use the specified filter method for 7z archive creating. 43 | - New switch -myfd={FilterID} to disallow 7-Zip to use the specified filter method for 7z archive creating. 44 | - Some bugs were fixed. 45 | 46 | 47 | 24.04 2024-04-05 48 | ------------------------- 49 | - New menu item in 7-Zip File Manager: "Tools / Delete Temporary Files...". 50 | This menu item opens a window showing temporary folders and files 51 | created by 7-Zip in the user's "Temp" folder on a Windows system. 52 | In this window, the user can delete temporary files. 53 | 54 | 55 | 24.03 2024-03-23 56 | ------------------------- 57 | - 7-Zip now can use new RISCV filter for compression to 7z and xz archives. 58 | RISCV filter can increase compression ratio for data containing executable 59 | files compiled for RISC-V architecture. 60 | - The speed for LZMA and LZMA2 decompression in ARM64 version for Windows 61 | was increased by 20%-60%. 62 | - 7-Zip GUI and 7-Zip File Manager can ask user permission to unpack RAR archives that 63 | require big amount of memory, if the dictionary size in RAR archive is larger than 4 GB. 64 | - new switch -smemx{size}g : to set allowed memory usage limit for RAR archive unpacking. 65 | RAR archives can use dictionary up 64 GB. Default allowed limit for RAR unpacking is 4 GB. 66 | - 7zg.exe (7-Zip GUI): -y switch disables user requests and messages. 67 | - 7-Zip shows hash methods XXH64 and BLAKE2sp in context menu. 68 | - -slmu switch : to show timestamps as UTC instead of LOCAL TIME. 69 | - -slsl switch : in console 7-Zip for Windows : to show file paths with 70 | linux path separator slash '/' instead of backslash separator '\'. 71 | - 7-Zip supports .sha256 files that use backslash path separator '\'. 72 | - Some bugs were fixed. 73 | 74 | 75 | 24.01 2024-01-31 76 | ------------------------- 77 | - 7-Zip now can unpack ZSTD archives (.zst filename extension). 78 | - 7-Zip now can unpack ZIP, SquashFS and RPM archives that use ZSTD compression method. 79 | - 7-Zip now supports fast hash algorithm XXH64 that is used in ZSTD. 80 | - 7-Zip now can unpack RAR archives (that use larger than 4 GB dictionary) created by new WinRAR 7.00. 81 | - 7-Zip now can unpack DMG archives that use XZ (ULMO/LZMA) compression method. 82 | - 7-Zip now can unpack NTFS images with cluster size larger than 64 KB. 83 | - 7-Zip now can unpack MBR and GPT images with 4 KB sectors. 84 | - Speed optimizations for archive unpacking: rar, cab, wim, zip, gz. 85 | - Speed optimizations for hash caclulation: CRC-32, CRC-64, BLAKE2sp. 86 | - The bug was fixed: 7-Zip for Linux could fail for multivolume creation in some cases. 87 | - Some bugs were fixed. 88 | 89 | 90 | 23.01 2023-06-20 91 | ------------------------- 92 | - The page "Language" in 7-Zip's menu Tools/Options now shows information 93 | about selected translation, including the number of translated lines. 94 | - Some bugs were fixed. 95 | 96 | 97 | 23.00 2023-05-07 98 | ------------------------- 99 | - 7-Zip now can use new ARM64 filter for compression to 7z and xz archives. 100 | ARM64 filter can increase compression ratio for data containing executable 101 | files compiled for ARM64 (AArch64) architecture. 102 | Also 7-Zip now parses executable files (that have exe and dll filename extensions) 103 | before compressing, and it selects appropriate filter for each parsed file: 104 | - BCJ or BCJ2 filter for x86 executable files, 105 | - ARM64 filter for ARM64 executable files. 106 | Previous versions by default used x86 filter BCJ or BCJ2 for all exe/dll files. 107 | - Default section size for BCJ2 filter was changed from 64 MiB to 240 MiB. 108 | It can increase compression ratio for executable files larger than 64 MiB. 109 | - UDF: support was improved. 110 | - cpio: support for hard links. 111 | - Some changes and optimizations in WIM creation code. 112 | - When new 7-Zip creates multivolume archive, 7-Zip keeps in open state 113 | only volumes that still can be changed. Previous versions kept all volumes 114 | in open state until the end of the archive creation. 115 | - 7-Zip for Linux and macOS now can reduce the number of simultaneously open files, 116 | when 7-Zip opens, extracts or creates multivolume archive. It allows to avoid 117 | the failures for cases with big number of volumes, bacause there is a limitation 118 | for number of open files allowed for a single program in Linux and macOS. 119 | - There are optimizations in code for 7-Zip's context menu in Explorer: 120 | the speed of preparing of the menu showing was improved for cases when big number of 121 | files were selected by external program for context menu that contains 7-Zip menu commands. 122 | - There are changes in code for the drag-and-drop operations to and from 7-Zip File Manager. 123 | And the drag-and-drop operation with right button of mouse now is supported for some cases. 124 | - The bugs were fixed: 125 | - ZIP archives: if multithreaded zip compression was performed with more than one 126 | file to stdout stream (-so switch), 7-Zip didn't write "data descriptor" for some files. 127 | - ext4 archives: 7-Zip couldn't correctly extract symbolic link to directory from ext4 archives. 128 | - HFS and APFS archives: 7-Zip incorrectly decoded uncompressed blocks (64 KiB) in compressed forks. 129 | - HFS : zlib without Adler supported 130 | - Some another bugs were fixed. 131 | 132 | 133 | == 134 | End of document 135 | -------------------------------------------------------------------------------- /7-Zip/License.txt: -------------------------------------------------------------------------------- 1 | 7-Zip 2 | ~~~~~ 3 | License for use and distribution 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | 7-Zip Copyright (C) 1999-2024 Igor Pavlov. 7 | 8 | The licenses for files are: 9 | 10 | - 7z.dll: 11 | - The "GNU LGPL" as main license for most of the code 12 | - The "GNU LGPL" with "unRAR license restriction" for some code 13 | - The "BSD 3-clause License" for some code 14 | - The "BSD 2-clause License" for some code 15 | - All other files: the "GNU LGPL". 16 | 17 | Redistributions in binary form must reproduce related license information from this file. 18 | 19 | Note: 20 | You can use 7-Zip on any computer, including a computer in a commercial 21 | organization. You don't need to register or pay for 7-Zip. 22 | 23 | 24 | GNU LGPL information 25 | -------------------- 26 | 27 | This library is free software; you can redistribute it and/or 28 | modify it under the terms of the GNU Lesser General Public 29 | License as published by the Free Software Foundation; either 30 | version 2.1 of the License, or (at your option) any later version. 31 | 32 | This library is distributed in the hope that it will be useful, 33 | but WITHOUT ANY WARRANTY; without even the implied warranty of 34 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 35 | Lesser General Public License for more details. 36 | 37 | You can receive a copy of the GNU Lesser General Public License from 38 | http://www.gnu.org/ 39 | 40 | 41 | 42 | 43 | BSD 3-clause License in 7-Zip code 44 | ---------------------------------- 45 | 46 | The "BSD 3-clause License" is used for the following code in 7z.dll 47 | 1) LZFSE data decompression. 48 | That code was derived from the code in the "LZFSE compression library" developed by Apple Inc, 49 | that also uses the "BSD 3-clause License". 50 | 2) ZSTD data decompression. 51 | that code was developed using original zstd decoder code as reference code. 52 | The original zstd decoder code was developed by Facebook Inc, 53 | that also uses the "BSD 3-clause License". 54 | 55 | Copyright (c) 2015-2016, Apple Inc. All rights reserved. 56 | Copyright (c) Facebook, Inc. All rights reserved. 57 | Copyright (c) 2023-2024 Igor Pavlov. 58 | 59 | Text of the "BSD 3-clause License" 60 | ---------------------------------- 61 | 62 | Redistribution and use in source and binary forms, with or without modification, 63 | are permitted provided that the following conditions are met: 64 | 65 | 1. Redistributions of source code must retain the above copyright notice, this 66 | list of conditions and the following disclaimer. 67 | 68 | 2. Redistributions in binary form must reproduce the above copyright notice, 69 | this list of conditions and the following disclaimer in the documentation 70 | and/or other materials provided with the distribution. 71 | 72 | 3. Neither the name of the copyright holder nor the names of its contributors may 73 | be used to endorse or promote products derived from this software without 74 | specific prior written permission. 75 | 76 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 77 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 78 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 79 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 80 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 81 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 82 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 83 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 84 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 85 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 86 | 87 | --- 88 | 89 | 90 | 91 | 92 | BSD 2-clause License in 7-Zip code 93 | ---------------------------------- 94 | 95 | The "BSD 2-clause License" is used for the XXH64 code in 7-Zip. 96 | 97 | XXH64 code in 7-Zip was derived from the original XXH64 code developed by Yann Collet. 98 | 99 | Copyright (c) 2012-2021 Yann Collet. 100 | Copyright (c) 2023-2024 Igor Pavlov. 101 | 102 | Text of the "BSD 2-clause License" 103 | ---------------------------------- 104 | 105 | Redistribution and use in source and binary forms, with or without modification, 106 | are permitted provided that the following conditions are met: 107 | 108 | 1. Redistributions of source code must retain the above copyright notice, this 109 | list of conditions and the following disclaimer. 110 | 111 | 2. Redistributions in binary form must reproduce the above copyright notice, 112 | this list of conditions and the following disclaimer in the documentation 113 | and/or other materials provided with the distribution. 114 | 115 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 116 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 117 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 118 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 119 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 120 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 121 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 122 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 123 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 124 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 125 | 126 | --- 127 | 128 | 129 | 130 | 131 | unRAR license restriction 132 | ------------------------- 133 | 134 | The decompression engine for RAR archives was developed using source 135 | code of unRAR program. 136 | All copyrights to original unRAR code are owned by Alexander Roshal. 137 | 138 | The license for original unRAR code has the following restriction: 139 | 140 | The unRAR sources cannot be used to re-create the RAR compression algorithm, 141 | which is proprietary. Distribution of modified unRAR sources in separate form 142 | or as a part of other software is permitted, provided that it is clearly 143 | stated in the documentation and source comments that the code may 144 | not be used to develop a RAR (WinRAR) compatible archiver. 145 | 146 | -- 147 | -------------------------------------------------------------------------------- /7-Zip/Uninstall.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/7-Zip/Uninstall.exe -------------------------------------------------------------------------------- /7-Zip/descript.ion: -------------------------------------------------------------------------------- 1 | 7-zip.chm 7-Zip Help 2 | 7-Zip.dll 7-Zip Plugin 3 | 7-Zip32.dll 7-Zip Plugin 32-bit 4 | 7z.dll 7-Zip Engine 5 | 7z.exe 7-Zip Console 6 | 7z.sfx 7-Zip GUI SFX 7 | 7zCon.sfx 7-Zip Console SFX 8 | 7zFM.exe 7-Zip File Manager 9 | 7zg.exe 7-Zip GUI 10 | descript.ion 7-Zip File Descriptions 11 | history.txt 7-Zip History 12 | Lang 7-Zip Translations 13 | license.txt 7-Zip License 14 | readme.txt 7-Zip Overview 15 | -------------------------------------------------------------------------------- /7-Zip/readme.txt: -------------------------------------------------------------------------------- 1 | 7-Zip 24.08 2 | ----------- 3 | 4 | 7-Zip is a file archiver for Windows. 5 | 6 | 7-Zip Copyright (C) 1999-2024 Igor Pavlov. 7 | 8 | The main features of 7-Zip: 9 | 10 | - High compression ratio in the new 7z format 11 | - Supported formats: 12 | - Packing / unpacking: 7z, XZ, BZIP2, GZIP, TAR, ZIP and WIM. 13 | - Unpacking only: APFS, AR, ARJ, Base64, CAB, CHM, CPIO, CramFS, DMG, EXT, FAT, GPT, HFS, 14 | IHEX, ISO, LZH, LZMA, MBR, MSI, NSIS, NTFS, QCOW2, RAR, 15 | RPM, SquashFS, UDF, UEFI, VDI, VHD, VHDX, VMDK, XAR, Z and ZSTD. 16 | - Fast compression and decompression 17 | - Self-extracting capability for 7z format 18 | - Strong AES-256 encryption in 7z and ZIP formats 19 | - Integration with Windows Shell 20 | - Powerful File Manager 21 | - Powerful command line version 22 | - Localizations for 90 languages 23 | 24 | 25 | 7-Zip is free software distributed under the GNU LGPL (except for unRar code). 26 | Read License.txt for more information about license. 27 | 28 | 29 | This distribution package contains the following files: 30 | 31 | 7zFM.exe - 7-Zip File Manager 32 | 7-zip.dll - Plugin for Windows Shell 33 | 7-zip32.dll - Plugin for Windows Shell (32-bit plugin for 64-bit system) 34 | 7zg.exe - GUI module 35 | 7z.exe - Command line version 36 | 7z.dll - 7-Zip engine module 37 | 7z.sfx - SFX module (Windows version) 38 | 7zCon.sfx - SFX module (Console version) 39 | 40 | License.txt - License information 41 | readme.txt - This file 42 | History.txt - History of 7-Zip 43 | 7-zip.chm - User's Manual in HTML Help format 44 | descript.ion - Description for files 45 | 46 | Lang\en.ttt - English (base) localization file 47 | Lang\*.txt - Localization files 48 | 49 | 50 | --- 51 | End of document 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 一个纯用于跑密码本的解压工具,类似于UZIP,解压功能基于7zip命令行,相当于省去了敲7zip命令行的操作。 2 | 3 | ### 下载地址 4 | 5 | 密码:1234 6 | 7 | ### 实现功能 8 | 1. 进行解压操作:直接拖入压缩文件或压缩文件所在的文件夹 9 | 2. 智能解压(单文件则直接解压,多文件则自动创建文件夹) 10 | 3. 解压时按密码使用次数降序测试压缩包密码 11 | 4. 支持zip、rar、7z分卷压缩包的解压,支持仅拖入部分分卷包来解压整个分卷压缩包 12 | 5. 解压后自动处理套娃文件夹、套娃压缩包 13 | 6. 解压时跳过指定后缀的文件 14 | 7. 进行解压密码测试:同解压操作 15 | 8. 添加解压密码字典:手工输入、读取剪切板添加 16 | 9. 导出解压密码字典(可附带使用次数) 17 | 10. 查看解压、密码测试的历史记录 18 | 11. 简单的设置选项(删除原文件、仅解压压缩包) 19 | 12. 解压以及测试密码时显示进度(解压进度读取自7zip命令行的输出流,可能有延迟问题) 20 | 13. 解压到指定文件夹 21 | 14. 支持在运行解压后点击按钮中止当前解压任务 22 | 23 | ### 运行截图 24 | ![主页面](https://i.postimg.cc/KYfPVFps/image.png) 25 | ![密码页](https://i.postimg.cc/3xMF0tD9/image.png) 26 | ![设置页](https://i.postimg.cc/kgxFQWQC/image.png) 27 | ![测试密码时](https://i.postimg.cc/yxtXPPp1/image.png) 28 | ![解压时](https://i.postimg.cc/BQPCvrtX/image.png) 29 | ![完成全部解压](https://i.postimg.cc/Zq4PV0Hd/image.png) 30 | 31 | ### 其他说明 32 | 1. 处理套娃文件夹的逻辑:类似于bandzip。存在多级文件夹且每级文件夹中只有一个文件夹时,递归路径直到找到最深一级的有多个文件/文件夹或仅有单个文件的文件夹 33 | 2. 处理套娃压缩包的逻辑:将解压后的文件/文件夹再次执行解压 34 | 3. 识别压缩包的方法:使用filetype库+指定文件名后缀,exe文件不会被认定为压缩包 35 | 4. 解压逻辑:创建临时文件夹->创建压缩包名文件夹->先使用l指令尝试测试密码->失败则用t、x指令测试密码或直接尝试解压文件->完成解压后处理套娃文件夹、套娃压缩包 36 | 37 | ### 存在的问题 38 | 1. 在解压exe文件时,可能无法正确测试压缩包的密码 39 | 2. 在解压exe文件时,如果exe压缩包已损坏,可能无法正确判断是文件损坏或没有找到对应密码 40 | 3. 解压zip文件时,可能会出现测试进度耗时较长的问题(解压zip文件时,7zip会测试其内的全部文件密码,导致耗时较长) 41 | 4. 目前存在压缩包编码问题,如果压缩包编码与本地编码不同,可能会导致解压文件名为乱码。 42 | -------------------------------------------------------------------------------- /constant.py: -------------------------------------------------------------------------------- 1 | # 常量 2 | import os 3 | import sys 4 | 5 | # 程序路径 6 | _PROGRAM_FOLDER = os.path.dirname(os.path.realpath(__file__)) + '/' # 源码使用 7 | # _PROGRAM_FOLDER = os.path.dirname(sys.argv[0]) + '/' # 打包使用 8 | print('程序路径', _PROGRAM_FOLDER) 9 | 10 | # 程序文件路径 11 | _CONFIG_FILE = _PROGRAM_FOLDER + 'config.ini' 12 | _BACKUP_FOLDER = _PROGRAM_FOLDER + 'backup' 13 | _PASSWORD_FILE = _PROGRAM_FOLDER + 'password.pickle' 14 | _PASSWORD_EXPORT = _PROGRAM_FOLDER + '密码导出.txt' 15 | _HISTORY_FILE = _PROGRAM_FOLDER + 'history.txt' 16 | _PATH_7ZIP = _PROGRAM_FOLDER + '7-Zip/7z.exe' 17 | _ICON_FOLDER = _PROGRAM_FOLDER + 'icon/' 18 | 19 | # 图标路径 20 | _ICON_MAIN_DEFAULT = _ICON_FOLDER + 'main_default.png' 21 | _ICON_MAIN_PATH = _ICON_FOLDER + 'main_path.png' 22 | _ICON_DISCONNECT = _ICON_FOLDER + 'disconnect.png' 23 | _ICON_TEST = _ICON_FOLDER + 'test.png' 24 | _ICON_EXTRACT = _ICON_FOLDER + 'extract.png' 25 | _ICON_EXTRACT_GIF = _ICON_FOLDER + 'extract_gif.gif' 26 | _ICON_TEST_GIF = _ICON_FOLDER + 'test_gif.gif' 27 | _ICON_WARNING = _ICON_FOLDER + 'warning.png' 28 | _ICON_ERROR = _ICON_FOLDER + 'error.png' 29 | _ICON_DROP = _ICON_FOLDER + 'drop.png' 30 | _ICON_SKIP = _ICON_FOLDER + 'skip.png' 31 | _ICON_FINISH = _ICON_FOLDER + 'finish.png' 32 | _ICON_SETTING = _ICON_FOLDER + 'setting.png' 33 | _ICON_PASSWORD = _ICON_FOLDER + 'password.png' 34 | _ICON_HOMEPAGE = _ICON_FOLDER + 'homepage.png' 35 | _ICON_HISTORY = _ICON_FOLDER + 'history.png' 36 | _ICON_OPEN_FOLDER = _ICON_FOLDER + 'open_folder.png' 37 | _ICON_APP = _ICON_FOLDER + 'app.ico' 38 | _ICON_CLEAR = _ICON_FOLDER + 'clear.png' 39 | _ICON_STOP = _ICON_FOLDER + 'stop.png' 40 | _ICON_ASK_PATH = _ICON_FOLDER + 'ask_path.png' 41 | 42 | # 7zip使用 43 | _TEMP_FOLDER = 'UnzipTempFolder' # 临时文件夹 44 | _PASSWORD_FAKE = 'FakePassword' # 用于测试是否存在密码的临时密码 45 | 46 | # 状态颜色 47 | _COLOR_SKIP = (128, 128, 128) 48 | _COLOR_ERROR = (254, 67, 101) 49 | _COLOR_WARNING = (255, 215, 0) 50 | _COLOR_SUCCESS = (0, 0, 0) 51 | 52 | # 其他 53 | _TIME_STAMP = ' %Y%m%d_%H%M%S' # 时间戳格式化字符串 54 | _HISTORY_FILE_MAX_SIZE = 10240 # 历史记录文件大小的最大值(单位:字节) 55 | 56 | # 分卷压缩包正则 57 | PATTERN_7Z = r'^(.+)\.7z\.\d+$' # test.7z.001/test.7z.002/test.7z.003 58 | PATTERN_RAR = r'^(.+)\.part(\d+)\.rar$' # test.part1.rar/test.part2.rar/test.part3.rar 59 | PATTERN_RAR_WITHOUT_SUFFIX = r'^(.+)\.part(\d+)$' # rar分卷文件无后缀时也能正常解压,test.part1/test.part2/test.part3 60 | PATTERN_ZIP = r'^(.+)\.zip$' # zip分卷文件的第一个分卷包一般都是.zip后缀,所以.zip后缀直接视为分卷压缩文件 test.zip 61 | PATTERN_ZIP_VOLUME = r'^(.+)\.z\d+$' # test.zip/test.z01/test.z02 62 | PATTERN_ZIP_TYPE2 = r'^(.+)\.zip\.\d+$' # test.zip.001/test.zip.002/test.zip.003 63 | -------------------------------------------------------------------------------- /icon/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/app.ico -------------------------------------------------------------------------------- /icon/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/archive.png -------------------------------------------------------------------------------- /icon/ask_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/ask_path.png -------------------------------------------------------------------------------- /icon/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/clear.png -------------------------------------------------------------------------------- /icon/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/disconnect.png -------------------------------------------------------------------------------- /icon/drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/drop.png -------------------------------------------------------------------------------- /icon/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/error.png -------------------------------------------------------------------------------- /icon/extract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/extract.png -------------------------------------------------------------------------------- /icon/extract_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/extract_gif.gif -------------------------------------------------------------------------------- /icon/finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/finish.png -------------------------------------------------------------------------------- /icon/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/history.png -------------------------------------------------------------------------------- /icon/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/homepage.png -------------------------------------------------------------------------------- /icon/main_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/main_default.png -------------------------------------------------------------------------------- /icon/main_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/main_path.png -------------------------------------------------------------------------------- /icon/open_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/open_folder.png -------------------------------------------------------------------------------- /icon/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/password.png -------------------------------------------------------------------------------- /icon/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/setting.png -------------------------------------------------------------------------------- /icon/skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/skip.png -------------------------------------------------------------------------------- /icon/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/stop.png -------------------------------------------------------------------------------- /icon/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/test.png -------------------------------------------------------------------------------- /icon/test_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/test_gif.gif -------------------------------------------------------------------------------- /icon/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PPJUST/OnlyUnzip/b4131c413609e83d8aca047801e407f0705003ca/icon/warning.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | import sys 4 | 5 | from PySide6.QtGui import QPalette, QColor, QIcon 6 | from PySide6.QtWidgets import QApplication, QMessageBox 7 | 8 | from constant import _ICON_APP 9 | from module import function_normal, function_password 10 | from ui.OnlyUnzip import OnlyUnzip 11 | 12 | paths = [] 13 | try: # 提取拖入路径 14 | cmd_args = sys.argv[1:] 15 | _folder = os.getcwd() 16 | for i in cmd_args: 17 | if os.path.isabs(i): 18 | paths.append(i) 19 | else: # 组合相对路径 20 | paths.append(os.path.normpath(os.path.join(_folder, i))) 21 | except IndexError: 22 | pass 23 | 24 | 25 | def main(): 26 | app_ = QApplication() 27 | app_.setStyle('Fusion') 28 | # 设置白色背景色 29 | palette = QPalette() 30 | palette.setColor(QPalette.Window, QColor(255, 255, 255)) 31 | app_.setPalette(palette) 32 | 33 | program_ui = OnlyUnzip(paths) 34 | program_ui.setWindowIcon(QIcon(_ICON_APP)) 35 | program_ui.setFixedSize(262, 232) 36 | program_ui.show() 37 | app_.exec() 38 | 39 | 40 | def check_software_is_running(): 41 | """使用互斥体检查是否已经打开了一个实例""" 42 | mutex_name = 'OnlyUnzip' 43 | mutex = ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name) 44 | if ctypes.windll.kernel32.GetLastError() == 183: # ERROR_ALREADY_EXISTS 45 | ctypes.windll.kernel32.CloseHandle(mutex) 46 | return True 47 | return False 48 | 49 | 50 | if __name__ == "__main__": 51 | if check_software_is_running(): 52 | app = QApplication([]) 53 | messagebox = QMessageBox() 54 | messagebox.setText('程序已经运行,请勿重复打开') 55 | messagebox.exec() 56 | sys.exit(1) 57 | else: 58 | function_normal.check_default_files() 59 | function_password.backup_password() 60 | main() 61 | -------------------------------------------------------------------------------- /module/function_7zip.py: -------------------------------------------------------------------------------- 1 | # 调用7zip的相关方法 2 | 3 | import subprocess 4 | 5 | from constant import _PATH_7ZIP, _PASSWORD_FAKE, _COLOR_SKIP, _COLOR_ERROR, _COLOR_WARNING, _COLOR_SUCCESS 6 | 7 | 8 | def call_7zip(command_type: str, filepath: str, password: str, check_path_inside=None): 9 | """调用7zip的l和t命令,返回测试结果 10 | :param command_type: 7zip的command,l/t 11 | :param filepath: 文件路径 12 | :param password: 需要测试的密码 13 | :param check_path_inside: 指定测试的内部文件路径(只在t指令时使用)""" 14 | command = [_PATH_7ZIP, 15 | command_type, 16 | filepath, 17 | "-p" + password] 18 | if command_type == 't' and check_path_inside: # 只在t指令使用,l指令不需要,t指令中使用时可以加快速度 19 | command.append(check_path_inside) 20 | print('测试 7zip命令', command) 21 | process = subprocess.run(command, 22 | stdout=subprocess.PIPE, 23 | stderr=subprocess.PIPE, 24 | creationflags=subprocess.CREATE_NO_WINDOW, 25 | text=True, 26 | universal_newlines=True) 27 | 28 | """ 29 | 7zip Exit Codes 30 | 0 没有错误 31 | 1 警告(非致命错误,例如被占用) 32 | 2 致命错误 33 | 7 命令行错误 34 | 8 内存不足,无法进行操作 35 | 255 用户已停止进程 36 | """ 37 | # 处理返回码 38 | if process.returncode == 0: # 没有错误 39 | result_class = Result7zip.Success(filepath, password) 40 | elif process.returncode == 1: # 警告(非致命错误,例如被占用) 41 | result_class = Result7zip.FileOccupied(filepath) 42 | elif process.returncode == 2: # 致命错误 43 | stderr = str(process.stderr) + str(process.stdout) # 错误信息在输出流中 44 | # 读取输出流 45 | print('【7zip测试信息:', stderr, '】') # 测试用 46 | # if not stderr: # 处理自解压文件时,返回的stderr流可能为空 47 | # result_class = Result7zip.WrongPassword(filepath) 48 | # 备忘录-如何更好的处理自解压文件 49 | if 'Wrong password' in stderr: 50 | result_class = Result7zip.WrongPassword(filepath) 51 | elif 'Missing volume' in stderr: 52 | result_class = Result7zip.MissingVolume(filepath) 53 | elif 'Cannot open the file as' in stderr: # 备忘录-如果是这个报错可能可以按其指定的文件类型进行测试 54 | result_class = Result7zip.NotArchiveOrDamaged(filepath) 55 | else: # 兜底 56 | result_class = Result7zip.UnknownError(filepath) 57 | elif process.returncode == 8: # 内存不足,无法进行操作 58 | result_class = Result7zip.NotEnoughSpace(filepath) 59 | else: # 兜底 60 | result_class = Result7zip.UnknownError(filepath) 61 | 62 | # 处理输出流 63 | archive_info_dict = get_info_from_stdout(process.stdout) 64 | 65 | return result_class, archive_info_dict 66 | 67 | 68 | def test_fake_password(file): 69 | """使用临时密码测试文件,判断是否进行进一步操作 70 | :return: True: 可以使用l命令进行后续测试; 71 | False: 无法使用l命令进行后续测试; 72 | Result7zip类: 文件本身存在问题. 73 | """ 74 | result_class, archive_info_dict = call_7zip('l', file, _PASSWORD_FAKE) 75 | # 返回密码错误,则该压缩文件已加密且可以使用l命令进行后续测试 76 | if isinstance(result_class, Result7zip.WrongPassword): 77 | return True, archive_info_dict 78 | # 返回成功,则该压缩文件无法使用l命令进行后续测试(内部文件名未加密或无密码时无法使用l命令测试密码,需要使用t/x) 79 | elif isinstance(result_class, Result7zip.Success): 80 | return False, archive_info_dict 81 | # 返回其他类型,则该压缩文件本身存在问题,中断后续操作 82 | # 返回文件占用/非压缩文件/空间不足/缺失分卷/未知错误,则说明文件本身存在问题 83 | else: 84 | return result_class, archive_info_dict 85 | 86 | 87 | class Result7zip: 88 | """7zip处理结果 89 | 7zip Exit Codes 90 | 0 没有错误 91 | 1 警告(非致命错误,例如被占用) 92 | 2 致命错误 93 | 7 命令行错误 94 | 8 内存不足,无法进行操作 95 | 255 用户已停止进程""" 96 | 97 | class _Template: 98 | def __init__(self, file, text, color): 99 | self.file = file 100 | self.text = text 101 | self.color = color 102 | 103 | class Skip(_Template): 104 | """跳过(通过filetype库判断后,不通过7zip调用返回结果判断)""" 105 | 106 | def __init__(self, file): 107 | super().__init__(file, '跳过', _COLOR_SKIP) 108 | 109 | class WrongPassword(_Template): 110 | """密码错误""" 111 | 112 | def __init__(self, file): 113 | super().__init__(file, '密码错误', _COLOR_ERROR) 114 | 115 | class MissingVolume(_Template): 116 | """缺少分卷""" 117 | 118 | def __init__(self, file): 119 | super().__init__(file, '缺少分卷', _COLOR_ERROR) 120 | 121 | class NotArchiveOrDamaged(_Template): 122 | """不是压缩文件或文件已经损坏""" 123 | 124 | def __init__(self, file): 125 | super().__init__(file, '不是压缩文件或文件已经损坏', _COLOR_ERROR) 126 | 127 | class UnknownError(_Template): 128 | """未知错误""" 129 | 130 | def __init__(self, file): 131 | super().__init__(file, '未知错误', _COLOR_ERROR) 132 | 133 | class FileOccupied(_Template): 134 | """文件被占用""" 135 | 136 | def __init__(self, file): 137 | super().__init__(file, '文件被占用', _COLOR_WARNING) 138 | 139 | class NotEnoughSpace(_Template): 140 | """磁盘空间不足""" 141 | 142 | def __init__(self, file): 143 | super().__init__(file, '磁盘空间不足', _COLOR_WARNING) 144 | 145 | class Success(_Template): 146 | """成功""" 147 | 148 | def __init__(self, file, password): 149 | super().__init__(file, '成功', _COLOR_SUCCESS) 150 | self.password = password if password != _PASSWORD_FAKE else '' 151 | 152 | 153 | class Collect7zipResult: 154 | """收集7zip的调用结果,并进行统计""" 155 | _instance = None 156 | _is_init = False 157 | 158 | def __new__(cls, *args, **kwargs): 159 | if cls._instance is None: 160 | cls._instance = super().__new__(cls) 161 | return cls._instance 162 | 163 | def __init__(self): 164 | if not self._is_init: 165 | super().__init__() 166 | self._is_init = True 167 | 168 | self._result_dict = dict() # 结果字典,key为文件路径,value为Result7zip对象 169 | 170 | def reset_count(self): 171 | """重置计数""" 172 | self._result_dict.clear() 173 | 174 | def collect(self, result_class): 175 | """收集结果""" 176 | file = result_class.file 177 | self._result_dict[file] = result_class 178 | 179 | def get_result_text(self): 180 | """获取结果文本""" 181 | wrong_password = 0 # 密码错误 182 | missing_volume = 0 # 缺少分卷 183 | not_archive_or_damaged = 0 # 不是压缩文件或文件已经损坏 184 | unknown_error = 0 # 未知错误 185 | file_occupied = 0 # 文件被占用 186 | not_enough_space = 0 # 磁盘空间不足 187 | success = 0 # 成功 188 | 189 | for result_class in self._result_dict.values(): 190 | if isinstance(result_class, Result7zip.WrongPassword): 191 | wrong_password += 1 192 | elif isinstance(result_class, Result7zip.MissingVolume): 193 | missing_volume += 1 194 | elif isinstance(result_class, Result7zip.NotArchiveOrDamaged): 195 | not_archive_or_damaged += 1 196 | elif isinstance(result_class, Result7zip.UnknownError): 197 | unknown_error += 1 198 | elif isinstance(result_class, Result7zip.FileOccupied): 199 | file_occupied += 1 200 | elif isinstance(result_class, Result7zip.NotEnoughSpace): 201 | not_enough_space += 1 202 | elif isinstance(result_class, Result7zip.Success): 203 | success += 1 204 | 205 | count_success = success 206 | count_wrong_password = wrong_password 207 | count_error = missing_volume + not_archive_or_damaged + unknown_error + file_occupied + not_enough_space 208 | join_text = f'成功:{count_success} 失败:{count_wrong_password} 错误:{count_error}' 209 | 210 | return join_text 211 | 212 | 213 | def get_info_from_stdout(stdout_text: str): 214 | """从7zip的stdout中获取相关信息""" 215 | data_dict = {'filetype': None, 'paths': None} 216 | if stdout_text: 217 | text_split = stdout_text.splitlines() 218 | else: 219 | return data_dict 220 | 221 | # 提取文件类型 222 | cut_ = [i for i in text_split if i.startswith('Type = ')] 223 | if cut_: 224 | cut_text = [i for i in text_split if i.startswith('Type = ')][0] 225 | filetype = cut_text[len('Type = '):] 226 | data_dict['filetype'] = filetype 227 | # 提取内部文件路径 228 | start_index = None 229 | end_index = None 230 | for index, i in enumerate(text_split): 231 | if i.startswith(' Date'): 232 | start_index = index 233 | if i.startswith('----------'): 234 | end_index = index 235 | if start_index or end_index: 236 | column_name_index = text_split[start_index].find('Name') 237 | cut_text = text_split[start_index + 2:end_index] 238 | paths = [i[column_name_index:] for i in cut_text if 'D....' not in i] 239 | data_dict['paths'] = paths 240 | 241 | return data_dict 242 | -------------------------------------------------------------------------------- /module/function_archive.py: -------------------------------------------------------------------------------- 1 | # 压缩文件相关方法 2 | import os 3 | import re 4 | from typing import Union 5 | 6 | import filetype 7 | 8 | from constant import PATTERN_7Z, PATTERN_RAR_WITHOUT_SUFFIX, PATTERN_ZIP, PATTERN_ZIP_VOLUME, PATTERN_ZIP_TYPE2, \ 9 | PATTERN_RAR 10 | 11 | 12 | def is_archive(filepath: str) -> Union[bool, str]: 13 | """文件是否为压缩文件 14 | :param filepath: str类型,文件路径 15 | :return: 压缩文件类型/False 16 | """ 17 | archive_type = ['zip', 'tar', 'rar', 'gz', '7z', 'xz', 'iso'] 18 | if not os.path.exists(filepath): 19 | return False 20 | elif os.path.isdir(filepath): 21 | return False 22 | else: 23 | # 先通过文件后缀判断 24 | file_suffix = os.path.splitext(filepath)[1][1:].lower() # 提取文件后缀名(不带.) 25 | if file_suffix in archive_type: 26 | print(filepath, ' 文件类型 ', file_suffix) 27 | return file_suffix 28 | 29 | # 再通过filetype库判断 30 | kind = filetype.guess(filepath) 31 | if kind is None: 32 | return False 33 | else: 34 | type_kind = kind.extension 35 | if type_kind in archive_type: 36 | print(filepath, ' 文件类型 ', type_kind) 37 | return type_kind 38 | 39 | return False 40 | 41 | 42 | def split_archive(archives: list) -> dict: 43 | """分离压缩文件(分卷压缩文件与其他) 44 | :return: dict结构:key为第一个分卷包路径/非分卷则为其本身,value为list,内部元素为其对应的所有分卷包""" 45 | file_dict = {} # 结构:key为第一个分卷包路径,value为list,内部元素为其对应的所有分卷包 46 | # 统一路径格式 47 | archives = [os.path.normpath(i) for i in archives] 48 | archives = list(set(archives)) 49 | # 初步区分分卷与非分卷 50 | volume_archives = [] # 分卷 51 | for file in archives: 52 | if is_volume_archive(file): 53 | volume_archives.append(file) 54 | else: 55 | file_dict[file] = set() 56 | file_dict[file].add(file) 57 | 58 | # 进一步处理分卷,合并同一分卷+补充缺失的分卷包 59 | # 按文件夹拆分,同一个文件夹下的放一起 60 | split_folder_dict = {} # 结构:key为文件夹路径,value为list,内部元素为对应的内部分卷包路径 61 | for path in volume_archives: 62 | parent_folder = os.path.dirname(path) 63 | if parent_folder not in split_folder_dict: 64 | split_folder_dict[parent_folder] = [] 65 | split_folder_dict[parent_folder].append(path) 66 | # 逐个处理分卷文件,生成虚拟包名 67 | dirpath_list = set() # 用于后续扩展补充缺失分卷包 68 | for dirpath, files in split_folder_dict.items(): 69 | # 逐个处理分卷包,生成虚拟的第一个分卷包名,合并生成相同虚拟包名的分卷包 70 | dirpath_list.add(dirpath) 71 | for file in files: 72 | first_volume_path = create_fake_first_volume_path(file) 73 | if first_volume_path not in file_dict: 74 | file_dict[first_volume_path] = set() 75 | file_dict[first_volume_path].add(file) 76 | # 检查同级文件,补充缺失的分卷包 77 | for dirpath in dirpath_list: 78 | listdir = [os.path.normpath(os.path.join(dirpath, i)) for i in os.listdir(dirpath)] 79 | for file in listdir: 80 | if is_volume_archive(file): 81 | first_volume_path = create_fake_first_volume_path(file) 82 | if first_volume_path in file_dict: 83 | file_dict[first_volume_path].add(file) 84 | 85 | return file_dict 86 | 87 | 88 | def is_volume_archive(file): 89 | """判断是否为分卷压缩文件(通过文件后缀名判断)""" 90 | filename = os.path.basename(file) 91 | if (re.match(PATTERN_7Z, filename, flags=re.I) 92 | or re.match(PATTERN_RAR, filename, flags=re.I) 93 | or re.match(PATTERN_RAR_WITHOUT_SUFFIX, filename, flags=re.I) 94 | or re.match(PATTERN_ZIP, filename, flags=re.I) 95 | or re.match(PATTERN_ZIP_VOLUME, filename, flags=re.I) 96 | or re.match(PATTERN_ZIP_TYPE2, filename, flags=re.I)): 97 | return True 98 | else: 99 | return False 100 | 101 | 102 | def create_fake_first_volume_path(file, return_filetitle=False): 103 | """生成第一个分卷包的虚拟路径""" 104 | dir_, filename = os.path.split(file) 105 | filetitle = os.path.splitext(filename)[0] # 兜底文件标题(不含后缀) 106 | # test.7z.001/test.7z.002/test.7z.003 107 | if re.match(PATTERN_7Z, filename, flags=re.I): 108 | filetitle = re.match(PATTERN_7Z, filename, flags=re.I).group(1) 109 | first_volume_path = os.path.normpath(os.path.join(dir_, filetitle + '.7z.001')) 110 | # test.part1.rar/test.part2.rar/test.part3.rar 111 | elif re.match(PATTERN_RAR, filename, flags=re.I): 112 | filetitle = re.match(PATTERN_RAR, filename, flags=re.I).group(1) 113 | number_length = len(re.match(PATTERN_RAR, filename, flags=re.I).group(2)) # 解决part1.rar和part01.rar的情况 114 | first_volume_path = os.path.normpath(os.path.join(dir_, filetitle + f'.part{"1".zfill(number_length)}.rar')) 115 | # test.part1/test.part2/test.part3 116 | elif re.match(PATTERN_RAR_WITHOUT_SUFFIX, filename, flags=re.I): 117 | filetitle = re.match(PATTERN_RAR_WITHOUT_SUFFIX, filename, flags=re.I).group(1) 118 | number_length = len(re.match(PATTERN_RAR_WITHOUT_SUFFIX, filename, flags=re.I).group(2)) 119 | first_volume_path = os.path.normpath(os.path.join(dir_, filetitle + f'.part{"1".zfill(number_length)}')) 120 | # test.zip 121 | elif re.match(PATTERN_ZIP, filename, flags=re.I): 122 | first_volume_path = file 123 | # test.zip/test.z01/test.z02 124 | elif re.match(PATTERN_ZIP_VOLUME, filename, flags=re.I): 125 | filetitle = re.match(PATTERN_ZIP_VOLUME, filename, flags=re.I).group(1) 126 | first_volume_path = os.path.normpath(os.path.join(dir_, filetitle + '.zip')) 127 | # test.zip.001/test.zip.002/test.zip.003 128 | elif re.match(PATTERN_ZIP_TYPE2, filename, flags=re.I): 129 | filetitle = re.match(PATTERN_ZIP_TYPE2, filename, flags=re.I).group(1) 130 | first_volume_path = os.path.normpath(os.path.join(dir_, filetitle + '.zip.001')) 131 | else: 132 | return False 133 | 134 | if return_filetitle: 135 | return filetitle 136 | else: 137 | return os.path.normpath(first_volume_path) 138 | -------------------------------------------------------------------------------- /module/function_config.py: -------------------------------------------------------------------------------- 1 | # 配置文件相关方法 2 | 3 | import configparser 4 | import os 5 | 6 | from constant import _CONFIG_FILE 7 | 8 | 9 | def create_default_config(): 10 | """创建默认配置文件""" 11 | if not os.path.exists(_CONFIG_FILE): 12 | with open(_CONFIG_FILE, 'w', encoding='utf-8') as _: 13 | config = configparser.ConfigParser() 14 | config.read(_CONFIG_FILE, encoding='utf-8') 15 | 16 | config.add_section('OPTION') 17 | config.set('OPTION', 'mode_extract', 'True') # 解压模式 18 | config.set('OPTION', 'mode_test', 'False') # 测试模式 19 | config.set('OPTION', 'smart_extract', 'True') # 智能解压 20 | config.set('OPTION', 'extract_to_folder', 'False') # 解压至同名文件夹 21 | config.set('OPTION', 'delete_file', 'False') # 解压后删除源文件 22 | config.set('OPTION', 'handle_multi_folder', 'True') # 处理多层嵌套文件夹 23 | config.set('OPTION', 'handle_multi_archive', 'True') # 处理多层嵌套压缩包 24 | config.set('OPTION', 'check_filetype', 'True') # 检查文件类型(仅处理压缩包) 25 | config.set('OPTION', 'output_folder', '') # 解压至指定目录 26 | config.set('OPTION', 'filter_suffix', '') # 解压时排除的文件后缀 27 | 28 | config.write(open(_CONFIG_FILE, 'w', encoding='utf-8')) 29 | 30 | 31 | def _get_value_normal(section, key): 32 | """获取指定section的key的value(原始格式)""" 33 | config = configparser.ConfigParser() 34 | config.read(_CONFIG_FILE, encoding='utf-8') 35 | value = config.get(section, key) 36 | return eval(value) 37 | 38 | 39 | def _get_value_str(section, key): 40 | """获取指定section的key的value(文本格式)""" 41 | config = configparser.ConfigParser() 42 | config.read(_CONFIG_FILE, encoding='utf-8') 43 | value = config.get(section, key) 44 | return value 45 | 46 | 47 | def _reset_value(section, key, value): 48 | """设置指定section的key的value""" 49 | config = configparser.ConfigParser() 50 | config.read(_CONFIG_FILE, encoding='utf-8') 51 | if isinstance(value, (list, set)): 52 | value = ' '.join(value) 53 | config.set(section, key, str(value)) 54 | config.write(open(_CONFIG_FILE, 'w', encoding='utf-8')) 55 | 56 | 57 | class GetSetting: 58 | 59 | @staticmethod 60 | def mode_extract(): 61 | return _get_value_normal('OPTION', 'mode_extract') 62 | 63 | @staticmethod 64 | def mode_test(): 65 | return _get_value_normal('OPTION', 'mode_test') 66 | 67 | @staticmethod 68 | def smart_extract(): 69 | return _get_value_normal('OPTION', 'smart_extract') 70 | 71 | @staticmethod 72 | def extract_to_folder(): 73 | return _get_value_normal('OPTION', 'extract_to_folder') 74 | 75 | @staticmethod 76 | def delete_file(): 77 | return _get_value_normal('OPTION', 'delete_file') 78 | 79 | @staticmethod 80 | def handle_multi_folder(): 81 | return _get_value_normal('OPTION', 'handle_multi_folder') 82 | 83 | @staticmethod 84 | def handle_multi_archive(): 85 | return _get_value_normal('OPTION', 'handle_multi_archive') 86 | 87 | @staticmethod 88 | def check_filetype(): 89 | return _get_value_normal('OPTION', 'check_filetype') 90 | 91 | @staticmethod 92 | def output_folder(): 93 | return _get_value_str('OPTION', 'output_folder') 94 | 95 | @staticmethod 96 | def filter_suffix(): 97 | return _get_value_str('OPTION', 'filter_suffix') 98 | 99 | 100 | class ResetSetting: 101 | 102 | @staticmethod 103 | def mode_extract(value): 104 | _reset_value('OPTION', 'mode_extract', value) 105 | 106 | @staticmethod 107 | def mode_test(value): 108 | _reset_value('OPTION', 'mode_test', value) 109 | 110 | @staticmethod 111 | def smart_extract(value): 112 | _reset_value('OPTION', 'smart_extract', value) 113 | 114 | @staticmethod 115 | def extract_to_folder(value): 116 | _reset_value('OPTION', 'extract_to_folder', value) 117 | 118 | @staticmethod 119 | def delete_file(value): 120 | _reset_value('OPTION', 'delete_file', value) 121 | 122 | @staticmethod 123 | def handle_multi_folder(value): 124 | _reset_value('OPTION', 'handle_multi_folder', value) 125 | 126 | @staticmethod 127 | def handle_multi_archive(value): 128 | _reset_value('OPTION', 'handle_multi_archive', value) 129 | 130 | @staticmethod 131 | def check_filetype(value): 132 | _reset_value('OPTION', 'check_filetype', value) 133 | 134 | @staticmethod 135 | def output_folder(value): 136 | _reset_value('OPTION', 'output_folder', value) 137 | 138 | @staticmethod 139 | def filter_suffix(value): 140 | _reset_value('OPTION', 'filter_suffix', value) 141 | -------------------------------------------------------------------------------- /module/function_normal.py: -------------------------------------------------------------------------------- 1 | # 一般方法 2 | 3 | import inspect 4 | import os 5 | import shutil 6 | import time 7 | from typing import Union 8 | 9 | import send2trash 10 | 11 | from constant import _BACKUP_FOLDER, _TEMP_FOLDER, _PASSWORD_FILE, _HISTORY_FILE, _HISTORY_FILE_MAX_SIZE, \ 12 | _TIME_STAMP 13 | from module import function_password, function_config, function_archive 14 | 15 | 16 | def print_function_info(mode: str = 'current'): 17 | """ 18 | 打印当前/上一个执行的函数信息 19 | :param mode: str类型,'current' 或 'last' 20 | """ 21 | # pass 22 | 23 | if mode == 'current': 24 | print(time.strftime('%H:%M:%S ', time.localtime()), 25 | inspect.getframeinfo(inspect.currentframe().f_back).function) 26 | elif mode == 'last': 27 | print(time.strftime('%H:%M:%S ', time.localtime()), 28 | inspect.getframeinfo(inspect.currentframe().f_back.f_back).function) 29 | 30 | 31 | def check_default_files(): 32 | """检查默认文件夹/文件是否存在""" 33 | # 检查备份文件夹 34 | if not os.path.exists(_BACKUP_FOLDER): 35 | os.mkdir(_BACKUP_FOLDER) 36 | # 检查密码数据库 37 | if not os.path.exists(_PASSWORD_FILE): 38 | function_password.create_default_password_file() 39 | # 检查配置文件 40 | function_config.create_default_config() 41 | 42 | 43 | def save_history(text: str): 44 | """保存文本到历史记录文件中""" 45 | # 检查历史记录文件大小是否超限 46 | if os.path.exists(_HISTORY_FILE) and os.path.getsize(_HISTORY_FILE) > _HISTORY_FILE_MAX_SIZE: 47 | _backup_history() 48 | # 写入记录 49 | with open(_HISTORY_FILE, 'a', encoding='utf-8') as f: 50 | f.write(text + '\n') 51 | 52 | 53 | def _backup_history(): 54 | """备份历史记录""" 55 | time_text = time.strftime(_TIME_STAMP, time.localtime()) 56 | original_filetitle = os.path.basename(os.path.splitext(_HISTORY_FILE)[0]) 57 | suffix = os.path.splitext(_HISTORY_FILE)[1] 58 | new_filepath = os.path.normpath(os.path.join(_BACKUP_FOLDER, original_filetitle + time_text + suffix)) 59 | shutil.move(_HISTORY_FILE, new_filepath) 60 | 61 | 62 | def is_temp_folder_exists(check_path: Union[list, str]) -> bool: 63 | """检查传入路径的同级文件夹中是否存在临时文件夹(上一次解压未正常删除的)""" 64 | print_function_info() 65 | # 统一格式 66 | if type(check_path) is str: 67 | check_path = [check_path] 68 | 69 | # 获取所有可能存在的临时文件夹虚拟路径(添加后缀生成,非真实路径) 70 | temp_folders = set() 71 | for path in check_path: 72 | if os.path.isfile(path): 73 | join_path = os.path.join(os.path.dirname(path), _TEMP_FOLDER) 74 | temp_folders.add(join_path) 75 | else: 76 | join_path = os.path.join(path, _TEMP_FOLDER) 77 | temp_folders.add(join_path) 78 | 79 | # 逐个检查虚拟路径 80 | for path in temp_folders: 81 | if os.path.exists(path) and os.listdir(path): # 路径存在且 82 | return True 83 | 84 | return False 85 | 86 | 87 | def get_folder_size(folder: str) -> int: 88 | """ 89 | 获取指定文件夹的总大小/byte 90 | :param folder: str类型,文件夹路径 91 | :return: int类型,总字节大小 92 | """ 93 | print_function_info() 94 | folder_size = 0 95 | for dirpath, dirnames, filenames in os.walk(folder): 96 | for item in filenames: 97 | filepath = os.path.join(dirpath, item) 98 | folder_size += os.path.getsize(filepath) 99 | 100 | return folder_size 101 | 102 | 103 | def get_files(folder: str) -> list: 104 | """ 105 | 获取指定文件夹中的所有文件路径 106 | :param folder: str类型,文件夹路径 107 | :return: list类型,所有文件路径的列表 108 | """ 109 | print_function_info() 110 | files = [] 111 | for dirpath, dirnames, filenames in os.walk(folder): 112 | for filename in filenames: 113 | file_path = os.path.normpath(os.path.join(dirpath, filename)) 114 | files.append(file_path) 115 | 116 | return files 117 | 118 | 119 | def get_files_in_paths(paths: list): 120 | """提取输入路径列表中所有文件路径""" 121 | files = set() 122 | for path in paths: 123 | if os.path.exists(path): 124 | if os.path.isfile(path): 125 | files.add(path) 126 | else: 127 | walk_files = get_files(path) 128 | files.update(walk_files) 129 | # 统一路径格式 130 | files = [os.path.normpath(i) for i in files] 131 | return files 132 | 133 | 134 | def get_first_multi_path(dirpath: str) -> str: 135 | """检查传入文件夹路径的层级,找出首个含多文件的文件夹路径或单文件路径 136 | :param dirpath: str类型,文件夹路径 137 | :return: str类型,文件夹路径 138 | """ 139 | if len(os.listdir(dirpath)) == 1: # 如果文件夹下只有一个文件/文件夹 140 | check_path = os.path.normpath(os.path.join(dirpath, os.listdir(dirpath)[0])) 141 | # 如果是文件,则直接返回 142 | if os.path.isfile(check_path): 143 | return check_path 144 | # 如果是文件夹,则递归 145 | else: 146 | return get_first_multi_path(check_path) 147 | else: 148 | return dirpath 149 | 150 | 151 | def create_nodup_filename(path: str, target_dirpath: str, dup_suffix: str = ' -New', 152 | target_filetitle: str = None) -> str: 153 | """生成指定路径对应的文件在目标文件夹中无重复的文件名(可指定目标文件名) 154 | :param path: str,文件路径或文件夹路径 155 | :param target_dirpath: str,目标文件夹路径 156 | :param dup_suffix: 若存在重复文件名则添加的后缀 157 | :param target_filetitle: str,目标文件名(不含后缀) 158 | :return: str,无重复的文件名(非完整路径,仅含后缀的文件名) 159 | """ 160 | # 提取原始文件名 161 | dirpath = os.path.dirname(path) 162 | if os.path.isfile(path): 163 | filetitle = os.path.basename(os.path.splitext(path)[0]) 164 | suffix = os.path.splitext(path)[1] 165 | else: 166 | filetitle = os.path.basename(path) 167 | suffix = '' 168 | 169 | # 剔除原始文件名中的自定义后缀 170 | index_suffix = filetitle.rfind(dup_suffix) 171 | if index_suffix != -1 and filetitle[index_suffix + len(dup_suffix):].isdigit(): 172 | filetitle = filetitle[0:index_suffix] 173 | 174 | # 生成目标文件名 175 | if target_filetitle: 176 | new_filename = target_filetitle + suffix 177 | temp_filename = target_filetitle + dup_suffix + '1' + suffix 178 | else: 179 | target_filetitle = filetitle 180 | new_filename = filetitle + suffix 181 | temp_filename = filetitle + dup_suffix + '1' + suffix 182 | # 生成无重复的目标文件名 183 | # 一直循环累加直到不存在相同文件名(同级目录也要检查,防止重命名时报错) 184 | count = 0 185 | while os.path.exists(os.path.join(target_dirpath, new_filename)) or os.path.exists( 186 | os.path.join(dirpath, temp_filename)): 187 | count += 1 188 | new_filename = f'{target_filetitle}{dup_suffix}{count}{suffix}' 189 | temp_filename = new_filename 190 | 191 | return new_filename 192 | 193 | 194 | def delete_empty_folder(folder: str) -> bool: 195 | """检查文件夹是否为空,是则删除""" 196 | print_function_info() 197 | if os.path.exists(folder) and os.path.isdir(folder) and get_folder_size(folder) == 0: 198 | shutil.rmtree(folder) 199 | return True 200 | else: 201 | return False 202 | 203 | 204 | def delete_files(files): 205 | """删除文件至回收站""" 206 | print_function_info() 207 | for file in files: 208 | file = os.path.normpath(file) 209 | try: 210 | send2trash.send2trash(file) 211 | except OSError: 212 | # 尝试重新删除 213 | try: 214 | for _ in range(10): 215 | time.sleep(0.5) 216 | if os.path.exists(file): 217 | send2trash.send2trash(file) 218 | else: 219 | break 220 | except OSError: 221 | # 放弃删除 222 | pass 223 | 224 | 225 | def get_filetitle(path: str) -> str: 226 | """提取路径的文件标题(不含后缀)""" 227 | # 先按文件类型直接提取 228 | if os.path.isdir(path): 229 | filetitle = os.path.basename(path) 230 | else: 231 | filetitle = os.path.basename(os.path.splitext(path)[0]) 232 | 233 | # 单独处理分卷压缩文件 234 | if function_archive.is_volume_archive(path): 235 | filetitle = function_archive.create_fake_first_volume_path(path, return_filetitle=True) 236 | 237 | # 处理文件标题两端多余的空格和. 238 | while filetitle[0] in [' ', '.'] or filetitle[-1] in [' ', '.']: 239 | filetitle = filetitle.strip() 240 | filetitle = filetitle.strip('.') 241 | 242 | return filetitle 243 | 244 | 245 | def move_file(path, target_folder=None): 246 | """移动文件至指定目录""" 247 | # 提取文件名,生成指定目录下无重复的文件/文件夹名 248 | target_folder = target_folder if target_folder else os.path.dirname(path) 249 | if not os.path.exists(target_folder): 250 | os.mkdir(target_folder) 251 | filename_nodup = create_nodup_filename(path, target_folder) 252 | 253 | # 先改名 254 | new_path_nodup = os.path.normpath(os.path.join(os.path.dirname(path), filename_nodup)) 255 | try: 256 | os.rename(path, new_path_nodup) 257 | except PermissionError: # 报错【PermissionError: [WinError 5] 拒绝访问。】,等待0.2秒再次尝试 258 | time.sleep(0.2) 259 | os.rename(path, new_path_nodup) 260 | 261 | # 再移动 262 | try: 263 | shutil.move(new_path_nodup, target_folder) 264 | except OSError: # 报错【OSError: [WinError 145] 目录不是空的。】,原路径下有残留的空文件夹,尝试直接删除 265 | delete_empty_folder(path) 266 | 267 | # 组合最终路径 268 | final_path = os.path.normpath(os.path.join(target_folder, filename_nodup)) 269 | # 如果原始文件夹为空,则直接删除 270 | delete_empty_folder(path) 271 | 272 | return final_path 273 | -------------------------------------------------------------------------------- /module/function_password.py: -------------------------------------------------------------------------------- 1 | # 密码数据库的相关方法 2 | # 密码数据库格式说明:存储一个dict,键为密码str,值为对应的使用次数int 3 | 4 | import os 5 | import pickle 6 | import shutil 7 | import time 8 | from typing import Union 9 | 10 | from constant import _PASSWORD_FILE, _PASSWORD_EXPORT, _BACKUP_FOLDER, _TIME_STAMP 11 | from module import function_normal 12 | 13 | 14 | def create_default_password_file(): 15 | """创建默认密码文件""" 16 | function_normal.print_function_info() 17 | with open(_PASSWORD_FILE, 'wb') as f: 18 | pickle.dump({}, f) 19 | 20 | 21 | def read_password(password_file=_PASSWORD_FILE) -> list: 22 | """读取密码,按使用次数排序后返回列表""" 23 | function_normal.print_function_info() 24 | # 读取 25 | with open(password_file, 'rb') as f: 26 | password_dict = pickle.load(f) 27 | # 排序 28 | passwords = sorted(password_dict, key=password_dict.get, reverse=True) 29 | passwords = list(passwords) 30 | 31 | return passwords 32 | 33 | 34 | def export_password(): 35 | """导出明文密码""" 36 | function_normal.print_function_info() 37 | passwords = read_password() 38 | 39 | with open(_PASSWORD_EXPORT, 'w', encoding='utf-8') as pw: 40 | pw.write('\n'.join(passwords)) 41 | 42 | 43 | def open_export(): 44 | """打开导出的密码文件""" 45 | os.startfile(_PASSWORD_EXPORT) 46 | 47 | 48 | def update_password(key: Union[list, str]): 49 | """更新密码""" 50 | function_normal.print_function_info() 51 | # 统一格式 52 | if isinstance(key, str): 53 | key = [key] 54 | # 读取 55 | with open(_PASSWORD_FILE, 'rb') as f: 56 | password_dict = pickle.load(f) 57 | # 添加 58 | for pw in key: 59 | if pw not in password_dict: 60 | password_dict[pw] = 0 61 | else: 62 | password_dict[pw] += 1 63 | # 保存 64 | with open(_PASSWORD_FILE, 'wb') as f: 65 | pickle.dump(password_dict, f) 66 | 67 | 68 | def backup_password(): 69 | """备份密码数据库""" 70 | function_normal.print_function_info() 71 | time_text = time.strftime(_TIME_STAMP, time.localtime()) 72 | original_filetitle = os.path.basename(os.path.splitext(_PASSWORD_FILE)[0]) 73 | suffix = os.path.splitext(_PASSWORD_FILE)[1] 74 | new_filepath = os.path.normpath(os.path.join(_BACKUP_FOLDER, original_filetitle + time_text + suffix)) 75 | shutil.copyfile(_PASSWORD_FILE, new_filepath) 76 | 77 | 78 | def read_password_from_files(files: list): 79 | """从文件列表中读取文件名中可能包含的密码""" 80 | pws_joined = set() 81 | for file in files: 82 | filename = function_normal.get_filetitle(file) 83 | pws = _read_password_from_filename(filename) 84 | pws_joined.update(pws) 85 | 86 | return list(pws_joined) 87 | 88 | 89 | def _read_password_from_filename(filename: str): 90 | """从文件名中读取可能包含的密码 91 | :param filename: str,不含后缀的文件名""" 92 | pws = set() 93 | # 密码类型 - 整个文件名 94 | pws.add(filename) 95 | # 密码类型 - 以空格为分隔符,拆分文件名 96 | split = filename.split(' ') 97 | pws.update(split) 98 | # 密码类型 - 拆分的文件名字段,剔除标识符# 99 | pws.update(_deal_split_set(split, left_edge_str='#')) 100 | # 密码类型 - 拆分的文件名字段,剔除标识符@ 101 | pws.update(_deal_split_set(split, left_edge_str='@')) 102 | # 密码类型 - 拆分的文件名字段,剔除标识符【】 103 | pws.update(_deal_split_set(split, left_edge_str='【', right_edge_str='】')) 104 | # 密码类型 - 拆分的文件名字段,剔除标识符[] 105 | pws.update(_deal_split_set(split, left_edge_str='[', right_edge_str=']')) 106 | # 密码类型 - 拆分的文件名字段,剔除标识符() 107 | pws.update(_deal_split_set(split, left_edge_str='(', right_edge_str=')')) 108 | # 密码类型 - 拆分的文件名字段,剔除文本"密码" 109 | pws.update(_deal_split_set(split, left_edge_str='密码')) 110 | # 密码类型 - 拆分的文件名字段,剔除文本"密码:" 111 | pws.update(_deal_split_set(split, left_edge_str='密码:')) 112 | # 密码类型 - 拆分的文件名字段,剔除文本"解压" 113 | pws.update(_deal_split_set(split, left_edge_str='解压')) 114 | # 密码类型 - 拆分的文件名字段,剔除文本"解压:" 115 | pws.update(_deal_split_set(split, left_edge_str='解压:')) 116 | # 密码类型 - 拆分的文件名字段,剔除文本"解压码" 117 | pws.update(_deal_split_set(split, left_edge_str='解压码')) 118 | # 密码类型 - 拆分的文件名字段,剔除文本"解压码:" 119 | pws.update(_deal_split_set(split, left_edge_str='解压码:')) 120 | # 密码类型 - 拆分的文件名字段,剔除文本"解压密码" 121 | pws.update(_deal_split_set(split, left_edge_str='解压密码')) 122 | # 密码类型 - 拆分的文件名字段,剔除文本"解压密码:" 123 | pws.update(_deal_split_set(split, left_edge_str='解压密码:')) 124 | # 密码类型 - 拆分的文件名字段,剔除文本"pw" 125 | pws.update(_deal_split_set(split, left_edge_str='pw')) 126 | # 密码类型 - 拆分的文件名字段,剔除文本"pw:" 127 | pws.update(_deal_split_set(split, left_edge_str='pw:')) 128 | 129 | return pws 130 | 131 | 132 | def _deal_split_set(splits: Union[list, set], left_edge_str: str = '', right_edge_str: str = ''): 133 | """处理拆分的字符段""" 134 | f_splits = set() # 处理结果 135 | for i in splits: 136 | i: str 137 | if len(i) <= len(left_edge_str) + len(right_edge_str): # 大于左右两端字符数才进行处理 138 | continue 139 | 140 | if left_edge_str: 141 | if i.startswith(left_edge_str): 142 | i = i[len(left_edge_str):] 143 | else: 144 | continue 145 | 146 | if right_edge_str: 147 | if i.endswith(right_edge_str): 148 | i = i[:-len(right_edge_str)] 149 | else: 150 | continue 151 | 152 | f_splits.add(i) 153 | 154 | return f_splits 155 | -------------------------------------------------------------------------------- /nuitka.txt: -------------------------------------------------------------------------------- 1 | python -m nuitka --standalone --onefile --remove-output --mingw64 --windows-console-mode=force --windows-icon-from-ico=icon\app.ico --enable-plugin=pyside6 main.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Python 3.10.11 2 | filetype==1.2.0 3 | PySide6==6.2.4 4 | Send2Trash==1.8.3 5 | -------------------------------------------------------------------------------- /thread/thread_7zip.py: -------------------------------------------------------------------------------- 1 | # 7zip子线程 2 | import os 3 | import re 4 | import subprocess 5 | 6 | from PySide6.QtCore import QThread, Signal 7 | 8 | from constant import _PASSWORD_FAKE, _TEMP_FOLDER, _PATH_7ZIP 9 | from module import function_password, function_7zip, function_normal 10 | from module.function_7zip import Result7zip 11 | from module.function_config import GetSetting 12 | 13 | 14 | class Thread7zip(QThread): 15 | # 运行信号 16 | signal_start = Signal() 17 | signal_stop = Signal() 18 | signal_finish = Signal() 19 | signal_finish_restart = Signal(list) 20 | # 进度信号 21 | signal_current_file = Signal(str) 22 | signal_schedule_file = Signal(str) 23 | signal_schedule_test = Signal(str) 24 | signal_schedule_extract = Signal(int) 25 | # 7zip调用结果信号 26 | signal_7zip_result = Signal(object) 27 | 28 | def __init__(self): 29 | super().__init__() 30 | self._is_stop_thread = False # 是否终止线程 31 | self._file_dict = dict() # dict结构:key为第一个分卷包路径/非分卷则为其本身,value为list,内部元素为其对应的所有分卷包 32 | # 读取密码 33 | self._passwords = None 34 | # 读取配置 35 | self._mode_extract = None 36 | self._extract_to_folder = None # 解压到同名文件夹 37 | self._delete_file = None # 解压后删除源文件 38 | self._handle_multi_folder = None # 处理多层嵌套文件夹 39 | self._handle_multi_archive = None # 处理多层嵌套压缩包 40 | self._output_folder = None # 解压至指定目录 41 | self._filter_suffix = None # 解压时排除的文件后缀 42 | 43 | # 最终解压结果列表,用于处理嵌套压缩文件(再次调用解压) 44 | self._extract_file_result = [] 45 | 46 | def set_file_dict(self, file_dict: dict): 47 | """设置需要处理的文件字典""" 48 | self._file_dict = file_dict 49 | 50 | def stop(self): 51 | """停止线程""" 52 | self._is_stop_thread = True 53 | 54 | def _update_setting(self): 55 | """更新设置""" 56 | # 读取密码 57 | # 虚拟密码放首位,用于测试无密码文件,文件名中提取的密码放第二位,密码表的密码放最后 58 | passwords_filename = function_password.read_password_from_files(list(self._file_dict.keys())) 59 | self._passwords = [_PASSWORD_FAKE] + passwords_filename + function_password.read_password() 60 | # 读取配置 61 | self._mode_extract = GetSetting.mode_extract() 62 | self._extract_to_folder = GetSetting.extract_to_folder() # 解压到同名文件夹 63 | self._delete_file = GetSetting.delete_file() # 解压后删除源文件 64 | self._handle_multi_folder = GetSetting.handle_multi_folder() # 处理多层嵌套文件夹 65 | self._handle_multi_archive = GetSetting.handle_multi_archive() # 处理多层嵌套压缩包 66 | self._output_folder = GetSetting.output_folder() # 解压至指定目录 67 | self._filter_suffix = ['-xr!*.' + i for i in GetSetting.filter_suffix().split(' ') if i] # 解压时排除的文件后缀 68 | 69 | def run(self): 70 | pass 71 | self._is_stop_thread = False 72 | self._extract_file_result.clear() 73 | self._update_setting() 74 | 75 | # 发送开始信号,更新主程序ui 76 | self.signal_start.emit() 77 | 78 | # 逐个处理文件 79 | count_file = len(self._file_dict) 80 | for index, file_first in enumerate(self._file_dict, start=1): 81 | if not os.path.exists(file_first): 82 | continue 83 | if self._is_stop_thread: 84 | break 85 | # 发送当前文件以及进度文本信号 86 | self.signal_current_file.emit(os.path.basename(file_first)) 87 | self.signal_schedule_file.emit(f'{index}/{count_file}') 88 | # 不论是测试模式还是解压模式,都先使用7zip的l命令进行一次测试(l命令在l/t/x中最快) 89 | # 在使用l命令前,先使用虚拟密码进行一次测试,判断该文件是否可以使用l命令进行正常测试 90 | fake_test_result, archive_info_dict = function_7zip.test_fake_password(file_first) 91 | if fake_test_result is True: # 可以使用l命令进行后续测试 92 | self._test_file_command_l(file_first, self._passwords) 93 | elif fake_test_result is False: # 无法使用l命令进行测试,执行对应模式操作 94 | if self._mode_extract: # 使用x命令进行解压 95 | self._extract_file(file_first, self._passwords) 96 | else: # 使用t命令进行测试 97 | paths_inside = archive_info_dict['paths'] 98 | if paths_inside: 99 | check_path_inside = paths_inside[0] # 仅测试一个文件即可 100 | else: 101 | check_path_inside = None 102 | self._test_file(file_first, self._passwords, check_path_inside=check_path_inside) 103 | else: # 文件本身存在问题,不进行后续操作 104 | self.signal_7zip_result.emit(fake_test_result) 105 | 106 | # 处理当前文件同目录的temp文件夹 107 | if self._mode_extract: 108 | self._delete_temp_folder(file_first) 109 | 110 | # 发送结束信号 111 | if self._is_stop_thread: 112 | self.signal_stop.emit() 113 | else: 114 | if self._mode_extract and self._handle_multi_archive: 115 | self.signal_finish_restart.emit(self._extract_file_result) 116 | else: 117 | self.signal_finish.emit() 118 | 119 | def _test_file(self, file, passwords, check_path_inside=None): 120 | """调用7zip的t命令测试文件""" 121 | count_password = len(passwords) 122 | result = Result7zip.WrongPassword # 兜底 123 | for index_password, password in enumerate(passwords, start=1): 124 | if self._is_stop_thread: 125 | break 126 | self.signal_schedule_test.emit(f'{index_password}/{count_password}') 127 | result, _ = function_7zip.call_7zip('t', file, password, check_path_inside=check_path_inside) 128 | if not isinstance(result, Result7zip.WrongPassword): # 测试结果不为“错误密码”时,确定正确密码 129 | break 130 | 131 | # 发送结果信号 132 | self.signal_7zip_result.emit(result) 133 | 134 | def _test_file_command_l(self, file, passwords): 135 | """调用7zip的l命令测试文件""" 136 | # 参照t测试的代码 137 | count_password = len(passwords) 138 | result = Result7zip.WrongPassword # 兜底 139 | right_password = _PASSWORD_FAKE # 兜底 140 | for index_password, password in enumerate(passwords, start=1): 141 | if self._is_stop_thread: 142 | break 143 | self.signal_schedule_test.emit(f'{index_password}/{count_password}') 144 | result, _ = function_7zip.call_7zip('l', file, password) 145 | if not isinstance(result, Result7zip.WrongPassword): # 测试结果不为“错误密码”时,确定正确密码 146 | right_password = password 147 | break 148 | 149 | # 发送结果信号 150 | if isinstance(result, Result7zip.Success) and self._mode_extract: # 如果是解压模式,则进行解压操作 151 | self._extract_file(file, right_password) 152 | else: 153 | self.signal_7zip_result.emit(result) 154 | 155 | def _extract_file(self, file, passwords): 156 | """解压文件""" 157 | if isinstance(passwords, str): 158 | passwords = [passwords] 159 | 160 | # 确认解压目录 161 | if self._output_folder: 162 | temp_folder = os.path.normpath(os.path.join(self._output_folder, _TEMP_FOLDER)) 163 | else: # 在该文件同级目录下创建临时文件夹 164 | temp_folder = os.path.normpath(os.path.join(os.path.dirname(file), _TEMP_FOLDER)) 165 | filetitle = function_normal.get_filetitle(file) 166 | extract_folder = os.path.normpath(os.path.join(temp_folder, filetitle)) 167 | 168 | # 调用7zip的x命令解压文件 169 | count_password = len(passwords) 170 | result = Result7zip.WrongPassword # 兜底 171 | 172 | for index_password, password in enumerate(passwords, start=1): 173 | if self._is_stop_thread: 174 | break 175 | self.signal_schedule_test.emit(f'{index_password}/{count_password}') 176 | result = self._run_7zip_x(file, extract_folder, password) 177 | if not isinstance(result, Result7zip.WrongPassword): # 结果不为“错误密码”时,确定正确密码 178 | break 179 | 180 | # 发送结果信号 181 | self.signal_7zip_result.emit(result) 182 | 183 | # 处理解压结果(如果成功解压) 184 | if isinstance(result, Result7zip.Success): 185 | # 删除源文件 186 | if self._delete_file: 187 | files_list = self._file_dict[file] 188 | function_normal.delete_files(files_list) 189 | 190 | # 智能解压/解压到同名文件夹和处理嵌套多层文件夹 191 | # 先确定需要移动的文件/文件夹路径 192 | need_move_path = extract_folder # 兜底 193 | # 第1次判断(智能解压) 194 | # 智能解压,参照bandizip的逻辑 195 | listdir = os.listdir(extract_folder) 196 | if len(listdir) == 1: 197 | need_move_path = os.path.normpath(os.path.join(extract_folder, listdir[0])) 198 | # 第2次判断(处理嵌套多层文件夹) 199 | if self._handle_multi_folder: # 处理嵌套多层文件夹,提取内部第一个多层文件夹路径 200 | need_move_path = function_normal.get_first_multi_path(extract_folder) 201 | # 再确定目标移动目录(临时文件夹同级+解压到同名文件夹) 202 | parent_folder = os.path.dirname(temp_folder) 203 | if self._extract_to_folder: 204 | # 生成不存在的无同名文件夹路径,防止移动至已存在的同名文件夹中 205 | target_folder = os.path.normpath(os.path.join(parent_folder, filetitle)) 206 | target_folder = os.path.normpath(os.path.join( 207 | parent_folder, function_normal.create_nodup_filename(target_folder, parent_folder))) 208 | # 如果解压目录下级只有一个文件夹,且该文件夹名称与目标文件夹名相同,则直接移动到目标文件夹父目录 209 | if len(listdir) == 1: 210 | _path = os.path.normpath(os.path.join(extract_folder, listdir[0])) 211 | if os.path.isdir(_path) and os.path.basename(_path) == os.path.basename(target_folder): 212 | target_folder = parent_folder 213 | else: 214 | target_folder = parent_folder 215 | 216 | # 移动 217 | final_path = function_normal.move_file(need_move_path, target_folder) 218 | # 收集解压结果(用于处理嵌套压缩文件) 219 | self._extract_file_result.append(final_path) 220 | 221 | # 删除临时文件夹下的多余文件/文件夹 222 | function_normal.delete_empty_folder(extract_folder) 223 | 224 | def _run_7zip_x(self, file, extract_folder, password): 225 | """调用7zip的x命令解压文件,并读取输出流""" 226 | # 同时读取stdout和stderr会导致管道堵塞,需要将这流重定向至1个管道中,使用switch 'bso1','bsp1',bse1' 227 | _7zip_command = [_PATH_7ZIP, 'x', '-y', file, 228 | '-bsp1', '-bse1', '-bso1', 229 | '-o' + extract_folder, 230 | '-p' + password] + self._filter_suffix 231 | 232 | # 调用 233 | print('测试 7zip命令', _7zip_command) 234 | process = subprocess.Popen(_7zip_command, 235 | stdout=subprocess.PIPE, 236 | stderr=subprocess.PIPE, 237 | creationflags=subprocess.CREATE_NO_WINDOW, 238 | text=True, 239 | universal_newlines=True) 240 | 241 | # 读取实时输出流 242 | # 使用subprocess.Popen调用7zip时,返回码为2时的错误信息为"<_io.TextIOWrapper name=4 encoding='cp936'>" 243 | # 无法正确判断错误情况,所以需要在实时输出的错误信息输出流中进行读取判断操作 244 | result_error = Result7zip.WrongPassword(file) # 兜底,默认为“密码错误” 245 | pre_progress = 0 # 解压进度,初始化0 246 | is_read_stderr = True # 是否读取stderr流,出现报错事件后/读取到进度信息后不需要再读取,节省性能 247 | is_read_progress = True # 是否读取进度信息,出现报错事件后不需要再读取,节省性能 248 | 249 | while True: 250 | try: 251 | output = process.stdout.readline() 252 | print('【7zip解压信息:', output, '】') # 测试用 253 | except UnicodeDecodeError: 254 | # 有时会报错编码错误 255 | # UnicodeDecodeError: 'gbk' codec can't decode byte 0xaa in position 32: illegal multibyte sequence 256 | output = '' 257 | if output == '' and process.poll() is not None: # 读取到空文本或返回码时,结束读取操作 258 | break 259 | if output and is_read_stderr: # 读取错误事件 260 | is_wrong_password = re.search('Wrong password', output) 261 | is_missing_volume = re.search('Missing volume', output) 262 | is_not_archive = re.search('Cannot open the file as', output) 263 | if is_wrong_password: 264 | result_error = Result7zip.WrongPassword(file) 265 | is_read_stderr = False 266 | is_read_progress = False 267 | elif is_missing_volume: 268 | result_error = Result7zip.MissingVolume(file) 269 | is_read_stderr = False 270 | is_read_progress = False 271 | elif is_not_archive: # 后缀指定格式不正确时zip会自动尝试正确格式解压,所以不需要停止读取输出流 272 | result_error = Result7zip.NotArchiveOrDamaged(file) 273 | 274 | if output and is_read_progress: # 读取进度事件 275 | # 单文件输出信息:34% - 061-090;多文件输出信息:19% 10 - 031-060。适用正则表达式 '(\d{1,3})% *\d* - ' 276 | # 某些压缩包的输出信息:80% 13。适用正则表达式 '(\d{1,3})% *\d*' 277 | match_progress = re.search(r'(\d{1,3})% *\d*', output) 278 | if match_progress: 279 | is_read_stderr = False 280 | current_progress = int(match_progress.group(1)) # 提取进度百分比(不含%) 281 | if current_progress > pre_progress: 282 | self.signal_schedule_extract.emit(current_progress) 283 | pre_progress = current_progress # 重置进度 284 | 285 | # 读取返回码 286 | if process.poll() == 0: # 没有错误 287 | # 实测有个特例,某个压缩包为rar格式,后缀为zip格式,解压时不会处理任何内部文件,没有生成解压结果,最终报错 288 | if os.path.exists(extract_folder): 289 | result = Result7zip.Success(file, password) 290 | else: 291 | result = Result7zip.UnknownError(file) 292 | elif process.poll() == 1: # 警告(非致命错误,例如被占用) 293 | result = Result7zip.FileOccupied(file) 294 | elif process.poll() == 2: # 致命错误 295 | result = result_error 296 | elif process.poll() == 8: # 内存不足,无法进行操作 297 | result = Result7zip.NotEnoughSpace(file) 298 | else: # 兜底 299 | result = Result7zip.UnknownError(file) 300 | 301 | return result 302 | 303 | def _delete_temp_folder(self, file): 304 | """删除文件同级目录中的空临时文件夹""" 305 | if self._output_folder: 306 | temp_folder = os.path.normpath(os.path.join(self._output_folder, _TEMP_FOLDER)) 307 | else: # 在该文件同级目录下创建临时文件夹 308 | temp_folder = os.path.normpath(os.path.join(os.path.dirname(file), _TEMP_FOLDER)) 309 | 310 | if os.path.exists(temp_folder): 311 | function_normal.delete_empty_folder(temp_folder) 312 | -------------------------------------------------------------------------------- /ui/OnlyUnzip.py: -------------------------------------------------------------------------------- 1 | # 主程序 2 | from PySide6.QtGui import QIcon 3 | from PySide6.QtWidgets import QMainWindow 4 | 5 | from constant import _ICON_HOMEPAGE, _ICON_PASSWORD, _ICON_SETTING, _ICON_HISTORY 6 | from ui.src.ui_main import Ui_MainWindow 7 | from ui.widget_page_history import ListWidgetHistory 8 | from ui.widget_page_homepage import WidgetPageHomepage 9 | from ui.widget_page_password import WidgetPagePassword 10 | from ui.widget_page_setting import WidgetPageSetting 11 | 12 | 13 | class OnlyUnzip(QMainWindow): 14 | def __init__(self, paths: list, parent=None): 15 | super().__init__(parent) 16 | self.ui = Ui_MainWindow() 17 | self.ui.setupUi(self) 18 | 19 | # ui设置 20 | self.ui.pushButton_page_homepage.setProperty('id', 0) 21 | self.ui.pushButton_page_homepage.setIcon(QIcon(_ICON_HOMEPAGE)) 22 | self.ui.pushButton_page_password.setProperty('id', 1) 23 | self.ui.pushButton_page_password.setIcon(QIcon(_ICON_PASSWORD)) 24 | self.ui.pushButton_page_setting.setProperty('id', 2) 25 | self.ui.pushButton_page_setting.setIcon(QIcon(_ICON_SETTING)) 26 | self.ui.pushButton_page_history.setProperty('id', 3) 27 | self.ui.pushButton_page_history.setIcon(QIcon(_ICON_HISTORY)) 28 | self.change_page(0) 29 | 30 | # 添加控件 31 | # 主页 32 | self.widget_homepage = WidgetPageHomepage() 33 | self.ui.page_homepage.layout().addWidget(self.widget_homepage) 34 | self.widget_homepage.signal_start_7zip.connect(lambda: self.set_widget_state(False)) 35 | self.widget_homepage.signal_finished_7zip.connect(lambda: self.set_widget_state(True)) 36 | self.widget_homepage.signal_7zip_result.connect(self.add_history) 37 | # 密码页 38 | self.widget_password = WidgetPagePassword() 39 | self.ui.page_password.layout().addWidget(self.widget_password) 40 | # 设置页 41 | self.widget_setting = WidgetPageSetting() 42 | self.ui.page_setting.layout().addWidget(self.widget_setting) 43 | self.widget_setting.signal_output_path.connect(self.set_default_drop_icon) 44 | # ;历史记录页 45 | self.widget_history = ListWidgetHistory() 46 | self.ui.page_history.layout().addWidget(self.widget_history) 47 | 48 | # 绑定槽函数 49 | self.ui.buttonGroup.buttonClicked.connect(self.change_page) 50 | 51 | # 响应初始传参 52 | if paths: 53 | self.widget_homepage.drop_paths(paths) 54 | 55 | def change_page(self, id_button): 56 | """切换页面""" 57 | # 转为int索引 58 | if isinstance(id_button, int): 59 | index = id_button 60 | else: 61 | index = id_button.property('id') 62 | # 切换页面 63 | self.ui.stackedWidget.setCurrentIndex(index) 64 | # 高亮被点击的按钮 65 | clicked_style = r'background-color: rgb(255, 228, 181);' 66 | for button in self.ui.buttonGroup.buttons(): 67 | id_c = button.property('id') 68 | if id_c == index: 69 | button.setStyleSheet(clicked_style) 70 | else: 71 | button.setStyleSheet('') 72 | 73 | def set_widget_state(self, enable: bool): 74 | """开始/结束解压和测试时禁用/启用部分控件""" 75 | self.widget_password.set_button_state(enable) 76 | self.widget_setting.set_widgets_state(enable) 77 | self.widget_history.reset_class() 78 | 79 | def add_history(self, state_class): 80 | """添加历史记录""" 81 | self.widget_history.insert_item(state_class) 82 | 83 | def set_default_drop_icon(self): 84 | self.widget_homepage.set_default_drop_icon() 85 | -------------------------------------------------------------------------------- /ui/label_drop.py: -------------------------------------------------------------------------------- 1 | # 支持拖入文件/文件夹的label 2 | 3 | from PySide6.QtCore import Signal 4 | from PySide6.QtGui import QPixmap, QMovie 5 | from PySide6.QtWidgets import QLabel 6 | 7 | from constant import _ICON_MAIN_DEFAULT, _ICON_DROP 8 | 9 | 10 | class LabelDrop(QLabel): 11 | """支持拖入文件/文件夹的label""" 12 | 13 | signal_dropped = Signal(list) # 发送拖入的所有路径list 14 | 15 | def __init__(self, parent=None): 16 | super().__init__(parent) 17 | self.setAcceptDrops(True) 18 | self.setScaledContents(True) 19 | 20 | self.setPixmap(QPixmap(_ICON_MAIN_DEFAULT)) 21 | self.last_icon_pixmap = None # 上一个图标的pixmap对象 22 | self.icon_movie = QMovie() # 动图对象,用于显示GIF 23 | 24 | def reset_icon(self, icon: str): 25 | if icon.endswith('.gif'): 26 | self.icon_movie = QMovie(icon) 27 | self.setMovie(self.icon_movie) 28 | self.icon_movie.start() 29 | else: 30 | self.icon_movie.stop() 31 | self.setPixmap(QPixmap(icon)) 32 | 33 | def dragEnterEvent(self, event): 34 | if event.mimeData().hasUrls(): 35 | event.accept() 36 | self.last_icon_pixmap = self.pixmap() 37 | self.setPixmap(QPixmap(_ICON_DROP)) 38 | else: 39 | event.ignore() 40 | 41 | def dragLeaveEvent(self, event): 42 | self.setPixmap(self.last_icon_pixmap) 43 | 44 | def dropEvent(self, event): 45 | if event.mimeData().hasUrls(): 46 | urls = event.mimeData().urls() 47 | drop_paths = [url.toLocalFile() for url in urls] 48 | self.signal_dropped.emit(drop_paths) 49 | -------------------------------------------------------------------------------- /ui/src/ui_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_mainsqxZjG.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.1.3 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import * # type: ignore 12 | from PySide6.QtGui import * # type: ignore 13 | from PySide6.QtWidgets import * # type: ignore 14 | 15 | 16 | class Ui_MainWindow(object): 17 | def setupUi(self, MainWindow): 18 | if not MainWindow.objectName(): 19 | MainWindow.setObjectName(u"MainWindow") 20 | MainWindow.resize(272, 238) 21 | self.centralwidget = QWidget(MainWindow) 22 | self.centralwidget.setObjectName(u"centralwidget") 23 | self.horizontalLayout = QHBoxLayout(self.centralwidget) 24 | self.horizontalLayout.setSpacing(0) 25 | self.horizontalLayout.setObjectName(u"horizontalLayout") 26 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 27 | self.verticalLayout = QVBoxLayout() 28 | self.verticalLayout.setSpacing(0) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.pushButton_page_homepage = QPushButton(self.centralwidget) 31 | self.buttonGroup = QButtonGroup(MainWindow) 32 | self.buttonGroup.setObjectName(u"buttonGroup") 33 | self.buttonGroup.addButton(self.pushButton_page_homepage) 34 | self.pushButton_page_homepage.setObjectName(u"pushButton_page_homepage") 35 | sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 36 | sizePolicy.setHorizontalStretch(0) 37 | sizePolicy.setVerticalStretch(0) 38 | sizePolicy.setHeightForWidth(self.pushButton_page_homepage.sizePolicy().hasHeightForWidth()) 39 | self.pushButton_page_homepage.setSizePolicy(sizePolicy) 40 | 41 | self.verticalLayout.addWidget(self.pushButton_page_homepage) 42 | 43 | self.pushButton_page_password = QPushButton(self.centralwidget) 44 | self.buttonGroup.addButton(self.pushButton_page_password) 45 | self.pushButton_page_password.setObjectName(u"pushButton_page_password") 46 | sizePolicy.setHeightForWidth(self.pushButton_page_password.sizePolicy().hasHeightForWidth()) 47 | self.pushButton_page_password.setSizePolicy(sizePolicy) 48 | 49 | self.verticalLayout.addWidget(self.pushButton_page_password) 50 | 51 | self.pushButton_page_setting = QPushButton(self.centralwidget) 52 | self.buttonGroup.addButton(self.pushButton_page_setting) 53 | self.pushButton_page_setting.setObjectName(u"pushButton_page_setting") 54 | sizePolicy.setHeightForWidth(self.pushButton_page_setting.sizePolicy().hasHeightForWidth()) 55 | self.pushButton_page_setting.setSizePolicy(sizePolicy) 56 | 57 | self.verticalLayout.addWidget(self.pushButton_page_setting) 58 | 59 | self.pushButton_page_history = QPushButton(self.centralwidget) 60 | self.buttonGroup.addButton(self.pushButton_page_history) 61 | self.pushButton_page_history.setObjectName(u"pushButton_page_history") 62 | sizePolicy.setHeightForWidth(self.pushButton_page_history.sizePolicy().hasHeightForWidth()) 63 | self.pushButton_page_history.setSizePolicy(sizePolicy) 64 | 65 | self.verticalLayout.addWidget(self.pushButton_page_history) 66 | 67 | self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) 68 | 69 | self.verticalLayout.addItem(self.verticalSpacer) 70 | 71 | self.verticalLayout.setStretch(0, 1) 72 | self.verticalLayout.setStretch(1, 1) 73 | self.verticalLayout.setStretch(2, 1) 74 | self.verticalLayout.setStretch(3, 1) 75 | 76 | self.horizontalLayout.addLayout(self.verticalLayout) 77 | 78 | self.stackedWidget = QStackedWidget(self.centralwidget) 79 | self.stackedWidget.setObjectName(u"stackedWidget") 80 | self.page_homepage = QWidget() 81 | self.page_homepage.setObjectName(u"page_homepage") 82 | self.verticalLayout_2 = QVBoxLayout(self.page_homepage) 83 | self.verticalLayout_2.setSpacing(0) 84 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 85 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 86 | self.stackedWidget.addWidget(self.page_homepage) 87 | self.page_password = QWidget() 88 | self.page_password.setObjectName(u"page_password") 89 | self.verticalLayout_3 = QVBoxLayout(self.page_password) 90 | self.verticalLayout_3.setSpacing(0) 91 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 92 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 93 | self.stackedWidget.addWidget(self.page_password) 94 | self.page_setting = QWidget() 95 | self.page_setting.setObjectName(u"page_setting") 96 | self.verticalLayout_4 = QVBoxLayout(self.page_setting) 97 | self.verticalLayout_4.setSpacing(0) 98 | self.verticalLayout_4.setObjectName(u"verticalLayout_4") 99 | self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) 100 | self.stackedWidget.addWidget(self.page_setting) 101 | self.page_history = QWidget() 102 | self.page_history.setObjectName(u"page_history") 103 | self.verticalLayout_5 = QVBoxLayout(self.page_history) 104 | self.verticalLayout_5.setSpacing(0) 105 | self.verticalLayout_5.setObjectName(u"verticalLayout_5") 106 | self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) 107 | self.stackedWidget.addWidget(self.page_history) 108 | 109 | self.horizontalLayout.addWidget(self.stackedWidget) 110 | 111 | self.horizontalLayout.setStretch(1, 1) 112 | MainWindow.setCentralWidget(self.centralwidget) 113 | 114 | self.retranslateUi(MainWindow) 115 | 116 | QMetaObject.connectSlotsByName(MainWindow) 117 | # setupUi 118 | 119 | def retranslateUi(self, MainWindow): 120 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"OnlyUnzip", None)) 121 | self.pushButton_page_homepage.setText(QCoreApplication.translate("MainWindow", u"\u4e3b\u9875", None)) 122 | self.pushButton_page_password.setText(QCoreApplication.translate("MainWindow", u"\u5bc6\u7801\u9875", None)) 123 | self.pushButton_page_setting.setText(QCoreApplication.translate("MainWindow", u"\u8bbe\u7f6e\u9875", None)) 124 | self.pushButton_page_history.setText(QCoreApplication.translate("MainWindow", u"\u5386\u53f2\u9875", None)) 125 | # retranslateUi 126 | 127 | -------------------------------------------------------------------------------- /ui/src/ui_main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 272 10 | 238 11 | 12 | 13 | 14 | OnlyUnzip 15 | 16 | 17 | 18 | 19 | 0 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 0 44 | 45 | 46 | 47 | 主页 48 | 49 | 50 | buttonGroup 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 0 59 | 0 60 | 61 | 62 | 63 | 密码页 64 | 65 | 66 | buttonGroup 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 0 76 | 77 | 78 | 79 | 设置页 80 | 81 | 82 | buttonGroup 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 0 91 | 0 92 | 93 | 94 | 95 | 历史页 96 | 97 | 98 | buttonGroup 99 | 100 | 101 | 102 | 103 | 104 | 105 | Qt::Vertical 106 | 107 | 108 | 109 | 20 110 | 40 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 124 | 125 | 0 126 | 127 | 128 | 0 129 | 130 | 131 | 0 132 | 133 | 134 | 0 135 | 136 | 137 | 138 | 139 | 140 | 141 | 0 142 | 143 | 144 | 0 145 | 146 | 147 | 0 148 | 149 | 150 | 0 151 | 152 | 153 | 0 154 | 155 | 156 | 157 | 158 | 159 | 160 | 0 161 | 162 | 163 | 0 164 | 165 | 166 | 0 167 | 168 | 169 | 0 170 | 171 | 172 | 0 173 | 174 | 175 | 176 | 177 | 178 | 179 | 0 180 | 181 | 182 | 0 183 | 184 | 185 | 0 186 | 187 | 188 | 0 189 | 190 | 191 | 0 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_homepage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_widget_page_homepageejiMfL.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.2.4 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QLabel, 19 | QProgressBar, QSizePolicy, QStackedWidget, QToolButton, 20 | QVBoxLayout, QWidget) 21 | 22 | class Ui_Form(object): 23 | def setupUi(self, Form): 24 | if not Form.objectName(): 25 | Form.setObjectName(u"Form") 26 | Form.resize(247, 245) 27 | self.verticalLayout = QVBoxLayout(Form) 28 | self.verticalLayout.setSpacing(6) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.verticalLayout.setContentsMargins(3, 3, 3, 3) 31 | self.layout_label_drop = QVBoxLayout() 32 | self.layout_label_drop.setSpacing(0) 33 | self.layout_label_drop.setObjectName(u"layout_label_drop") 34 | 35 | self.verticalLayout.addLayout(self.layout_label_drop) 36 | 37 | self.line = QFrame(Form) 38 | self.line.setObjectName(u"line") 39 | self.line.setFrameShape(QFrame.HLine) 40 | self.line.setFrameShadow(QFrame.Sunken) 41 | 42 | self.verticalLayout.addWidget(self.line) 43 | 44 | self.horizontalLayout_4 = QHBoxLayout() 45 | self.horizontalLayout_4.setSpacing(0) 46 | self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") 47 | self.label_schedule_file = QLabel(Form) 48 | self.label_schedule_file.setObjectName(u"label_schedule_file") 49 | 50 | self.horizontalLayout_4.addWidget(self.label_schedule_file) 51 | 52 | self.toolButton_stop = QToolButton(Form) 53 | self.toolButton_stop.setObjectName(u"toolButton_stop") 54 | 55 | self.horizontalLayout_4.addWidget(self.toolButton_stop) 56 | 57 | 58 | self.verticalLayout.addLayout(self.horizontalLayout_4) 59 | 60 | self.label_current_file = QLabel(Form) 61 | self.label_current_file.setObjectName(u"label_current_file") 62 | 63 | self.verticalLayout.addWidget(self.label_current_file) 64 | 65 | self.stackedWidget = QStackedWidget(Form) 66 | self.stackedWidget.setObjectName(u"stackedWidget") 67 | self.page = QWidget() 68 | self.page.setObjectName(u"page") 69 | self.horizontalLayout = QHBoxLayout(self.page) 70 | self.horizontalLayout.setSpacing(0) 71 | self.horizontalLayout.setObjectName(u"horizontalLayout") 72 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 73 | self.label_state = QLabel(self.page) 74 | self.label_state.setObjectName(u"label_state") 75 | 76 | self.horizontalLayout.addWidget(self.label_state) 77 | 78 | self.stackedWidget.addWidget(self.page) 79 | self.page_2 = QWidget() 80 | self.page_2.setObjectName(u"page_2") 81 | self.horizontalLayout_2 = QHBoxLayout(self.page_2) 82 | self.horizontalLayout_2.setSpacing(0) 83 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 84 | self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) 85 | self.label_5 = QLabel(self.page_2) 86 | self.label_5.setObjectName(u"label_5") 87 | 88 | self.horizontalLayout_2.addWidget(self.label_5) 89 | 90 | self.label_schedule_test = QLabel(self.page_2) 91 | self.label_schedule_test.setObjectName(u"label_schedule_test") 92 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) 93 | sizePolicy.setHorizontalStretch(0) 94 | sizePolicy.setVerticalStretch(0) 95 | sizePolicy.setHeightForWidth(self.label_schedule_test.sizePolicy().hasHeightForWidth()) 96 | self.label_schedule_test.setSizePolicy(sizePolicy) 97 | 98 | self.horizontalLayout_2.addWidget(self.label_schedule_test) 99 | 100 | self.stackedWidget.addWidget(self.page_2) 101 | self.page_3 = QWidget() 102 | self.page_3.setObjectName(u"page_3") 103 | self.horizontalLayout_3 = QHBoxLayout(self.page_3) 104 | self.horizontalLayout_3.setSpacing(0) 105 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") 106 | self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) 107 | self.label_6 = QLabel(self.page_3) 108 | self.label_6.setObjectName(u"label_6") 109 | 110 | self.horizontalLayout_3.addWidget(self.label_6) 111 | 112 | self.progressBar_schedule_extract = QProgressBar(self.page_3) 113 | self.progressBar_schedule_extract.setObjectName(u"progressBar_schedule_extract") 114 | self.progressBar_schedule_extract.setMaximumSize(QSize(16777215, 14)) 115 | font = QFont() 116 | font.setKerning(True) 117 | self.progressBar_schedule_extract.setFont(font) 118 | self.progressBar_schedule_extract.setValue(0) 119 | self.progressBar_schedule_extract.setTextVisible(False) 120 | 121 | self.horizontalLayout_3.addWidget(self.progressBar_schedule_extract) 122 | 123 | self.stackedWidget.addWidget(self.page_3) 124 | 125 | self.verticalLayout.addWidget(self.stackedWidget) 126 | 127 | self.verticalLayout.setStretch(0, 1) 128 | 129 | self.retranslateUi(Form) 130 | 131 | self.stackedWidget.setCurrentIndex(2) 132 | 133 | 134 | QMetaObject.connectSlotsByName(Form) 135 | # setupUi 136 | 137 | def retranslateUi(self, Form): 138 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 139 | self.label_schedule_file.setText(QCoreApplication.translate("Form", u"\u603b\u8fdb\u5ea6", None)) 140 | self.toolButton_stop.setText(QCoreApplication.translate("Form", u"...", None)) 141 | self.label_current_file.setText(QCoreApplication.translate("Form", u"\u5f53\u524d\u6587\u4ef6", None)) 142 | self.label_state.setText(QCoreApplication.translate("Form", u"\u5f53\u524d\u8fdb\u5ea6", None)) 143 | self.label_5.setText(QCoreApplication.translate("Form", u"\u6d4b\u8bd5\u5bc6\u7801\uff1a", None)) 144 | self.label_schedule_test.setText(QCoreApplication.translate("Form", u"0/0", None)) 145 | self.label_6.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u8fdb\u5ea6\uff1a", None)) 146 | # retranslateUi 147 | 148 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_homepage.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 247 10 | 245 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 6 19 | 20 | 21 | 3 22 | 23 | 24 | 3 25 | 26 | 27 | 3 28 | 29 | 30 | 3 31 | 32 | 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | Qt::Horizontal 43 | 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 51 | 52 | 53 | 54 | 总进度 55 | 56 | 57 | 58 | 59 | 60 | 61 | ... 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 当前文件 71 | 72 | 73 | 74 | 75 | 76 | 77 | 2 78 | 79 | 80 | 81 | 82 | 0 83 | 84 | 85 | 0 86 | 87 | 88 | 0 89 | 90 | 91 | 0 92 | 93 | 94 | 0 95 | 96 | 97 | 98 | 99 | 当前进度 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 0 109 | 110 | 111 | 0 112 | 113 | 114 | 0 115 | 116 | 117 | 0 118 | 119 | 120 | 0 121 | 122 | 123 | 124 | 125 | 测试密码: 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 0 134 | 0 135 | 136 | 137 | 138 | 0/0 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 0 148 | 149 | 150 | 0 151 | 152 | 153 | 0 154 | 155 | 156 | 0 157 | 158 | 159 | 0 160 | 161 | 162 | 163 | 164 | 解压进度: 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 16777215 173 | 14 174 | 175 | 176 | 177 | 178 | true 179 | 180 | 181 | 182 | 0 183 | 184 | 185 | false 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_password.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_widget_page_passwordrQgHQv.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.2.4 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QHBoxLayout, QPlainTextEdit, QPushButton, 19 | QSizePolicy, QVBoxLayout, QWidget) 20 | 21 | class Ui_Form(object): 22 | def setupUi(self, Form): 23 | if not Form.objectName(): 24 | Form.setObjectName(u"Form") 25 | Form.resize(241, 260) 26 | self.verticalLayout_2 = QVBoxLayout(Form) 27 | self.verticalLayout_2.setSpacing(0) 28 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 29 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 30 | self.plainTextEdit_password = QPlainTextEdit(Form) 31 | self.plainTextEdit_password.setObjectName(u"plainTextEdit_password") 32 | 33 | self.verticalLayout_2.addWidget(self.plainTextEdit_password) 34 | 35 | self.horizontalLayout = QHBoxLayout() 36 | self.horizontalLayout.setSpacing(0) 37 | self.horizontalLayout.setObjectName(u"horizontalLayout") 38 | self.verticalLayout = QVBoxLayout() 39 | self.verticalLayout.setSpacing(0) 40 | self.verticalLayout.setObjectName(u"verticalLayout") 41 | self.pushButton_read_clipboard = QPushButton(Form) 42 | self.pushButton_read_clipboard.setObjectName(u"pushButton_read_clipboard") 43 | 44 | self.verticalLayout.addWidget(self.pushButton_read_clipboard) 45 | 46 | self.pushButton_export_password = QPushButton(Form) 47 | self.pushButton_export_password.setObjectName(u"pushButton_export_password") 48 | 49 | self.verticalLayout.addWidget(self.pushButton_export_password) 50 | 51 | self.pushButton_open_export = QPushButton(Form) 52 | self.pushButton_open_export.setObjectName(u"pushButton_open_export") 53 | 54 | self.verticalLayout.addWidget(self.pushButton_open_export) 55 | 56 | 57 | self.horizontalLayout.addLayout(self.verticalLayout) 58 | 59 | self.pushButton_update_password = QPushButton(Form) 60 | self.pushButton_update_password.setObjectName(u"pushButton_update_password") 61 | sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 62 | sizePolicy.setHorizontalStretch(0) 63 | sizePolicy.setVerticalStretch(0) 64 | sizePolicy.setHeightForWidth(self.pushButton_update_password.sizePolicy().hasHeightForWidth()) 65 | self.pushButton_update_password.setSizePolicy(sizePolicy) 66 | 67 | self.horizontalLayout.addWidget(self.pushButton_update_password) 68 | 69 | 70 | self.verticalLayout_2.addLayout(self.horizontalLayout) 71 | 72 | 73 | self.retranslateUi(Form) 74 | 75 | QMetaObject.connectSlotsByName(Form) 76 | # setupUi 77 | 78 | def retranslateUi(self, Form): 79 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 80 | self.pushButton_read_clipboard.setText(QCoreApplication.translate("Form", u"\u8bfb\u53d6\u526a\u5207\u677f", None)) 81 | self.pushButton_export_password.setText(QCoreApplication.translate("Form", u"\u5bfc\u51fa\u5bc6\u7801", None)) 82 | self.pushButton_open_export.setText(QCoreApplication.translate("Form", u"\u6253\u5f00\u5bfc\u51fa\u6587\u4ef6", None)) 83 | self.pushButton_update_password.setText(QCoreApplication.translate("Form", u"\u66f4\u65b0\u5bc6\u7801", None)) 84 | # retranslateUi 85 | 86 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_password.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 241 10 | 260 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 40 | 41 | 42 | 43 | 0 44 | 45 | 46 | 47 | 48 | 读取剪切板 49 | 50 | 51 | 52 | 53 | 54 | 55 | 导出密码 56 | 57 | 58 | 59 | 60 | 61 | 62 | 打开导出文件 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 0 73 | 0 74 | 75 | 76 | 77 | 更新密码 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'ui_widget_page_settingkAifck.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.1.3 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import * # type: ignore 12 | from PySide6.QtGui import * # type: ignore 13 | from PySide6.QtWidgets import * # type: ignore 14 | 15 | 16 | class Ui_Form(object): 17 | def setupUi(self, Form): 18 | if not Form.objectName(): 19 | Form.setObjectName(u"Form") 20 | Form.resize(213, 287) 21 | self.verticalLayout = QVBoxLayout(Form) 22 | self.verticalLayout.setSpacing(0) 23 | self.verticalLayout.setObjectName(u"verticalLayout") 24 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 25 | self.scrollArea = QScrollArea(Form) 26 | self.scrollArea.setObjectName(u"scrollArea") 27 | self.scrollArea.setWidgetResizable(True) 28 | self.scrollAreaWidgetContents = QWidget() 29 | self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") 30 | self.scrollAreaWidgetContents.setGeometry(QRect(0, -54, 194, 339)) 31 | self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents) 32 | self.verticalLayout_2.setSpacing(5) 33 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 34 | self.verticalLayout_2.setContentsMargins(3, 3, 3, 3) 35 | self.checkBox_mode_extract = QCheckBox(self.scrollAreaWidgetContents) 36 | self.buttonGroup_mode = QButtonGroup(Form) 37 | self.buttonGroup_mode.setObjectName(u"buttonGroup_mode") 38 | self.buttonGroup_mode.addButton(self.checkBox_mode_extract) 39 | self.checkBox_mode_extract.setObjectName(u"checkBox_mode_extract") 40 | 41 | self.verticalLayout_2.addWidget(self.checkBox_mode_extract) 42 | 43 | self.checkBox_mode_test = QCheckBox(self.scrollAreaWidgetContents) 44 | self.buttonGroup_mode.addButton(self.checkBox_mode_test) 45 | self.checkBox_mode_test.setObjectName(u"checkBox_mode_test") 46 | 47 | self.verticalLayout_2.addWidget(self.checkBox_mode_test) 48 | 49 | self.line = QFrame(self.scrollAreaWidgetContents) 50 | self.line.setObjectName(u"line") 51 | self.line.setFrameShape(QFrame.HLine) 52 | self.line.setFrameShadow(QFrame.Sunken) 53 | 54 | self.verticalLayout_2.addWidget(self.line) 55 | 56 | self.checkBox_extract_smart = QCheckBox(self.scrollAreaWidgetContents) 57 | self.buttonGroup_folder = QButtonGroup(Form) 58 | self.buttonGroup_folder.setObjectName(u"buttonGroup_folder") 59 | self.buttonGroup_folder.addButton(self.checkBox_extract_smart) 60 | self.checkBox_extract_smart.setObjectName(u"checkBox_extract_smart") 61 | 62 | self.verticalLayout_2.addWidget(self.checkBox_extract_smart) 63 | 64 | self.checkBox_extract_folder = QCheckBox(self.scrollAreaWidgetContents) 65 | self.buttonGroup_folder.addButton(self.checkBox_extract_folder) 66 | self.checkBox_extract_folder.setObjectName(u"checkBox_extract_folder") 67 | 68 | self.verticalLayout_2.addWidget(self.checkBox_extract_folder) 69 | 70 | self.line_2 = QFrame(self.scrollAreaWidgetContents) 71 | self.line_2.setObjectName(u"line_2") 72 | self.line_2.setFrameShape(QFrame.HLine) 73 | self.line_2.setFrameShadow(QFrame.Sunken) 74 | 75 | self.verticalLayout_2.addWidget(self.line_2) 76 | 77 | self.checkBox_delete_file = QCheckBox(self.scrollAreaWidgetContents) 78 | self.checkBox_delete_file.setObjectName(u"checkBox_delete_file") 79 | 80 | self.verticalLayout_2.addWidget(self.checkBox_delete_file) 81 | 82 | self.checkBox_handle_multi_folder = QCheckBox(self.scrollAreaWidgetContents) 83 | self.checkBox_handle_multi_folder.setObjectName(u"checkBox_handle_multi_folder") 84 | 85 | self.verticalLayout_2.addWidget(self.checkBox_handle_multi_folder) 86 | 87 | self.line_3 = QFrame(self.scrollAreaWidgetContents) 88 | self.line_3.setObjectName(u"line_3") 89 | self.line_3.setFrameShape(QFrame.HLine) 90 | self.line_3.setFrameShadow(QFrame.Sunken) 91 | 92 | self.verticalLayout_2.addWidget(self.line_3) 93 | 94 | self.checkBox_check_filetype = QCheckBox(self.scrollAreaWidgetContents) 95 | self.checkBox_check_filetype.setObjectName(u"checkBox_check_filetype") 96 | 97 | self.verticalLayout_2.addWidget(self.checkBox_check_filetype) 98 | 99 | self.checkBox_handle_multi_archive = QCheckBox(self.scrollAreaWidgetContents) 100 | self.checkBox_handle_multi_archive.setObjectName(u"checkBox_handle_multi_archive") 101 | 102 | self.verticalLayout_2.addWidget(self.checkBox_handle_multi_archive) 103 | 104 | self.line_5 = QFrame(self.scrollAreaWidgetContents) 105 | self.line_5.setObjectName(u"line_5") 106 | self.line_5.setFrameShape(QFrame.HLine) 107 | self.line_5.setFrameShadow(QFrame.Sunken) 108 | 109 | self.verticalLayout_2.addWidget(self.line_5) 110 | 111 | self.label_2 = QLabel(self.scrollAreaWidgetContents) 112 | self.label_2.setObjectName(u"label_2") 113 | 114 | self.verticalLayout_2.addWidget(self.label_2) 115 | 116 | self.horizontalLayout = QHBoxLayout() 117 | self.horizontalLayout.setSpacing(0) 118 | self.horizontalLayout.setObjectName(u"horizontalLayout") 119 | self.lineEdit_output_path = QLineEdit(self.scrollAreaWidgetContents) 120 | self.lineEdit_output_path.setObjectName(u"lineEdit_output_path") 121 | 122 | self.horizontalLayout.addWidget(self.lineEdit_output_path) 123 | 124 | self.toolButton_ask_path = QToolButton(self.scrollAreaWidgetContents) 125 | self.toolButton_ask_path.setObjectName(u"toolButton_ask_path") 126 | 127 | self.horizontalLayout.addWidget(self.toolButton_ask_path) 128 | 129 | self.toolButton_clear_path = QToolButton(self.scrollAreaWidgetContents) 130 | self.toolButton_clear_path.setObjectName(u"toolButton_clear_path") 131 | 132 | self.horizontalLayout.addWidget(self.toolButton_clear_path) 133 | 134 | self.toolButton_open_path = QToolButton(self.scrollAreaWidgetContents) 135 | self.toolButton_open_path.setObjectName(u"toolButton_open_path") 136 | 137 | self.horizontalLayout.addWidget(self.toolButton_open_path) 138 | 139 | 140 | self.verticalLayout_2.addLayout(self.horizontalLayout) 141 | 142 | self.line_4 = QFrame(self.scrollAreaWidgetContents) 143 | self.line_4.setObjectName(u"line_4") 144 | self.line_4.setFrameShape(QFrame.HLine) 145 | self.line_4.setFrameShadow(QFrame.Sunken) 146 | 147 | self.verticalLayout_2.addWidget(self.line_4) 148 | 149 | self.label = QLabel(self.scrollAreaWidgetContents) 150 | self.label.setObjectName(u"label") 151 | 152 | self.verticalLayout_2.addWidget(self.label) 153 | 154 | self.lineEdit_filter_suffix = QLineEdit(self.scrollAreaWidgetContents) 155 | self.lineEdit_filter_suffix.setObjectName(u"lineEdit_filter_suffix") 156 | 157 | self.verticalLayout_2.addWidget(self.lineEdit_filter_suffix) 158 | 159 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 160 | 161 | self.verticalLayout.addWidget(self.scrollArea) 162 | 163 | 164 | self.retranslateUi(Form) 165 | 166 | QMetaObject.connectSlotsByName(Form) 167 | # setupUi 168 | 169 | def retranslateUi(self, Form): 170 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 171 | self.checkBox_mode_extract.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u6a21\u5f0f", None)) 172 | self.checkBox_mode_test.setText(QCoreApplication.translate("Form", u"\u6d4b\u8bd5\u6a21\u5f0f", None)) 173 | self.checkBox_extract_smart.setText(QCoreApplication.translate("Form", u"\u667a\u80fd\u89e3\u538b", None)) 174 | self.checkBox_extract_folder.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u5230\u540c\u540d\u6587\u4ef6\u5939", None)) 175 | self.checkBox_delete_file.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u540e\u5220\u9664\u6e90\u6587\u4ef6", None)) 176 | self.checkBox_handle_multi_folder.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u540e\u89e3\u5957\u591a\u5c42\u6587\u4ef6\u5939", None)) 177 | self.checkBox_check_filetype.setText(QCoreApplication.translate("Form", u"\u4ec5\u5904\u7406\u538b\u7f29\u5305", None)) 178 | self.checkBox_handle_multi_archive.setText(QCoreApplication.translate("Form", u"\u9012\u5f52\u89e3\u538b\u5957\u5a03\u538b\u7f29\u5305", None)) 179 | self.label_2.setText(QCoreApplication.translate("Form", u"\u89e3\u538b\u81f3\u6307\u5b9a\u76ee\u5f55\uff1a", None)) 180 | self.toolButton_ask_path.setText(QCoreApplication.translate("Form", u"...", None)) 181 | self.toolButton_clear_path.setText(QCoreApplication.translate("Form", u"...", None)) 182 | self.toolButton_open_path.setText(QCoreApplication.translate("Form", u"...", None)) 183 | self.label.setText(QCoreApplication.translate("Form", u"\u6587\u4ef6\u8fc7\u6ee4\uff1a", None)) 184 | # retranslateUi 185 | 186 | -------------------------------------------------------------------------------- /ui/src/ui_widget_page_setting.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 213 10 | 287 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 0 41 | -54 42 | 194 43 | 339 44 | 45 | 46 | 47 | 48 | 5 49 | 50 | 51 | 3 52 | 53 | 54 | 3 55 | 56 | 57 | 3 58 | 59 | 60 | 3 61 | 62 | 63 | 64 | 65 | 解压模式 66 | 67 | 68 | buttonGroup_mode 69 | 70 | 71 | 72 | 73 | 74 | 75 | 测试模式 76 | 77 | 78 | buttonGroup_mode 79 | 80 | 81 | 82 | 83 | 84 | 85 | Qt::Horizontal 86 | 87 | 88 | 89 | 90 | 91 | 92 | 智能解压 93 | 94 | 95 | buttonGroup_folder 96 | 97 | 98 | 99 | 100 | 101 | 102 | 解压到同名文件夹 103 | 104 | 105 | buttonGroup_folder 106 | 107 | 108 | 109 | 110 | 111 | 112 | Qt::Horizontal 113 | 114 | 115 | 116 | 117 | 118 | 119 | 解压后删除源文件 120 | 121 | 122 | 123 | 124 | 125 | 126 | 解压后解套多层文件夹 127 | 128 | 129 | 130 | 131 | 132 | 133 | Qt::Horizontal 134 | 135 | 136 | 137 | 138 | 139 | 140 | 仅处理压缩包 141 | 142 | 143 | 144 | 145 | 146 | 147 | 递归解压套娃压缩包 148 | 149 | 150 | 151 | 152 | 153 | 154 | Qt::Horizontal 155 | 156 | 157 | 158 | 159 | 160 | 161 | 解压至指定目录: 162 | 163 | 164 | 165 | 166 | 167 | 168 | 0 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | ... 177 | 178 | 179 | 180 | 181 | 182 | 183 | ... 184 | 185 | 186 | 187 | 188 | 189 | 190 | ... 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | Qt::Horizontal 200 | 201 | 202 | 203 | 204 | 205 | 206 | 文件过滤: 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /ui/widget_page_history.py: -------------------------------------------------------------------------------- 1 | # 历史记录页 2 | import os 3 | import time 4 | 5 | from PySide6.QtCore import Qt 6 | from PySide6.QtGui import QAction, QColor 7 | from PySide6.QtWidgets import QListWidget, QApplication, QMenu, QListWidgetItem 8 | 9 | from module import function_normal, function_password 10 | from module.function_7zip import Result7zip, Collect7zipResult 11 | 12 | 13 | class ListWidgetHistory(QListWidget): 14 | 15 | def __init__(self, parent=None): 16 | super().__init__(parent) 17 | self.setContextMenuPolicy(Qt.CustomContextMenu) 18 | self.setWordWrap(True) 19 | self.customContextMenuRequested.connect(self._context_menu) # 右键菜单 20 | self._last_state_class = None # 上一个状态类,用于防止同一文件添加重复状态项 21 | 22 | self.collect_result = Collect7zipResult() 23 | 24 | def reset_class(self): 25 | """重置""" 26 | self._last_state_class = None 27 | 28 | def insert_item(self, state_class): 29 | """插入行项目""" 30 | # 只在当前状态类与上个状态类不同时,才进行插入操作,防止插入同一文件的同一状态项 31 | if self._last_state_class and state_class.file == self._last_state_class.file and state_class.text == self._last_state_class.text: 32 | return 33 | 34 | # 收集结果 35 | self.collect_result.collect(state_class) 36 | 37 | # 生成文本 38 | text_time = time.strftime('%Y.%m.%d %H:%M:%S', time.localtime()) 39 | text_filetitle = os.path.basename(state_class.file) 40 | text_result = state_class.text 41 | color = state_class.color 42 | text_line = '----------' 43 | 44 | # 创建行项目 45 | item = QListWidgetItem() 46 | text_item = text_line + '\n■' + text_time + '\n■' + text_filetitle + '\n■' + text_result 47 | item.setText(text_item) 48 | item.setForeground(QColor(color[0], color[1], color[2])) 49 | # 如果状态类时“成功”,则额外添加密码文本,并设置UserRole属性 50 | # 单独处理成功的记录,添加右键菜单,设置UserRole由于右键获取密码 51 | if isinstance(state_class, Result7zip.Success): 52 | text_password = state_class.password 53 | text_item += '\n■' + text_password 54 | item.setText(text_item) 55 | item.setFlags(item.flags() | Qt.ItemIsUserCheckable) 56 | item.setData(Qt.UserRole, text_password) 57 | # 正确密码在数据库中的使用次数+1 58 | if text_password: 59 | function_password.update_password(text_password) 60 | 61 | self.addItem(item) 62 | self.scrollToBottom() # 滚动到底部 63 | self._last_state_class = state_class 64 | 65 | # 保存历史记录 66 | function_normal.save_history(text_item) 67 | 68 | def copy_password(self): 69 | """复制密码到剪切板""" 70 | selected_item = self.currentItem() 71 | password = selected_item.data(Qt.UserRole) 72 | QApplication.clipboard().setText(password) 73 | 74 | def _context_menu(self, pos): 75 | """右键菜单""" 76 | selected_item = self.currentItem() 77 | if selected_item and selected_item.data(Qt.UserRole): 78 | menu = QMenu() 79 | menu.adjustSize() 80 | copy_action = QAction('复制解压密码', menu) 81 | copy_action.triggered.connect(self.copy_password) 82 | menu.addAction(copy_action) 83 | menu.exec(self.mapToGlobal(pos)) 84 | -------------------------------------------------------------------------------- /ui/widget_page_homepage.py: -------------------------------------------------------------------------------- 1 | # 主页 2 | 3 | from PySide6.QtCore import Signal 4 | from PySide6.QtGui import QIcon 5 | from PySide6.QtWidgets import QApplication, QMessageBox, QFrame 6 | 7 | from constant import _ICON_STOP, _ICON_SKIP, _ICON_WARNING, _ICON_EXTRACT_GIF, _ICON_TEST_GIF, _ICON_FINISH, \ 8 | _ICON_DISCONNECT, _ICON_MAIN_PATH, _ICON_MAIN_DEFAULT 9 | from module import function_normal, function_archive 10 | from module.function_7zip import Collect7zipResult 11 | from module.function_config import GetSetting 12 | from thread.thread_7zip import Thread7zip 13 | from ui.label_drop import LabelDrop 14 | from ui.src.ui_widget_page_homepage import Ui_Form 15 | 16 | 17 | class WidgetPageHomepage(QFrame): 18 | signal_start_7zip = Signal() 19 | signal_finished_7zip = Signal() 20 | signal_7zip_result = Signal(object) 21 | 22 | def __init__(self, parent=None): 23 | super().__init__(parent) 24 | self.ui = Ui_Form() 25 | self.ui.setupUi(self) 26 | 27 | # ui设置 28 | self.setFrameShape(QFrame.Box) 29 | self.setFrameShadow(QFrame.Sunken) 30 | self.ui.toolButton_stop.setIcon(QIcon(_ICON_STOP)) 31 | self.ui.toolButton_stop.clicked.connect(self._show_stop_dialog) 32 | self.ui.stackedWidget.setCurrentIndex(0) 33 | self.ui.toolButton_stop.setEnabled(False) 34 | 35 | # 添加控件 36 | self.label_drop = LabelDrop() 37 | self.ui.layout_label_drop.addWidget(self.label_drop) 38 | self.label_drop.signal_dropped.connect(self.drop_paths) 39 | self.set_default_drop_icon() 40 | 41 | # 实例化7zip线程 42 | self.thread_7zip = Thread7zip() 43 | # 绑定7zip线程运行信号 44 | self.thread_7zip.signal_stop.connect(self._state_stop_7zip) 45 | self.thread_7zip.signal_finish.connect(self._state_finished_7zip) 46 | self.thread_7zip.signal_finish_restart.connect(self._restart_7zip) 47 | # 绑定7zip线程进度信号 48 | self.thread_7zip.signal_current_file.connect(self._update_info_current_file) 49 | self.thread_7zip.signal_schedule_file.connect(self._update_info_schedule_file) 50 | self.thread_7zip.signal_schedule_test.connect(self._update_info_schedule_test) 51 | self.thread_7zip.signal_schedule_extract.connect(self._update_info_schedule_extract) 52 | # 中转7zip线程调用结果信号 53 | self.thread_7zip.signal_7zip_result.connect(self.signal_7zip_result.emit) 54 | 55 | # 实例收集类 56 | self.collect_result = Collect7zipResult() 57 | 58 | def set_default_drop_icon(self): 59 | output_path = GetSetting.output_folder() 60 | if output_path: 61 | self.label_drop.reset_icon(_ICON_MAIN_PATH) 62 | else: 63 | self.label_drop.reset_icon(_ICON_MAIN_DEFAULT) 64 | 65 | def drop_paths(self, paths): 66 | """拖入文件""" 67 | files = function_normal.get_files_in_paths(paths) 68 | self._handle_files(files) 69 | 70 | def _handle_files(self, files): 71 | """处理文件""" 72 | # 检查传入列表是否为空 73 | if not files: 74 | self._state_no_archive() 75 | return 76 | 77 | # 检查同级目录是否存在遗留的(非空)临时文件夹 78 | if function_normal.is_temp_folder_exists(files): 79 | self._state_temp_folder() 80 | return 81 | 82 | # 指定解压目录时,检查解压目录中是否存在遗留的(非空)临时文件夹 83 | output_path = GetSetting.output_folder() 84 | if function_normal.is_temp_folder_exists(output_path): 85 | self._state_temp_folder() 86 | return 87 | 88 | # 分类压缩包 89 | # dict结构:key为第一个分卷包路径/非分卷则为其本身,value为list,内部元素为其对应的所有分卷包 90 | file_dict = function_archive.split_archive(files) 91 | 92 | # 根据“是否仅处理压缩文件”选项再次筛选文件 93 | if GetSetting.check_filetype(): 94 | file_dict = {key: value for key, value in file_dict.items() if function_archive.is_archive(key)} 95 | 96 | # 检查筛选后的文件是否为空 97 | if not file_dict: 98 | self._state_no_archive() 99 | return 100 | 101 | # 将最终需要处理的文件传递给7zip子线程,并启动子线程 102 | self._state_start_7zip() 103 | self.thread_7zip.set_file_dict(file_dict) 104 | self.thread_7zip.start() 105 | 106 | def _restart_7zip(self, paths): 107 | """将7zip线程传回的解压文件列表再次进行解压""" 108 | files = function_normal.get_files_in_paths(paths) 109 | if not files: 110 | self._state_finished_7zip() 111 | return 112 | 113 | # 检查同级目录是否存在遗留的(非空)临时文件夹 114 | if function_normal.is_temp_folder_exists(files): 115 | self._state_temp_folder() 116 | return 117 | 118 | # 指定解压目录时,检查解压目录中是否存在遗留的(非空)临时文件夹 119 | output_path = GetSetting.output_folder() 120 | if function_normal.is_temp_folder_exists(output_path): 121 | self._state_temp_folder() 122 | return 123 | 124 | # 分类压缩包 125 | # dict结构:key为第一个分卷包路径/非分卷则为其本身,value为list,内部元素为其对应的所有分卷包 126 | file_dict = function_archive.split_archive(files) 127 | 128 | # 根据“是否仅处理压缩文件”选项再次筛选文件 129 | if GetSetting.check_filetype(): 130 | file_dict = {key: value for key, value in file_dict.items() if function_archive.is_archive(key)} 131 | 132 | # 检查筛选后的文件是否为空 133 | if not file_dict: 134 | self._state_finished_7zip() 135 | return 136 | 137 | # 将最终需要处理的文件传递给7zip子线程,并启动子线程 138 | self.signal_start_7zip.emit() 139 | self._state_start_7zip() 140 | self.thread_7zip.set_file_dict(file_dict) 141 | self.thread_7zip.start() 142 | 143 | def _show_stop_dialog(self): 144 | """显示停止对话框""" 145 | info = '是否停止当前的密码搜索任务\n(不会终止正在进行的解压操作)' 146 | reply = QMessageBox.question(self, '是否停止', info, QMessageBox.Yes | QMessageBox.No) 147 | if reply == QMessageBox.Yes: 148 | self.thread_7zip.stop() 149 | self.ui.toolButton_stop.setEnabled(False) 150 | 151 | def _set_drop_label_state(self, enable: bool): 152 | """启用/禁用拖入控件""" 153 | self.label_drop.setEnabled(enable) 154 | 155 | def _state_no_archive(self): 156 | """状态-没有压缩文件""" 157 | self.label_drop.reset_icon(_ICON_SKIP) 158 | self.ui.label_schedule_file.setText('-/-') 159 | self.ui.label_current_file.setText('-') 160 | self.ui.label_state.setText('无压缩文件') 161 | self.ui.stackedWidget.setCurrentIndex(0) 162 | 163 | def _state_temp_folder(self): 164 | """状态-存在非空临时文件夹""" 165 | self.label_drop.reset_icon(_ICON_WARNING) 166 | self.ui.label_schedule_file.setText('-/-') 167 | self.ui.label_current_file.setText('-') 168 | self.ui.label_state.setText('存在遗留的临时文件夹,请检查') 169 | self.ui.stackedWidget.setCurrentIndex(0) 170 | 171 | def _state_start_7zip(self): 172 | """状态-启动7zip子线程""" 173 | self.signal_start_7zip.emit() 174 | self._set_drop_label_state(False) 175 | self.ui.toolButton_stop.setEnabled(True) 176 | if GetSetting.mode_extract(): 177 | self.label_drop.reset_icon(_ICON_EXTRACT_GIF) 178 | else: 179 | self.label_drop.reset_icon(_ICON_TEST_GIF) 180 | 181 | def _state_stop_7zip(self): 182 | """状态-终止7zip子线程""" 183 | self.signal_finished_7zip.emit() 184 | self._set_drop_label_state(True) 185 | self.ui.toolButton_stop.setEnabled(False) 186 | self.label_drop.reset_icon(_ICON_DISCONNECT) 187 | self.ui.label_schedule_file.setText('-/-') 188 | self.ui.label_current_file.setText('-') 189 | self.ui.stackedWidget.setCurrentIndex(0) 190 | self.ui.label_state.setText(self.collect_result.get_result_text()) 191 | # 重置收集类的计数 192 | self.collect_result.reset_count() 193 | 194 | def _state_finished_7zip(self): 195 | """状态-7zip子线程运行结束""" 196 | self.signal_finished_7zip.emit() 197 | self._set_drop_label_state(True) 198 | self.ui.toolButton_stop.setEnabled(False) 199 | self.label_drop.reset_icon(_ICON_FINISH) 200 | self.ui.label_schedule_file.setText('-/-') 201 | self.ui.label_current_file.setText('-') 202 | self.ui.stackedWidget.setCurrentIndex(0) 203 | self.ui.label_state.setText(self.collect_result.get_result_text()) 204 | # 重置收集类的计数 205 | self.collect_result.reset_count() 206 | 207 | def _update_info_current_file(self, text): 208 | """更新当前文件""" 209 | self.ui.label_current_file.setText(text) 210 | 211 | def _update_info_schedule_file(self, text): 212 | """更新文件进度""" 213 | self.ui.label_schedule_file.setText(text) 214 | 215 | def _update_info_schedule_test(self, text): 216 | """更新密码测试进度""" 217 | self.ui.label_schedule_test.setText(text) 218 | if self.ui.stackedWidget.currentIndex() != 1: 219 | self.ui.stackedWidget.setCurrentIndex(1) 220 | 221 | def _update_info_schedule_extract(self, value): 222 | """更新解压进度""" 223 | self.ui.progressBar_schedule_extract.setValue(value) 224 | if self.ui.stackedWidget.currentIndex() != 2: 225 | self.ui.stackedWidget.setCurrentIndex(2) 226 | 227 | 228 | if __name__ == "__main__": 229 | app_ = QApplication() 230 | app_.setStyle('Fusion') 231 | program_ui = WidgetPageHomepage() 232 | program_ui.show() 233 | app_.exec() 234 | -------------------------------------------------------------------------------- /ui/widget_page_password.py: -------------------------------------------------------------------------------- 1 | # 密码页 2 | from PySide6.QtWidgets import QWidget, QApplication 3 | 4 | from module import function_password 5 | from ui.src.ui_widget_page_password import Ui_Form 6 | 7 | 8 | class WidgetPagePassword(QWidget): 9 | def __init__(self, parent=None): 10 | super().__init__(parent) 11 | self.ui = Ui_Form() 12 | self.ui.setupUi(self) 13 | 14 | # ui设置 15 | self.ui.pushButton_open_export.setEnabled(False) 16 | self._show_password() 17 | 18 | # 绑定槽函数 19 | self.ui.pushButton_read_clipboard.clicked.connect(self._read_clipboard) 20 | self.ui.pushButton_export_password.clicked.connect(self._export_password) 21 | self.ui.pushButton_open_export.clicked.connect(self._open_export) 22 | self.ui.pushButton_update_password.clicked.connect(self._update_password) 23 | self.ui.plainTextEdit_password.textChanged.connect(self._show_password) 24 | 25 | def set_button_state(self, enable: bool): 26 | """启用/禁用解压相关的按钮""" 27 | self.ui.pushButton_update_password.setEnabled(enable) 28 | 29 | def _read_clipboard(self): 30 | """读取剪切板""" 31 | clipboard = QApplication.clipboard() 32 | self.ui.plainTextEdit_password.setPlainText(clipboard.text()) 33 | 34 | def _export_password(self): 35 | """导出密码""" 36 | function_password.export_password() 37 | self.ui.pushButton_open_export.setEnabled(True) 38 | 39 | def _open_export(self): 40 | """打开导出的密码文件""" 41 | function_password.open_export() 42 | 43 | def _update_password(self): 44 | """更新密码""" 45 | text = self.ui.plainTextEdit_password.toPlainText() 46 | passwords = [i for i in text.split('\n') if i.strip()] 47 | passwords_strip = [i.strip() for i in passwords] # 考虑到密码两端的空格,同时添加两种形式的密码 48 | passwords_join = [] 49 | for i in passwords+passwords_strip: 50 | if i not in passwords_join: 51 | passwords_join.append(i) 52 | function_password.update_password(passwords_join) 53 | function_password.backup_password() 54 | 55 | self.ui.plainTextEdit_password.clear() 56 | 57 | def _show_password(self): 58 | """在文本框中显示密码(淡色)""" 59 | text_info = '添加密码时,一个密码占一行,点击“更新密码”即可更新。\n\n' 60 | if not self.ui.plainTextEdit_password.toPlainText(): 61 | passwords = function_password.read_password() 62 | text_password = '当前密码(按次数排列)\n' + '\n'.join(passwords) 63 | self.ui.plainTextEdit_password.setPlaceholderText(text_info + text_password) 64 | 65 | 66 | if __name__ == "__main__": 67 | app_ = QApplication() 68 | app_.setStyle('Fusion') 69 | program_ui = WidgetPagePassword() 70 | program_ui.show() 71 | app_.exec() 72 | -------------------------------------------------------------------------------- /ui/widget_page_setting.py: -------------------------------------------------------------------------------- 1 | # 设置页 2 | import os 3 | 4 | from PySide6.QtCore import Signal 5 | from PySide6.QtGui import QIcon 6 | from PySide6.QtWidgets import QWidget, QApplication, QFileDialog 7 | 8 | from constant import _ICON_ASK_PATH, _ICON_CLEAR, _ICON_OPEN_FOLDER 9 | from module.function_config import GetSetting, ResetSetting 10 | from ui.src.ui_widget_page_setting import Ui_Form 11 | 12 | 13 | class WidgetPageSetting(QWidget): 14 | signal_output_path = Signal() 15 | 16 | def __init__(self, parent=None): 17 | super().__init__(parent) 18 | self.ui = Ui_Form() 19 | self.ui.setupUi(self) 20 | 21 | # 设置ui 22 | self.ui.lineEdit_output_path.setReadOnly(True) 23 | self.ui.toolButton_ask_path.setIcon(QIcon(_ICON_ASK_PATH)) 24 | self.ui.toolButton_clear_path.setIcon(QIcon(_ICON_CLEAR)) 25 | self.ui.toolButton_open_path.setIcon(QIcon(_ICON_OPEN_FOLDER)) 26 | 27 | # 初始化 28 | self._load_setting() 29 | self._set_extract_checkbox_state() # 刷新一次,防止测试模式时没有禁用设置 30 | 31 | # 绑定槽函数 32 | self.ui.checkBox_mode_extract.stateChanged.connect(self._change_setting_mode) 33 | self.ui.checkBox_mode_extract.stateChanged.connect(self._set_extract_checkbox_state) 34 | self.ui.checkBox_extract_smart.stateChanged.connect(self._change_setting_smart_extract) 35 | self.ui.checkBox_delete_file.stateChanged.connect(self._change_setting_delete_file) 36 | self.ui.checkBox_handle_multi_folder.stateChanged.connect(self._change_setting_handle_multi_folder) 37 | self.ui.checkBox_check_filetype.stateChanged.connect(self._change_setting_check_filetype) 38 | self.ui.checkBox_handle_multi_archive.stateChanged.connect(self._change_setting_handle_multi_archive) 39 | self.ui.checkBox_handle_multi_archive.stateChanged.connect(self._sync_button_state) 40 | self.ui.lineEdit_output_path.textChanged.connect(self._change_setting_output_path) 41 | self.ui.lineEdit_output_path.textChanged.connect(self._send_output_path) 42 | self.ui.lineEdit_filter_suffix.textChanged.connect(self._change_setting_filter_suffix) 43 | self.ui.toolButton_ask_path.clicked.connect(self._ask_dirpath) 44 | self.ui.toolButton_clear_path.clicked.connect(self._clear_dirpath) 45 | self.ui.toolButton_open_path.clicked.connect(self._open_dirpath) 46 | 47 | def set_widgets_state(self, enable: bool): 48 | """启用/禁用控件""" 49 | for i in range(self.layout().count()): 50 | widget = self.layout().itemAt(i).widget() 51 | widget.setEnabled(enable) 52 | 53 | def _load_setting(self): 54 | """加载设置""" 55 | self.ui.checkBox_mode_extract.setChecked(GetSetting.mode_extract()) 56 | self.ui.checkBox_mode_test.setChecked(not GetSetting.mode_extract()) 57 | self.ui.checkBox_extract_smart.setChecked(GetSetting.smart_extract()) 58 | self.ui.checkBox_extract_folder.setChecked(not GetSetting.smart_extract()) 59 | self.ui.checkBox_delete_file.setChecked(GetSetting.delete_file()) 60 | self.ui.checkBox_handle_multi_folder.setChecked(GetSetting.handle_multi_folder()) 61 | self.ui.checkBox_check_filetype.setChecked(GetSetting.check_filetype()) 62 | self.ui.checkBox_handle_multi_archive.setChecked(GetSetting.handle_multi_archive()) 63 | self.ui.lineEdit_output_path.setText(GetSetting.output_folder()) 64 | self.ui.lineEdit_filter_suffix.setText(GetSetting.filter_suffix()) 65 | 66 | def _send_output_path(self): 67 | """发送解压路径信号""" 68 | self.signal_output_path.emit() 69 | 70 | def _ask_dirpath(self): 71 | """选择解压目录""" 72 | dirpath = QFileDialog.getExistingDirectory(self, "指定解压目录") 73 | if dirpath: 74 | self.ui.lineEdit_output_path.setText(os.path.normpath(dirpath)) 75 | 76 | def _clear_dirpath(self): 77 | """清除解压目录""" 78 | self.ui.lineEdit_output_path.clear() 79 | 80 | def _open_dirpath(self): 81 | path = self.ui.lineEdit_output_path.text() 82 | if path and os.path.exists(path): 83 | os.startfile(self.ui.lineEdit_output_path.text()) 84 | 85 | def _sync_button_state(self): 86 | """同步按钮状态(递归解压&仅处理压缩包)""" 87 | if self.ui.checkBox_handle_multi_archive.isChecked(): 88 | self.ui.checkBox_check_filetype.setChecked(True) 89 | 90 | def _change_setting_mode(self): 91 | """修改设置项""" 92 | ResetSetting.mode_extract(self.ui.checkBox_mode_extract.isChecked()) 93 | ResetSetting.mode_test(not self.ui.checkBox_mode_extract.isChecked()) 94 | 95 | def _set_extract_checkbox_state(self): 96 | """启用/禁用解压相关的勾选框""" 97 | state = self.ui.checkBox_mode_extract.isChecked() 98 | self.ui.checkBox_extract_smart.setEnabled(state) 99 | self.ui.checkBox_extract_folder.setEnabled(state) 100 | self.ui.checkBox_delete_file.setEnabled(state) 101 | self.ui.checkBox_handle_multi_folder.setEnabled(state) 102 | self.ui.checkBox_handle_multi_archive.setEnabled(state) 103 | self.ui.lineEdit_output_path.setEnabled(state) 104 | self.ui.toolButton_ask_path.setEnabled(state) 105 | self.ui.toolButton_clear_path.setEnabled(state) 106 | self.ui.toolButton_open_path.setEnabled(state) 107 | self.ui.lineEdit_filter_suffix.setEnabled(state) 108 | 109 | def _change_setting_smart_extract(self): 110 | """修改设置项""" 111 | ResetSetting.smart_extract(self.ui.checkBox_extract_smart.isChecked()) 112 | ResetSetting.extract_to_folder(not self.ui.checkBox_extract_smart.isChecked()) 113 | 114 | def _change_setting_delete_file(self): 115 | """修改设置项""" 116 | ResetSetting.delete_file(self.ui.checkBox_delete_file.isChecked()) 117 | 118 | def _change_setting_handle_multi_folder(self): 119 | """修改设置项""" 120 | ResetSetting.handle_multi_folder(self.ui.checkBox_handle_multi_folder.isChecked()) 121 | 122 | def _change_setting_check_filetype(self): 123 | """修改设置项""" 124 | ResetSetting.check_filetype(self.ui.checkBox_check_filetype.isChecked()) 125 | 126 | def _change_setting_handle_multi_archive(self): 127 | """修改设置项""" 128 | ResetSetting.handle_multi_archive(self.ui.checkBox_handle_multi_archive.isChecked()) 129 | 130 | def _change_setting_output_path(self): 131 | """修改设置项""" 132 | ResetSetting.output_folder(self.ui.lineEdit_output_path.text()) 133 | 134 | def _change_setting_filter_suffix(self): 135 | """修改设置项""" 136 | ResetSetting.filter_suffix(self.ui.lineEdit_filter_suffix.text()) 137 | 138 | 139 | if __name__ == "__main__": 140 | app_ = QApplication() 141 | app_.setStyle('Fusion') 142 | program_ui = WidgetPageSetting() 143 | program_ui.show() 144 | app_.exec() 145 | --------------------------------------------------------------------------------