diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..3e211ea Binary files /dev/null and b/.DS_Store differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..464efbb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# 向 Class Widgets 贡献代码 + +## 反馈 +### 反馈 Bug + +如果您在使用 Class Widgets 时遇到问题,可以在 Issues 中提交 **Bug 反馈**。 + +您的 Bug 反馈需要: +- 在最新版 ![GitHub Release](https://img.shields.io/github/v/release/RinLit-233-shiroko/Class-Widgets?include_prereleases)和[最新提交](https://github.com/RinLit-233-shiroko/Class-Widgets/commits)中未修复; +- 没有与您的 Bug 反馈相同或相似的 Issue。 + +如果您的 Bug 反馈与其他重复,我们将以“重复”原因关闭(Close as Duplicate)您的 Issue。您可以在 Issue 状态中找到对应的 Issue。 + +### 提交新功能请求 + +如果您在使用 Class Widgets 时有关于新功能的想法,您可以在 Discussions 中提交 **新功能请求**。 + +请注意,您的功能请求需要: +- 在 Class Widgets 版本 和最新提交中没有实现; +- 没有与您的功能请求相同或相似的 Discussion; +- 提交的功能是用户广泛需要的,插件不能替代的,且没有超出 **软件本来** 的开发目标,而非 **添加与课表及教学辅助无关的内容** 。 + +如果您的请求不符合上述要求,您的请求可能会被关闭,或转为 插件请求。 + +### 提交插件请求 + +如果您在使用 Class Widgets 时有关于新功能的想法,且该想法可以用插件实现,您可以在 Issues 中提交插件请求。 + +注意,您的插件请求需要: +- 在 Class Widgets 版本 和最新提交中没有实现; +- 没有与您的插件请求相同或相似的 Issue; +- 提交的功能是用户需要的,不是 **添加与课表及教学辅助无关的内容** 。 +- 尽量详细说明插件的背景与动机,用途以及效果,以便插件开发人员能理解您想要实现的功能。 + +如果开发者认为您提交的功能是用户广泛需要的,您的请求可能会被转为 功能请求。您的 Issue 会被关闭并锁定,您可以通过页面上方的链接找到您的请求。 + +## 贡献代码 + +### 贡献准则 + +您为 Class Widgets 贡献的代码需要: +- **稳定** +您贡献的代码需要能尽可能在多个平台稳定工作。 +- **具有泛用性** +与功能请求一样,您贡献的代码需要面向大部分用户。如果您的代码专用性较强,可以考虑开发插件或与开发者讨论。 + +### 提交 + +提交时,请尽量遵守[约定式提交](https://www.conventionalcommits.org/zh-hans/v1.0.0/)规范。 + +在约定式提交规范上,我们还建议您: +- **添加动词** +如果您的提交是 fix(修复)类型,请您在提交信息中添加“修复”等动词和“的问题”等词汇。 +如果您的提交是 feat(功能)类型,您可以在提交信息中添加“增加”等动词和“功能”等词汇。这不是硬性要求。 +- **注明范围(scope)** +如果您的提交是 feat(功能)类型,且添加的是一整个功能,您不需要注明范围;如果添加的功能是一个大功能下的小功能(如 天气(weather) 功能下的 高德地图天气 功能),请您添加范围。 +如果您的提交是 fix(修复)类型,且修复的部分不属于下表的任何一个,您可以不添加范围;反之,请您添加范围。 +在文档底部有一份对照表。 + > 啊不对现在还没有 快了快了 + + +### 发起拉取请求 + +在提交拉取请求前,请先对您的代码进行测试。随后您可以向本仓库提交拉取请求。请您在拉取请求中简要说明您的更改。 + +> [!IMPORTANT] +> 因为 Class Widgets 同时兼容 Windows 7 及更新版本,Linux 和 macOS,请确保您引入的库同时兼容以上三类操作系统的对应版本,或对不兼容的系统或版本进行了规避。 +> 如果您不能在三个系统分别进行测试,您仍然可以提交拉取请求,但请在拉取请求描述中注明已测试的操作系统,以便我们进行测试。 + +### 合并拉取请求 + +在经过团队成员的代码审查和测试后,您的拉取请求会被合并。 + +## 还有问题? + +您可以加入 [QQ 群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=yHXKCAjOxlpTpJ4mNdXm0mxOneYUinRs&authKey=sd3%2F06iGdOZUjkXXPBeIzGnFDIeYwmdwuM8dhk25fi%2B1CUL32MkeN2EEfjdo2pzE&noverify=0&group_code=169200380)或 [Discord 服务器](https://discord.gg/EFF4PpqpqZ)与开发者和其他用户讨论。 + +## 附录 + +### 功能对照表 + +中|英 +-:|:- +主程序
小组件|/ +天气|weather +提醒|tip +TTS|tts +插件广场|pp
plugin_plaza +设置|settings +设置 - 课表|schedule +设置 - CSES|cses +设置 - 个性化|customize +设置 - 高级选项|advanced +额外选项|exact +额外选项 - 调休
额外选项 - 换课|temp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 4272560..e9ea11f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,147 @@ -# Class-Widgets +> [!Warning] +> Class Widgets *1* 目前完全由社区开发者进行开发 +> +> [![](https://github.com/pizeroLOL.png?size=80)](https://github.com/pizeroLOL) [![](https://github.com/IsHPDuwu.png?size=80)](https://github.com/IsHPDuwu) [![](https://github.com/baiyao105.png?size=80)](https://github.com/baiyao105) [![](https://github.com/Artist-MOBAI.png?size=80)](https://github.com/Artist-MOBAI) +> +> 有任何需要社区开发者帮忙的地方,请前往 QQ 群或提 issue -Class Widgets \ No newline at end of file +> [!NOTE] +> Class Widgets 有 QQ 群和 Discord 服务器啦!详见[此处](#社区) + +

+ logo +

+

+ Class Widgets +

+

+ 全新桌面课表 +

+ +
+ +[![星标](https://img.shields.io/github/stars/Class-Widgets/Class-Widgets?style=for-the-badge&color=orange&label=星标)](https://github.com/Class-Widgets/Class-Widgets) +[![当前版本](https://img.shields.io/github/v/release/Class-Widgets/Class-Widgets?style=for-the-badge&color=purple&label=当前版本)](https://github.com/Class-Widgets/Class-Widgets/releases/latest) +[![测试版](https://img.shields.io/github/v/tag/Class-Widgets/Class-Widgets?include_prereleases&label=当前测试版&color=yellow&style=for-the-badge)](https://github.com/Class-Widgets/Class-Widgets/releases) +[![开源许可](https://img.shields.io/badge/license-GPLv3-blue.svg?label=开源许可证&style=for-the-badge)](https://github.com/Class-Widgets/Class-Widgets?tab=GPL-3.0-1-ov-file) +[![下载量](https://img.shields.io/github/downloads/Class-Widgets/Class-Widgets/total.svg?label=下载量&color=green&style=for-the-badge)](https://github.com/Class-Widgets/Class-Widgets) + + + + + + +![Banner](img/Banner.png) + +[![Q群](https://img.shields.io/badge/QQ%20群-169200380-blue.svg?logo=qq&color=blue&style=for-the-badge)](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=yHXKCAjOxlpTpJ4mNdXm0mxOneYUinRs&authKey=sd3%2F06iGdOZUjkXXPBeIzGnFDIeYwmdwuM8dhk25fi%2B1CUL32MkeN2EEfjdo2pzE&noverify=0&group_code=169200380) +[![DC](https://img.shields.io/discord/1332636953719476284?style=for-the-badge&logo=discord&logoColor=ffffff&label=discord%20%E6%9C%8D%E5%8A%A1%E5%99%A8&labelColor=5865f2)](https://discord.gg/EFF4PpqpqZ) + +#### [了解更多 >](https://www.bilibili.com/video/BV1xwW9eyEGu/) + +
+ + +## 特性 +- 由 Python 编写的**插件**系统和插件广场(详见最新构建) +- 将今日的课程安排以**小组件**的样式为你呈现; +- 具有 [上下课提醒](https://www.yuque.com/rinlit/class-widgets_help/fv2ou1i1ngap0hrl) 和预备铃; +- 拥有主题系统支持你高度自定义。 +- 简洁直观的 [课程表编辑](https://www.yuque.com/rinlit/class-widgets_help/oozelh8r56tmw0xb) 界面; +- 同时存储多个课程表文件,并能在各个 Class Widgets 导入和导出; +- 支持 [**通用课程表交换格式**(Course Schedule Exchange Schema)](https://github.com/SmartTeachCN/CSES) ,能在不同格式间转换; +- 提供快捷的调休、换课 [应对方案](https://www.yuque.com/rinlit/class-widgets_help/gc4epffu7g5bf9os)。 +- 提供“天气”、“自定义倒计时”等实用小组件; +- 通过 [“自定义”](https://www.yuque.com/rinlit/class-widgets_help/qyly70ht1ogge1pi) 个性化你的 Class Widgets; +- 具有亮/暗色主题; +- …… + +## 软件截图 +#### 主界面(亮色) +![scrshot_0](img/screenshot_0.png) +#### 主界面(暗色) +![scrshot_0](img/screenshot_1.png) + +## 安装&使用 +> [!TIP] +> 可在 [Class Widgets 官方文档](https://www.yuque.com/rinlit/class-widgets_help/gs3gsbms1iivgibm) 查看教程。 + +> [!IMPORTANT] +> 若要体验此页面的特性,请前往[此处](https://github.com/Class-Widgets/Class-Widgets/releases/tag/v1.1.7-b3)预发行版的页面下载。 + +下载 ![当前版本](https://img.shields.io/github/v/release/Class-Widgets/Class-Widgets?style=flat&color=purple&label=当前版本) 中最新版的压缩文件,解压到合适位置后,打开 `ClassWidgets.exe` 即可。 +可通过托盘菜单进入设置、或退出此程序。 + +## 协议 +此项目 (Class Widgets) 基于 GPL-3.0 许可证授权发布,详情请参阅 [LICENSE](./LICENSE) 文件。 + +Copyright © 2025 RinLit. + +## 致谢 + +### 第三方库和框架 + +- [PyQt5](https://www.riverbankcomputing.com/static/Docs/PyQt5/) +- [PyQt-Fluent-Widgets](https://github.com/zhiyiYo/PyQt-Fluent-Widgets) +- [Loguru](https://github.com/Delgan/loguru) +- [Requests](https://github.com/psf/requests) + +### 资源 + +- [SF Symbols](https://developer.apple.com/cn/sf-symbols/) (部分图标已做修改) +- [和风天气图标](https://icons.qweather.com/)(部分图标已做修改) +- [HarmonyOS Sans](https://developer.huawei.com/consumer/cn/design/resource/) + +### 贡献 + +感谢以下同学为 Class Widgets 作出贡献。 + +[![Contributors](http://contrib.nn.ci/api?repo=Class-Widgets/Class-Widgets&repo=Class-Widgets/plugin-plaza&repo=Class-Widgets/cw-interim-site)](https://github.com/Class-Widgets/Class-Widgets/graphs/contributors) +![Repobeats](https://repobeats.axiom.co/api/embed/9d06f1435d1b14cb7837d1e863e55f24cc98df23.svg "Repobeats analytics image") + +如果您想要为 Class Widgets 作出贡献,请阅读[贡献指南](CONTRIBUTING.md) + +### 赞助商 / Sponsors + +感谢以下人员对本项目的支持。 +- [猞猁](http://dq6666.cn/) + +感谢以下赞助商对本项目的支持。 + + + + + + +
+ SignPath + + Free code signing on Windows provided by SignPath.io, certficate by SignPath Foundation
+ 由 SignPath.io 提供 Windows 上的免费代码签名,由 SignPath Foundation 提供证书 +
+ +## 代码签名策略 / Code signing policy + +- Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) +由 [SignPath.io](https://about.signpath.io/) 提供代码签名,由 [SignPath Foundation](https://signpath.org/) 提供证书 + +- Committers and reviewers: [Organization Members](https://github.com/orgs/Class-Widgets/people) +提交者和审阅者:[团队成员](https://github.com/orgs/Class-Widgets/people) + +- Approvers: [Owners](https://github.com/orgs/Class-Widgets/people?query=role%3Aowner) +审批者:[所有者](https://github.com/orgs/Class-Widgets/people?query=role%3Aowner) + +- This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. +除非用户或安装或操作它的人特别要求,否则本程序不会将任何信息传输到其他网络系统。 + +## 社区 +我们目前开通了 [Discussions](https://github.com/orgs/Class-Widgets/discussions)、[QQ群](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=yHXKCAjOxlpTpJ4mNdXm0mxOneYUinRs&authKey=sd3%2F06iGdOZUjkXXPBeIzGnFDIeYwmdwuM8dhk25fi%2B1CUL32MkeN2EEfjdo2pzE&noverify=0&group_code=169200380) 和 [Discord 服务器](https://discord.gg/EFF4PpqpqZ)。 + +## 星标历史 + + + + Star History Chart + + +## +这仅是我作为新人的练习作品,欢迎提供更多意见! diff --git a/Scripts/buildLinux.sh b/Scripts/buildLinux.sh new file mode 100644 index 0000000..32ef85f --- /dev/null +++ b/Scripts/buildLinux.sh @@ -0,0 +1,18 @@ +#!/bin/bash +uv venv +source .venv/bin/activate +uv pip install -r requirements.txt +uv pip install nuitka imageio +python -m nuitka main.py \ +--enable-plugin=pyqt5 \ +--mode=app \ +-o"ClassWidgets" \ +--include-data-dir=img=img \ +--include-data-dir=ui=ui \ +--include-data-dir=view=view \ +--include-data-dir=config=config \ +--include-data-dir=plugins=plugins \ +--include-data-dir=font=font \ +--include-data-dir=audio=audio \ +--include-data-files=LICENSE=LICENSE \ +--include-package=pyttsx3.drivers diff --git a/Scripts/buildOSX.sh b/Scripts/buildOSX.sh new file mode 100644 index 0000000..03f722e --- /dev/null +++ b/Scripts/buildOSX.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +uv venv +source .venv/bin/activate +uv pip install -r requirements.txt +uv pip install nuitka imageio +python -m nuitka main.py \ +--enable-plugin=pyqt5 \ +--mode=app \ +-o"ClassWidgets" \ +--macos-app-icon=img/favicon.icns \ +--product-name="Class Widgets" \ +--product-version="1.1.7.1" \ +--file-description="全新桌面课表" \ +--include-data-dir=img=img \ +--include-data-dir=ui=ui \ +--include-data-dir=view=view \ +--include-data-dir=config=config \ +--include-data-dir=plugins=plugins \ +--include-data-dir=font=font \ +--include-data-dir=audio=audio \ +--include-data-files=LICENSE=LICENSE \ +--include-package=pyttsx3.drivers + +mv main.app Class\ Widgets.app diff --git a/Scripts/buildWin.bat b/Scripts/buildWin.bat new file mode 100644 index 0000000..9e34461 --- /dev/null +++ b/Scripts/buildWin.bat @@ -0,0 +1,26 @@ +@echo off +echo 创建虚拟环境 +uv venv +call .venv\Scripts\activate +echo 安装依赖 +uv pip install -r requirements.txt +uv pip install nuitka imageio +echo 打包 +python -m nuitka main.py ^ +--enable-plugin=pyqt5 ^ +--disable-console ^ +--mode=app ^ +-o"ClassWidgets" ^ +--windows-icon-from-ico=img/favicon.icns ^ +--product-name="Class Widgets" ^ +--product-version="1.1.7.1" ^ +--file-description="全新桌面课表" ^ +--include-data-dir=img=img ^ +--include-data-dir=ui=ui ^ +--include-data-dir=view=view ^ +--include-data-dir=config=config ^ +--include-data-dir=plugins=plugins ^ +--include-data-dir=font=font ^ +--include-data-dir=audio=audio ^ +--include-data-files=LICENSE=LICENSE ^ +--include-package=pyttsx3.drivers diff --git a/audio/attend_class.wav b/audio/attend_class.wav new file mode 100644 index 0000000..f1ac549 Binary files /dev/null and b/audio/attend_class.wav differ diff --git a/audio/finish_class.wav b/audio/finish_class.wav new file mode 100644 index 0000000..86d2430 Binary files /dev/null and b/audio/finish_class.wav differ diff --git a/audio/prepare_class.wav b/audio/prepare_class.wav new file mode 100644 index 0000000..e49734a Binary files /dev/null and b/audio/prepare_class.wav differ diff --git a/basic_dirs.py b/basic_dirs.py new file mode 100644 index 0000000..410e426 --- /dev/null +++ b/basic_dirs.py @@ -0,0 +1,77 @@ +import os +from pathlib import Path +from sys import platform +from loguru import logger + +APP_NAME = "Class Widgets" +CW_HOME = Path(__file__).parent + +if str(CW_HOME).endswith("MacOS"): + CW_HOME = Path(__file__).absolute().parent.parent / "Resources" + +IS_PORTABLE = os.environ.get("CLASSWIDGETS_NOT_PORTABLE", "") == "" + + +def _ensure_dir(path: Path) -> Path: + path.mkdir(parents=True, exist_ok=True) + return path + + +# 公共基础函数 +def _get_app_dir( + purpose: str, + default_subdir: str, + win_env_var: str, + mac_subpath: str, + xdg_env_var: str, + xdg_fallback: str, +) -> Path: + """获取应用目录的通用实现""" + if IS_PORTABLE: + return _ensure_dir(CW_HOME / default_subdir) + + # 处理自定义路径 + if custom := os.environ.get(f"CLASSWIDGETS_CUSTOM_{purpose.upper()}_HOME"): + return _ensure_dir(Path(custom)) + + # Windows 逻辑 + if platform == "win32": + if base := os.environ.get(win_env_var): + return _ensure_dir(Path(base) / APP_NAME / default_subdir) + logger.error(f"Missing Windows environment variable: {win_env_var}") + return _ensure_dir(CW_HOME / default_subdir) + + # macOS 逻辑 + if platform == "darwin": + return _ensure_dir(Path.home() / mac_subpath / APP_NAME / default_subdir) + + # Linux/Unix 逻辑 + base = os.environ.get(xdg_env_var) or str(Path.home() / xdg_fallback) + return _ensure_dir(Path(base) / APP_NAME / default_subdir) + + +# 最终路径 +CONFIG_HOME = _get_app_dir( + purpose="CONFIG", + default_subdir="config", + win_env_var="APPDATA", + mac_subpath="Library/Application Support", + xdg_env_var="XDG_CONFIG_HOME", + xdg_fallback=".config", +) +LOG_HOME = _get_app_dir( + purpose="LOG", + default_subdir="log", + win_env_var="TMP", + mac_subpath="Library/Caches", + xdg_env_var="XDG_CACHE_HOME", + xdg_fallback=".cache", +) +PLUGIN_HOME = _get_app_dir( + purpose="PLUGIN", + default_subdir="plugins", + win_env_var="APPDATA", + mac_subpath="Library/Application Support", + xdg_env_var="XDG_DATA_HOME", + xdg_fallback=".local/share", +) diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..f9aafe7 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,62 @@ +[changelog] +body = """ + +## Class Widgets 新版本!{% if version %}({{ version }}){%- endif -%} +{% for group, commits in commits | group_by(attribute="group") %} + + ### {{ group | upper_first }} + {% for commit in commits | unique(attribute="message") %} + - {% if commit.scope %}{{ commit.scope }}: {%- endif -%}{{ commit.message | upper_first }}\ + {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} + {% if commit.remote.pr_number %} in \ + [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ + {%- endif %} +{% endfor %} +{%- endfor -%} + +{%- if github -%} +{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + {% raw %}\n{% endraw -%} + ## 新贡献者 +{%- endif %}\ +{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + * @{{ contributor.username }} {%- if contributor.pr_number %} 在 + [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ + {%- endif %}\ + 第一次贡献 + +{%- endfor -%} +{%- endif -%} + +{% if version %} + {% if previous.version %} + **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} + {% endif %} +{% else -%} + {% raw %}\n{% endraw %} +{% endif %} + +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +""" +trim = true +footer = "" +output = "CHANGELOG.md" + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "新功能"}, + { message = "^fix", group = "Bug 修复"}, + { message = "^refactor", group = "重构"}, +] +protect_breaking_commits = false +filter_commits = true +#tag_pattern = "v[0-9].*" +topo_order = false +sort_commits = "oldest" +link_parsers = [] +limit_commits = 42 diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..152df9c --- /dev/null +++ b/conf.py @@ -0,0 +1,344 @@ +import json +import os +import re +import configparser as config +from pathlib import Path + +from datetime import datetime +import time +from dateutil import parser +from loguru import logger +from file import base_directory, config_center + +import list_ + +if os.name == 'nt': + from win32com.client import Dispatch + +conf = config.ConfigParser() +name = 'Class Widgets' + +PLUGINS_DIR = Path(base_directory) / 'plugins' + +# app 图标 +if os.name == 'nt': + app_icon = os.path.join(base_directory, 'img', 'favicon.ico') +elif os.name == 'darwin': + app_icon = os.path.join(base_directory, 'img', 'favicon.icns') +else: + app_icon = os.path.join(base_directory, 'img', 'favicon.png') + +update_countdown_custom_last = 0 +countdown_cnt = 0 + +def load_theme_config(theme): + try: + with open(f'{base_directory}/ui/{theme}/theme.json', 'r', encoding='utf-8') as file: + data = json.load(file) + return data + except FileNotFoundError: + logger.warning(f"主题配置文件 {theme} 不存在,返回默认配置") + return f'{base_directory}/ui/default/theme.json' + except Exception as e: + logger.error(f"加载主题数据时出错: {e}") + return None + + +def load_plugin_config(): + try: + if os.path.exists(f'{base_directory}/config/plugin.json'): # 如果配置文件存在 + with open(f'{base_directory}/config/plugin.json', 'r', encoding='utf-8') as file: + data = json.load(file) + else: + with open(f'{base_directory}/config/plugin.json', 'w', encoding='utf-8') as file: + data = {"enabled_plugins": []} + json.dump(data, file, ensure_ascii=False, indent=4) + return data + except Exception as e: + logger.error(f"加载启用插件数据时出错: {e}") + return None + + +def save_plugin_config(data): + data_dict = load_plugin_config() + data_dict.update(data) + try: + with open(f'{base_directory}/config/plugin.json', 'w', encoding='utf-8') as file: + json.dump(data_dict, file, ensure_ascii=False, indent=4) + return True + except Exception as e: + logger.error(f"保存启用插件数据时出错: {e}") + return False + + +def save_installed_plugin(data): + data = {"plugins": data} + try: + with open(f'{base_directory}/plugins/plugins_from_pp.json', 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + return True + except Exception as e: + logger.error(f"保存已安装插件数据时出错: {e}") + return False + + +def load_theme_width(theme): + try: + with open(f'{base_directory}/ui/{theme}/theme.json', 'r', encoding='utf-8') as file: + data = json.load(file) + return data['widget_width'] + except Exception as e: + logger.error(f"加载主题宽度时出错: {e}") + return list_.widget_width + + +def is_temp_week(): + if config_center.read_conf('Temp', 'set_week') is None or config_center.read_conf('Temp', 'set_week') == '': + return False + else: + return config_center.read_conf('Temp', 'set_week') + + +def is_temp_schedule(): + if ( + config_center.read_conf('Temp', 'temp_schedule') is None + or config_center.read_conf('Temp', 'temp_schedule') == '' + ): + return False + else: + return config_center.read_conf('Temp', 'temp_schedule') + + +def add_shortcut_to_startmenu(file='', icon=''): + if os.name != 'nt': + return + try: + if file == "": + file_path = os.path.realpath(__file__) + else: + file_path = os.path.abspath(file) # 将相对路径转换为绝对路径 + + if icon == "": + icon_path = file_path # 如果未指定图标路径,则使用程序路径 + else: + icon_path = os.path.abspath(icon) # 将相对路径转换为绝对路径 + + # 获取开始菜单文件夹路径 + menu_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs') + + # 快捷方式文件名(使用文件名或自定义名称) + name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称 + shortcut_path = os.path.join(menu_folder, f'{name}.lnk') + + # 创建快捷方式 + shell = Dispatch('WScript.Shell') + shortcut = shell.CreateShortCut(shortcut_path) + shortcut.Targetpath = file_path + shortcut.WorkingDirectory = os.path.dirname(file_path) + shortcut.IconLocation = icon_path # 设置图标路径 + shortcut.save() + except Exception as e: + logger.error(f"创建开始菜单快捷方式时出错: {e}") + + +def add_shortcut(file='', icon=''): + if os.name != 'nt': + return + try: + if file == "": + file_path = os.path.realpath(__file__) + else: + file_path = os.path.abspath(file) + + if icon == "": + icon_path = file_path + else: + icon_path = os.path.abspath(icon) + + # 获取桌面文件夹路径 + desktop_folder = os.path.join(os.environ['USERPROFILE'], 'Desktop') + + # 快捷方式文件名(使用文件名或自定义名称) + name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称 + shortcut_path = os.path.join(desktop_folder, f'{name}.lnk') + + # 创建快捷方式 + shell = Dispatch('WScript.Shell') + shortcut = shell.CreateShortCut(shortcut_path) + shortcut.Targetpath = file_path + shortcut.WorkingDirectory = os.path.dirname(file_path) + shortcut.IconLocation = icon_path # 设置图标路径 + shortcut.save() + except Exception as e: + logger.error(f"创建桌面快捷方式时出错: {e}") + + +def add_to_startup(file_path=f'{base_directory}/ClassWidgets.exe', icon_path=''): # 注册到开机启动 + if os.name != 'nt': + return + if file_path == "": + file_path = os.path.realpath(__file__) + else: + file_path = os.path.abspath(file_path) # 将相对路径转换为绝对路径 + + if icon_path == "": + icon_path = file_path # 如果未指定图标路径,则使用程序路径 + else: + icon_path = os.path.abspath(icon_path) # 将相对路径转换为绝对路径 + + # 获取启动文件夹路径 + startup_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup') + + # 快捷方式文件名(使用文件名或自定义名称) + name = os.path.splitext(os.path.basename(file_path))[0] # 使用文件名作为快捷方式名称 + shortcut_path = os.path.join(startup_folder, f'{name}.lnk') + + # 创建快捷方式 + shell = Dispatch('WScript.Shell') + shortcut = shell.CreateShortCut(shortcut_path) + shortcut.Targetpath = file_path + shortcut.WorkingDirectory = os.path.dirname(file_path) + shortcut.IconLocation = icon_path # 设置图标路径 + shortcut.save() + + +def remove_from_startup(): + startup_folder = os.path.join(os.getenv('APPDATA'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup') + shortcut_path = os.path.join(startup_folder, f'{name}.lnk') + if os.path.exists(shortcut_path): + os.remove(shortcut_path) + + +def get_time_offset(): # 获取时差偏移 + time_offset = config_center.read_conf('General', 'time_offset') + if time_offset is None or time_offset == '' or time_offset == '0': + return 0 + else: + return int(time_offset) + +def update_countdown(cnt): + global update_countdown_custom_last + global countdown_cnt + if (length:=len(config_center.read_conf('Date', 'cd_text_custom').split(','))) == 0: + countdown_cnt = -1 + elif config_center.read_conf('Date', 'countdown_custom_mode') == '1': + countdown_cnt = cnt + elif (nowtime:=time.time()) - update_countdown_custom_last > int(config_center.read_conf('Date', 'countdown_upd_cd')): + update_countdown_custom_last = nowtime + countdown_cnt += 1 + if countdown_cnt >= length: + countdown_cnt = 0 if length != 0 else -1 + +def get_cd_text_custom(): + global countdown_cnt + if countdown_cnt == -1: + return '未设置' + if countdown_cnt >= len(li:=config_center.read_conf('Date', 'cd_text_custom').split(',')): + return '未设置' + return li[countdown_cnt] if countdown_cnt >= 0 else '' + + +def get_custom_countdown(): + global countdown_cnt + if countdown_cnt == -1: + return '未设置' + li = config_center.read_conf('Date', 'countdown_date').split(',') + if countdown_cnt == -1 or countdown_cnt >= len(li): + return '未设置' # 获取自定义倒计时 + else: + custom_countdown = li[countdown_cnt] + if custom_countdown == '': + return '未设置' + try: + custom_countdown = parser.parse(custom_countdown) + except Exception as e: + logger.error(f"解析日期时出错: {custom_countdown}, 错误: {e}") + return '解析失败' + if custom_countdown < datetime.now(): + return '0 天' + else: + cd_text = custom_countdown - datetime.now() + return f'{cd_text.days + 1} 天' + # return ( + # f"{cd_text.days} 天 {cd_text.seconds // 3600} 小时 {cd_text.seconds // 60 % 60} 分" + # ) + + +def get_week_type(): + if (temp_schedule := config_center.read_conf('Temp', 'set_schedule')) not in ('', None): # 获取单双周 + return int(temp_schedule) + start_date_str = config_center.read_conf('Date', 'start_date') + if start_date_str not in ('', None): + try: + start_date = parser.parse(start_date_str) + except (ValueError, TypeError): + logger.error(f"解析日期时出错: {start_date_str}") + return 0 # 解析失败默认单周 + today = datetime.now() + week_num = (today - start_date).days // 7 + 1 + if week_num % 2 == 0: + return 1 # 双周 + else: + return 0 # 单周 + else: + return 0 # 默认单周 + + +def get_is_widget_in(widget='example.ui'): + widgets_list = list_.get_widget_config() + if widget in widgets_list: + return True + else: + return False + + +def save_widget_conf_to_json(new_data): + # 初始化 data_dict 为一个空字典 + data_dict = {} + if os.path.exists(f'{base_directory}/config/widget.json'): + try: + with open(f'{base_directory}/config/widget.json', 'r', encoding='utf-8') as file: + data_dict = json.load(file) + except Exception as e: + print(f"读取现有数据时出错: {e}") + return e + data_dict.update(new_data) + try: + with open(f'{base_directory}/config/widget.json', 'w', encoding='utf-8') as file: + json.dump(data_dict, file, ensure_ascii=False, indent=4) + return True + except Exception as e: + print(f"保存数据时出错: {e}") + return e + + +def load_plugins(): # 加载插件配置文件 + plugin_dict = {} + for folder in Path(PLUGINS_DIR).iterdir(): + if folder.is_dir() and (folder / 'plugin.json').exists(): + try: + with open(f'{base_directory}/plugins/{folder.name}/plugin.json', 'r', encoding='utf-8') as file: + data = json.load(file) + except Exception as e: + logger.error(f"加载插件配置文件数据时出错,将跳过: {e}") # 跳过奇怪的文件夹 + plugin_dict[str(folder.name)] = {} + plugin_dict[str(folder.name)]['name'] = data['name'] # 名称 + plugin_dict[str(folder.name)]['version'] = data['version'] # 插件版本号 + plugin_dict[str(folder.name)]['author'] = data['author'] # 作者 + plugin_dict[str(folder.name)]['description'] = data['description'] # 描述 + plugin_dict[str(folder.name)]['plugin_ver'] = data['plugin_ver'] # 插件架构版本 + plugin_dict[str(folder.name)]['settings'] = data['settings'] # 设置 + return plugin_dict + + +if __name__ == '__main__': + print('AL_1S') + print(get_week_type()) + print(load_plugins()) + # save_data_to_json(test_data_dict, 'schedule-1.json') + # loaded_data = load_from_json('schedule-1.json') + # print(loaded_data) + # schedule = loaded_data.get('schedule') + + # print(schedule['0']) + # add_shortcut_to_startmenu('Settings.exe', 'img/favicon.ico') diff --git a/config/config.json b/config/config.json new file mode 100644 index 0000000..3ded55b --- /dev/null +++ b/config/config.json @@ -0,0 +1,6 @@ +{ + "QFluentWidgets": { + "ThemeColor": "#ff009faa", + "ThemeMode": "Light" + } +} \ No newline at end of file diff --git a/config/data/amap_weather.db b/config/data/amap_weather.db new file mode 100644 index 0000000..053ebd7 Binary files /dev/null and b/config/data/amap_weather.db differ diff --git a/config/data/amap_weather_status.json b/config/data/amap_weather_status.json new file mode 100644 index 0000000..d95db7b --- /dev/null +++ b/config/data/amap_weather_status.json @@ -0,0 +1,156 @@ +{ + "weatherinfo": [ + { + "code": 0, + "wea": "晴" + }, + { + "code": 1, + "wea": "多云" + }, + { + "code": 2, + "wea": "阴" + }, + { + "code": 3, + "wea": "阵雨" + }, + { + "code": 4, + "wea": "雷阵雨" + }, + { + "code": 5, + "wea": "雷阵雨并伴有冰雹" + }, + { + "code": 6, + "wea": "雨夹雪" + }, + { + "code": 7, + "wea": "小雨" + }, + { + "code": 8, + "wea": "中雨" + }, + { + "code": 9, + "wea": "大雨" + }, + { + "code": 10, + "wea": "暴雨" + }, + { + "code": 11, + "wea": "大暴雨" + }, + { + "code": 12, + "wea": "特大暴雨" + }, + { + "code": 13, + "wea": "阵雪" + }, + { + "code": 14, + "wea": "小雪" + }, + { + "code": 15, + "wea": "中雪" + }, + { + "code": 16, + "wea": "大雪" + }, + { + "code": 17, + "wea": "暴雪" + }, + { + "code": 18, + "wea": "雾" + }, + { + "code": 19, + "wea": "冻雨" + }, + { + "code": 20, + "wea": "沙尘暴" + }, + { + "code": 21, + "wea": "小雨-中雨" + }, + { + "code": 22, + "wea": "中雨-大雨" + }, + { + "code": 23, + "wea": "大雨-暴雨" + }, + { + "code": 24, + "wea": "暴雨-大暴雨" + }, + { + "code": 25, + "wea": "大暴雨-特大暴雨" + }, + { + "code": 26, + "wea": "小雪-中雪" + }, + { + "code": 27, + "wea": "中雪-大雪" + }, + { + "code": 28, + "wea": "大雪-暴雪" + }, + { + "code": 29, + "wea": "浮沉" + }, + { + "code": 30, + "wea": "扬沙" + }, + { + "code": 31, + "wea": "强沙尘暴" + }, + { + "code": 32, + "wea": "飑" + }, + { + "code": 33, + "wea": "龙卷风" + }, + { + "code": 34, + "wea": "若高吹雪" + }, + { + "code": 35, + "wea": "轻雾" + }, + { + "code": 53, + "wea": "霾" + }, + { + "code": 99, + "wea": "未知" + } + ] +} \ No newline at end of file diff --git a/config/data/qq_weather_status.json b/config/data/qq_weather_status.json new file mode 100644 index 0000000..2e32e8d --- /dev/null +++ b/config/data/qq_weather_status.json @@ -0,0 +1,222 @@ +{ + "weatherinfo": [ + { + "code": 100, + "wea": "晴", + "original_code": 0 + }, + { + "code": 101, + "wea": "多云", + "original_code": 1 + }, + { + "code": 102, + "wea": "少云", + "original_code": 1 + }, + { + "code": 103, + "wea": "晴间多云", + "original_code": 1 + }, + { + "code": 104, + "wea": "阴", + "original_code": 2 + }, + { + "code": 150, + "wea": "晴", + "original_code": 0 + }, + { + "code": 151, + "wea": "多云", + "original_code": 1 + }, + { + "code": 152, + "wea": "少云", + "original_code": 1 + }, + { + "code": 153, + "wea": "晴间多云", + "original_code": 1 + }, + { + "code": 300, + "wea": "阵雨", + "original_code": 3 + }, + { + "code": 301, + "wea": "强阵雨", + "original_code": 3 + }, + { + "code": 302, + "wea": "雷阵雨", + "original_code": 4 + }, + { + "code": 303, + "wea": "强雷阵雨", + "original_code": 4 + }, + { + "code": 304, + "wea": "雷阵雨伴有冰雹", + "original_code": 5 + }, + { + "code": 305, + "wea": "小雨", + "original_code": 7 + }, + { + "code": 306, + "wea": "中雨", + "original_code": 8 + }, + { + "code": 307, + "wea": "大雨", + "original_code": 9 + }, + { + "code": 308, + "wea": "极端降雨", + "original_code": 10 + }, + { + "code": 309, + "wea": "毛毛雨/细雨", + "original_code": 7 + }, + { + "code": 310, + "wea": "暴雨", + "original_code": 10 + }, + { + "code": 311, + "wea": "大暴雨", + "original_code": 11 + }, + { + "code": 312, + "wea": "特大暴雨", + "original_code": 12 + }, + { + "code": 313, + "wea": "冻雨", + "original_code": 19 + }, + { + "code": 400, + "wea": "小雪", + "original_code": 14 + }, + { + "code": 401, + "wea": "中雪", + "original_code": 15 + }, + { + "code": 402, + "wea": "大雪", + "original_code": 16 + }, + { + "code": 403, + "wea": "暴雪", + "original_code": 17 + }, + { + "code": 404, + "wea": "雨夹雪", + "original_code": 6 + }, + { + "code": 405, + "wea": "雨雪天气", + "original_code": 6 + }, + { + "code": 406, + "wea": "阵雨夹雪", + "original_code": 3 + }, + { + "code": 407, + "wea": "阵雪", + "original_code": 13 + }, + { + "code": 408, + "wea": "小到中雪", + "original_code": 14 + }, + { + "code": 409, + "wea": "中到大雪", + "original_code": 15 + }, + { + "code": 410, + "wea": "大到暴雪", + "original_code": 17 + }, + { + "code": 500, + "wea": "薄雾", + "original_code": 18 + }, + { + "code": 501, + "wea": "雾", + "original_code": 18 + }, + { + "code": 502, + "wea": "霾", + "original_code": 53 + }, + { + "code": 503, + "wea": "扬沙", + "original_code": 30 + }, + { + "code": 507, + "wea": "沙尘暴", + "original_code": 20 + }, + { + "code": 508, + "wea": "强沙尘暴", + "original_code": 31 + }, + { + "code": 509, + "wea": "浓雾", + "original_code": 18 + }, + { + "code": 900, + "wea": "热" + }, + { + "code": 901, + "wea": "冷" + }, + { + "code": 999, + "wea": "未知", + "original_code": 99 + } + ] +} diff --git a/config/data/qweather_status.json b/config/data/qweather_status.json new file mode 100644 index 0000000..2e32e8d --- /dev/null +++ b/config/data/qweather_status.json @@ -0,0 +1,222 @@ +{ + "weatherinfo": [ + { + "code": 100, + "wea": "晴", + "original_code": 0 + }, + { + "code": 101, + "wea": "多云", + "original_code": 1 + }, + { + "code": 102, + "wea": "少云", + "original_code": 1 + }, + { + "code": 103, + "wea": "晴间多云", + "original_code": 1 + }, + { + "code": 104, + "wea": "阴", + "original_code": 2 + }, + { + "code": 150, + "wea": "晴", + "original_code": 0 + }, + { + "code": 151, + "wea": "多云", + "original_code": 1 + }, + { + "code": 152, + "wea": "少云", + "original_code": 1 + }, + { + "code": 153, + "wea": "晴间多云", + "original_code": 1 + }, + { + "code": 300, + "wea": "阵雨", + "original_code": 3 + }, + { + "code": 301, + "wea": "强阵雨", + "original_code": 3 + }, + { + "code": 302, + "wea": "雷阵雨", + "original_code": 4 + }, + { + "code": 303, + "wea": "强雷阵雨", + "original_code": 4 + }, + { + "code": 304, + "wea": "雷阵雨伴有冰雹", + "original_code": 5 + }, + { + "code": 305, + "wea": "小雨", + "original_code": 7 + }, + { + "code": 306, + "wea": "中雨", + "original_code": 8 + }, + { + "code": 307, + "wea": "大雨", + "original_code": 9 + }, + { + "code": 308, + "wea": "极端降雨", + "original_code": 10 + }, + { + "code": 309, + "wea": "毛毛雨/细雨", + "original_code": 7 + }, + { + "code": 310, + "wea": "暴雨", + "original_code": 10 + }, + { + "code": 311, + "wea": "大暴雨", + "original_code": 11 + }, + { + "code": 312, + "wea": "特大暴雨", + "original_code": 12 + }, + { + "code": 313, + "wea": "冻雨", + "original_code": 19 + }, + { + "code": 400, + "wea": "小雪", + "original_code": 14 + }, + { + "code": 401, + "wea": "中雪", + "original_code": 15 + }, + { + "code": 402, + "wea": "大雪", + "original_code": 16 + }, + { + "code": 403, + "wea": "暴雪", + "original_code": 17 + }, + { + "code": 404, + "wea": "雨夹雪", + "original_code": 6 + }, + { + "code": 405, + "wea": "雨雪天气", + "original_code": 6 + }, + { + "code": 406, + "wea": "阵雨夹雪", + "original_code": 3 + }, + { + "code": 407, + "wea": "阵雪", + "original_code": 13 + }, + { + "code": 408, + "wea": "小到中雪", + "original_code": 14 + }, + { + "code": 409, + "wea": "中到大雪", + "original_code": 15 + }, + { + "code": 410, + "wea": "大到暴雪", + "original_code": 17 + }, + { + "code": 500, + "wea": "薄雾", + "original_code": 18 + }, + { + "code": 501, + "wea": "雾", + "original_code": 18 + }, + { + "code": 502, + "wea": "霾", + "original_code": 53 + }, + { + "code": 503, + "wea": "扬沙", + "original_code": 30 + }, + { + "code": 507, + "wea": "沙尘暴", + "original_code": 20 + }, + { + "code": 508, + "wea": "强沙尘暴", + "original_code": 31 + }, + { + "code": 509, + "wea": "浓雾", + "original_code": 18 + }, + { + "code": 900, + "wea": "热" + }, + { + "code": 901, + "wea": "冷" + }, + { + "code": 999, + "wea": "未知", + "original_code": 99 + } + ] +} diff --git a/config/data/subject.json b/config/data/subject.json new file mode 100644 index 0000000..d296866 --- /dev/null +++ b/config/data/subject.json @@ -0,0 +1,49 @@ +{ + "subject_icon": { + "语文": "chinese", + "数学": "math", + "英语": "abc", + "生物": "biology", + "地理": "geography", + "政治": "chinese", + "历史": "history", + "物理": "physics", + "化学": "chemistry", + "美术": "art", + "音乐": "music", + "体育": "pe", + "信息技术": "it", + "电脑": "it", + "课程表未加载": "xmark", + "班会": "meeting", + "自习": "self_study", + "课间": "break", + "大课间": "pe", + "放学": "after_school", + "暂无课程": "break" + }, + "subject_abbreviation": { + "历史": "史", + "升旗": "旗" + }, + "subject_list": [ + "语文", + "数学", + "英语", + "政治", + "历史", + "生物", + "地理", + "物理", + "化学", + "体育", + "升旗", + "班会", + "自习", + "早读", + "大课间", + "美术", + "音乐", + "信息技术" + ] +} diff --git a/config/data/weather_api.json b/config/data/weather_api.json new file mode 100644 index 0000000..0606015 --- /dev/null +++ b/config/data/weather_api.json @@ -0,0 +1,62 @@ +{ + "weather_api": { + "xiaomi_weather": "https://weatherapi.market.xiaomi.com/wtr-v3/weather/all?latitude=0&longitude=0&locationKey=weathercn:{location_key}&appKey=weather20151024&sign=zUFJoAR2ZVrDy1vF3D07&isGlobal=false&locale=zh_cn&days={days}", + "qweather": "https://devapi.qweather.com/v7/weather/now?location={location_key}&key={key}", + "amap_weather": "https://restapi.amap.com/v3/weather/weatherInfo?key={key}&city={location_key}", + "qq_weather": "https://apis.map.qq.com/ws/weather/v1?key={key}&adcode={location_key}" + }, + "weather_api_parameters": { + "xiaomi_weather": { + "temp": "current.temperature.value", + "icon": "current.weather", + "alerts": { + "url": null, + "title": "alerts.0.title", + "type": "alerts.0.level", + "description": "alerts.0.detail", + "types": { + "蓝色": "blue.png", + "黄色": "yellow.png", + "橙色": "orange.png", + "红色": "red.png" + } + }, + "database": "xiaomi_weather.db", + "return_desc": false + }, + "qweather": { + "temp": "now.temp", + "icon": "now.icon", + "alerts": { + "url": "https://devapi.qweather.com/v7/warning/now?location={location_key}&key={key}", + "title": "warning.title", + "description": "warning.0.text", + "type": "warning.0.severityColor", + "types": { + "Blue": "blue.png", + "Yellow": "yellow.png", + "Orange": "orange.png", + "Red": "red.png" + } + }, + "database": "xiaomi_weather.db", + "return_desc": false + }, + "amap_weather": { + "temp": "temperature", + "icon": "weather", + "alerts": {}, + "database": "amap_weather.db", + "return_desc": true + }, + "qq_weather": { + "temp": "temperature", + "icon": "weather", + "alerts": {}, + "database": "amap_weather.db", + "return_desc": true + } + }, + "weather_api_list": ["xiaomi_weather", "qweather", "amap_weather", "qq_weather"], + "weather_api_list_zhCN": ["小米天气", "和风天气 (需 API Key)", "高德天气 (需 API Key)", "腾讯天气 (需 API Key)"] +} \ No newline at end of file diff --git a/config/data/xiaomi_weather.db b/config/data/xiaomi_weather.db new file mode 100644 index 0000000..fcfe264 Binary files /dev/null and b/config/data/xiaomi_weather.db differ diff --git a/config/data/xiaomi_weather_status.json b/config/data/xiaomi_weather_status.json new file mode 100644 index 0000000..d95db7b --- /dev/null +++ b/config/data/xiaomi_weather_status.json @@ -0,0 +1,156 @@ +{ + "weatherinfo": [ + { + "code": 0, + "wea": "晴" + }, + { + "code": 1, + "wea": "多云" + }, + { + "code": 2, + "wea": "阴" + }, + { + "code": 3, + "wea": "阵雨" + }, + { + "code": 4, + "wea": "雷阵雨" + }, + { + "code": 5, + "wea": "雷阵雨并伴有冰雹" + }, + { + "code": 6, + "wea": "雨夹雪" + }, + { + "code": 7, + "wea": "小雨" + }, + { + "code": 8, + "wea": "中雨" + }, + { + "code": 9, + "wea": "大雨" + }, + { + "code": 10, + "wea": "暴雨" + }, + { + "code": 11, + "wea": "大暴雨" + }, + { + "code": 12, + "wea": "特大暴雨" + }, + { + "code": 13, + "wea": "阵雪" + }, + { + "code": 14, + "wea": "小雪" + }, + { + "code": 15, + "wea": "中雪" + }, + { + "code": 16, + "wea": "大雪" + }, + { + "code": 17, + "wea": "暴雪" + }, + { + "code": 18, + "wea": "雾" + }, + { + "code": 19, + "wea": "冻雨" + }, + { + "code": 20, + "wea": "沙尘暴" + }, + { + "code": 21, + "wea": "小雨-中雨" + }, + { + "code": 22, + "wea": "中雨-大雨" + }, + { + "code": 23, + "wea": "大雨-暴雨" + }, + { + "code": 24, + "wea": "暴雨-大暴雨" + }, + { + "code": 25, + "wea": "大暴雨-特大暴雨" + }, + { + "code": 26, + "wea": "小雪-中雪" + }, + { + "code": 27, + "wea": "中雪-大雪" + }, + { + "code": 28, + "wea": "大雪-暴雪" + }, + { + "code": 29, + "wea": "浮沉" + }, + { + "code": 30, + "wea": "扬沙" + }, + { + "code": 31, + "wea": "强沙尘暴" + }, + { + "code": 32, + "wea": "飑" + }, + { + "code": 33, + "wea": "龙卷风" + }, + { + "code": 34, + "wea": "若高吹雪" + }, + { + "code": 35, + "wea": "轻雾" + }, + { + "code": 53, + "wea": "霾" + }, + { + "code": 99, + "wea": "未知" + } + ] +} \ No newline at end of file diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..c0ce321 --- /dev/null +++ b/config/default.json @@ -0,0 +1,80 @@ +{ + "part": { + + }, + "part_name": { + + }, + "timeline": { + "default": { + + }, + "0": { + + }, + "1": { + + }, + "2": { + + }, + "3": { + + }, + "4": { + + }, + "5": { + + }, + "6": { + + } + }, + "schedule": { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ] + }, + "schedule_even": { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ] + } +} \ No newline at end of file diff --git a/config/default_config.json b/config/default_config.json new file mode 100644 index 0000000..70dd92b --- /dev/null +++ b/config/default_config.json @@ -0,0 +1,77 @@ +{ + "General": { + "schedule": "新课表 - 1.json", + "pin_on_top": 1, + "margin": 10, + "time_offset": 0, + "opacity": 95, + "auto_startup": 0, + "hide": 0, + "hide_method": 0, + "color_mode": 2, + "enable_alt_schedule": 0, + "blur_floating_countdown": "1", + "blur_countdown": 0, + "theme": "default", + "scale": 1, + "excluded_lesson": 0, + "excluded_lessons": "", + "enable_click": 1 + }, + "Toast": { + "wave": 1, + "pin_on_top": 1, + "ringtone": 1, + "prepare_minutes": 2, + "attend_class": 1, + "finish_class": 1, + "prepare_class": 1, + "after_school": 1, + "smooth_volume": 0 + }, + "Weather": { + "city": 0, + "api": "xiaomi_weather", + "api_key": "" + }, + "Color": { + "floating_time": "959595", + "attend_class": "DD986F", + "finish_class": "46B878", + "prepare_class": "7065D8" + }, + "Plugin": { + "version": 2, + "mirror": "gh_proxy", + "auto_delay": 5, + "auto_enable_plugin": 1 + }, + "Date": { + "start_date": "", + "cd_text_custom": "自定义", + "countdown_date": "", + "countdown_upd_cd": 30, + "countdown_custom_mode": 1 + }, + "Audio": { + "volume": 75, + "attend_class": "attend_class.wav", + "finish_class": "finish_class.wav", + "prepare_class": "prepare_class.wav" + }, + "Temp": { + "set_week": "", + "temp_schedule": "", + "set_schedule": "" + }, + "Other": { + "do_not_log": 0, + "safe_mode": 0, + "initialstartup": 1, + "multiple_programs": 0, + "version_channel": 0, + "auto_check_update": 1, + "cses_version": 1, + "version": "v1.1.7.2" + } +} diff --git a/config/mirror.json b/config/mirror.json new file mode 100644 index 0000000..92e8e77 --- /dev/null +++ b/config/mirror.json @@ -0,0 +1,8 @@ +{ + "gh_mirror": { + "original": "", + "gh_proxy": "https://gh-proxy.com/", + "git_mirror": "https://hub.gitmirror.com/", + "moeyy": "https://github.moeyy.xyz/" + } +} \ No newline at end of file diff --git a/config/schedule/新课表 - 1.json b/config/schedule/新课表 - 1.json new file mode 100644 index 0000000..c0ce321 --- /dev/null +++ b/config/schedule/新课表 - 1.json @@ -0,0 +1,80 @@ +{ + "part": { + + }, + "part_name": { + + }, + "timeline": { + "default": { + + }, + "0": { + + }, + "1": { + + }, + "2": { + + }, + "3": { + + }, + "4": { + + }, + "5": { + + }, + "6": { + + } + }, + "schedule": { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ] + }, + "schedule_even": { + "0": [ + + ], + "1": [ + + ], + "2": [ + + ], + "3": [ + + ], + "4": [ + + ], + "5": [ + + ], + "6": [ + + ] + } +} \ No newline at end of file diff --git a/cses_mgr.py b/cses_mgr.py new file mode 100644 index 0000000..e577fc3 --- /dev/null +++ b/cses_mgr.py @@ -0,0 +1,271 @@ +""" +CSES Format Support +what is CSES: https://github.com/CSES-org/CSES +""" +import json +import typing +import cses +from datetime import datetime, timedelta +from loguru import logger + +import list_ as list_ +import conf +from file import base_directory, config_center + +CSES_WEEKS_TEXTS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] +CSES_WEEKS = [1, 2, 3, 4, 5, 6, 7] + + +def _get_time(time: typing.Union[str, int]) -> datetime: + if isinstance(time, str): + return datetime.strptime(str(time), '%H:%M:%S') + elif isinstance(time, int): + return datetime.strptime(f'{int(time / 60 / 60)}:{int(time / 60 % 60)}:{time % 60}','%H:%M:%S') + else: + raise ValueError(f'需要 int 或 HH:MM:SS 类型,得到 {type(time)},值为 {time}') + + +class CSES_Converter: + """ + CSES 文件管理器 + 集成导入/导出CSES文件的功能 + """ + + def __init__(self, path='./'): + self.generator = None + self.parser = None + self.path = path + + def load_parser(self): + if not cses.CSESParser.is_cses_file(self.path): + return "Error: Not a CSES file" # 判定格式 + + self.parser = cses.CSESParser(self.path) + return self.parser + + def load_generator(self): + self.generator = cses.CSESGenerator(version=int(config_center.read_conf('Other', 'cses_version'))) + + def convert_to_cw(self): + """ + 将CSES文件转换为Class Widgets格式 + """ + try: + with open(f'{base_directory}/config/default.json', 'r', encoding='utf-8') as file: # 加载默认配置 + cw_format = json.load(file) + except FileNotFoundError: + logger.error(f'File {base_directory}/config/default.json not found') + return False + + if not self.parser: + raise Exception("Parser not loaded, please load_parser() first.") + # 课程表 + cses_schedules = self.parser.get_schedules() + print(cses_schedules) + + part_count = 0 + part_list = [] + + for day in cses_schedules: # 课程 + # name = day['name'] + enable_day = day['enable_day'] + weeks = day['weeks'] + classes = day['classes'] + + last_end_time = None + class_count = 0 + + for class_ in classes: # 时间线 + week = str(CSES_WEEKS.index(enable_day)) # 星期 + subject = class_['subject'] # 课程名 + time_diff = None + + # 节点 + if class_ == classes[0]: + raw_time = _get_time(class_['start_time']) + time = [raw_time.hour, raw_time.minute] + if time not in part_list: # 跳过重复的(已创建的)节点 + cw_format['part'][str(part_count)] = time + cw_format['part_name'][str(part_count)] = f'Part {part_count}' + part_count += 1 + part_list.append(time) + + # 时间线 + start_time = _get_time(class_['start_time']) + end_time = _get_time(class_['end_time']) + class_count += 1 + + # 计算时长 + duration = int((end_time - start_time).total_seconds() / 60) + if last_end_time: + time_diff = int((start_time - last_end_time).total_seconds() / 60) # 时差 + + if not time_diff: # 如果连堂或第一节课 + cw_format['timeline'][week][f'a{part_count - 1}{class_count}'] = duration + else: + cw_format['timeline'][week][f'f{part_count - 1}{class_count - 1}'] = time_diff + cw_format['timeline'][week][f'a{part_count - 1}{class_count}'] = duration + + last_end_time = end_time + + # 课程 + if weeks == 'even': + cw_format['schedule_even'][week].append(subject) + elif weeks == 'odd': + cw_format['schedule'][week].append(subject) + elif weeks == 'all': + cw_format['schedule'][week].append(subject) + cw_format['schedule_even'][week].append(subject) + else: + logger.warning('本软件暂时不支持更多的周数循环') + + print(cw_format) + return cw_format + + def convert_to_cses(self, cw_data=None, cw_path='./'): + """ + 将Class Widgets格式转换为CSES文件,需提供保存路径和Class Widgets数据/路径 + Args: + cw_data: Class Widgets格式数据 (Optional) + cw_path: Class Widgets文件路径(Optional) + """ + def convert(schedules, type_='odd'): + class_counter_dict = {} # 记录一个节点当天的课程数 + for part in parts: # 节点循环 + name = part_names[part] + part_start_time = datetime.strptime(f'{parts[part][0]}:{parts[part][1]}', '%H:%M') + print(f'Part {part}: {name} - {part_start_time.strftime("%H:%M")}') + class_counter_dict[part] = {} + + for day, subjects in schedules.items(): + time_counter = 0 + class_counter = 0 + if timelines[day]: # 自定时间线存在 + timeline = timelines[day] + else: # 自定时间线不存在 + timeline = timelines['default'] + + timelines_part = {str(day): []} # 一个节点的时间线列表 + for key, time in timeline.items(): # 时间线循环 + if key.startswith(f'a{part}'): # 科目 + class_dict = {} + + other_parts_classes = 0 + for p, t in class_counter_dict.items(): # 超级嵌套 + if p == part: # 排除当前节点 + continue + all_time = 0 + for c, d in t.items(): # 超级嵌套 + if c != str(day): # 排除其他天 + continue + all_time += d + other_parts_classes += all_time + + start_time = part_start_time + timedelta(minutes=time_counter) + end_time = start_time + timedelta(minutes=int(time)) + subject = subjects[int(key[2:]) - 1 + other_parts_classes] + class_counter += 1 + + if subject == '未添加': # 跳过未添加的科目 + time_counter += int(time) # 时间叠加 + continue + + class_dict['subject'] = subject + class_dict['start_time'] = start_time.strftime('%H:%M:00') + class_dict['end_time'] = end_time.strftime('%H:%M:00') + + timelines_part[str(day)].append(class_dict) + if key[1] == part: # 时间叠加counter + time_counter += int(time) + + class_counter_dict[part][day] = class_counter # 记录一个节点当天的课程数 + + print(timelines_part) + if not timelines_part[str(day)]: # 跳过空时间线 + continue + + self.generator.add_schedule( + name=f'{name}_{CSES_WEEKS_TEXTS[int(day)]}', + enable_day=CSES_WEEKS[int(day)], + weeks=type_, + classes=[timelines_part[str(day)][i] for i in range(len(timelines_part[str(day)]))] + ) + + def check_subjects(schedule): # 检查课表是否有未正式设定的科目 + unset_subjects = [] + for _, classes in schedule.items(): + for class_ in classes: + if class_ == '未添加': + continue + if class_ not in cw_subjects['subject_list']: + unset_subjects.append(class_) + return unset_subjects + + """ + 转换/CONVERT + """ + # 科目 + try: + with open(f'{base_directory}/config/data/subject.json', 'r', encoding='utf-8') as data: + cw_subjects = json.load(data) + except FileNotFoundError: + logger.error(f'File {base_directory}/config/data/subject.json not found') + return False + + for subject_ in cw_subjects['subject_list']: + self.generator.add_subject( + name=subject_, simplified_name=list_.get_subject_abbreviation(subject_), + teacher=None, room=None + ) + + # 课表 + if not self.generator: + raise Exception("Generator not loaded, please load_generator() first.") + + if cw_path != './' and cw_data is None: # 加载Class Widgets数据 + try: + with open(cw_path, 'r', encoding='utf-8') as data: + cw_data = json.load(data) + except FileNotFoundError: + logger.error(f'File {cw_path} not found') + return False + else: + raise Exception("Please provide a path or a cw_data") + + parts = cw_data['part'] + part_names = cw_data['part_name'] + timelines = cw_data['timeline'] + schedules_odd = cw_data['schedule'] + schedule_even = cw_data['schedule_even'] + + convert(schedules_odd) + convert(schedule_even, 'even') + us_set_odd = set(check_subjects(schedules_odd)) + us_set_even = set(check_subjects(schedule_even)) + us_union = us_set_odd.union(us_set_even) + + for subject_ in list(us_union): + self.generator.add_subject( + name=subject_, simplified_name=list_.get_subject_abbreviation(subject_), + teacher=None, room=None + ) + + try: + self.generator.save_to_file(self.path) + return True + except Exception as e: + logger.error(f'Error: {e}') + return False + + +if __name__ == '__main__': + # EXAMPLE + importer = CSES_Converter(path='./config/cses_schedule/test.yaml') + importer.load_parser() + importer.convert_to_cw() + + print('_____________________________', end='\n') # 输出分割线 + + exporter = CSES_Converter(path='./config/cses_schedule/test2.yaml') + exporter.load_generator() + exporter.convert_to_cses(cw_path='./config/schedule/default (3).json') diff --git a/extra_menu.py b/extra_menu.py new file mode 100644 index 0000000..7f0b3e2 --- /dev/null +++ b/extra_menu.py @@ -0,0 +1,224 @@ +import datetime as dt +import sys +from shutil import copy + +from PyQt5 import uic +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QApplication, QScroller +from loguru import logger +from qfluentwidgets import FluentWindow, FluentIcon as fIcon, ComboBox, \ + PrimaryPushButton, Flyout, FlyoutAnimationType, InfoBarIcon, ListWidget, LineEdit, ToolButton, HyperlinkButton, \ + SmoothScrollArea, Dialog + +import conf +import file +from conf import base_directory +import list_ +from file import config_center, schedule_center +from menu import SettingsMenu +import platform +from loguru import logger + +# 适配高DPI缩放 +if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: + QApplication.setHighDpiScaleFactorRoundingPolicy( + Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) +else: + logger.warning('不兼容的系统,跳过高DPI标识') + +settings = None + +current_week = dt.datetime.today().weekday() +temp_schedule = {'schedule': {}, 'schedule_even': {}} + + +def open_settings(): + if config_center.read_conf('Temp', 'temp_schedule'): + w = Dialog( + "暂时无法使用“设置”", + "由于您正在使用临时课表,将无法使用“设置”的课程表功能;\n若要启用“设置”,请重新启动 Class Widgets。" + "\n(重启后,临时课表也将会恢复)", + None + ) + w.cancelButton.hide() + w.buttonLayout.insertStretch(1) + w.exec() + + return + + global settings + if settings is None or not settings.isVisible(): + settings = SettingsMenu() + settings.closed.connect(cleanup_settings) + settings.show() + logger.info('打开“设置”') + else: + settings.raise_() + settings.activateWindow() + + +def cleanup_settings(): + global settings + logger.info('关闭“设置”') + del settings + settings = None + + +class ExtraMenu(FluentWindow): + def __init__(self): + super().__init__() + self.menu = None + self.interface = uic.loadUi(f'{base_directory}/view/extra_menu.ui') + self.initUI() + self.init_interface() + + def init_interface(self): + ex_scroll = self.findChild(SmoothScrollArea, 'ex_scroll') + QScroller.grabGesture(ex_scroll, QScroller.LeftMouseButtonGesture) + select_temp_week = self.findChild(ComboBox, 'select_temp_week') # 选择替换日期 + select_temp_week.addItems(list_.week) + select_temp_week.setCurrentIndex(current_week) + select_temp_week.currentIndexChanged.connect(self.refresh_schedule_list) # 日期选择变化 + + select_temp_schedule = self.findChild(ComboBox, 'select_temp_schedule') # 选择替换课表 + select_temp_schedule.addItems(list_.week_type) + select_temp_schedule.setCurrentIndex(conf.get_week_type()) + select_temp_schedule.currentIndexChanged.connect(self.refresh_schedule_list) # 日期选择变化 + + tmp_schedule_list = self.findChild(ListWidget, 'schedule_list') # 换课列表 + tmp_schedule_list.addItems(self.load_schedule()) + tmp_schedule_list.itemChanged.connect(self.upload_item) + + class_kind_combo = self.findChild(ComboBox, 'class_combo') # 课程类型 + class_kind_combo.addItems(list_.class_kind) + + set_button = self.findChild(ToolButton, 'set_button') + set_button.setIcon(fIcon.EDIT) + set_button.clicked.connect(self.edit_item) + + save_temp_conf = self.findChild(PrimaryPushButton, 'save_temp_conf') # 保存设置 + save_temp_conf.clicked.connect(self.save_temp_conf) + + redirect_to_settings = self.findChild(HyperlinkButton, 'redirect_to_settings') + redirect_to_settings.clicked.connect(open_settings) + + @staticmethod + def load_schedule(): + if conf.get_week_type(): + return schedule_center.schedule_data['schedule_even'][str(current_week)] + else: + return schedule_center.schedule_data['schedule'][str(current_week)] + + def save_temp_conf(self): + try: + temp_week = self.findChild(ComboBox, 'select_temp_week') + temp_schedule_set = self.findChild(ComboBox, 'select_temp_schedule') + if temp_schedule != {'schedule': {}, 'schedule_even': {}}: + if config_center.read_conf('Temp', 'temp_schedule') == '': # 备份检测 + copy(f'{base_directory}/config/schedule/{config_center.schedule_name}', + f'{base_directory}/config/schedule/backup.json') # 备份课表配置 + logger.info(f'备份课表配置成功:已将 {config_center.schedule_name} -备份至-> backup.json') + config_center.write_conf('Temp', 'temp_schedule', config_center.schedule_name) + file.save_data_to_json(temp_schedule, config_center.schedule_name) + schedule_center.update_schedule() + config_center.write_conf('Temp', 'set_week', str(temp_week.currentIndex())) + config_center.write_conf('Temp', 'set_schedule',str(temp_schedule_set.currentIndex())) + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='保存成功', + content=f"已保存至 ./config.ini \n重启后恢复。", + target=self.findChild(PrimaryPushButton, 'save_temp_conf'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + except Exception as e: + Flyout.create( + icon=InfoBarIcon.ERROR, + title='保存失败', + content=f"错误信息:{e}", + target=self.findChild(PrimaryPushButton, 'save_temp_conf'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + def refresh_schedule_list(self): + global current_week + current_week = self.findChild(ComboBox, 'select_temp_week').currentIndex() + current_schedule = self.findChild(ComboBox, 'select_temp_schedule').currentIndex() + logger.debug(f'current_week: {current_week}, current_schedule: {current_schedule}') + tmp_schedule_list = self.findChild(ListWidget, 'schedule_list') # 换课列表 + tmp_schedule_list.clear() + tmp_schedule_list.clearSelection() + if config_center.read_conf('Temp', 'temp_schedule') == '': + if current_schedule: + tmp_schedule_list.addItems( + schedule_center.schedule_data['schedule_even'][str(current_week)] + ) + else: + tmp_schedule_list.addItems( + schedule_center.schedule_data['schedule'][str(current_week)] + ) + else: + if current_schedule: + tmp_schedule_list.addItems(file.load_from_json('backup.json')['schedule_even'][str(current_week)]) + else: + tmp_schedule_list.addItems(file.load_from_json('backup.json')['schedule'][str(current_week)]) + + def upload_item(self): + global temp_schedule + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + cache_list = [] + for i in range(se_schedule_list.count()): # 缓存ListWidget数据至列表 + item_text = se_schedule_list.item(i).text() + cache_list.append(item_text) + if conf.get_week_type(): + temp_schedule['schedule_even'][str(current_week)] = cache_list + else: + temp_schedule['schedule'][str(current_week)] = cache_list + + def edit_item(self): + tmp_schedule_list = self.findChild(ListWidget, 'schedule_list') + class_combo = self.findChild(ComboBox, 'class_combo') + custom_class = self.findChild(LineEdit, 'custom_class') + selected_items = tmp_schedule_list.selectedItems() + + if selected_items: + selected_item = selected_items[0] + if class_combo.currentIndex() != 0: + selected_item.setText(class_combo.currentText()) + else: + if custom_class.text() != '': + selected_item.setText(custom_class.text()) + + def initUI(self): + # 修复设置窗口在各个屏幕分辨率DPI下的窗口大小 + screen_geometry = QApplication.primaryScreen().geometry() + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + width = int(screen_width * 0.55) + height = int(screen_height * 0.65) + + self.move(int(screen_width / 2 - width / 2), 150) + self.resize(width, height) + + self.setWindowTitle('Class Widgets - 更多功能') + self.setWindowIcon(QIcon(f'{base_directory}/img/logo/favicon-exmenu.ico')) + + self.addSubInterface(self.interface, fIcon.INFO, '更多设置') + + def closeEvent(self, e): + self.deleteLater() + return super().closeEvent(e) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + ex = ExtraMenu() + ex.show() + sys.exit(app.exec()) diff --git a/file.py b/file.py new file mode 100644 index 0000000..d765117 --- /dev/null +++ b/file.py @@ -0,0 +1,201 @@ +import json +import os +import sys +from pathlib import Path +from shutil import copy + +from loguru import logger +import configparser as config + +base_directory = os.path.dirname(os.path.abspath(__file__)) +''' +if base_directory.endswith('MacOS'): + base_directory = os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)), 'Resources') +''' +path = f'{base_directory}/config.ini' + + +class ConfigCenter: + """ + Config中心 + """ + def __init__(self): + self.config = config.ConfigParser() + self.config.read(path, encoding='utf-8') + with open(f'{base_directory}/config/default_config.json', encoding="utf-8") as default: + self.default_data = json.load(default) + + self.check_config() + self.schedule_name = self.read_conf('General', 'schedule') + self.old_schedule_name = self.schedule_name + + def update_conf(self): + try: + self.config.read_file(open(path, 'r', encoding='utf-8')) + + self.schedule_name = self.read_conf('General', 'schedule') + if self.schedule_name != self.old_schedule_name: + logger.info(f'已切换课程表: {self.schedule_name}') + schedule_center.update_schedule() + self.old_schedule_name = self.schedule_name + except Exception as e: + logger.error(f'更新配置文件时出错: {e}') + + def read_conf(self, section='General', key=''): + if section in self.config and key in self.config[section]: + return self.config[section][key] + elif section in self.config and key == '': + return dict(self.config[section]) + elif section in self.default_data and key in self.default_data[section]: + logger.info('配置文件出现问题,已尝试修复') + self.write_conf(section, key, self.default_data[section][key]) + return self.default_data[section][key] + elif section in self.default_data and key == '': + logger.info('配置文件出现问题,已尝试修复') + self.write_conf(section, '', self.default_data[section]) + return dict(self.default_data[section]) + else: + return None + + def write_conf(self, section, key, value): + if section not in self.config: + self.config.add_section(section) + + self.config.set(section, key, str(value)) + + with open(path, 'w', encoding='utf-8') as configfile: + self.config.write(configfile) + + def check_config(self): + if not os.path.exists(path): # 如果配置文件不存在,则copy默认配置文件 + self.config.read_dict(self.default_data) + with open(path, 'w', encoding='utf-8') as configfile: + self.config.write(configfile) + if sys.platform != 'win32': + self.config.set('General', 'hide_method', '2') + with open(path, 'w', encoding='utf-8') as configfile: + self.config.write(configfile) + logger.info("配置文件不存在,已创建并写入默认配置。") + copy(f'{base_directory}/config/default.json', f'{base_directory}/config/schedule/新课表 - 1.json') + else: + with open(path, 'r', encoding='utf-8') as configfile: + self.config.read_file(configfile) + + if self.config['Other']['version'] != self.default_data['Other']['version']: # 如果配置文件版本不同,则更新配置文件 + logger.info(f"配置文件版本不同,将重新适配") + try: + for section, options in self.default_data.items(): + if section not in self.config: + self.config[section] = options + else: + for key, value in options.items(): + if key not in self.config[section]: + self.config[section][key] = str(value) + self.config.set('Other', 'version', self.default_data['Other']['version']) + with open(path, 'w', encoding='utf-8') as configfile: + self.config.write(configfile) + logger.info(f"配置文件已更新") + except Exception as e: + logger.error(f"配置文件更新失败: {e}") + + if not os.path.exists(f"{base_directory}/config/schedule/{self.read_conf('General', 'schedule')}"): + # 如果config.ini课程表不存在,则创建 + + schedule_config = [] + # 遍历目标目录下的所有文件 + for file_name in os.listdir(f'{base_directory}/config/schedule'): + # 找json + if file_name.endswith('.json') and file_name != 'backup.json': + # 将文件路径添加到列表 + schedule_config.append(file_name) + if not schedule_config: + copy(f'{base_directory}/config/default.json', + f'{base_directory}/config/schedule/{self.read_conf("General", "schedule")}') + logger.info(f"课程表不存在,已创建默认课程表") + else: + config_center.write_conf('General', 'schedule', schedule_config[0]) + print(os.path.join(os.getcwd(), 'config', 'schedule')) + + # 判断是否存在 Plugins 文件夹 + plugins_dir = Path(base_directory) / 'plugins' + if not plugins_dir.exists(): + plugins_dir.mkdir() + logger.info("Plugins 文件夹不存在,已创建。") + + # 判断 Plugins 文件夹内是否存在 plugins_from_pp.json 文件 + plugins_file = plugins_dir / 'plugins_from_pp.json' + if not plugins_file.exists(): + with open(plugins_file, 'w', encoding='utf-8') as file: + # 使用 indent=4 来缩进,并确保数组元素在多行显示 + json.dump({"plugins": []}, file, ensure_ascii=False, indent=4) + logger.info("plugins_from_pp.json 文件不存在,已创建。") + + +class ScheduleCenter: + """ + 课程表中心 + """ + def __init__(self): + self.schedule_data = None + self.update_schedule() + + def update_schedule(self): + """ + 更新课程表 + """ + self.schedule_data = load_from_json(config_center.schedule_name) + + def save_data(self, new_data, filename): + # 更新,添加或覆盖新的数据 + self.schedule_data.update(new_data) + + # 将更新后的数据保存回文件 + try: + with open(f'{base_directory}/config/schedule/{filename}', 'w', encoding='utf-8') as file: + json.dump(self.schedule_data, file, ensure_ascii=False, indent=4) + return f"数据已成功保存到 config/schedule/{filename}" + except Exception as e: + logger.error(f"保存数据时出错: {e}") + + +def load_from_json(filename): + """ + 从 JSON 文件中加载数据。 + :param filename: 要加载的文件 + :return: 返回从文件中加载的数据字典 + """ + try: + with open(f'{base_directory}/config/schedule/{filename}', 'r', encoding='utf-8') as file: + data = json.load(file) + return data + except Exception as e: + logger.error(f"加载数据时出错: {e}") + return None + + +def save_data_to_json(new_data, filename): + # 初始化 data_dict 为一个空字典 + data_dict = {} + + # 如果文件存在,先读取文件中的现有数据 + if os.path.exists(f'{base_directory}/config/schedule/{filename}'): + try: + with open(f'{base_directory}/config/schedule/{filename}', 'r', encoding='utf-8') as file: + data_dict = json.load(file) + except Exception as e: + logger.error(f"读取现有数据时出错: {e}") + + # 更新 data_dict,添加或覆盖新的数据 + data_dict.update(new_data) + + # 将更新后的数据保存回文件 + try: + with open(f'{base_directory}/config/schedule/{filename}', 'w', encoding='utf-8') as file: + json.dump(data_dict, file, ensure_ascii=False, indent=4) + return f"数据已成功保存到 config/schedule/{filename}" + except Exception as e: + logger.error(f"保存数据时出错: {e}") + + +config_center = ConfigCenter() +schedule_center = ScheduleCenter() diff --git a/font/HarmonyOS_Sans_SC_Bold.ttf b/font/HarmonyOS_Sans_SC_Bold.ttf new file mode 100644 index 0000000..5c925d1 Binary files /dev/null and b/font/HarmonyOS_Sans_SC_Bold.ttf differ diff --git a/font/LICENSE.txt b/font/LICENSE.txt new file mode 100644 index 0000000..211a701 Binary files /dev/null and b/font/LICENSE.txt differ diff --git a/generate_speech.py b/generate_speech.py new file mode 100644 index 0000000..236ac51 --- /dev/null +++ b/generate_speech.py @@ -0,0 +1,317 @@ +import asyncio +import hashlib +import os +import platform +import re +import time +from pathlib import Path +from typing import Optional + +import edge_tts +import pyttsx3 +from loguru import logger + + +class TTSEngine: + """支持多平台和智能语音选择的多引擎TTS工具类""" + + def __init__(self): + """ + 初始化TTS引擎实例 + 属性: + - cache_dir: 音频缓存目录路径(软件运行目录下 cache/audio文件夹) + - engine_priority: 引擎优先级列表 + - voice_mapping: 跨平台语音映射配置表 + """ + self.cache_dir = os.path.join(os.getcwd(), "cache", "audio") + self._ensure_cache_dir() + self.engine_priority = ['edge', 'pyttsx3'] + + # 跨平台语音映射表 + self.voice_mapping = { + 'edge': { + 'zh-CN': 'zh-CN-YunxiNeural', + 'en-US': 'en-US-AriaNeural' + }, + 'pyttsx3': self._get_platform_voices() + } + + @staticmethod + def _get_platform_voices(): + """ + 获取当前平台的默认语音配置 + + 返回: + - dict: 包含中英文语音ID的字典,结构为{'zh-CN': voice_id, 'en-US': voice_id} + + 平台支持: + - Windows: 使用注册表路径标识语音 + - macOS: 使用Apple语音标识符 + - Linux: 使用espeak语音名称 + """ + current_os = platform.system() + + # Windows默认配置 + if current_os == 'Windows': + return { + 'zh-CN': 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_ZH-CN_HUIHUI_11.0', + 'en-US': 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\TTS_MS_EN-US_DAVID_11.0' + } + # macOS默认配置 + elif current_os == 'Darwin': + return { + 'zh-CN': 'com.apple.speech.synthesis.voice.ting-ting.premium', + 'en-US': 'com.apple.speech.synthesis.voice.Alex' + } + # Linux默认配置 (espeak) + else: + return { + 'zh-CN': 'chinese', + 'en-US': 'english-us' + } + + def _ensure_cache_dir(self): + Path(self.cache_dir).mkdir(parents=True, exist_ok=True) + + @staticmethod + def _generate_filename(text: str, engine: str) -> str: + timestamp = str(int(time.time())) + hash_str = hashlib.md5(text.encode()).hexdigest()[:8] + return f"{engine}_{hash_str}_{timestamp}.mp3" + + @staticmethod + async def _edge_tts(text: str, voice: str, file_path: str) -> str: + communicate = edge_tts.Communicate(text, voice) + await communicate.save(file_path) + return file_path + + async def _pyttsx3_tts(self, text: str, voice: str, file_path: str) -> str: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, + lambda: self._sync_pyttsx3(text, voice, file_path) + ) + + @staticmethod + def _sync_pyttsx3(text: str, voice: str, file_path: str): + engine = None + try: + engine = pyttsx3.init() + engine.connect('started-utterance', lambda name: None) + engine.connect('finished-utterance', lambda name, completed: engine.stop()) + + # 应用语音设置 + if voice: + voices = engine.getProperty('voices') + found_voice = next((v for v in voices if v.id == voice), None) + if not found_voice: + raise ValueError(f"无效语音ID:{voice}") + engine.setProperty('voice', found_voice.id) + + engine.save_to_file(text, file_path) + start_time = time.time() + engine.startLoop(False) + while engine.isBusy(): + if time.time() - start_time > 10: + raise TimeoutError("pyttsx3生成超时") + time.sleep(0.1) + engine.iterate() + engine.endLoop() + finally: + if engine: + engine.stop() + + @staticmethod + def _detect_language(text: str) -> str: + """改进的语言检测方法""" + if re.search(u'[\u4e00-\u9fff]', text): + return 'zh-CN' + return 'en-US' + + @staticmethod + def _validate_pyttsx3_voice(voice_id: str, lang: str) -> str: + """验证语音有效性,自动回退""" + try: + engine = pyttsx3.init() + voices = engine.getProperty('voices') + + if any(v.id == voice_id for v in voices): + return voice_id + + lang_voices = [v for v in voices if lang in str(v.languages)] + if lang_voices: + return lang_voices[0].id + + return engine.getProperty('voice') + except Exception as e: + logger.error(f"语音验证失败: {str(e)}") + return '' + + async def _execute_engine( + self, + engine: str, + text: str, + voice: str, + file_path: str, + timeout: float + ) -> str: + """ + 生成语音文件的核心异步方法 + + 参数: + text (str): 要转换的文本内容(支持中英文自动检测) + engine (str): 首选TTS引擎(默认edge) + voice (str): 指定语音ID(可选),不指定则根据语言自动选择 + auto_fallback (bool): 引擎失败时是否自动回退(默认False) + timeout (float): 单引擎超时时间(秒,默认10) + filename (str): 自定义文件名(可选),不指定则自动生成 + + 返回: + str: 生成的音频文件绝对路径 + + 异常: + RuntimeError: 所有尝试的引擎均失败时抛出 + """ + try: + if engine == "edge": + task = self._edge_tts(text, voice, file_path) + elif engine == "pyttsx3": + task = self._pyttsx3_tts(text, voice, file_path) + else: + raise ValueError(f"不支持的引擎:{engine}") + + return await asyncio.wait_for(task, timeout=timeout) + except asyncio.TimeoutError: + raise RuntimeError(f"{engine}引擎执行超时") + except Exception as e: + raise RuntimeError(f"{engine}引擎错误:{str(e)}") + + async def generate_speech( + self, + text: str, + engine: str = "edge", + voice: Optional[str] = None, + auto_fallback: bool = False, + timeout: float = 10.0, + filename: Optional[str] = None + ) -> str: + """核心生成方法""" + + # 自动语音选择逻辑 + lang = self._detect_language(text) + if not voice: + if engine == 'pyttsx3': + voice = self.voice_mapping[engine].get(lang) + voice = self._validate_pyttsx3_voice(voice, lang) + else: + voice = self.voice_mapping[engine][lang] + + filename = filename or self._generate_filename(text, engine) + file_path = os.path.join(self.cache_dir, filename) + + errors = [] + attempted_engines = set() + engines_to_try = [engine] + if auto_fallback: + for e in self.engine_priority: + if e != engine and e not in engines_to_try: + engines_to_try.append(e) + + for current_engine in engines_to_try: + if current_engine in attempted_engines: + continue + if current_engine not in self.engine_priority: + continue + + attempted_engines.add(current_engine) + + try: + await self._execute_engine( + engine=current_engine, + text=text, + voice=voice, + file_path=file_path, + timeout=timeout + ) + + actual_filename = self._generate_filename(text, current_engine) + actual_path = os.path.join(self.cache_dir, actual_filename) + os.rename(file_path, actual_path) + + if not os.path.exists(actual_path): + raise RuntimeError(f"语音文件生成失败: {actual_path}") + + logger.info(f"成功生成语音 | 引擎: {current_engine} | 路径: {actual_path}") + return actual_path + + except Exception as e: + errors.append(f"{current_engine}: {str(e)}") + continue + + raise RuntimeError( + f"所有引擎尝试失败\n" + + "\n".join(errors) + ) + + def cleanup(self, max_age: int = 86400): + now = time.time() + for f in Path(self.cache_dir).glob("*.*"): + if f.is_file() and (now - f.stat().st_mtime) > max_age: + f.unlink() + + @staticmethod + def delete_audio_file(file_path: str, retries: int = 3, delay: float = 0.5): + """ + 安全删除音频文件 + 参数: + retries: 重试次数 + delay: 重试间隔(秒) + """ + for attempt in range(retries): + try: + if os.path.exists(file_path): + os.remove(file_path) + logger.info(f"成功删除音频文件: {file_path}") + return True + except Exception as e: + if attempt < retries - 1: + logger.warning(f"删除失败,正在重试 ({attempt + 1}/{retries}): {str(e)}") + time.sleep(delay) + else: + logger.error(f"最终删除失败: {file_path} | 错误: {str(e)}") + return False + + +def generate_speech_sync( + text: str, + engine: str = "edge", + voice: Optional[str] = None, + auto_fallback: bool = False, + timeout: float = 10.0, + filename: Optional[str] = None +) -> str: + """同步生成方法""" + tts = TTSEngine() + return asyncio.run(tts.generate_speech( + text=text, + engine=engine, + voice=voice, + auto_fallback=auto_fallback, + timeout=timeout, + filename=filename + )) + + +def list_pyttsx3_voices(): + """跨平台语音列表显示""" + engine = pyttsx3.init() + voices = engine.getProperty('voices') + current_os = platform.system() + + for idx, voice in enumerate(voices): + logger.info(f"\n[{current_os} 平台Pyttsx3可用语音包]" + f"\n{idx + 1}. ID: {voice.id}" + f"\n 名称: {voice.name}" + f"\n 语言: {voice.languages[0] if voice.languages else '未知'}" + f"\n 性别: {voice.gender}" + f"\n" + "-" * 60) diff --git a/img/Banner.png b/img/Banner.png new file mode 100644 index 0000000..0f6601a Binary files /dev/null and b/img/Banner.png differ diff --git a/img/Logo.png b/img/Logo.png new file mode 100644 index 0000000..f546636 Binary files /dev/null and b/img/Logo.png differ diff --git a/img/Octicons-mark-github.svg b/img/Octicons-mark-github.svg new file mode 100644 index 0000000..89ff12f --- /dev/null +++ b/img/Octicons-mark-github.svg @@ -0,0 +1,3 @@ + + + diff --git a/img/attend_class.svg b/img/attend_class.svg new file mode 100644 index 0000000..d2d26bd --- /dev/null +++ b/img/attend_class.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/img/bilibili-website.favicon.svg b/img/bilibili-website.favicon.svg new file mode 100644 index 0000000..2a2e144 --- /dev/null +++ b/img/bilibili-website.favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/img/favicon.icns b/img/favicon.icns new file mode 100644 index 0000000..d4401a4 Binary files /dev/null and b/img/favicon.icns differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000..03ca8c7 Binary files /dev/null and b/img/favicon.ico differ diff --git a/img/favicon.png b/img/favicon.png new file mode 100644 index 0000000..6be578d Binary files /dev/null and b/img/favicon.png differ diff --git a/img/logo/favicon-error.ico b/img/logo/favicon-error.ico new file mode 100644 index 0000000..7dca200 Binary files /dev/null and b/img/logo/favicon-error.ico differ diff --git a/img/logo/favicon-exmenu.ico b/img/logo/favicon-exmenu.ico new file mode 100644 index 0000000..f2db168 Binary files /dev/null and b/img/logo/favicon-exmenu.ico differ diff --git a/img/logo/favicon-settings.ico b/img/logo/favicon-settings.ico new file mode 100644 index 0000000..597e5ec Binary files /dev/null and b/img/logo/favicon-settings.ico differ diff --git a/img/logo/favicon-update.png b/img/logo/favicon-update.png new file mode 100644 index 0000000..025df78 Binary files /dev/null and b/img/logo/favicon-update.png differ diff --git a/img/logo/favicon.ico b/img/logo/favicon.ico new file mode 100644 index 0000000..7d9830d Binary files /dev/null and b/img/logo/favicon.ico differ diff --git a/img/logo/favicon.png b/img/logo/favicon.png new file mode 100644 index 0000000..b1cc0e1 Binary files /dev/null and b/img/logo/favicon.png differ diff --git a/img/plaza/banner_network-failed.png b/img/plaza/banner_network-failed.png new file mode 100644 index 0000000..0e66a64 Binary files /dev/null and b/img/plaza/banner_network-failed.png differ diff --git a/img/plaza/banner_pre.png b/img/plaza/banner_pre.png new file mode 100644 index 0000000..e4b10ae Binary files /dev/null and b/img/plaza/banner_pre.png differ diff --git a/img/plaza/plugin_pre.png b/img/plaza/plugin_pre.png new file mode 100644 index 0000000..2aac04d Binary files /dev/null and b/img/plaza/plugin_pre.png differ diff --git a/img/pp_favicon.png b/img/pp_favicon.png new file mode 100644 index 0000000..a370d73 Binary files /dev/null and b/img/pp_favicon.png differ diff --git a/img/screenshot_0.png b/img/screenshot_0.png new file mode 100644 index 0000000..b8d6570 Binary files /dev/null and b/img/screenshot_0.png differ diff --git a/img/screenshot_1.png b/img/screenshot_1.png new file mode 100644 index 0000000..5d720c0 Binary files /dev/null and b/img/screenshot_1.png differ diff --git a/img/settings/default.png b/img/settings/default.png new file mode 100644 index 0000000..aceb4fa Binary files /dev/null and b/img/settings/default.png differ diff --git a/img/settings/floating.png b/img/settings/floating.png new file mode 100644 index 0000000..db643aa Binary files /dev/null and b/img/settings/floating.png differ diff --git a/img/settings/hide_all.png b/img/settings/hide_all.png new file mode 100644 index 0000000..a2ba6d9 Binary files /dev/null and b/img/settings/hide_all.png differ diff --git a/img/settings/plugin-icon.png b/img/settings/plugin-icon.png new file mode 100644 index 0000000..b5162d0 Binary files /dev/null and b/img/settings/plugin-icon.png differ diff --git a/img/subject/abc.svg b/img/subject/abc.svg new file mode 100644 index 0000000..5a8cf87 --- /dev/null +++ b/img/subject/abc.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/img/subject/after_school.svg b/img/subject/after_school.svg new file mode 100644 index 0000000..c05d98c --- /dev/null +++ b/img/subject/after_school.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/subject/art.svg b/img/subject/art.svg new file mode 100644 index 0000000..3bbcc78 --- /dev/null +++ b/img/subject/art.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/img/subject/biology.svg b/img/subject/biology.svg new file mode 100644 index 0000000..2dcf1b0 --- /dev/null +++ b/img/subject/biology.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/img/subject/break.svg b/img/subject/break.svg new file mode 100644 index 0000000..7c72410 --- /dev/null +++ b/img/subject/break.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/subject/chemistry.svg b/img/subject/chemistry.svg new file mode 100644 index 0000000..88e3917 --- /dev/null +++ b/img/subject/chemistry.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/chinese.svg b/img/subject/chinese.svg new file mode 100644 index 0000000..128d90d --- /dev/null +++ b/img/subject/chinese.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/geography.svg b/img/subject/geography.svg new file mode 100644 index 0000000..a1342fe --- /dev/null +++ b/img/subject/geography.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/img/subject/history.svg b/img/subject/history.svg new file mode 100644 index 0000000..973e1ec --- /dev/null +++ b/img/subject/history.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/subject/it.svg b/img/subject/it.svg new file mode 100644 index 0000000..e78c4d2 --- /dev/null +++ b/img/subject/it.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/subject/math.svg b/img/subject/math.svg new file mode 100644 index 0000000..ec2e8e6 --- /dev/null +++ b/img/subject/math.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/meeting.svg b/img/subject/meeting.svg new file mode 100644 index 0000000..784201b --- /dev/null +++ b/img/subject/meeting.svg @@ -0,0 +1,3 @@ + + + diff --git a/img/subject/music.svg b/img/subject/music.svg new file mode 100644 index 0000000..a645f48 --- /dev/null +++ b/img/subject/music.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/pe.svg b/img/subject/pe.svg new file mode 100644 index 0000000..e2bef6e --- /dev/null +++ b/img/subject/pe.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/physics.svg b/img/subject/physics.svg new file mode 100644 index 0000000..c72d2e9 --- /dev/null +++ b/img/subject/physics.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/subject/self_study.svg b/img/subject/self_study.svg new file mode 100644 index 0000000..eaef2cb --- /dev/null +++ b/img/subject/self_study.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/subject/xmark.svg b/img/subject/xmark.svg new file mode 100644 index 0000000..c96a688 --- /dev/null +++ b/img/subject/xmark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/0.svg b/img/weather/0.svg new file mode 100644 index 0000000..27a5348 --- /dev/null +++ b/img/weather/0.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/0d.svg b/img/weather/0d.svg new file mode 100644 index 0000000..a3e92bd --- /dev/null +++ b/img/weather/0d.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/img/weather/1.svg b/img/weather/1.svg new file mode 100644 index 0000000..f7de58b --- /dev/null +++ b/img/weather/1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/10.svg b/img/weather/10.svg new file mode 100644 index 0000000..86a14bb --- /dev/null +++ b/img/weather/10.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/11.svg b/img/weather/11.svg new file mode 100644 index 0000000..b9bcc10 --- /dev/null +++ b/img/weather/11.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/12.svg b/img/weather/12.svg new file mode 100644 index 0000000..f26dcd3 --- /dev/null +++ b/img/weather/12.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/13.svg b/img/weather/13.svg new file mode 100644 index 0000000..b8fdce9 --- /dev/null +++ b/img/weather/13.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/13d.svg b/img/weather/13d.svg new file mode 100644 index 0000000..89f1986 --- /dev/null +++ b/img/weather/13d.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/14.svg b/img/weather/14.svg new file mode 100644 index 0000000..5a689be --- /dev/null +++ b/img/weather/14.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/img/weather/15.svg b/img/weather/15.svg new file mode 100644 index 0000000..83c8a96 --- /dev/null +++ b/img/weather/15.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/16.svg b/img/weather/16.svg new file mode 100644 index 0000000..2292ac2 --- /dev/null +++ b/img/weather/16.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/weather/17.svg b/img/weather/17.svg new file mode 100644 index 0000000..4370e46 --- /dev/null +++ b/img/weather/17.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/weather/18.svg b/img/weather/18.svg new file mode 100644 index 0000000..f966e57 --- /dev/null +++ b/img/weather/18.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/img/weather/19.svg b/img/weather/19.svg new file mode 100644 index 0000000..f20fcbe --- /dev/null +++ b/img/weather/19.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/1d.svg b/img/weather/1d.svg new file mode 100644 index 0000000..5509ba4 --- /dev/null +++ b/img/weather/1d.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/2.svg b/img/weather/2.svg new file mode 100644 index 0000000..e510e47 --- /dev/null +++ b/img/weather/2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/img/weather/20.svg b/img/weather/20.svg new file mode 100644 index 0000000..9fddc17 --- /dev/null +++ b/img/weather/20.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/21.svg b/img/weather/21.svg new file mode 100644 index 0000000..8e3c5a8 --- /dev/null +++ b/img/weather/21.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/22.svg b/img/weather/22.svg new file mode 100644 index 0000000..3e0e83b --- /dev/null +++ b/img/weather/22.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/23.svg b/img/weather/23.svg new file mode 100644 index 0000000..668d134 --- /dev/null +++ b/img/weather/23.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/24.svg b/img/weather/24.svg new file mode 100644 index 0000000..e4e74d4 --- /dev/null +++ b/img/weather/24.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/25.svg b/img/weather/25.svg new file mode 100644 index 0000000..dc4460b --- /dev/null +++ b/img/weather/25.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/26.svg b/img/weather/26.svg new file mode 100644 index 0000000..de52050 --- /dev/null +++ b/img/weather/26.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/27.svg b/img/weather/27.svg new file mode 100644 index 0000000..bf13a0c --- /dev/null +++ b/img/weather/27.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/weather/28.svg b/img/weather/28.svg new file mode 100644 index 0000000..16fe1da --- /dev/null +++ b/img/weather/28.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/weather/29.svg b/img/weather/29.svg new file mode 100644 index 0000000..7e9b3f3 --- /dev/null +++ b/img/weather/29.svg @@ -0,0 +1,3 @@ + + ? + diff --git a/img/weather/3.svg b/img/weather/3.svg new file mode 100644 index 0000000..f2ae20a --- /dev/null +++ b/img/weather/3.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/30.svg b/img/weather/30.svg new file mode 100644 index 0000000..45361d2 --- /dev/null +++ b/img/weather/30.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/31.svg b/img/weather/31.svg new file mode 100644 index 0000000..9f21685 --- /dev/null +++ b/img/weather/31.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/32.svg b/img/weather/32.svg new file mode 100644 index 0000000..b49b559 --- /dev/null +++ b/img/weather/32.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/img/weather/33.svg b/img/weather/33.svg new file mode 100644 index 0000000..f6e30f7 --- /dev/null +++ b/img/weather/33.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/img/weather/35.svg b/img/weather/35.svg new file mode 100644 index 0000000..147f930 --- /dev/null +++ b/img/weather/35.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/img/weather/3d.svg b/img/weather/3d.svg new file mode 100644 index 0000000..e948453 --- /dev/null +++ b/img/weather/3d.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/4.svg b/img/weather/4.svg new file mode 100644 index 0000000..39de26f --- /dev/null +++ b/img/weather/4.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/5.svg b/img/weather/5.svg new file mode 100644 index 0000000..69cd3a4 --- /dev/null +++ b/img/weather/5.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/img/weather/53.svg b/img/weather/53.svg new file mode 100644 index 0000000..10d191c --- /dev/null +++ b/img/weather/53.svg @@ -0,0 +1,3 @@ + + + diff --git a/img/weather/6.svg b/img/weather/6.svg new file mode 100644 index 0000000..ae12377 --- /dev/null +++ b/img/weather/6.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/img/weather/7.svg b/img/weather/7.svg new file mode 100644 index 0000000..b7b2459 --- /dev/null +++ b/img/weather/7.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/img/weather/8.svg b/img/weather/8.svg new file mode 100644 index 0000000..9dad947 --- /dev/null +++ b/img/weather/8.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/9.svg b/img/weather/9.svg new file mode 100644 index 0000000..5573adc --- /dev/null +++ b/img/weather/9.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/img/weather/900.svg b/img/weather/900.svg new file mode 100644 index 0000000..7071fff --- /dev/null +++ b/img/weather/900.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/img/weather/901.svg b/img/weather/901.svg new file mode 100644 index 0000000..4c15251 --- /dev/null +++ b/img/weather/901.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/img/weather/99.svg b/img/weather/99.svg new file mode 100644 index 0000000..344ee7b --- /dev/null +++ b/img/weather/99.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/img/weather/alerts/blue.png b/img/weather/alerts/blue.png new file mode 100644 index 0000000..f225f0b Binary files /dev/null and b/img/weather/alerts/blue.png differ diff --git a/img/weather/alerts/orange.png b/img/weather/alerts/orange.png new file mode 100644 index 0000000..9e2bae5 Binary files /dev/null and b/img/weather/alerts/orange.png differ diff --git a/img/weather/alerts/red.png b/img/weather/alerts/red.png new file mode 100644 index 0000000..2ad5cf8 Binary files /dev/null and b/img/weather/alerts/red.png differ diff --git a/img/weather/alerts/yellow.png b/img/weather/alerts/yellow.png new file mode 100644 index 0000000..c72da5c Binary files /dev/null and b/img/weather/alerts/yellow.png differ diff --git a/img/weather/bkg/day.png b/img/weather/bkg/day.png new file mode 100644 index 0000000..25b36c8 Binary files /dev/null and b/img/weather/bkg/day.png differ diff --git a/img/weather/bkg/night.png b/img/weather/bkg/night.png new file mode 100644 index 0000000..163836d Binary files /dev/null and b/img/weather/bkg/night.png differ diff --git a/img/weather/bkg/rain.png b/img/weather/bkg/rain.png new file mode 100644 index 0000000..fbe649f Binary files /dev/null and b/img/weather/bkg/rain.png differ diff --git a/list_.py b/list_.py new file mode 100644 index 0000000..7b7f871 --- /dev/null +++ b/list_.py @@ -0,0 +1,338 @@ +import json +import os +from copy import deepcopy +from shutil import copy + +from loguru import logger +from file import base_directory, config_center, save_data_to_json + +week = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] +week_type = ['单周', '双周'] +part_type = ['节点', '休息段'] +window_status = ['无', '置于顶部', '置于底部'] +color_mode = ['浅色', '深色', '跟随系统'] +hide_mode = ['无', '上课时自动隐藏', '窗口最大化时隐藏', '灵活隐藏'] +non_nt_hide_mode = ['无', '上课时自动隐藏'] +version_channel = ['正式版 (Release)', '测试版 (Beta)'] + +theme_folder = [] +theme_names = [] + +subject = { + '语文': '(255, 151, 135', # 红 + '数学': '(105, 84, 255', # 蓝 + '英语': '(236, 135, 255', # 粉 + '生物': '(68, 200, 94', # 绿 + '地理': '(80, 214, 200', # 浅蓝 + '政治': '(255, 110, 110', # 红 + '历史': '(180, 130, 85', # 棕 + '物理': '(130, 85, 180', # 紫 + '化学': '(84, 135, 190', # 蓝 + '美术': '(0, 186, 255', # 蓝 + '音乐': '(255, 101, 158', # 红 + '体育': '(255, 151, 135', # 红 + '信息技术': '(84, 135, 190', # 蓝 + '电脑': '(84, 135, 190', # 蓝 + '课程表未加载': '(255, 151, 135', # 红 + + '班会': '(255, 151, 135', # 红 + '自习': '(115, 255, 150', # 绿 + '课间': '(135, 255, 191', # 绿 + '大课间': '(255, 151, 135', # 红 + '放学': '(84, 255, 101', # 绿 + '暂无课程': '(84, 255, 101', # 绿 +} + +schedule_dir = os.path.join(base_directory, 'config', 'schedule') + +class_activity = ['课程', '课间'] +time = ['上午', '下午', '晚修'] +class_kind = [ + '自定义', + '语文', + '数学', + '英语', + '政治', + '历史', + '生物', + '地理', + '物理', + '化学', + '体育', + '班会', + '自习', + '早读', + '大课间', + '美术', + '音乐', + '心理', + '信息技术' +] + +default_widgets = [ + 'widget-time.ui', + 'widget-countdown.ui', + 'widget-current-activity.ui', + 'widget-next-activity.ui' +] + +widget_width = { # 默认宽度 + 'widget-time.ui': 210, + 'widget-countdown.ui': 200, + 'widget-current-activity.ui': 360, + 'widget-next-activity.ui': 290, + 'widget-countdown-day.ui': 200, + 'widget-weather.ui': 200 +} + +widget_conf = { + '当前日期': 'widget-time.ui', + '活动倒计时': 'widget-countdown.ui', + '当前活动': 'widget-current-activity.ui', + '更多活动': 'widget-next-activity.ui', + '倒计日': 'widget-countdown-day.ui', + '天气': 'widget-weather.ui' +} + +widget_name = { + 'widget-time.ui': '当前日期', + 'widget-countdown.ui': '活动倒计时', + 'widget-current-activity.ui': '当前活动', + 'widget-next-activity.ui': '更多活动', + 'widget-countdown-day.ui': '倒计日', + 'widget-weather.ui': '天气' +} + +native_widget_name = [widget_name[i] for i in widget_name] + +try: # 加载课程/主题配置文件 + subject_info = json.load(open(f'{base_directory}/config/data/subject.json', 'r', encoding='utf-8')) + subject_icon = subject_info['subject_icon'] + subject_abbreviation = subject_info['subject_abbreviation'] + theme_folder = [f for f in os.listdir(f'{base_directory}/ui/') + if os.path.isdir(os.path.join(f'{base_directory}/ui/', f))] +except Exception as e: + logger.error(f'加载课程/主题配置文件发生错误,使用默认配置:{e}') + config_center.write_conf('General', 'theme', 'default') + subject_icon = { + '语文': 'chinese', + '数学': 'math', + '英语': 'abc', + '生物': 'biology', + '地理': 'geography', + '政治': 'chinese', + '历史': 'history', + '物理': 'physics', + '化学': 'chemistry', + '美术': 'art', + '音乐': 'music', + '体育': 'pe', + '信息技术': 'it', + '电脑': 'it', + '课程表未加载': 'xmark', + + '班会': 'meeting', + '自习': 'self_study', + '课间': 'break', + '大课间': 'pe', + '放学': 'after_school', + '暂无课程': 'break', + } + # 简称 + subject_abbreviation = { + '历史': '史' + } + +not_exist_themes = [] + +countdown_modes = ['轮播', '多小组件'] + +for folder in theme_folder: + try: + json_file = json.load(open(f'{base_directory}/ui/{folder}/theme.json', 'r', encoding='utf-8')) + theme_names.append(json_file['name']) + except Exception as e: + logger.error(f'加载主题文件 theme.json {folder} 发生错误,跳过:{e}') + not_exist_themes.append(folder) + +for folder in not_exist_themes: + theme_folder.remove(folder) + + +def get_widget_list(): + rl = [] + for item, value in widget_conf.items(): + rl.append(item) + return rl + + +def get_widget_names(): + rl = [] + for item, value in widget_name.items(): + rl.append(value) + return rl + + +def get_current_theme_num(): + for i in range(len(theme_folder)): + if not os.path.exists(f'{base_directory}/config/schedule/{theme_folder[i]}.json'): + return "default" + if theme_folder[i] == config_center.read_conf('General', 'theme'): + return i + + +def get_theme_ui_path(name): + for i in range(len(theme_folder)): + if theme_names[i] == name: + return theme_folder[i] + return 'default' + + +def get_subject_abbreviation(key): + if key in subject_abbreviation: + return subject_abbreviation[key] + else: + return key[:1] + + +# 学科图标 +def get_subject_icon(key): + if key in subject_icon: + return f'{base_directory}/img/subject/{subject_icon[key]}.svg' + else: + return f'{base_directory}/img/subject/self_study.svg' + + +# 学科主题色 +def subject_color(key): + if key in subject: + return f'{subject[key]}' + else: + return '(75, 170, 255' + + +def get_schedule_config(): + schedule_config = [] + # 遍历目标目录下的所有文件 + for file_name in os.listdir(schedule_dir): + # 找json + if file_name.endswith('.json') and file_name != 'backup.json': + # 将文件路径添加到列表 + schedule_config.append(file_name) + schedule_config.append('添加新课表') + return schedule_config + + +def return_default_schedule_number(): + total = 0 + for file_name in os.listdir(schedule_dir): + # 找json + if file_name.startswith('新课表 - '): + total += 1 + return total + + +def create_new_profile(filename): + copy(f'{base_directory}/config/default.json', f'{base_directory}/config/schedule/{filename}') + + +def import_schedule(filepath, filename): # 导入课表 + try: + with open(filepath, 'r', encoding='utf-8') as file: + check_data = json.load(file) + except Exception as e: + logger.error(f"加载数据时出错: {e}") + return False + + checked_data = convert_schedule(check_data) + # 保存文件 + try: + print(check_data) + copy(filepath, f'{base_directory}/config/schedule/{filename}') + save_data_to_json(checked_data, filename) + config_center.write_conf('General', 'schedule', filename) + return True + except Exception as e: + logger.error(f"保存数据时出错: {e}") + return e + + +def convert_schedule(check_data): # 转换课表 + # 校验课程表 + if check_data is None: + logger.warning('此文件为空') + return False + elif not check_data.get('timeline') and not check_data.get('schedule'): + logger.warning('此文件不是课程表文件') + return False + # 转换为标准格式 + if not check_data.get('schedule_even'): + logger.warning('此课程表格式不支持单双周') + check_data['schedule_even'] = {str(i): [] for i in range(0, 6)} + + if len(check_data.get('part').get('0')) == 2: + logger.warning('此课程表格式不支持休息段') + for i in range(len(check_data.get('part'))): + check_data['part'][str(i)].append('节点') + + if not check_data.get('part') or not check_data.get('part_name'): # 兼容旧版本 + logger.warning('此课程表格式不支持节点') + try: + check_data['part'] = { # 转换旧版本时间线为新版 + "0": check_data['timeline']['start_time_m']['part'], "1": check_data['timeline']['start_time_a']['part'] + } + check_data['part_name'] = {"0": "上午", "1": "下午"} + del check_data['timeline']['start_time_m'] + del check_data['timeline']['start_time_a'] + old_timeline = deepcopy(check_data['timeline']) + # 转换为标准格式 + check_data['timeline']['default'] = {} + for i in range(0, 6): + check_data['timeline'][i] = {} + + for item_name, _ in old_timeline.items(): + if item_name[1] == 'a': + ma_to_num = 1 + else: + ma_to_num = 0 + new_name = item_name[0]+str(ma_to_num)+item_name[2] + check_data['timeline']['default'][new_name] = check_data['timeline'][item_name] + del check_data['timeline'][item_name] + except Exception as e: + logger.error(f"转换数据时出错: {e}") + return False + + return check_data + + +def export_schedule(filepath, filename): # 导出课表 + try: + copy(f'{base_directory}/config/schedule/{filename}', filepath) + return True + except Exception as e: + logger.error(f"导出文件时出错: {e}") + return e + + +def get_widget_config(): + try: + if os.path.exists(f'{base_directory}/config/widget.json'): + with open(f'{base_directory}/config/widget.json', 'r', encoding='utf-8') as file: + data = json.load(file) + else: + with open(f'{base_directory}/config/widget.json', 'w', encoding='utf-8') as file: + data = {'widgets': [ + 'widget-weather.ui', 'widget-countdown.ui', 'widget-current-activity.ui', 'widget-next-activity.ui' + ]} + json.dump(data, file, indent=4) + return data['widgets'] + except Exception as e: + logger.error(f'ReadWidgetConfigFAILD: {e}') + return default_widgets + + +if __name__ == '__main__': + print(theme_folder) + print(theme_names) + print('AL-1S') + print(get_widget_list()) diff --git a/main.py b/main.py new file mode 100644 index 0000000..4b60f5d --- /dev/null +++ b/main.py @@ -0,0 +1,2856 @@ +import ctypes +import datetime as dt +import json +import os +import platform +import re +import subprocess +import sys +import psutil +import signal +import traceback +from shutil import copy +from typing import Optional + +from PyQt5 import uic +from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QRect, QEasingCurve, QSize, QPoint, QUrl, QObject, QParallelAnimationGroup +from PyQt5.QtGui import QColor, QIcon, QPixmap, QPainter, QDesktopServices +from PyQt5.QtGui import QFontDatabase +from PyQt5.QtSvg import QSvgRenderer +from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QProgressBar, QGraphicsBlurEffect, QPushButton, \ + QGraphicsDropShadowEffect, QSystemTrayIcon, QFrame, QGraphicsOpacityEffect, QHBoxLayout +from loguru import logger +from packaging.version import Version +from qfluentwidgets import Theme, setTheme, setThemeColor, SystemTrayMenu, Action, FluentIcon as fIcon, isDarkTheme, \ + Dialog, ProgressRing, PlainTextEdit, ImageLabel, PushButton, InfoBarIcon, Flyout, FlyoutAnimationType, CheckBox, \ + PrimaryPushButton, IconWidget + +import conf +import list_ +import tip_toast +from tip_toast import active_windows +import utils +import weather_db as db +from conf import base_directory +from extra_menu import ExtraMenu, open_settings +from generate_speech import generate_speech_sync, list_pyttsx3_voices +from menu import open_plaza +from network_thread import check_update, weatherReportThread +from play_audio import play_audio +from plugin import p_loader +from utils import restart, stop, share, update_timer, DarkModeWatcher +from file import config_center, schedule_center + +if os.name == 'nt': + import pygetwindow + +# 适配高DPI缩放 +if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: + QApplication.setHighDpiScaleFactorRoundingPolicy( + Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) +else: + logger.warning('不兼容的系统,跳过高DPI标识') + +today = dt.date.today() + +# 存储窗口对象 +windows = [] +order = [] +error_dialog = None + +current_lesson_name = '课程表未加载' +current_state = 0 # 0:课间 1:上课 2: 休息段 +current_time = dt.datetime.now().strftime('%H:%M:%S') +current_week = dt.datetime.now().weekday() +current_lessons = {} +loaded_data = {} +parts_type = [] +notification = tip_toast +excluded_lessons = [] +last_notify_time = None +notify_cooldown = 2 # 2秒内仅能触发一次通知(防止触发114514个通知导致爆炸 + +timeline_data = {} +next_lessons = [] +parts_start_time = [] + +temperature = '未设置' +weather_icon = 0 +weather_name = '' +weather_data_temp = None +city = 101010100 # 默认城市 +theme = None + +time_offset = 0 # 时差偏移 +first_start = True +error_cooldown = dt.timedelta(seconds=2) # 冷却时间(s) +ignore_errors = [] +last_error_time = dt.datetime.now() - error_cooldown # 上一次错误 + +ex_menu = None +dark_mode_watcher = None +was_floating_mode = False # 浮窗状态 + +if config_center.read_conf('Other', 'do_not_log') != '1': + logger.add(f"{base_directory}/log/ClassWidgets_main_{{time}}.log", rotation="1 MB", encoding="utf-8", + retention="1 minute") + logger.info('未禁用日志输出') +else: + logger.info('已禁用日志输出功能,若需保存日志,请在“设置”->“高级选项”中关闭禁用日志功能') + + +def global_exceptHook(exc_type, exc_value, exc_tb): # 全局异常捕获 + if config_center.read_conf('Other', 'safe_mode') == '1': # 安全模式 + return + + error_details = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) # 异常详情 + if error_details in ignore_errors: # 忽略重复错误 + return + + global last_error_time, error_dialog, error_cooldown + + current_time = dt.datetime.now() + if current_time - last_error_time > error_cooldown: # 冷却时间 + last_error_time = current_time + logger.error(f"全局异常捕获:{exc_type} {exc_value} {exc_tb}") + logger.error(f"详细堆栈信息:\n{error_details}") + if not error_dialog: + w = ErrorDialog(error_details) + w.exec() + else: + # 忽略冷却时间 + pass + + +sys.excepthook = global_exceptHook # 设置全局异常捕获 + +def handle_dark_mode_change(is_dark): + """处理DarkModeWatcher触发的UI更新""" + if config_center.read_conf('General', 'color_mode') == '2': + logger.info(f"系统颜色模式更新: {'深色' if is_dark else '浅色'}") + current_theme = Theme.DARK if is_dark else Theme.LIGHT + setTheme(current_theme) + if mgr: + mgr.clear_widgets() + else: + logger.warning("主题更改时,mgr还未初始化") + # if current_state == 1: + # setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") + # else: + # setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") + + +def setTheme_(): # 设置主题 + global theme + color_mode = config_center.read_conf('General', 'color_mode') + if color_mode == '2': # 自动 + logger.info(f'颜色模式: 自动({color_mode})') + if platform.system() == 'Darwin' and Version(platform.mac_ver()[0]) < Version('10.14'): + return + if platform.system() == 'Windows': + # Windows 7特殊处理 + if sys.getwindowsversion().major == 6 and sys.getwindowsversion().minor == 1: + setTheme(Theme.LIGHT) + return + # 检查Windows版本是否支持深色模式(Windows 10 build 14393及以上) + try: + win_build = sys.getwindowsversion().build + if win_build < 14393: # 不支持深色模式的最低版本 + return + except AttributeError: + # 无法获取版本信息,保守返回 + return + if platform.system() == 'Linux': + return + if dark_mode_watcher: + is_dark = dark_mode_watcher.isDark() + if is_dark is not None: + logger.info(f"当前颜色模式: {'深色' if is_dark else '浅色'}") + setTheme(Theme.DARK if is_dark else Theme.LIGHT) + else: + logger.warning("无法获取系统颜色模式,暂时使用浅色主题") + setTheme(Theme.LIGHT) + else: + logger.warning("DarkModeWatcher 未被初始化,使用浅色主题") + setTheme(Theme.LIGHT) + elif color_mode == '1': + logger.info(f'颜色模式: 深色({color_mode})') + setTheme(Theme.DARK) + else: + logger.info(f'颜色模式: 浅色({color_mode})') + setTheme(Theme.LIGHT) + + +def get_timeline_data(): + if len(loaded_data['timeline']) == 1: + return loaded_data['timeline']['default'] + else: + if str(current_week) in loaded_data['timeline'] and loaded_data['timeline'][str(current_week)]: # 如果此周有时间线 + return loaded_data['timeline'][str(current_week)] + else: + return loaded_data['timeline']['default'] + + +# 获取Part开始时间 +def get_start_time(): + global parts_start_time, timeline_data, loaded_data, order, parts_type + loaded_data = schedule_center.schedule_data + timeline = get_timeline_data() + part = loaded_data['part'] + parts_start_time = [] + timeline_data = {} + order = [] + + for item_name, item_value in part.items(): + try: + h, m = item_value[:2] + try: + part_type = item_value[2] + except IndexError: + part_type = 'part' + except Exception as e: + logger.error(f'加载课程表文件[节点类型]出错:{e}') + part_type = 'part' + + # 应用时差偏移到课程表时间 + start_time = dt.datetime.combine(today, dt.time(h, m)) + dt.timedelta(seconds=time_offset) + parts_start_time.append(start_time) + order.append(item_name) + parts_type.append(part_type) + except Exception as e: + logger.error(f'加载课程表文件[起始时间]出错:{e}') + + paired = zip(parts_start_time, order) + paired_sorted = sorted(paired, key=lambda x: x[0]) # 按时间大小排序 + if paired_sorted: + parts_start_time, order = zip(*paired_sorted) + + def sort_timeline_key(item): + item_name = item[0] + prefix = item_name[0] + if len(item_name) > 1: + try: + # 提取节点序数 + part_num = int(item_name[1]) + # 提取课程序数 + class_num = 0 + if len(item_name) > 2: + class_num = int(item_name[2:]) + if prefix == 'a': + return part_num, class_num, 0 + else: + return part_num, class_num, 1 + except ValueError: + # 如果转换失败,返回原始字符串 + return item_name + return item_name + + # 对timeline排序后添加到timeline_data + sorted_timeline = sorted(timeline.items(), key=sort_timeline_key) + for item_name, item_time in sorted_timeline: + try: + timeline_data[item_name] = item_time + except Exception as e: + logger.error(f'加载课程表文件[课程数据]出错:{e}') + + +def get_part(): + if not parts_start_time: + return None + + def return_data(): + c_time = parts_start_time[i] + return c_time, int(order[i]) # 返回开始时间、Part序号 + + current_dt = dt.datetime.now() # 当前时间 + + for i in range(len(parts_start_time)): # 遍历每个Part + time_len = dt.timedelta(minutes=0) # Part长度 + + for item_name, item_time in timeline_data.items(): + if item_name.startswith(f'a{str(order[i])}') or item_name.startswith(f'f{str(order[i])}'): + time_len += dt.timedelta(minutes=int(item_time)) # 累计Part的时间点总长度 + time_len += dt.timedelta(seconds=1) + + if time_len != dt.timedelta(seconds=1): # 有课程 + if i == len(parts_start_time) - 1: # 最后一个Part + return return_data() + else: + if current_dt <= parts_start_time[i] + time_len: + return return_data() + + return parts_start_time[0] + dt.timedelta(seconds=time_offset), 0, 'part' + +def get_excluded_lessons(): + global excluded_lessons + if config_center.read_conf('General', 'excluded_lesson') == "0": + excluded_lessons = [] + return + excluded_lessons_raw = config_center.read_conf('General', 'excluded_lessons') + excluded_lessons = excluded_lessons_raw.split(',') if excluded_lessons_raw != '' else [] + +# 获取当前活动 +def get_current_lessons(): # 获取当前课程 + global current_lessons + timeline = get_timeline_data() + if config_center.read_conf('General', 'enable_alt_schedule') == '1' or conf.is_temp_week(): + try: + if conf.get_week_type(): + schedule = loaded_data.get('schedule_even') + else: + schedule = loaded_data.get('schedule') + except Exception as e: + logger.error(f'加载课程表文件[单双周]出错:{e}') + schedule = loaded_data.get('schedule') + else: + schedule = loaded_data.get('schedule') + class_count = 0 + for item_name, _ in timeline.items(): + if item_name.startswith('a'): + if schedule[str(current_week)]: + try: + if schedule[str(current_week)][class_count] != '未添加': + current_lessons[item_name] = schedule[str(current_week)][class_count] + else: + current_lessons[item_name] = '暂无课程' + except IndexError: + current_lessons[item_name] = '暂无课程' + except Exception as e: + current_lessons[item_name] = '暂无课程' + logger.debug(f'加载课程表文件出错:{e}') + class_count += 1 + else: + current_lessons[item_name] = '暂无课程' + class_count += 1 + + +# 获取倒计时、弹窗提示 +def get_countdown(toast=False): # 重构好累aaaa + global last_notify_time + current_dt = dt.datetime.now() + if last_notify_time and (current_dt - last_notify_time).seconds < notify_cooldown: + return + def after_school(): # 放学 + if parts_type[part] == 'break': # 休息段 + notification.push_notification(0, current_lesson_name) # 下课 + else: + if config_center.read_conf('Toast', 'after_school') == '1': + notification.push_notification(2) # 放学 + + current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 + return_text = [] + got_return_data = False + + if parts_start_time: + c_time, part = get_part() + + if current_dt >= c_time: + for item_name, item_time in timeline_data.items(): + if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): + # 判断时间是否上下课,发送通知 + if current_dt == c_time and toast: + if item_name.startswith('a'): + notification.push_notification(1, current_lesson_name) # 上课 + last_notify_time = current_dt + else: + if next_lessons: # 下课/放学 + notification.push_notification(0, next_lessons[0]) # 下课 + last_notify_time = current_dt + else: + after_school() + + if current_dt == c_time - dt.timedelta( + minutes=int(config_center.read_conf('Toast', 'prepare_minutes'))): + if config_center.read_conf('Toast', + 'prepare_minutes') != '0' and toast and item_name.startswith('a'): + if not current_state: # 课间 + notification.push_notification(3, next_lessons[0]) # 准备上课(预备铃) + last_notify_time = current_dt + + # 放学 + if (c_time + dt.timedelta(minutes=int(item_time)) == current_dt and not next_lessons and + not current_state and toast): + after_school() + last_notify_time = current_dt + + add_time = int(item_time) + c_time += dt.timedelta(minutes=add_time) + + if got_return_data: + break + + if c_time >= current_dt: + # 根据所在时间段使用不同标语 + if item_name.startswith('a'): + return_text.append('当前活动结束还有') + else: + return_text.append('课间时长还有') + # 返回倒计时、进度条 + time_diff = c_time - current_dt + minute, sec = divmod(time_diff.seconds, 60) + return_text.append(f'{minute:02d}:{sec:02d}') + # 进度条 + seconds = time_diff.seconds + return_text.append(int(100 - seconds / (int(item_time) * 60) * 100)) + got_return_data = True + if not return_text: + return_text = ['目前课程已结束', f'00:00', 100] + else: + prepare_minutes_str = config_center.read_conf('Toast', 'prepare_minutes') + if prepare_minutes_str != '0' and toast: + prepare_minutes = int(prepare_minutes_str) + if current_dt == c_time - dt.timedelta(minutes=prepare_minutes): + next_lesson_name = None + next_lesson_key = None + if timeline_data: + for key in sorted(timeline_data.keys()): + if key.startswith(f'a{str(part)}'): + next_lesson_key = key + break + if next_lesson_key and next_lesson_key in current_lessons: + lesson_name = current_lessons[next_lesson_key] + if lesson_name != '暂无课程': + next_lesson_name = lesson_name + if current_state == 0: + now = dt.datetime.now() + if not last_notify_time or (now - last_notify_time).seconds >= notify_cooldown: + if next_lesson_name != None: + notification.push_notification(3, next_lesson_name) + if f'a{part}1' in timeline_data: + time_diff = c_time - current_dt + minute, sec = divmod(time_diff.seconds, 60) + return_text = ['距离上课还有', f'{minute:02d}:{sec:02d}', 100] + else: + return_text = ['目前课程已结束', f'00:00', 100] + return return_text + + +# 获取将发生的活动 +def get_next_lessons(): + global current_lesson_name + global next_lessons + next_lessons = [] + part = 0 + current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 + + if parts_start_time: + c_time, part = get_part() + + def before_class(): + if part == 0 or part == 3: + return True + else: + if current_dt >= parts_start_time[part] - dt.timedelta(minutes=60): + return True + else: + return False + + if before_class(): + for item_name, item_time in timeline_data.items(): + if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): + add_time = int(item_time) + if c_time > current_dt and item_name.startswith('a'): + next_lessons.append(current_lessons[item_name]) + c_time += dt.timedelta(minutes=add_time) + + +def get_next_lessons_text(): + if not next_lessons: + cache_text = '当前暂无课程' + else: + cache_text = '' + if len(next_lessons) >= 5: + range_time = 5 + else: + range_time = len(next_lessons) + for i in range(range_time): + if range_time > 2: + if next_lessons[i] != '暂无课程': + cache_text += f'{list_.get_subject_abbreviation(next_lessons[i])} ' # 获取课程简称 + else: + cache_text += f'无 ' + else: + if next_lessons[i] != '暂无课程': + cache_text += f'{next_lessons[i]} ' + else: + cache_text += f'暂无 ' + return cache_text + + +# 获取当前活动 +def get_current_lesson_name(): + global current_lesson_name, current_state + current_dt = dt.datetime.combine(today, dt.datetime.strptime(current_time, '%H:%M:%S').time()) # 当前时间 + current_lesson_name = '暂无课程' + current_state = 0 + + if parts_start_time: + c_time, part = get_part() + + if current_dt >= c_time: + if parts_type[part] == 'break': # 休息段 + current_lesson_name = loaded_data['part_name'][str(part)] + current_state = 2 + + for item_name, item_time in timeline_data.items(): + if item_name.startswith(f'a{str(part)}') or item_name.startswith(f'f{str(part)}'): + add_time = int(item_time) + c_time += dt.timedelta(minutes=add_time) + if c_time > current_dt: + if item_name.startswith('a'): + current_lesson_name = current_lessons[item_name] + current_state = 1 + else: + current_lesson_name = '课间' + current_state = 0 + return + +def get_hide_status(): + # 1 -> hide, 0 -> show + # 满分啦( + # 祝所有用 Class Widgets 的、不用 Class Widgets 的学子体测满分啊(( + global current_state, current_lesson_name, excluded_lessons + return 1 if { + '0': lambda: 0, + '1': lambda: current_state, + '2': lambda: check_windows_maximize() or check_fullscreen(), + '3': lambda: current_state + }[config_center.read_conf('General', 'hide')]() and not (current_lesson_name in excluded_lessons) else 0 + + +# 定义 RECT 结构体 +class RECT(ctypes.Structure): + _fields_ = [("left", ctypes.c_long), + ("top", ctypes.c_long), + ("right", ctypes.c_long), + ("bottom", ctypes.c_long)] + +def get_process_name(pid): # 获取进程名称 + try: + if isinstance(pid, int): + pid = ctypes.windll.user32.GetWindowThreadProcessId(pid, None) + return psutil.Process(pid).name().lower() + except (psutil.NoSuchProcess, AttributeError, ValueError): + return "unknown" + +def check_fullscreen(): # 检查是否全屏 + if os.name != 'nt': + return False + user32 = ctypes.windll.user32 + hwnd = user32.GetForegroundWindow() + if not hwnd: + return False + if hwnd == user32.GetDesktopWindow(): + return False + if hwnd == user32.GetShellWindow(): + return False + pid = ctypes.c_ulong() + user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) + process_name = get_process_name(pid.value) + current_pid = os.getpid() + # logger.debug(f"前景窗口句柄: {hwnd}, PID: {pid.value}, 进程名: {process_name}") + if pid.value == current_pid: + return False + # 排除特定系统进程 + excluded_system_processes = { + 'explorer.exe', # 文件资源管理器/桌面 + 'shellexperiencehost.exe', # Shell体验主机 (开始菜单、操作中心) + 'searchui.exe', # Cortana/搜索界面 + 'applicationframehost.exe', # UWP应用框架 + 'systemsettings.exe', # 设置 + 'taskmgr.exe' # 任务管理器 + } + if process_name in excluded_system_processes: + # logger.debug(f"前景窗口进程 '{process_name}' 在排除列表 (系统进程), 排除.") + return False + title_buffer = ctypes.create_unicode_buffer(256) + user32.GetWindowTextW(hwnd, title_buffer, 256) + window_title_lower = title_buffer.value.strip().lower() + # logger.debug(f"前景窗口标题: '{title_buffer.value}' (小写: '{window_title_lower}')") + # 排除特定窗口标题 + excluded_system_window_titles = { + "program manager", # 桌面窗口 + "windows input experience", # 输入法相关 + "msctfmonitor window", # 输入法相关 + "startmenuexperiencehost" # 开始菜单 + } + if window_title_lower in excluded_system_window_titles: + # logger.debug(f"前景窗口标题 '{window_title_lower}' 在排除列表 (系统窗口), 排除.") + return False + rect = RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rect)) + # 使用桌面窗口作为屏幕尺寸参考 + screen_rect_desktop = RECT() + user32.GetWindowRect(user32.GetDesktopWindow(), ctypes.byref(screen_rect_desktop)) + # logger.debug(f"窗口矩形: 左={rect.left}, 上={rect.top}, 右={rect.right}, 下={rect.bottom}") + # logger.debug(f"桌面矩形: 左={screen_rect_desktop.left}, 上={screen_rect_desktop.top}, 右={screen_rect_desktop.right}, 下={screen_rect_desktop.bottom}") + is_covering_screen = ( + rect.left <= screen_rect_desktop.left and + rect.top <= screen_rect_desktop.top and + rect.right >= screen_rect_desktop.right and + rect.bottom >= screen_rect_desktop.bottom + ) + if is_covering_screen: + screen_area = (screen_rect_desktop.right - screen_rect_desktop.left) * (screen_rect_desktop.bottom - screen_rect_desktop.top) + window_area = (rect.right - rect.left) * (rect.bottom - rect.top) + is_fullscreen = window_area >= screen_area * 0.95 + # logger.debug(f"覆盖屏幕: {is_covering_screen}, 窗口面积: {window_area}, 屏幕面积: {screen_area}, 是否全屏判断: {is_fullscreen}") + return is_fullscreen + return False + + +class ErrorDialog(Dialog): # 重大错误提示框 + def __init__(self, error_details='Traceback (most recent call last):', parent=None): + # KeyboardInterrupt 直接 exit + if error_details.endswith('KeyboardInterrupt') or error_details.endswith('KeyboardInterrupt\n'): + stop() + + super().__init__( + 'Class Widgets 崩溃报告', + '抱歉!Class Widgets 发生了严重的错误从而无法正常运行。您可以保存下方的错误信息并向他人求助。' + '若您认为这是程序的Bug,请点击“报告此问题”或联系开发者。', + parent + ) + global error_dialog + error_dialog = True + + self.is_dragging = False + self.drag_position = QPoint() + self.title_bar_height = 30 + + self.title_layout = QHBoxLayout() + + self.iconLabel = ImageLabel() + self.iconLabel.setImage(f"{base_directory}/img/logo/favicon-error.ico") + self.error_log = PlainTextEdit() + self.report_problem = PushButton(fIcon.FEEDBACK, '报告此问题') + self.copy_log_btn = PushButton(fIcon.COPY, '复制日志') + self.ignore_error_btn = PushButton(fIcon.INFO, '忽略错误') + self.ignore_same_error = CheckBox() + self.ignore_same_error.setText('在下次启动之前,忽略此错误') + self.restart_btn = PrimaryPushButton(fIcon.SYNC, '重新启动') + + self.iconLabel.setScaledContents(True) + self.iconLabel.setFixedSize(50, 50) + self.titleLabel.setText('出错啦!ヽ(*。>Д<)o゜') + self.titleLabel.setStyleSheet("font-family: Microsoft YaHei UI; font-size: 25px; font-weight: 500;") + self.error_log.setReadOnly(True) + self.error_log.setPlainText(error_details) + self.error_log.setFixedHeight(200) + self.restart_btn.setFixedWidth(150) + self.yesButton.hide() + self.cancelButton.hide() # 隐藏取消按钮 + self.title_layout.setSpacing(12) + + # 按钮事件 + self.report_problem.clicked.connect( + lambda: QDesktopServices.openUrl(QUrl( + 'https://github.com/Class-Widgets/Class-Widgets/issues/' + 'new?assignees=&labels=Bug&projects=&template=BugReport.yml&title=[Bug]:')) + ) + self.copy_log_btn.clicked.connect(self.copy_log) + self.ignore_error_btn.clicked.connect(self.ignore_error) + self.restart_btn.clicked.connect(restart) + + self.title_layout.addWidget(self.iconLabel) # 标题布局 + self.title_layout.addWidget(self.titleLabel) + self.textLayout.insertLayout(0, self.title_layout) # 页面 + self.textLayout.addWidget(self.error_log) + self.textLayout.addWidget(self.ignore_same_error) + self.buttonLayout.insertStretch(0, 1) # 按钮布局 + self.buttonLayout.insertWidget(0, self.copy_log_btn) + self.buttonLayout.insertWidget(1, self.report_problem) + self.buttonLayout.insertStretch(1) + self.buttonLayout.insertWidget(4, self.ignore_error_btn) + self.buttonLayout.insertWidget(5, self.restart_btn) + + def copy_log(self): # 复制日志 + QApplication.clipboard().setText(self.error_log.toPlainText()) + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='复制成功!ヾ(^▽^*)))', + content="日志已成功复制到剪贴板。", + target=self.copy_log_btn, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + def ignore_error(self): + global ignore_errors + if self.ignore_same_error.isChecked(): + ignore_errors.append(self.error_log.toPlainText()) + self.close() + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and event.y() <= self.title_bar_height: + self.is_dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + + def mouseMoveEvent(self, event): + if self.is_dragging: + self.move(event.globalPos() - self.drag_position) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + self.is_dragging = False + + + +class PluginManager: # 插件管理器 + def __init__(self): + self.cw_contexts = {} + self.get_app_contexts() + self.temp_window = [] + self.method = PluginMethod(self.cw_contexts) + + def get_app_contexts(self, path=None): + self.cw_contexts = { + "Widgets_Width": list_.widget_width, + "Widgets_Name": list_.widget_name, + "Widgets_Code": list_.widget_conf, # 小组件列表 + + "Current_Lesson": current_lesson_name, # 当前课程名 + "State": current_state, # 0:课间 1:上课(上下课状态) + "Current_Part": get_part(), # 返回开始时间、Part序号 + "Next_Lessons_text": get_next_lessons_text(), # 下节课程 + "Next_Lessons": next_lessons, # 下节课程 + "Current_Lessons": current_lessons, # 当前课程 + "Current_Week": current_week, # 当前周次 + "Excluded_Lessons": excluded_lessons, # 排除的课程 + + "Current_Time": current_time, # 当前时间 + "Timeline_Data": timeline_data, # 时间线数据 + "Parts_Start_Time": parts_start_time, # 节点开始时间 + "Parts_Type": parts_type, # 节点类型 + "Time_Offset": time_offset, # 时差偏移 + + "Schedule_Name": config_center.schedule_name, # 课程表名称 + "Loaded_Data": loaded_data, # 加载的课程表数据 + "Order": order, # 课程顺序 + + "Weather": weather_name, # 天气情况 + "Temp": temperature, # 温度 + "Weather_Data": weather_data_temp, # 天气数据 + "Weather_Icon": weather_icon, # 天气图标 + "Weather_API": config_center.read_conf('Weather', 'api'), # 天气API + "City": city, # 城市代码 + + "Notification": notification.notification_contents, # 检测到的通知内容 + "Last_Notify_Time": last_notify_time, # 上次通知时间 + + "PLUGIN_PATH": os.path.normpath(os.path.join(conf.PLUGINS_DIR, path)) if path else conf.PLUGINS_DIR, # 传递插件目录 + "Config_Center": config_center, # 配置中心实例 + "Schedule_Center": schedule_center, # 课程表中心实例 + "Base_Directory": base_directory, # 资源目录 + "Widgets_Mgr": mgr, # 组件管理器实例 + "Theme": theme, # 当前主题 + } + return self.cw_contexts + + +class PluginMethod: # 插件方法 + def __init__(self, app_context): + self.app_contexts = app_context + + def register_widget(self, widget_code, widget_name, widget_width): # 注册小组件 + self.app_contexts['Widgets_Width'][widget_code] = widget_width + self.app_contexts['Widgets_Name'][widget_code] = widget_name + self.app_contexts['Widgets_Code'][widget_name] = widget_code + + def adjust_widget_width(self, widget_code, width): # 调整小组件宽度 + self.app_contexts['Widgets_Width'][widget_code] = width + + @staticmethod + def get_widget(widget_code): # 获取小组件实例 + for widget in mgr.widgets: + if widget.path == widget_code: + return widget + return None + + @staticmethod + def change_widget_content(widget_code, title, content): # 修改小组件内容 + for widget in mgr.widgets: + if widget.path == widget_code: + widget.update_widget_for_plugin([title, content]) + + @staticmethod + def is_get_notification(): # 检查是否有通知 + if notification.pushed_notification: + return True + else: + return False + + @staticmethod + def send_notification(state=1, lesson_name='示例课程', title='通知示例', subtitle='副标题', + content='这是一条通知示例', icon=None, duration=2000): # 发送通知 + notification.main(state, lesson_name, title, subtitle, content, icon, duration) + + @staticmethod + def subprocess_exec(title, action): # 执行系统命令 + w = openProgressDialog(title, action) + p_mgr.temp_window = [w] + w.show() + + @staticmethod + def read_config(path, section, option): # 读取配置文件 + try: + with open(path, 'r', encoding='utf-8') as r: + config = json.load(r) + return config.get(section, option) + except Exception as e: + logger.error(f"插件读取配置文件失败:{e}") + + @staticmethod + def generate_speech( + text: str, + engine: str = "edge", + voice: Optional[str] = None, + timeout: float = 10.0, + auto_fallback: bool = True + + ) -> str: + """ + 同步生成语音文件(供插件调用) + + 参数: + text (str): 要转换的文本(支持中英文混合) + engine (str): 首选的TTS引擎(默认edge) + voice (str): 指定语音ID(可选,默认自动选择) + timeout (float): 超时时间(秒,默认10) + auto_fallback (bool): 是否自动回退引擎(默认True) + + 返回: + str: 生成的音频文件路径 + """ + return generate_speech_sync( + text=text, + engine=engine, + voice=voice, + auto_fallback=auto_fallback, + timeout=timeout + ) + + @staticmethod + def play_audio(file_path: str, tts_delete_after: bool = True): + """ + 播放音频文件 + + 参数: + file_path (str): 要播放的音频文件路径 + tts_delete_after (bool): 播放后是否删除文件(默认True) + + 说明: + - 删除操作有重试机制(3次尝试) + """ + play_audio(file_path, tts_delete_after) + + +class WidgetsManager: + def __init__(self): + self.widgets = [] # 小组件实例 + self.widgets_list = [] # 小组件列表配置 + self.state = 1 + + self.widgets_width = 0 # 小组件总宽度 + self.spacing = 0 # 小组件间隔 + + self.start_pos_x = 0 # 小组件起始位置 + self.start_pos_y = 0 + + self.hide_status = None # [0] -> 在 current_state 设置的灵活隐藏, [1] -> 隐藏模式 + + def sync_widget_animation(self, target_pos): + for widget in self.widgets: + if widget.path == 'widget-current-activity.ui': + widget.animate_expand(target_pos) # 主组件形变动画 + + def init_widgets(self): # 初始化小组件 + self.widgets_list = list_.get_widget_config() + self.check_widgets_exist() + self.spacing = conf.load_theme_config(theme)['spacing'] + + self.get_start_pos() + cnt_all = {} + + # 添加小组件实例 + for w in range(len(self.widgets_list)): + cnt_all[self.widgets_list[w]] = cnt_all.get(self.widgets_list[w], -1) + 1 + widget = DesktopWidget(self, self.widgets_list[w], True if w == 0 else False,cnt = cnt_all[self.widgets_list[w]], position=self.get_widget_pos("", w), widget_cnt = w) + self.widgets.append(widget) + + self.create_widgets() + + def close_all_widgets(self): + # 统一关闭所有组件 + if hasattr(self, '_closing'): + return + self._closing = True + for widget in self.widgets: + widget.close() # 触发各个widget的closeEvent + + def check_widgets_exist(self): + for widget in self.widgets_list: + if widget not in list_.widget_width.keys(): + self.widgets_list.remove(widget) + + @staticmethod + def get_widget_width(path): + try: + width = conf.load_theme_width(theme)[path] + except KeyError: + width = list_.widget_width[path] + return int(width) + + @staticmethod + def get_widgets_height(): + return int(conf.load_theme_config(theme)['height']) + + def create_widgets(self): + for widget in self.widgets: + widget.show() + logger.info(f'显示小组件:{widget.path, widget.windowTitle()}') + + def adjust_ui(self): # 更新小组件UI + for widget in self.widgets: + # 调整窗口尺寸 + width = self.get_widget_width(widget.path) + height = self.get_widgets_height() + pos_x = self.get_widget_pos(widget.path, widget.widget_cnt)[0] + op = int(config_center.read_conf('General', 'opacity')) / 100 + + if widget.animation is None: + widget.widget_transition(pos_x, width, height, op) + + def get_widget_pos(self, path, cnt=None): # 获取小组件位置 + num = self.widgets_list.index(path) if cnt is None else cnt + self.get_start_pos() + pos_x = self.start_pos_x + self.spacing * num + for i in range(num): + try: + pos_x += conf.load_theme_width(theme)[self.widgets_list[i]] + except KeyError: + pos_x += list_.widget_width[self.widgets_list[i]] + except: + pos_x += 0 + return [int(pos_x), int(self.start_pos_y)] + + def get_start_pos(self): + self.calculate_widgets_width() + screen_geometry = app.primaryScreen().availableGeometry() + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + margin = max(0, int(config_center.read_conf('General', 'margin'))) + self.start_pos_y = margin + self.start_pos_x = (screen_width - self.widgets_width) // 2 + + def calculate_widgets_width(self): # 计算小组件占用宽度 + self.widgets_width = 0 + # 累加小组件宽度 + for widget in self.widgets_list: + try: + self.widgets_width += self.get_widget_width(widget) + except Exception as e: + logger.warning(f'计算小组件宽度发生错误:{e}') + self.widgets_width += 0 + + self.widgets_width += self.spacing * (len(self.widgets_list) - 1) + + def hide_windows(self): + self.state = 0 + for widget in self.widgets: + widget.animate_hide() + + def full_hide_windows(self): + self.state = 0 + for widget in self.widgets: + widget.animate_hide(True) + + def show_windows(self): + if fw.animating: # 避免动画Bug + return + if fw.isVisible(): + fw.close() + self.state = 1 + for widget in self.widgets: + widget.animate_show() + + def clear_widgets(self): + global fw, was_floating_mode + if fw and fw.isVisible(): + fw.close() + was_floating_mode = True + else: + was_floating_mode = False + for widget in self.widgets: + widget.animate_hide_opacity() + for widget in self.widgets: + self.widgets.remove(widget) + init() + + def update_widgets(self): + c = 0 + self.adjust_ui() + + for widget in self.widgets: + if c == 0: + get_countdown(True) + widget.update_data(path=widget.path) + c += 1 + p_loader.update_plugins() + + if notification.pushed_notification: + notification.pushed_notification = False + + def decide_to_hide(self): + if config_center.read_conf('General', 'hide_method') == '0': # 正常 + self.hide_windows() + elif config_center.read_conf('General', 'hide_method') == '1': # 单击即完全隐藏 + self.full_hide_windows() + elif config_center.read_conf('General', 'hide_method') == '2': # 最小化为浮窗 + if not fw.animating: + self.full_hide_windows() + fw.show() + else: + self.hide_windows() + + def cleanup_resources(self): + self.hide_status = None # 重置hide_status + widgets_to_clean = list(self.widgets) + self.widgets.clear() + for widget in widgets_to_clean: + widget_path = getattr(widget, 'path', '未知组件') + try: + if hasattr(widget, 'weather_timer') and widget.weather_timer: + try: + widget.weather_timer.stop() + except RuntimeError: + pass + if hasattr(widget, 'weather_thread') and widget.weather_thread: + try: + if widget.weather_thread.isRunning(): + widget.weather_thread.quit() + if not widget.weather_thread.wait(500): + logger.warning(f"组件 {widget_path} 的天气线程未正常退出,强制终止") + widget.weather_thread.terminate() + widget.weather_thread.wait() + except RuntimeError: + pass + widget.deleteLater() + except Exception as ex: + logger.error(f"清理组件 {widget_path} 时发生异常: {ex}") + + def stop(self): + if mgr: + mgr.cleanup_resources() + for widget in self.widgets: + widget.stop() + if self.animation: + self.animation.stop() + if self.opacity_animation: + self.opacity_animation.stop() + self.close() + +class openProgressDialog(QWidget): + def __init__(self, action_title='打开 记事本', action='notepad'): + super().__init__() + self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) + time = int(config_center.read_conf('Plugin', 'auto_delay')) + self.action = action + + screen_geometry = app.primaryScreen().availableGeometry() + self.screen_width = screen_geometry.width() + self.screen_height = screen_geometry.height() + self.init_ui() + self.init_font() + self.move((self.screen_width - self.width()) // 2, self.screen_height - self.height() - 100) + + self.action_name = self.findChild(QLabel, 'action_name') + self.action_name.setText(action_title) + + self.opening_countdown = self.findChild(ProgressRing, 'opening_countdown') + self.opening_countdown.setRange(0, time - 1) + self.progress_timer = QTimer(self) + self.progress_timer.timeout.connect(self.update_progress) + self.progress_timer.start(1000) + + self.timer = QTimer(self) + self.timer.timeout.connect(self.execute_action) + self.timer.start(time * 1000) + + self.cancel_opening = self.findChild(QPushButton, 'cancel_opening') + self.cancel_opening.clicked.connect(self.cancel_action) + + self.intro_animation() + + def update_progress(self): + self.opening_countdown.setValue(self.opening_countdown.value() + 1) + + def execute_action(self): + self.timer.stop() + subprocess.Popen(self.action) + self.close() + + def cancel_action(self): + self.timer.stop() + self.close() + + def save_position(self): + pass + + def init_ui(self): + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + if isDarkTheme(): + uic.loadUi(f'{base_directory}/ui/default/dark/toast-open_dialog.ui', self) + else: + uic.loadUi(f'{base_directory}/ui/default/toast-open_dialog.ui', self) + + backgnd = self.findChild(QFrame, 'backgnd') + shadow_effect = QGraphicsDropShadowEffect(self) + shadow_effect.setBlurRadius(28) + shadow_effect.setXOffset(0) + shadow_effect.setYOffset(6) + shadow_effect.setColor(QColor(0, 0, 0, 80)) + backgnd.setGraphicsEffect(shadow_effect) + + def init_font(self): + font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + font_family = QFontDatabase.applicationFontFamilies(font_id)[0] + + self.setStyleSheet(f""" + QLabel, ProgressRing, PushButton{{ + font-family: "{font_family}"; + font-weight: bold + }} + """) + + def intro_animation(self): # 弹出动画 + self.setMinimumWidth(300) + label_width = self.action_name.sizeHint().width() - 120 + self.animation = QPropertyAnimation(self, b'windowOpacity') + self.animation.setDuration(400) + self.animation.setStartValue(0) + self.animation.setEndValue(1) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) + + self.animation_rect = QPropertyAnimation(self, b'geometry') + self.animation_rect.setDuration(450) + self.animation_rect.setStartValue( + QRect(self.x(), self.screen_height, self.width(), self.height()) + ) + self.animation_rect.setEndValue( + QRect((self.screen_width - (self.width() + label_width)) // 2, + self.screen_height - 250, + self.width() + label_width, + self.height()) + ) + self.animation_rect.setEasingCurve(QEasingCurve.Type.InOutCirc) + + self.animation.start() + self.animation_rect.start() + + def closeEvent(self, event): + event.ignore() + self.setMinimumWidth(0) + self.position = self.pos() + # 关闭时保存一次位置 + self.save_position() + self.deleteLater() + self.hide() + p_mgr.temp_window.clear() + + +class FloatingWidget(QWidget): # 浮窗 + def __init__(self): + super().__init__() + self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) + self.animation_rect = None + self.animation = None + self.m_Position = None + self.p_Position = None + self.m_flag = None + self.r_Position = None + self._is_topmost_callback_added = False + self.init_ui() + self.init_font() + self.position = None + self.animating = False + self.focusing = False + self.text_changed = False + + self.current_lesson_name_text = self.findChild(QLabel, 'subject') + self.activity_countdown = self.findChild(QLabel, 'activity_countdown') + self.countdown_progress_bar = self.findChild(ProgressRing, 'progressBar') + + self.setFocusPolicy(Qt.FocusPolicy.NoFocus) + self.setAttribute(Qt.WA_TransparentForMouseEvents, False) + + # 动态获取屏幕尺寸 + screen_geometry = QApplication.primaryScreen().availableGeometry() + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + # 加载保存的位置 + saved_pos = self.load_position() + if saved_pos: + # 边界检查 + saved_pos = self.adjust_position_to_screen(saved_pos) + self.position = saved_pos + else: + # 使用动态计算的默认位置 + self.position = QPoint( + (screen_width - self.width()) // 2, # 居中横向 + 50 # 距离顶部 50px + ) + + update_timer.add_callback(self.update_data) + + def adjust_position_to_screen(self, pos): + screen = QApplication.screenAt(pos) + if not screen: + screen = QApplication.primaryScreen() + screen_geometry = screen.availableGeometry() + window_width = self.width() + window_height = self.height() + # 计算屏幕边界 + screen_left = screen_geometry.x() + screen_right = screen_geometry.x() + screen_geometry.width() + screen_top = screen_geometry.y() + screen_bottom = screen_geometry.y() + screen_geometry.height() + + new_x, new_y = pos.x(), pos.y() + if pos.x() < screen_left: + # 当窗口可见部分不足50%时调整 + visible_width = (pos.x() + window_width) - screen_left + if visible_width < window_width / 2: + new_x = screen_left + elif (pos.x() + window_width) > screen_right: + visible_width = screen_right - pos.x() + if visible_width < window_width / 2: + new_x = screen_right - window_width + if pos.y() < screen_top: + visible_height = (pos.y() + window_height) - screen_top + if visible_height < window_height / 2: + new_y = screen_top + elif (pos.y() + window_height) > screen_bottom: + visible_height = screen_bottom - pos.y() + if visible_height < window_height / 2: + new_y = screen_bottom - window_height + return QPoint(new_x, new_y) + + def _ensure_topmost(self): + # 始终处于顶层 + if active_windows: + return + if os.name == 'nt': + try: + hwnd = self.winId().__int__() + if ctypes.windll.user32.IsWindow(hwnd): + HWND_TOPMOST = -1 + SWP_NOMOVE = 0x0002 + SWP_NOSIZE = 0x0001 + SWP_SHOWWINDOW = 0x0040 + SWP_NOACTIVATE = 0x0010 + ctypes.windll.user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSIZE | SWP_SHOWWINDOW) + self.raise_() + else: + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass # 可能已经被移除了 + self._is_topmost_callback_added = False + logger.debug(f"句柄 {hwnd} 无效,已移除置顶回调。") + except RuntimeError as e: + if 'Internal C++ object' in str(e) and 'already deleted' in str(e): + logger.debug(f"尝试访问已删除的 FloatingWidget 时出错,移除回调: {e}") + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass # 可能已经被移除了 + self._is_topmost_callback_added = False + else: + logger.error(f"检查或设置浮窗置顶时发生运行时错误: {e}") + except Exception as e: + logger.error(f"检查或设置浮窗置顶时出错: {e}") + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass + self._is_topmost_callback_added = False + logger.debug(f"因错误 {e} 移除浮窗置顶回调。") + + def save_position(self): + current_screen = QApplication.screenAt(self.pos()) + if not current_screen: + current_screen = QApplication.primaryScreen() + screen_geometry = current_screen.availableGeometry() + pos = self.pos() + x = pos.x() + window_width = self.width() + if mgr.state: + return + screen_left = screen_geometry.left() + screen_right = screen_geometry.right() + if x < screen_left: + visible_width = (x + window_width) - screen_left + if visible_width < window_width / 2: + x = screen_left + elif (x + window_width) > screen_right: + if self.animating: + return + visible_width = screen_right - x + if visible_width < window_width / 2: + x = screen_right - window_width + y = min(max(pos.y(), screen_geometry.top()), screen_geometry.bottom()) + pos = QPoint(x, y) + config_center.write_conf('FloatingWidget', 'pos_x', str(pos.x())) + if not self.animating: + config_center.write_conf('FloatingWidget', 'pos_y', str(pos.y())) + + def load_position(self): + x = config_center.read_conf('FloatingWidget', 'pos_x') + y = config_center.read_conf('FloatingWidget', 'pos_y') + if x and y: + return QPoint(int(x), int(y)) + return None + + def init_ui(self): + setTheme_() + if os.path.exists(f'{base_directory}/ui/{theme}/widget-floating.ui'): + if isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode']: + uic.loadUi(f'{base_directory}/ui/{theme}/dark/widget-floating.ui', self) + else: + uic.loadUi(f'{base_directory}/ui/{theme}/widget-floating.ui', self) + else: + if isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode']: + uic.loadUi(f'{base_directory}/ui/default/dark/widget-floating.ui', self) + else: + uic.loadUi(f'{base_directory}/ui/default/widget-floating.ui', self) + + # 设置窗口无边框和透明背景 + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + # 根据平台和设置应用窗口标志 + if sys.platform == 'darwin': + flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Widget | Qt.X11BypassWindowManagerHint + else: + flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool | Qt.X11BypassWindowManagerHint + + self.setWindowFlags(flags) + + # 始终添加置顶回调逻辑 + if os.name == 'nt': + if not self._is_topmost_callback_added: + try: + if hasattr(utils, 'update_timer') and utils.update_timer: + utils.update_timer.add_callback(self._ensure_topmost) + self._is_topmost_callback_added = True + self._ensure_topmost() # 立即执行一次确保初始置顶 + else: + logger.warning("utils.update_timer 不可用,无法为浮窗添加置顶回调。") + except Exception as e: + logger.error(f"为浮窗添加置顶回调时出错: {e}") + + if sys.platform == 'darwin': + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.Widget | # macOS 失焦时仍然显示 + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + else: + self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.Tool | + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + + backgnd = self.findChild(QFrame, 'backgnd') + shadow_effect = QGraphicsDropShadowEffect(self) + shadow_effect.setBlurRadius(28) + shadow_effect.setXOffset(0) + shadow_effect.setYOffset(6) + shadow_effect.setColor(QColor(0, 0, 0, 75)) + backgnd.setGraphicsEffect(shadow_effect) + + def init_font(self): + font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + font_family = QFontDatabase.applicationFontFamilies(font_id)[0] + + self.setStyleSheet(f""" + QLabel, ProgressRing{{ + font-family: "{font_family}"; + }} + """) + + def update_data(self): + time_color = QColor(f'#{config_center.read_conf("Color", "floating_time")}') + self.activity_countdown.setStyleSheet(f"color: {time_color.name()};") + if self.animating: # 执行动画时跳过更新 + return + if platform.system() == 'Windows' and platform.release() != '7': + self.setWindowOpacity(int(config_center.read_conf('General', 'opacity')) / 100) # 设置窗口透明度 + else: + self.setWindowOpacity(1.0) + cd_list = get_countdown() + self.text_changed = False + if self.current_lesson_name_text.text() != current_lesson_name: + self.text_changed = True + + self.current_lesson_name_text.setText(current_lesson_name) + + if cd_list: # 模糊倒计时 + blur_floating = config_center.read_conf('General', 'blur_floating_countdown') == '1' + if blur_floating: # 模糊显示 + if cd_list[1] == '00:00': + self.activity_countdown.setText(f"< - 分钟") + else: + minutes = int(cd_list[1].split(':')[0]) + 1 + self.activity_countdown.setText(f"< {minutes} 分钟") + else: # 精确显示 + self.activity_countdown.setText(cd_list[1]) + self.countdown_progress_bar.setValue(cd_list[2]) + + self.adjustSize_animation() + + self.update() + + def showEvent(self, event): # 窗口显示 + logger.info('显示浮窗') + current_screen = QApplication.screenAt(self.pos()) or QApplication.primaryScreen() + screen_geometry = current_screen.availableGeometry() + + if self.position: + if self.position.y() > screen_geometry.center().y(): + # 下半屏 + start_pos = QPoint( + self.position.x(), + screen_geometry.bottom() + self.height() + ) + else: + # 上半屏 + start_pos = QPoint( + self.position.x(), + screen_geometry.top() - self.height() + ) + else: + # 默认:顶部中央滑入 + start_pos = QPoint( + (screen_geometry.width() - self.width()) // 2, + screen_geometry.top() - self.height() + ) + self.position = QPoint( + (screen_geometry.width() - self.width()) // 2, + max(50, int(config_center.read_conf('General', 'margin'))) + ) + + self.animation = QPropertyAnimation(self, b'windowOpacity') + self.animation.setDuration(450) + self.animation.setStartValue(0) + self.animation.setEndValue(int(config_center.read_conf('General', 'opacity')) / 100) + self.animation.setEasingCurve(QEasingCurve.Type.OutCubic) + + self.animation_rect = QPropertyAnimation(self, b'geometry') + self.animation_rect.setDuration(600) + self.animation_rect.setStartValue(QRect(start_pos, self.size())) + self.animation_rect.setEndValue(QRect(self.position, self.size())) + + if platform.system() == 'Darwin': + self.animation_rect.setEasingCurve(QEasingCurve.Type.OutQuad) + elif platform.system() == 'Windows': + self.animation_rect.setEasingCurve(QEasingCurve.Type.OutBack) + else: + self.animation_rect.setEasingCurve(QEasingCurve.Type.OutCubic) + + self.animating = True + self.animation.start() + self.animation_rect.start() + self.animation_rect.finished.connect(self.animation_done) + + def animation_done(self): + self.animating = False + + def closeEvent(self, event): + # 跳过动画 + if QApplication.instance().closingDown(): + self.save_position() + event.accept() + return + event.ignore() + self.setMinimumWidth(0) + self.position = self.pos() + self.save_position() + current_screen = QApplication.screenAt(self.pos()) + if not current_screen: + current_screen = QApplication.primaryScreen() + screen_geometry = current_screen.availableGeometry() + screen_center_y = screen_geometry.y() + (screen_geometry.height() // 2) + # 动态动画 + current_pos = self.pos() + base_duration = 350 # 基础 + max_duration = 550 # 最大 + min_duration = 250 # 最小 + # 获取主组件位置 + main_widget = next( + (w for w in mgr.widgets if w.path == 'widget-current-activity.ui'), + None + ) + if main_widget: + if current_pos.y() > screen_center_y: # 下半屏 + # 屏幕底部 + target_y = screen_geometry.bottom() + self.height() + 10 + # 任务栏补偿 + if platform.system() == "Windows": + target_y += 30 + + target_pos = QPoint( + main_widget.x(), + target_y + ) + distance = abs(current_pos.y() - target_y) + else: # 上半屏 + target_pos = main_widget.pos() + distance = abs(current_pos.y() - target_pos.y()) + else: + target_pos = QPoint( + screen_geometry.center().x() - self.width() // 2, + int(config_center.read_conf('General', 'margin')) + ) + distance = abs(current_pos.y() - target_pos.y()) + + max_distance = screen_geometry.height() + distance_ratio = min(distance / max_distance, 1.0) + duration = int(base_duration + (max_duration - base_duration) * (distance_ratio ** 0.7)) + duration = max(min_duration, min(duration, max_duration)) + # 多平台兼容 + if platform.system() == "Darwin": + curve = QEasingCurve.Type.OutQuad + duration = int(duration * 0.85) + elif platform.system() == "Windows": + curve = QEasingCurve.Type.OutCubic + if current_pos.y() > screen_center_y: + duration += 50 # 底部移动稍慢 + curve = QEasingCurve.Type.InOutQuad + + self.animation = QPropertyAnimation(self, b"windowOpacity") + self.animation.setDuration(int(duration * 1.15)) + self.animation.setStartValue(self.windowOpacity()) + self.animation.setEndValue(0.0) + + self.animation_rect = QPropertyAnimation(self, b"geometry") + self.animation_rect.setDuration(duration) + self.animation_rect.setStartValue(self.geometry()) + self.animation_rect.setEndValue(QRect(target_pos, self.size())) + self.animation_rect.setEasingCurve(curve) + + self.animating = True + self.animation.start() + self.animation_rect.start() + + def cleanup(): + self.hide() + self.save_position() + self.animating = False + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass + self._is_topmost_callback_added = False + + self.animation_rect.finished.connect(cleanup) + + def hideEvent(self, event): + event.accept() + logger.info('隐藏浮窗') + self.animating = False + self.setMinimumSize(QSize(self.width(), self.height())) + + def adjustSize_animation(self): + if not self.text_changed: + return + self.setMinimumWidth(200) + current_geometry = self.geometry() + label_width = self.current_lesson_name_text.sizeHint().width() + 120 + offset = label_width - current_geometry.width() + target_geometry = current_geometry.adjusted(0, 0, offset, 0) + self.animation = QPropertyAnimation(self, b'geometry') + self.animation.setDuration(450) + self.animation.setStartValue(current_geometry) + self.animation.setEndValue(target_geometry) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) + self.animating = True # 避免动画Bug x114514 + self.animation.start() + self.animation.finished.connect(self.animation_done) + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.m_flag = True + self.m_Position = event.globalPos() - self.pos() # 获取鼠标相对窗口的位置 + self.p_Position = event.globalPos() # 获取鼠标相对屏幕的位置 + event.accept() + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.MouseButton.LeftButton and self.m_flag: + self.move(event.globalPos() - self.m_Position) # 更改窗口位置 + event.accept() + + def mouseReleaseEvent(self, event): + self.r_Position = event.globalPos() # 获取鼠标相对窗口的位置 + self.m_flag = False + # 保存位置到配置文件 + self.save_position() + # 特定隐藏模式下不执行操作 + hide_mode = config_center.read_conf('General', 'hide') + if hide_mode == '1' or hide_mode == '2': + return # 阻止手动展开/收起 + if ( + hasattr(self, "p_Position") + and self.r_Position == self.p_Position + and not self.animating + ): # 非特定隐藏模式下执行点击事件 + if hide_mode == '3': + if mgr.state: + mgr.decide_to_hide() + mgr.hide_status = (current_state, 1) + else: + mgr.show_windows() + mgr.hide_status = (current_state, 0) + elif hide_mode == '1': + mgr.show_windows() + self.close() + + def focusInEvent(self, event): + self.focusing = True + + def focusOutEvent(self, event): + self.focusing = False + + def stop(self): + if mgr: + mgr.cleanup_resources() + for widget in self.widgets: + widget.stop() + if self.animation: + self.animation.stop() + if self.opacity_animation: + self.opacity_animation.stop() + self.close() + +class DesktopWidget(QWidget): # 主要小组件 + def __init__(self, parent=WidgetsManager, path='widget-time.ui', enable_tray=False, cnt=0, position=None, widget_cnt = None): + super().__init__() + self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.Tool) + + self.cnt = cnt + self.widget_cnt = widget_cnt + + self.tray_menu = None + + self.last_widgets = list_.get_widget_config() + self.path = path + + self.last_code = 101010100 + self.radius = conf.load_theme_config(theme)['radius'] + self.last_theme = config_center.read_conf('General', 'theme') + self.last_color_mode = config_center.read_conf('General', 'color_mode') + self.w = 100 + + # 天气预警动画相关 + self.weather_alert_timer = None + self.weather_alert_animation = None + self.weather_alert_text = None + self.alert_showing = False + + self.position = parent.get_widget_pos(self.path) if position is None else position + self.animation = None + self.opacity_animation = None + mgr.hide_status = None + self._is_topmost_callback_added = False # 添加一个标志来跟踪回调是否已添加 + + try: + self.w = conf.load_theme_config(theme)['widget_width'][self.path] + except KeyError: + self.w = list_.widget_width[self.path] + self.h = conf.load_theme_config(theme)['height'] + + init_config() + self.init_ui(path) + self.init_font() + + if enable_tray: + self.init_tray_menu() # 初始化托盘菜单 + + # 样式 + self.backgnd = self.findChild(QFrame, 'backgnd') + if self.backgnd is None: + self.backgnd = self.findChild(QLabel, 'backgnd') + + stylesheet = self.backgnd.styleSheet() # 应用圆角 + updated_stylesheet = re.sub(r'border-radius:\d+px;', f'border-radius:{self.radius}px;', stylesheet) + self.backgnd.setStyleSheet(updated_stylesheet) + + if path == 'widget-time.ui': # 日期显示 + self.date_text = self.findChild(QLabel, 'date_text') + self.date_text.setText(f'{today.year} 年 {today.month} 月') + self.day_text = self.findChild(QLabel, 'day_text') + self.day_text.setText(f'{today.day}日 {list_.week[today.weekday()]}') + + elif path == 'widget-countdown.ui': # 活动倒计时 + self.countdown_progress_bar = self.findChild(QProgressBar, 'progressBar') + self.activity_countdown = self.findChild(QLabel, 'activity_countdown') + self.ac_title = self.findChild(QLabel, 'activity_countdown_title') + + elif path == 'widget-current-activity.ui': # 当前活动 + self.current_subject = self.findChild(QPushButton, 'subject') + self.blur_effect_label = self.findChild(QLabel, 'blurEffect') + # 模糊效果 + self.blur_effect = QGraphicsBlurEffect() + self.current_subject.mouseReleaseEvent = self.rightReleaseEvent + + update_timer.add_callback(self.detect_theme_changed) + + elif path == 'widget-next-activity.ui': # 接下来的活动 + self.nl_text = self.findChild(QLabel, 'next_lesson_text') + + elif path == 'widget-countdown-day.ui': # 自定义倒计时 + self.custom_title = self.findChild(QLabel, 'countdown_custom_title') + self.custom_countdown = self.findChild(QLabel, 'custom_countdown') + + elif path == 'widget-weather.ui': # 天气组件 + content_layout = self.findChild(QHBoxLayout, 'horizontalLayout_2') + content_layout.setSpacing(1) + self.temperature = self.findChild(QLabel, 'temperature') + self.weather_icon = self.findChild(QLabel, 'weather_icon') + self.alert_icon = IconWidget(self) + self.alert_icon.setFixedSize(22,22) + self.alert_icon.hide() + + # 预警标签 + self.weather_alert_text = QLabel(self) + self.weather_alert_text.setAlignment(Qt.AlignCenter) + self.weather_alert_text.setStyleSheet(self.temperature.styleSheet()) + self.weather_alert_text.setFont(self.temperature.font()) + self.weather_alert_text.hide() + content_layout.addWidget(self.alert_icon) + content_layout.addWidget(self.weather_alert_text) + + self.weather_alert_timer = None + self.weather_alert_opacity = QGraphicsOpacityEffect(self) + self.weather_alert_opacity.setOpacity(1.0) + self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) + self.weather_alert_animation = QPropertyAnimation(self.weather_alert_opacity, b"opacity") + self.weather_alert_animation.setDuration(700) + self.weather_alert_animation.setEasingCurve(QEasingCurve.OutCubic) + self.alert_icon_opacity = QGraphicsOpacityEffect(self) + self.alert_icon_opacity.setOpacity(1.0) + self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) + self.alert_icon_animation = QPropertyAnimation(self.alert_icon_opacity, b"opacity") + self.alert_icon_animation.setDuration(700) + self.alert_icon_animation.setEasingCurve(QEasingCurve.OutCubic) + + self.showing_temperature = True # 跟踪状态(预警/气温) + + self.get_weather_data() + self.weather_timer = QTimer(self) + self.weather_timer.setInterval(30 * 60 * 1000) # 30分钟更新一次 + self.weather_timer.timeout.connect(self.get_weather_data) + self.weather_timer.start() + update_timer.add_callback(self.detect_weather_code_changed) + + if hasattr(self, 'img'): # 自定义图片主题兼容 + img = self.findChild(QLabel, 'img') + if platform.system() == 'Windows' and platform.release() != '7': + opacity = QGraphicsOpacityEffect(self) + opacity.setOpacity(0.65) + img.setGraphicsEffect(opacity) + + self.resize(self.w, self.height()) + + # 设置窗口位置 + if first_start: + self.animate_window(self.position) + if platform.system() == 'Windows' and platform.release() != '7': + self.setWindowOpacity(int(config_center.read_conf('General', 'opacity')) / 100) + else: + self.setWindowOpacity(1.0) + else: + self.move(self.position[0], self.position[1]) + self.resize(self.w, self.height()) + if platform.system() == 'Windows' and platform.release() != '7': + self.setWindowOpacity(0) + self.animate_show_opacity() + else: + self.setWindowOpacity(1.0) + self.show() + + self.update_data('') + + @staticmethod + def _onThemeChangedFinished(): + print('theme_changed') + + def update_widget_for_plugin(self, context=None): + if context is None: + context = ['title', 'desc'] + try: + title = self.findChild(QLabel, 'title') + desc = self.findChild(QLabel, 'content') + if title is not None: + title.setText(context[0]) + if desc is not None: + desc.setText(context[1]) + except Exception as e: + logger.error(f"更新插件小组件时出错:{e}") + + def init_ui(self, path): + if conf.load_theme_config(theme)['support_dark_mode']: + if os.path.exists(f'{base_directory}/ui/{theme}/{path}'): + if isDarkTheme(): + uic.loadUi(f'{base_directory}/ui/{theme}/dark/{path}', self) + else: + uic.loadUi(f'{base_directory}/ui/{theme}/{path}', self) + else: + if isDarkTheme(): + uic.loadUi(f'{base_directory}/ui/{theme}/dark/widget-base.ui', self) + else: + uic.loadUi(f'{base_directory}/ui/{theme}/widget-base.ui', self) + else: + if os.path.exists(f'{base_directory}/ui/{theme}/{path}'): + uic.loadUi(f'{base_directory}/ui/{theme}/{path}', self) + else: + uic.loadUi(f'{base_directory}/ui/{theme}/widget-base.ui', self) + + # 设置窗口无边框和透明背景 + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + if config_center.read_conf('General', 'hide') == '2' or (not int(config_center.read_conf('General', 'enable_click'))): + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + else: + self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False) + + if config_center.read_conf('General', 'pin_on_top') == '1': # 置顶 + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | + Qt.WindowType.WindowDoesNotAcceptFocus | Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + # 修改为使用定时器确保持续置顶 + if os.name == 'nt': + if not self._is_topmost_callback_added: + try: + # 确保 utils.update_timer 存在且有效 + if hasattr(utils, 'update_timer') and utils.update_timer: + utils.update_timer.add_callback(self._ensure_topmost) + self._is_topmost_callback_added = True + self._ensure_topmost() # 立即执行一次确保初始置顶 + # logger.debug("已添加置顶定时回调。") + else: + logger.warning("utils.update_timer 不可用,无法添加置顶回调。") + except Exception as e: + logger.error(f"添加置顶回调时出错: {e}") + + elif config_center.read_conf('General', 'pin_on_top') == '2': # 置底 + # 避免使用WindowStaysOnBottomHint,防止争夺底层 + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowDoesNotAcceptFocus + ) + if os.name == 'nt': + def set_window_pos(): + hwnd = self.winId().__int__() + # 稍高于最底层的值 + ctypes.windll.user32.SetWindowPos(hwnd, 2, 0, 0, 0, 0, 0x0214) + QTimer.singleShot(100, set_window_pos) + else: + QTimer.singleShot(100, self.lower) + else: + self.setWindowFlags( + Qt.WindowType.FramelessWindowHint + ) + + if sys.platform == 'darwin': + self.setWindowFlag(Qt.WindowType.Widget, True) + else: + self.setWindowFlag(Qt.WindowType.Tool, True) + + def _ensure_topmost(self): + # 突然忘记写移除了,不写了,应该没事( + if active_windows: + return + if os.name == 'nt': + try: + hwnd = self.winId().__int__() + if ctypes.windll.user32.IsWindow(hwnd): + HWND_TOPMOST = -1 + SWP_NOMOVE = 0x0002 + SWP_NOSIZE = 0x0001 + SWP_SHOWWINDOW = 0x0040 + SWP_NOACTIVATE = 0x0010 + ctypes.windll.user32.SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOSIZE | SWP_SHOWWINDOW) + self.raise_() + else: + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass # 可能已经被移除了 + self._is_topmost_callback_added = False + logger.debug(f"窗口句柄 {hwnd} 无效,已自动移除置顶回调。") + except RuntimeError as e: + if 'Internal C++ object' in str(e) and 'already deleted' in str(e): + logger.debug(f"尝试访问已删除的 DesktopWidget 时出错,移除回调: {e}") + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass # 可能已经被移除了 + self._is_topmost_callback_added = False + else: + logger.error(f"检查或设置窗口置顶时发生运行时错误: {e}") + except Exception as e: + logger.error(f"检查或设置窗口置顶时出错: {e}") + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + except ValueError: + pass + self._is_topmost_callback_added = False + logger.debug(f"因错误 {e} 移除置顶回调。") + + def closeEvent(self, event): + if self._is_topmost_callback_added: + try: + utils.update_timer.remove_callback(self._ensure_topmost) + self._is_topmost_callback_added = False + # logger.debug("窗口关闭,已移除置顶回调。") + except ValueError: + logger.debug("尝试移除不存在的置顶回调。") + except Exception as e: + logger.error(f"关闭窗口时移除置顶回调出错: {e}") + super().closeEvent(event) + + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + # 添加阴影效果 + if conf.load_theme_config(theme)['shadow']: # 修改阴影问题 + shadow_effect = QGraphicsDropShadowEffect(self) + shadow_effect.setBlurRadius(28) + shadow_effect.setXOffset(0) + shadow_effect.setYOffset(6) + shadow_effect.setColor(QColor(0, 0, 0, 75)) + + self.backgnd.setGraphicsEffect(shadow_effect) + + def init_font(self): + font_path = f'{base_directory}/font/HarmonyOS_Sans_SC_Bold.ttf' + font_id = QFontDatabase.addApplicationFont(font_path) + if font_id != -1: + font_family = QFontDatabase.applicationFontFamilies(font_id)[0] + + self.setStyleSheet(f""" + QLabel, QPushButton{{ + font-family: "{font_family}"; + }} + """) + + def animate_expand(self, target_geometry): + self.animation = QPropertyAnimation(self, b"geometry") + self.animation.setDuration(400) + self.animation.setStartValue(QRect(target_geometry.x(), -self.height(), + self.width(), self.height())) + self.animation.setEndValue(target_geometry) + self.animation.setEasingCurve(QEasingCurve.Type.OutBack) + self.raise_() + self.show() + + def init_tray_menu(self): + if not first_start: + return + + utils.tray_icon = utils.TrayIcon(self) + utils.tray_icon.setToolTip(f"Class Widgets - {config_center.schedule_name[:-5]}") + self.tray_menu = SystemTrayMenu(title='Class Widgets', parent=self) + self.tray_menu.addActions([ + Action(fIcon.HIDE, '完全隐藏/显示小组件', triggered=lambda: self.hide_show_widgets()), + Action(fIcon.BACK_TO_WINDOW, '最小化为浮窗', triggered=lambda: self.minimize_to_floating()), + ]) + self.tray_menu.addSeparator() + self.tray_menu.addActions([ + Action(fIcon.SHOPPING_CART, '插件广场', triggered=open_plaza), + Action(fIcon.DEVELOPER_TOOLS, '额外选项', triggered=self.open_extra_menu), + Action(fIcon.SETTING, '设置', triggered=open_settings) + ]) + self.tray_menu.addSeparator() + self.tray_menu.addAction(Action(fIcon.SYNC, '重新启动', triggered=restart)) + self.tray_menu.addAction(Action(fIcon.CLOSE, '退出', triggered=stop)) + utils.tray_icon.setContextMenu(self.tray_menu) + + utils.tray_icon.activated.connect(self.on_tray_icon_clicked) + utils.tray_icon.show() + + @staticmethod + def on_tray_icon_clicked(reason): # 点击托盘图标隐藏 + if config_center.read_conf('General', 'hide') == '0': + if reason == QSystemTrayIcon.ActivationReason.Trigger: + if mgr.state: + mgr.decide_to_hide() + else: + mgr.show_windows() + elif config_center.read_conf('General', 'hide') == '3': + if reason == QSystemTrayIcon.ActivationReason.Trigger: + if mgr.state: + mgr.decide_to_hide() + mgr.hide_status = (current_state, 1) + else: + mgr.show_windows() + mgr.hide_status = (current_state, 0) + + + + def rightReleaseEvent(self, event): # 右键事件 + event.ignore() + if event.button() == Qt.MouseButton.RightButton: + self.open_extra_menu() + + def update_data(self, path=''): + global current_time, current_week, start_y, time_offset, today + + today = dt.date.today() + current_time = dt.datetime.now().strftime('%H:%M:%S') + time_offset = conf.get_time_offset() + + get_start_time() + get_current_lessons() + get_current_lesson_name() + get_excluded_lessons() + get_next_lessons() + hide_status = get_hide_status() + + if (hide_mode:=config_center.read_conf('General', 'hide')) in ['1','2']: # 上课自动隐藏 + if hide_status: + mgr.decide_to_hide() + else: + mgr.show_windows() + elif hide_mode == '3': # 灵活隐藏 + if mgr.hide_status is None: + mgr.hide_status = (-1, hide_status) + elif mgr.hide_status[0] != current_state: + mgr.hide_status = (-1, hide_status) + if mgr.hide_status[1]: + mgr.decide_to_hide() + else: + mgr.show_windows() + + + + if conf.is_temp_week(): # 调休日 + current_week = config_center.read_conf('Temp', 'set_week') + else: + current_week = dt.datetime.now().weekday() + + cd_list = get_countdown() + + if path == 'widget-time.ui': # 日期显示 + self.date_text.setText(f'{today.year} 年 {today.month} 月') + self.day_text.setText(f'{today.day} 日 {list_.week[today.weekday()]}') + + if path == 'widget-current-activity.ui': # 当前活动 + self.current_subject.setText(f' {current_lesson_name}') + + if current_state != 2: # 非休息段 + render = QSvgRenderer(list_.get_subject_icon(current_lesson_name)) + self.blur_effect_label.setStyleSheet( + f'background-color: rgba{list_.subject_color(current_lesson_name)}, 200);' + ) + else: # 休息段 + render = QSvgRenderer(list_.get_subject_icon('课间')) + self.blur_effect_label.setStyleSheet( + f'background-color: rgba{list_.subject_color("课间")}, 200);' + ) + pixmap = QPixmap(render.defaultSize()) + pixmap.fill(Qt.GlobalColor.transparent) + + painter = QPainter(pixmap) + render.render(painter) + if (isDarkTheme() and conf.load_theme_config(theme)['support_dark_mode'] + or isDarkTheme() and conf.load_theme_config(theme)['default_theme'] == 'dark'): # 在暗色模式显示亮色图标 + painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn) + painter.fillRect(pixmap.rect(), QColor("#FFFFFF")) + painter.end() + + self.current_subject.setIcon(QIcon(pixmap)) + self.blur_effect.setBlurRadius(25) # 模糊半径 + self.blur_effect_label.setGraphicsEffect(self.blur_effect) + + elif path == 'widget-next-activity.ui': # 接下来的活动 + self.nl_text.setText(get_next_lessons_text()) + + if path == 'widget-countdown.ui': # 活动倒计时 + if cd_list: + if config_center.read_conf('General', 'blur_countdown') == '1': # 模糊倒计时 + if cd_list[1] == '00:00': + self.activity_countdown.setText(f"< - 分钟") + else: + self.activity_countdown.setText(f"< {int(cd_list[1].split(':')[0]) + 1} 分钟") + else: + self.activity_countdown.setText(cd_list[1]) + self.ac_title.setText(cd_list[0]) + self.countdown_progress_bar.setValue(cd_list[2]) + + if path == 'widget-countdown-day.ui': # 自定义倒计时 + conf.update_countdown(self.cnt) + self.custom_title.setText(f'距离 {conf.get_cd_text_custom()} 还有') + self.custom_countdown.setText(conf.get_custom_countdown()) + self.update() + + def get_weather_data(self): + logger.info('获取天气数据') + self.weather_thread = weatherReportThread() + self.weather_thread.weather_signal.connect(self.update_weather_data) + self.weather_thread.start() + + def detect_weather_code_changed(self): + current_code = config_center.read_conf('Weather') + if current_code != self.last_code: + self.last_code = current_code + self.get_weather_data() + + def toggle_weather_alert(self): + if not hasattr(self, 'weather_alert_level') or not self.weather_alert_level: + # logger.warning("未获取到天气预警等级") + return + if not hasattr(self, 'weather_alert_text') or not self.weather_alert_text.text(): + # logger.warning("未获取到天气预警文本") + return + if self.showing_temperature: + # 切换预警 + self.weather_alert_animation.setStartValue(0.0) + self.weather_alert_animation.setEndValue(1.0) + self.alert_icon_animation.setStartValue(0.0) + self.alert_icon_animation.setEndValue(1.0) + # 渐隐 + self.weather_opacity = QGraphicsOpacityEffect(self.weather_icon) + self.temperature_opacity = QGraphicsOpacityEffect(self.temperature) + self.weather_icon.setGraphicsEffect(self.weather_opacity) + self.temperature.setGraphicsEffect(self.temperature_opacity) + weather_fade_out = QPropertyAnimation(self.weather_opacity, b'opacity') + temp_fade_out = QPropertyAnimation(self.temperature_opacity, b'opacity') + weather_fade_out.setDuration(700) + temp_fade_out.setDuration(700) + weather_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) + temp_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) + weather_fade_out.setStartValue(1.0) + weather_fade_out.setEndValue(0.0) + temp_fade_out.setStartValue(1.0) + temp_fade_out.setEndValue(0.0) + # 重置不透明度 + self.fade_out_group = QParallelAnimationGroup(self) + self.fade_out_group.addAnimation(weather_fade_out) + self.fade_out_group.addAnimation(temp_fade_out) + if not hasattr(self, 'weather_alert_opacity') or not self.weather_alert_opacity: + self.weather_alert_opacity = QGraphicsOpacityEffect(self.weather_alert_text) + self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) + if not hasattr(self, 'alert_icon_opacity') or not self.alert_icon_opacity: + self.alert_icon_opacity = QGraphicsOpacityEffect(self.alert_icon) + self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) + + alert_text_fade_in = QPropertyAnimation(self.weather_alert_opacity, b'opacity') + alert_icon_fade_in = QPropertyAnimation(self.alert_icon_opacity, b'opacity') + alert_text_fade_in.setDuration(700) + alert_icon_fade_in.setDuration(700) + alert_text_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) + alert_icon_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) + alert_text_fade_in.setStartValue(0.0) + alert_text_fade_in.setEndValue(1.0) + alert_icon_fade_in.setStartValue(0.0) + alert_icon_fade_in.setEndValue(1.0) + + self.fade_in_group = QParallelAnimationGroup(self) + self.fade_in_group.addAnimation(alert_text_fade_in) + self.fade_in_group.addAnimation(alert_icon_fade_in) + try: self.fade_out_group.finished.disconnect() + except TypeError: pass + + def _start_alert_fade_in(): + if hasattr(self, 'alert_icon') and isinstance(self.alert_icon, IconWidget) and self.alert_icon.icon() is not None and not self.alert_icon.icon().isNull(): + self.weather_icon.hide() + self.temperature.hide() + self.weather_alert_opacity.setOpacity(0.0) + self.alert_icon_opacity.setOpacity(0.0) + self.weather_alert_text.show() + self.alert_icon.show() + self.fade_in_group.start() + self.weather_info_timer.start(3000) + else: + self.weather_icon.show() + self.temperature.show() + if hasattr(self, 'weather_opacity'): self.weather_opacity.setOpacity(1.0) + if hasattr(self, 'temperature_opacity'): self.temperature_opacity.setOpacity(1.0) + self.showing_temperature = True + + self.fade_out_group.finished.connect(_start_alert_fade_in) + + self.fade_out_group.start() + else: + # 切换到气温 + self.weather_alert_animation.setStartValue(1.0) + self.weather_alert_animation.setEndValue(0.0) + self.alert_icon_animation.setStartValue(1.0) + self.alert_icon_animation.setEndValue(0.0) + if not hasattr(self, 'weather_alert_opacity') or not self.weather_alert_opacity: + self.weather_alert_opacity = QGraphicsOpacityEffect(self.weather_alert_text) + self.weather_alert_text.setGraphicsEffect(self.weather_alert_opacity) + if not hasattr(self, 'alert_icon_opacity') or not self.alert_icon_opacity: + self.alert_icon_opacity = QGraphicsOpacityEffect(self.alert_icon) + self.alert_icon.setGraphicsEffect(self.alert_icon_opacity) + + alert_text_fade_out = QPropertyAnimation(self.weather_alert_opacity, b'opacity') + alert_icon_fade_out = QPropertyAnimation(self.alert_icon_opacity, b'opacity') + alert_text_fade_out.setDuration(500) + alert_icon_fade_out.setDuration(500) + alert_text_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) + alert_icon_fade_out.setEasingCurve(QEasingCurve.Type.OutCubic) + alert_text_fade_out.setStartValue(1.0) + alert_text_fade_out.setEndValue(0.0) + alert_icon_fade_out.setStartValue(1.0) + alert_icon_fade_out.setEndValue(0.0) + + self.fade_out_group = QParallelAnimationGroup(self) + self.fade_out_group.addAnimation(alert_text_fade_out) + self.fade_out_group.addAnimation(alert_icon_fade_out) + if not hasattr(self, 'weather_opacity') or not self.weather_opacity: + self.weather_opacity = QGraphicsOpacityEffect(self.weather_icon) + self.weather_icon.setGraphicsEffect(self.weather_opacity) + if not hasattr(self, 'temperature_opacity') or not self.temperature_opacity: + self.temperature_opacity = QGraphicsOpacityEffect(self.temperature) + self.temperature.setGraphicsEffect(self.temperature_opacity) + + weather_fade_in = QPropertyAnimation(self.weather_opacity, b'opacity') + temp_fade_in = QPropertyAnimation(self.temperature_opacity, b'opacity') + weather_fade_in.setDuration(500) + temp_fade_in.setDuration(500) + weather_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) + temp_fade_in.setEasingCurve(QEasingCurve.Type.OutCubic) + weather_fade_in.setStartValue(0.0) + weather_fade_in.setEndValue(1.0) + temp_fade_in.setStartValue(0.0) + temp_fade_in.setEndValue(1.0) + + self.fade_in_group = QParallelAnimationGroup(self) + self.fade_in_group.addAnimation(weather_fade_in) + self.fade_in_group.addAnimation(temp_fade_in) + try: self.fade_out_group.finished.disconnect() + except TypeError: pass + + def _start_temperature_fade_in(): + self.weather_alert_text.hide() + self.alert_icon.hide() + self.weather_opacity.setOpacity(0.0) + self.temperature_opacity.setOpacity(0.0) + self.weather_icon.show() + self.temperature.show() + self.fade_in_group.start() + # 连接淡出组完成信号 + self.fade_out_group.finished.connect(_start_temperature_fade_in) + self.fade_out_group.start() + + self.showing_temperature = not self.showing_temperature + + def detect_theme_changed(self): + theme_ = config_center.read_conf('General', 'theme') + color_mode = config_center.read_conf('General', 'color_mode') + widgets = list_.get_widget_config() + if theme_ != self.last_theme or color_mode != self.last_color_mode or widgets != self.last_widgets: + self.last_theme = theme_ + self.last_color_mode = color_mode + self.last_widgets = widgets + logger.info(f'切换主题:{theme_},颜色模式{color_mode}') + mgr.clear_widgets() + + def update_weather_data(self, weather_data): # 更新天气数据(已兼容多api) + global weather_name, temperature, weather_data_temp + if type(weather_data) is dict and hasattr(self, 'weather_icon') and 'error' not in weather_data: + logger.success('已获取天气数据') + alert_data = weather_data.get('alert') + weather_data = weather_data.get('now') + weather_data_temp = weather_data + + weather_name = db.get_weather_by_code(db.get_weather_data('icon', weather_data)) + current_city = self.findChild(QLabel, 'current_city') + try: # 天气组件 + self.weather_icon.setPixmap( + QPixmap(db.get_weather_icon_by_code(db.get_weather_data('icon', weather_data))) + ) + self.alert_icon.hide() + if db.is_supported_alert(): + alert_type = db.get_weather_data('alert', alert_data if alert_data else weather_data) + if alert_type: + self.alert_icon.setIcon( + db.get_alert_image(alert_type) + ) + self.alert_icon.hide() + try: + alert_title = db.get_weather_data('alert_title', alert_data if alert_data else weather_data) + if alert_title: + alert_type_match = re.search(r'发布(\w+)(蓝|黄|橙|红)色预警', alert_title) + if alert_type_match: + alert_type = alert_type_match.group(1) # 类型 + logger.success(f'天气预警: {alert_title} --> {alert_type}预警') + alert_text = alert_type + '预警' + else: + logger.success(f'天气预警: {alert_title} --> {alert_title}') + alert_text = alert_title + self.weather_alert_text.setFixedWidth(80) + self.weather_alert_text.setFixedHeight(40) + # 调整字体大小 + font = self.weather_alert_text.font() + if len(alert_text) <= 4: + font.setPointSize(14) + elif len(alert_text) <= 6: + font.setPointSize(12) + else: + font.setPointSize(10) + + self.weather_alert_text.setFont(font) + self.weather_alert_text.setText(alert_text) + self.weather_alert_text.setAlignment(Qt.AlignCenter) + if not self.weather_alert_timer: + self.weather_alert_timer = QTimer(self) + self.weather_alert_timer.timeout.connect(self.toggle_weather_alert) + self.weather_alert_timer.start(6000) + self.weather_info_timer = QTimer(self) + self.weather_info_timer.timeout.connect(self.toggle_weather_alert) + self.weather_info_timer.setSingleShot(True) + except Exception as e: + logger.warning(f'获取天气预警标题失败:{e}') + self.weather_alert_text.setText('暂无预警信息') + + self.temperature.setText(f"{db.get_weather_data('temp', weather_data)}") + current_city.setText(f"{db.search_by_num(config_center.read_conf('Weather', 'city'))} · " + f"{weather_name}") + update_stylesheet = re.sub( + r'border-image: url\((.*?)\);', + f"border-image: url({db.get_weather_stylesheet(db.get_weather_data('icon', weather_data))});", + self.backgnd.styleSheet() + ) + self.backgnd.setStyleSheet(update_stylesheet) + except Exception as e: + logger.error(f'天气组件出错:{e}') + else: + logger.error(f'获取天气数据出错:{weather_data}') + try: + if hasattr(self, 'weather_icon'): + self.weather_icon.setPixmap(QPixmap(f'{base_directory}/img/weather/99.svg')) + self.alert_icon.hide() + self.weather_alert_text.hide() + self.temperature.setText('--°') + current_city = self.findChild(QLabel, 'current_city') + if current_city: + current_city.setText(f"{db.search_by_num(config_center.read_conf('Weather', 'city'))} · 未知") + if hasattr(self, 'backgnd'): + update_stylesheet = re.sub( + r'border-image: url\((.*?)\);', + f"border-image: url({db.get_weather_stylesheet('99')});", + self.backgnd.styleSheet() + ) + self.backgnd.setStyleSheet(update_stylesheet) + except Exception as e: + logger.error(f'天气图标设置失败:{e}') + + def open_extra_menu(self): + global ex_menu + if ex_menu is None or not ex_menu.isVisible(): + ex_menu = ExtraMenu() + ex_menu.show() + ex_menu.destroyed.connect(self.cleanup_extra_menu) + logger.info('打开“额外选项”') + else: + ex_menu.raise_() + ex_menu.activateWindow() + + @staticmethod + def cleanup_extra_menu(): + global ex_menu + ex_menu = None + + @staticmethod + def hide_show_widgets(): # 隐藏/显示主界面(全部隐藏) + hide_mode = config_center.read_conf('General', 'hide') + if hide_mode == '1' or hide_mode == '2': + hide_mode_text = "上课时自动隐藏" if hide_mode == '1' else "窗口最大化时隐藏" + w = Dialog( + "暂时无法变更“状态”", + f"您正在使用 {hide_mode_text} 模式,无法变更隐藏状态\n" + "若变更状态,将修改隐藏模式“灵活隐藏” (您稍后可以在“设置”中更改此选项)\n" + "您确定要隐藏组件吗?", + None + ) + w.yesButton.setText("确定") + w.yesButton.clicked.connect(lambda: config_center.write_conf('General', 'hide', '3')) + w.cancelButton.setText("取消") + w.buttonLayout.insertStretch(1) + w.setFixedWidth(550) + if w.exec(): + if mgr.state: + mgr.full_hide_windows() + else: + mgr.show_windows() + else: + if mgr.state: + mgr.full_hide_windows() + else: + mgr.show_windows() + + @staticmethod + def minimize_to_floating(): # 最小化到浮窗 + hide_mode = config_center.read_conf('General', 'hide') + if hide_mode == '1' or hide_mode == '2': + hide_mode_text = "上课时自动隐藏" if hide_mode == '1' else "窗口最大化时隐藏" + w = Dialog( + "暂时无法变更“状态”", + f"您正在使用 {hide_mode_text} 模式,无法变更隐藏状态\n" + "若变更状态,将修改隐藏模式“灵活隐藏” (您可以在“设置”中更改此选项)\n" + "您确定要隐藏组件吗?", + None + ) + w.yesButton.setText("确定") + w.yesButton.clicked.connect(lambda: config_center.write_conf('General', 'hide', '3')) + w.cancelButton.setText("取消") + w.buttonLayout.insertStretch(1) + w.setFixedWidth(550) + if w.exec(): + if mgr.state: + fw.show() + mgr.full_hide_windows() + else: + mgr.show_windows() + else: + if mgr.state: + fw.show() + mgr.full_hide_windows() + else: + mgr.show_windows() + + def clear_animation(self): # 清除动画 + self.animation = None + + def animate_window(self, target_pos): # **初次**启动动画 + # 创建位置动画 + self.animation = QPropertyAnimation(self, b"geometry") + self.animation.setDuration(300) # 持续时间 + if os.name == 'nt': + self.animation.setStartValue(QRect(target_pos[0], -self.height(), self.w, self.h)) + else: + self.animation.setStartValue(QRect(target_pos[0], 0, self.w, self.h)) + self.animation.setEndValue(QRect(target_pos[0], target_pos[1], self.w, self.h)) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 + self.animation.start() + self.animation.finished.connect(self.clear_animation) + + def animate_hide(self, full=False): # 隐藏窗口 + self.animation = QPropertyAnimation(self, b"geometry") + self.animation.setDuration(625) # 持续时间 + height = self.height() + self.setFixedHeight(height) # 防止连续打断窗口高度变小 + + if full and os.name == 'nt': + '''全隐藏 windows''' + self.animation.setEndValue(QRect(self.x(), -height, self.width(), self.height())) + elif os.name == 'nt': + '''半隐藏 windows''' + self.animation.setEndValue(QRect(self.x(), -height + 40, self.width(), self.height())) + else: + '''其他系统''' + self.animation.setEndValue(QRect(self.x(), 0, self.width(), self.height())) + self.animation.finished.connect(lambda: self.hide()) + + self.animation.setEasingCurve(QEasingCurve.Type.OutExpo) # 设置动画效果 + self.animation.start() + self.animation.finished.connect(self.clear_animation) + + def animate_hide_opacity(self): # 隐藏窗口透明度 + self.animation = QPropertyAnimation(self, b"windowOpacity") + self.animation.setDuration(300) # 持续时间 + self.animation.setStartValue(int(config_center.read_conf('General', 'opacity')) / 100) + self.animation.setEndValue(0) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 + self.animation.start() + self.animation.finished.connect(self.close) + + def animate_show_opacity(self): # 显示窗口透明度 + self.animation = QPropertyAnimation(self, b"windowOpacity") + self.animation.setDuration(350) # 持续时间 + self.animation.setStartValue(0) + self.animation.setEndValue(int(config_center.read_conf('General', 'opacity')) / 100) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 + self.animation.start() + self.animation.finished.connect(self.clear_animation) + + def animate_show(self): # 显示窗口 + self.animation = QPropertyAnimation(self, b"geometry") + self.animation.setDuration(525) # 持续时间 + # 获取当前窗口的宽度和高度,确保动画过程中保持一致 + self.animation.setEndValue( + QRect(self.x(), int(config_center.read_conf('General', 'margin')), self.width(), self.height())) + self.animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 + self.animation.finished.connect(self.clear_animation) + + if os.name != 'nt': + self.show() + + self.animation.start() + + def widget_transition(self, pos_x, width, height, opacity=1): # 窗口形变 + self.animation = QPropertyAnimation(self, b"geometry") + self.animation.setDuration(525) # 持续时间 + self.animation.setStartValue(QRect(self.x(), self.y(), self.width(), self.height())) + self.animation.setEndValue(QRect(pos_x, self.y(), width, height)) + self.animation.setEasingCurve(QEasingCurve.Type.OutCubic) # 设置动画效果 + self.animation.start() + + self.opacity_animation = QPropertyAnimation(self, b"windowOpacity") + self.opacity_animation.setDuration(525) # 持续时间 + self.opacity_animation.setStartValue(self.windowOpacity()) + self.opacity_animation.setEndValue(opacity) + self.opacity_animation.setEasingCurve(QEasingCurve.Type.InOutCirc) # 设置动画效果 + self.opacity_animation.start() + + self.animation.finished.connect(self.clear_animation) + + # 点击自动隐藏 + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.RightButton: + return # 右键不执行 + if config_center.read_conf('General', 'pin_on_top') == '2': # 置底 + return # 置底不执行 + if config_center.read_conf('General', 'hide') == '0': # 置顶 + if mgr.state: + mgr.decide_to_hide() + else: + mgr.show_windows() + elif config_center.read_conf('General', 'hide') == '3': # 隐藏 + if mgr.state: + mgr.decide_to_hide() + mgr.hide_status = (current_state, 1) + else: + mgr.show_windows() + mgr.hide_status = (current_state, 0) + else: + event.ignore() + + def stop(self): + if mgr: + mgr.cleanup_resources() + for widget in self.widgets: + widget.stop() + if self.animation: + self.animation.stop() + if self.opacity_animation: + self.opacity_animation.stop() + self.close() + + +def check_windows_maximize(): # 检查窗口是否最大化 + if os.name != 'nt' or not pygetwindow: + # logger.debug("非Windows NT系统或pygetwindow未加载, 无法检查最大化.") + return False + # 需要排除的特定窗口标题 (全字匹配, 大小写不敏感) + excluded_titles_exact_lower = { + 'residentsidebar', # 希沃侧边栏 + 'program manager', # Windows桌面 + 'desktop', # Windows桌面 (备用) + 'snippingtool', # 系统截图工具 + # '' 空标题不再默认排除 + } + # 需要排除的标题中包含的关键词 (大小写不敏感) + excluded_keywords_in_title_lower = { + 'overlay', + 'snipping', + 'sidebar', + 'flyout' # qfluentwidgets的浮出控件 + } + # 需要排除的进程名 (全字或部分匹配, 大小写不敏感) + excluded_process_names_lower = { + 'shellexperiencehost.exe', + 'searchui.exe', + 'startmenuexperiencehost.exe', + 'applicationframehost.exe', + 'systemsettings.exe', + 'taskmgr.exe' + } + # 用户自定义的忽略进程列表 (全字匹配, 大小写不敏感) + # 例:easinote.exe 每行一个,用逗号分隔 + ignored_process_names_for_maximize_lower = { + 'easinote.exe' + } + + current_pid = os.getpid() + + try: + all_windows = pygetwindow.getAllWindows() + except Exception as e: + logger.warning(f"获取窗口列表时发生错误 (pygetwindow): {str(e)}") + # logger.debug("获取窗口列表失败.") + return False + + for window in all_windows: + try: + if not window._hWnd: + # logger.debug(f"窗口 '{getattr(window, 'title', 'N/A')}' 无效句柄, 跳过.") + continue + if not window.visible: + # logger.debug(f"窗口 '{window.title}' 不可见, 跳过.") + continue + if not window.isMaximized: + # logger.debug(f"窗口 '{window.title}' 未最大化, 跳过.") + continue + # logger.debug(f"发现可见且已最大化的窗口: '{window.title}' (句柄: {window._hWnd})") + try: + hwnd_int = window._hWnd + pid_val = ctypes.c_ulong() + ctypes.windll.user32.GetWindowThreadProcessId(hwnd_int, ctypes.byref(pid_val)) + win_pid = pid_val.value + if win_pid == 0: + continue # 无效PID + process_name = psutil.Process(win_pid).name().lower() + except (psutil.NoSuchProcess, psutil.AccessDenied, AttributeError, ValueError, OSError) : + # logger.debug(f"无法获取窗口 '{title}' 的进程信息,跳过.") + continue + + if win_pid == current_pid: + # logger.debug(f"窗口 '{title}' (PID: {win_pid}, 进程: {process_name}) 是自身进程, 排除.") + continue + + title = window.title.strip() + title_lower = title.lower() + + if process_name in ignored_process_names_for_maximize_lower: + # logger.debug(f"窗口 '{title}' (进程: {process_name}) 在忽略列表, 排除.") + continue + + if process_name in excluded_process_names_lower: + # logger.debug(f"窗口 '{title}' (进程: {process_name}) 在排除的进程名列表, 排除.") + continue + + if title_lower in excluded_titles_exact_lower: + # logger.debug(f"窗口标题 '{title_lower}' 在排除列表, 排除.") + continue + + if any(keyword in title_lower for keyword in excluded_keywords_in_title_lower): + # logger.debug(f"窗口标题 '{title_lower}' 包含排除的关键词, 排除.") + continue + + # 如果进程是 explorer.exe,但不是“资源管理器”则认为是特殊explorer(应该是桌面) + if process_name == 'explorer.exe': + if title_lower in excluded_titles_exact_lower or \ + any(keyword in title_lower for keyword in excluded_keywords_in_title_lower): + # logger.debug(f"explorer.exe 窗口 '{title_lower}' 命中标题排除规则, 排除.") + continue + # logger.debug(f"找到有效最大化窗口: '{title}' (PID: {win_pid}, 进程: {process_name}). 返回 True.") + return True + + except Exception as e: + if window and hasattr(window, 'title'): + logger.debug(f"处理窗口 '{getattr(window, 'title', 'N/A')}' 时发生错误: {str(e)}") + else: + logger.debug(f"处理一个未知窗口时发生错误: {str(e)}") + continue + return False + + + +def init_config(): # 重设配置文件 + config_center.write_conf('Temp', 'set_week', '') + config_center.write_conf('Temp', 'set_schedule', '') + if config_center.read_conf('Temp', 'temp_schedule') != '': # 修复换课重置 + copy(f'{base_directory}/config/schedule/backup.json', + f'{base_directory}/config/schedule/{config_center.schedule_name}') + config_center.write_conf('Temp', 'temp_schedule', '') + schedule_center.update_schedule() + + +def init(): + global theme, radius, mgr, screen_width, first_start, fw, was_floating_mode + update_timer.remove_all_callbacks() + + theme = config_center.read_conf('General', 'theme') # 主题 + if not os.path.exists(f'{base_directory}/ui/{theme}/theme.json'): + logger.warning(f'主题 {theme} 不存在,使用默认主题') + theme = 'default' + logger.info(f'应用主题:{theme}') + + mgr = WidgetsManager() + fw = FloatingWidget() + + # 获取屏幕横向分辨率 + screen_geometry = app.primaryScreen().availableGeometry() + screen_width = screen_geometry.width() + + widgets = list_.get_widget_config() + + for widget in widgets: # 检查组件 + if widget not in list_.widget_name: + widgets.remove(widget) # 移除不存在的组件(确保移除插件后不会出错) + + mgr.init_widgets() + if not first_start and was_floating_mode: + if fw: + fw.show() + mgr.full_hide_windows() + + update_timer.add_callback(mgr.update_widgets) + update_timer.start() + + logger.info(f'Class Widgets 初始化完成。版本: {config_center.read_conf("Other", "version")}') + p_loader.run_plugins() # 运行插件 + + first_start = False + + +def setup_signal_handlers_optimized(app): + """退出信号处理器""" + def signal_handler(signum, frame): + logger.debug(f'收到信号 {signal.Signals(signum).name},退出...') + # utils.stop 处理退出 + utils.stop(0) + + signal.signal(signal.SIGTERM, signal_handler) # taskkill + signal.signal(signal.SIGINT, signal_handler) # Ctrl+C + if os.name == 'posix': + signal.signal(signal.SIGQUIT, signal_handler) # 终端退出 + signal.signal(signal.SIGHUP, signal_handler) # 终端挂起 + +if __name__ == '__main__': + if share.attach() and config_center.read_conf('Other', 'multiple_programs') != '1': + logger.debug('不允许多开实例') + from qfluentwidgets import Dialog + app = QApplication.instance() or QApplication(sys.argv) + dlg = Dialog( + 'Class Widgets 正在运行', + 'Class Widgets 正在运行!请勿打开多个实例,否则将会出现不可预知的问题。' + '\n(若您需要打开多个实例,请在“设置”->“高级选项”中启用“允许程序多开”)' + ) + dlg.yesButton.setText('好') + dlg.cancelButton.hide() + dlg.buttonLayout.insertStretch(0, 1) + dlg.setFixedWidth(550) + dlg.exec() + sys.exit(0) + if not share.create(1): + print(f'无法创建共享内存: {share.errorString()}') # logger 可能还没准备好 + sys.exit(1) + + scale_factor = float(config_center.read_conf('General', 'scale')) + os.environ['QT_SCALE_FACTOR'] = str(scale_factor) + logger.info(f"当前缩放系数:{scale_factor * 100}%") + + app = QApplication(sys.argv) + app.setQuitOnLastWindowClosed(False) + share.create(1) # 创建共享内存 + logger.info( + f"共享内存:{share.isAttached()} 是否允许多开实例:{config_center.read_conf('Other', 'multiple_programs')}") + try: + dark_mode_watcher = DarkModeWatcher(parent=app) + dark_mode_watcher.darkModeChanged.connect(handle_dark_mode_change) # 连接信号 + # 初始主题设置依赖于 darkModeChanged 信号 + except Exception as e: + logger.error(f"初始化颜色模式监测器时出错: {e}") + dark_mode_watcher = None + + if scale_factor > 1.8 or scale_factor < 1.0: + logger.warning("当前缩放系数可能导致显示异常,建议使缩放系数在 100% 到 180% 之间") + msg_box = Dialog('缩放系数过大', + f"当前缩放系数为 {scale_factor * 100}%,可能导致显示异常。\n建议将缩放系数设置为 100% 到 180% 之间。") + msg_box.yesButton.setText('好') + msg_box.cancelButton.hide() + msg_box.buttonLayout.insertStretch(0, 1) + msg_box.setFixedWidth(550) + msg_box.exec() + + # 优化操作系统和版本输出 + system = platform.system() + if system == 'Darwin': + system = 'macOS' + osRelease = platform.release() + if system == 'Windows': + osRelease = 'Windows ' + osRelease + if system == 'macOS': + osRelease = 'Darwin Kernel Version ' + osRelease + osVersion = platform.version() + if system == 'macOS': + osVersion = 'macOS ' + platform.mac_ver()[0] + + logger.info(f"操作系统:{system},版本:{osRelease}/{osVersion}") + + # list_pyttsx3_voices() + + if share.attach() and config_center.read_conf('Other', 'multiple_programs') != '1': + msg_box = Dialog( + 'Class Widgets 正在运行', + 'Class Widgets 正在运行!请勿打开多个实例,否则将会出现不可预知的问题。' + '\n(若您需要打开多个实例,请在“设置”->“高级选项”中启用“允许程序多开”)' + ) + msg_box.yesButton.setText('好') + msg_box.cancelButton.hide() + msg_box.buttonLayout.insertStretch(0, 1) + msg_box.setFixedWidth(550) + msg_box.exec() + stop(-1) + else: + mgr = WidgetsManager() + app.aboutToQuit.connect(mgr.cleanup_resources) + setup_signal_handlers_optimized(app) + + if config_center.read_conf('Other', 'initialstartup') == '1': # 首次启动 + try: + conf.add_shortcut('ClassWidgets.exe', f'{base_directory}/img/favicon.ico') + conf.add_shortcut_to_startmenu(f'{base_directory}/ClassWidgets.exe', + f'{base_directory}/img/favicon.ico') + config_center.write_conf('Other', 'initialstartup', '') + except Exception as e: + logger.error(f'添加快捷方式失败:{e}') + try: + list_.create_new_profile('新课表 - 1.json') + except Exception as e: + logger.error(f'创建新课表失败:{e}') + + p_mgr = PluginManager() + p_loader.set_manager(p_mgr) + p_loader.load_plugins() + + init() + get_start_time() + get_current_lessons() + get_current_lesson_name() + get_next_lessons() + + # 如果在全屏或最大化模式下启动,首先折叠主组件后显示浮动窗口动画。 + if check_windows_maximize() or check_fullscreen(): + mgr.decide_to_hide() # 折叠动画,其实这里可用`mgr.full_hide_windows()`但是播放动画似乎更好() + + if current_state == 1: + setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") + else: + setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") + + # w = ErrorDialog() + # w.exec() + if config_center.read_conf('Other', 'auto_check_update') == '1': + check_update() + + status = app.exec() + + utils.stop(status) diff --git a/menu.py b/menu.py new file mode 100644 index 0000000..345d775 --- /dev/null +++ b/menu.py @@ -0,0 +1,2140 @@ +import datetime as dt +import json +import os +import subprocess +import sys +from copy import deepcopy +from pathlib import Path +from shutil import rmtree + +from PyQt5 import uic, QtCore +from PyQt5.QtCore import Qt, QTime, QUrl, QDate, pyqtSignal +from PyQt5.QtGui import QIcon, QDesktopServices, QColor +from PyQt5.QtWidgets import QApplication, QHeaderView, QTableWidgetItem, QLabel, QHBoxLayout, QSizePolicy, \ + QSpacerItem, QFileDialog, QVBoxLayout, QScroller +from packaging.version import Version +from loguru import logger +from qfluentwidgets import ( + Theme, setTheme, FluentWindow, FluentIcon as fIcon, ToolButton, ListWidget, ComboBox, CaptionLabel, + SpinBox, LineEdit, PrimaryPushButton, TableWidget, Flyout, InfoBarIcon, InfoBar, InfoBarPosition, + FlyoutAnimationType, NavigationItemPosition, MessageBox, SubtitleLabel, PushButton, SwitchButton, + CalendarPicker, BodyLabel, ColorDialog, isDarkTheme, TimeEdit, EditableComboBox, MessageBoxBase, + SearchLineEdit, Slider, PlainTextEdit, ToolTipFilter, ToolTipPosition, RadioButton, HyperlinkLabel, + PrimaryDropDownPushButton, Action, RoundMenu, CardWidget, ImageLabel, StrongBodyLabel, + TransparentDropDownToolButton, Dialog, SmoothScrollArea, TransparentToolButton, HyperlinkButton +) + +import conf +import list_ as list_ +import tip_toast +import utils +from utils import update_tray_tooltip +import weather_db +import weather_db as wd +from conf import base_directory +from cses_mgr import CSES_Converter +from file import config_center, schedule_center +from network_thread import VersionThread +from plugin import p_loader +from plugin_plaza import PluginPlaza + +# 适配高DPI缩放 +QApplication.setHighDpiScaleFactorRoundingPolicy( + Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) +QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) +QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + +today = dt.date.today() +plugin_plaza = None + +plugin_dict = {} # 插件字典 +enabled_plugins = {} # 启用的插件列表 + +morning_st = 0 +afternoon_st = 0 + +current_week = 0 + +loaded_data = schedule_center.schedule_data + +schedule_dict = {} # 对应时间线的课程表 +schedule_even_dict = {} # 对应时间线的课程表(双周) + +timeline_dict = {} # 时间线字典 + +countdown_dict = {} + + +def open_plaza(): + global plugin_plaza + if plugin_plaza is None or not plugin_plaza.isVisible(): + plugin_plaza = PluginPlaza() + plugin_plaza.show() + plugin_plaza.closed.connect(cleanup_plaza) + logger.info('打开“插件广场”') + else: + plugin_plaza.raise_() + plugin_plaza.activateWindow() + + +def cleanup_plaza(): + global plugin_plaza + logger.info('关闭“插件广场”') + del plugin_plaza + plugin_plaza = None + + +def get_timeline(): + global loaded_data + loaded_data = schedule_center.schedule_data + return loaded_data['timeline'] + + +def open_dir(path: str): + if sys.platform.startswith('win32'): + os.startfile(path) + elif sys.platform.startswith('linux'): + subprocess.run(['xdg-open', path]) + else: + msg_box = Dialog( + '无法打开文件夹', f'Class Widgets 在您的系统下不支持自动打开文件夹,请手动打开以下地址:\n{path}' + ) + msg_box.yesButton.setText('好') + msg_box.cancelButton.hide() + msg_box.buttonLayout.insertStretch(0, 1) + msg_box.setFixedWidth(550) + msg_box.exec() + + +def switch_checked(section, key, checked): + if checked: + config_center.write_conf(section, key, '1') + else: + config_center.write_conf(section, key, '0') + if key == 'auto_startup': + if checked: + conf.add_to_startup() + else: + conf.remove_from_startup() + + +def get_theme_name(): + theme = config_center.read_conf('General', 'theme') + if os.path.exists(f'{base_directory}/ui/{theme}/theme.json'): + return theme + else: + return 'default' + + +def load_schedule_dict(schedule, part, part_name): + """ + 加载课表字典 + """ + schedule_dict_ = {} + for week, item in schedule.items(): + all_class = [] + count = [] # 初始化计数器 + for i in range(len(part)): + count.append(0) + if str(week) in loaded_data['timeline'] and loaded_data['timeline'][str(week)]: + timeline = get_timeline()[str(week)] + else: + timeline = get_timeline()['default'] + + for item_name, item_time in timeline.items(): + if item_name.startswith('a'): + try: + if int(item_name[1]) == 0: + count_num = 0 + else: + count_num = sum(count[:int(item_name[1])]) + + prefix = item[int(item_name[2:]) - 1 + count_num] + period = part_name[str(item_name[1])] + all_class.append(f'{prefix}-{period}') + except IndexError or ValueError: # 未设置值 + prefix = '未添加' + period = part_name[str(item_name[1])] + all_class.append(f'{prefix}-{period}') + count[int(item_name[1])] += 1 + schedule_dict_[week] = all_class + return schedule_dict_ + + +def convert_to_dict(data_dict_): + data_dict = {} + for week, item in data_dict_.items(): + cache_list = item + replace_list = [] + for activity_num in range(len(cache_list)): + item_info = cache_list[int(activity_num)].split('-') + replace_list.append(item_info[0]) + data_dict[str(week)] = replace_list + return data_dict + + +def se_load_item(): + global schedule_dict + global schedule_even_dict + global loaded_data + loaded_data = schedule_center.schedule_data + part_name = loaded_data.get('part_name') + part = loaded_data.get('part') + schedule = loaded_data.get('schedule') + schedule_even = loaded_data.get('schedule_even') + + schedule_dict = load_schedule_dict(schedule, part, part_name) + schedule_even_dict = load_schedule_dict(schedule_even, part, part_name) + + +def cd_load_item(): + global countdown_dict + text = config_center.read_conf('Date', 'cd_text_custom').split(',') + date = config_center.read_conf('Date', 'countdown_date').split(',') + if len(text) != len(date): + countdown_dict = {"Err": f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)})"} + raise Exception( + f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)})"f"len(cd_text_custom) (={len(text)}) != len(countdown_date) (={len(date)}) \n 请检查 config.ini [Date] 项!!") + countdown_dict = dict(zip(date, text)) + + +class selectCity(MessageBoxBase): # 选择城市 + def __init__(self, parent=None): + super().__init__(parent) + title_label = SubtitleLabel() + subtitle_label = BodyLabel() + self.search_edit = SearchLineEdit() + + title_label.setText('搜索城市') + subtitle_label.setText('请输入当地城市名进行搜索') + self.yesButton.setText('选择此城市') # 按钮组件汉化 + self.cancelButton.setText('取消') + + self.search_edit.setPlaceholderText('输入城市名') + self.search_edit.setClearButtonEnabled(True) + self.search_edit.textChanged.connect(self.search_city) + + self.city_list = ListWidget() + self.city_list.addItems(wd.search_by_name('')) + self.get_selected_city() + + # 将组件添加到布局中 + self.viewLayout.addWidget(title_label) + self.viewLayout.addWidget(subtitle_label) + self.viewLayout.addWidget(self.search_edit) + self.viewLayout.addWidget(self.city_list) + self.widget.setMinimumWidth(500) + self.widget.setMinimumHeight(600) + + def search_city(self): + self.city_list.clear() + self.city_list.addItems(wd.search_by_name(self.search_edit.text())) + self.city_list.clearSelection() # 清除选中项 + + def get_selected_city(self): + selected_city = self.city_list.findItems( + wd.search_by_num(str(config_center.read_conf('Weather', 'city'))), QtCore.Qt.MatchFlag.MatchExactly + ) + if selected_city: # 若找到该城市 + item = selected_city[0] + # 选中该项 + self.city_list.setCurrentItem(item) + # 聚焦该项 + self.city_list.scrollToItem(item) + + +class licenseDialog(MessageBoxBase): # 显示软件许可协议 + def __init__(self, parent=None): + super().__init__(parent) + title_label = SubtitleLabel() + subtitle_label = BodyLabel() + self.license_text = PlainTextEdit() + + title_label.setText('软件许可协议') + subtitle_label.setText('此项目 (Class Widgets) 基于 GPL-3.0 许可证授权发布,详情请参阅:') + self.yesButton.setText('好') # 按钮组件汉化 + self.cancelButton.hide() + self.buttonLayout.insertStretch(0, 1) + self.license_text.setPlainText(open('LICENSE', 'r', encoding='utf-8').read()) + self.license_text.setReadOnly(True) + + # 将组件添加到布局中 + self.viewLayout.addWidget(title_label) + self.viewLayout.addWidget(subtitle_label) + self.viewLayout.addWidget(self.license_text) + self.widget.setMinimumWidth(600) + self.widget.setMinimumHeight(500) + + +class PluginSettingsDialog(MessageBoxBase): # 插件设置对话框 + def __init__(self, plugin_dir=None, parent=None): + super().__init__(parent) + self.plugin_widget = None + self.plugin_dir = plugin_dir + self.parent = parent + self.init_ui() + + def init_ui(self): + # 加载已定义的UI + self.plugin_widget = p_loader.plugins_settings[self.plugin_dir] + self.viewLayout.addWidget(self.plugin_widget) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + + self.cancelButton.hide() + self.buttonLayout.insertStretch(0, 1) + + self.widget.setMinimumWidth(875) + self.widget.setMinimumHeight(625) + + +class PluginCard(CardWidget): # 插件卡片 + def __init__( + self, icon, title='Unknown', content='Unknown', version='1.0.0', plugin_dir='', author=None, parent=None, + enable_settings=None + ): + super().__init__(parent) + icon_radius = 5 + self.plugin_dir = plugin_dir + self.title = title + self.parent = parent + + self.iconWidget = ImageLabel(icon) # 插件图标 + self.titleLabel = StrongBodyLabel(title, self) # 插件名 + self.versionLabel = BodyLabel(version, self) # 插件版本 + self.authorLabel = BodyLabel(author, self) # 插件作者 + self.contentLabel = CaptionLabel(content, self) # 插件描述 + self.enableButton = SwitchButton() + self.moreButton = TransparentDropDownToolButton() + self.moreMenu = RoundMenu(parent=self.moreButton) + self.settingsBtn = TransparentToolButton() # 设置按钮 + self.settingsBtn.hide() + + self.hBoxLayout = QHBoxLayout(self) + self.hBoxLayout_Title = QHBoxLayout(self) + self.vBoxLayout = QVBoxLayout(self) + + self.moreMenu.addActions([ + Action( + fIcon.FOLDER, f'打开“{title}”插件文件夹', + triggered=lambda: open_dir(os.path.join(base_directory, conf.PLUGINS_DIR, self.plugin_dir)) + ), + Action( + fIcon.DELETE, f'卸载“{title}”插件', + triggered=self.remove_plugin + ) + ]) + + if plugin_dir in enabled_plugins['enabled_plugins']: # 插件是否启用 + self.enableButton.setChecked(True) + if enable_settings: + self.moreMenu.addSeparator() + self.moreMenu.addAction(Action(fIcon.SETTING, f'“{title}”插件设置', triggered=self.show_settings)) + self.settingsBtn.show() + + self.setFixedHeight(73) + self.iconWidget.setFixedSize(48, 48) + self.moreButton.setFixedSize(34, 34) + self.iconWidget.setBorderRadius(icon_radius, icon_radius, icon_radius, icon_radius) # 圆角 + self.contentLabel.setTextColor("#606060", "#d2d2d2") + self.contentLabel.setMaximumWidth(500) + self.contentLabel.setWordWrap(True) # 自动换行 + self.versionLabel.setTextColor("#999999", "#999999") + self.authorLabel.setTextColor("#606060", "#d2d2d2") + self.enableButton.checkedChanged.connect(self.set_enable) + self.enableButton.setOffText('禁用') + self.enableButton.setOnText('启用') + self.moreButton.setMenu(self.moreMenu) + self.settingsBtn.setIcon(fIcon.SETTING) + self.settingsBtn.clicked.connect(self.show_settings) + + self.hBoxLayout.setContentsMargins(20, 11, 11, 11) + self.hBoxLayout.setSpacing(15) + self.hBoxLayout.addWidget(self.iconWidget) + + # 内容 + self.vBoxLayout.setContentsMargins(0, 0, 0, 0) + self.vBoxLayout.setSpacing(0) + self.vBoxLayout.addLayout(self.hBoxLayout_Title) + self.vBoxLayout.addWidget(self.contentLabel, 0, Qt.AlignmentFlag.AlignVCenter) + self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter) + self.hBoxLayout.addLayout(self.vBoxLayout, 1) # !!! + + # 标题栏 + self.hBoxLayout_Title.setSpacing(12) + self.hBoxLayout_Title.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.hBoxLayout_Title.addWidget(self.titleLabel, 0, Qt.AlignmentFlag.AlignVCenter) + self.hBoxLayout_Title.addWidget(self.authorLabel, 0, Qt.AlignmentFlag.AlignVCenter) + self.hBoxLayout_Title.addWidget(self.versionLabel, 0, Qt.AlignmentFlag.AlignVCenter) + + self.hBoxLayout.addStretch(1) + self.hBoxLayout.addWidget(self.settingsBtn, 0, Qt.AlignmentFlag.AlignRight) + self.hBoxLayout.addWidget(self.enableButton, 0, Qt.AlignmentFlag.AlignRight) + self.hBoxLayout.addWidget(self.moreButton, 0, Qt.AlignmentFlag.AlignRight) + + def set_enable(self): + global enabled_plugins + if self.enableButton.isChecked(): + enabled_plugins['enabled_plugins'].append(self.plugin_dir) + conf.save_plugin_config(enabled_plugins) + else: + enabled_plugins['enabled_plugins'].remove(self.plugin_dir) + conf.save_plugin_config(enabled_plugins) + + def show_settings(self): + w = PluginSettingsDialog(self.plugin_dir, self.parent) + w.exec() + + def remove_plugin(self): + alert = MessageBox(f"您确定要删除插件“{self.title}”吗?", "删除此插件后,将无法恢复。", self.parent) + alert.yesButton.setText('永久删除') + alert.yesButton.setStyleSheet(""" + PushButton{ + border-radius: 5px; + padding: 5px 12px 6px 12px; + outline: none; + } + PrimaryPushButton{ + color: white; + background-color: #FF6167; + border: 1px solid #FF8585; + border-bottom: 1px solid #943333; + } + PrimaryPushButton:hover{ + background-color: #FF7E83; + border: 1px solid #FF8084; + border-bottom: 1px solid #B13939; + } + PrimaryPushButton:pressed{ + color: rgba(255, 255, 255, 0.63); + background-color: #DB5359; + border: 1px solid #DB5359; + } + """) + alert.cancelButton.setText('我再想想……') + if alert.exec(): + success = p_loader.delete_plugin(self.plugin_dir) + if success: + try: + with open(f'{base_directory}/plugins/plugins_from_pp.json', 'r', encoding='utf-8') as f: + installed_data = json.load(f) + installed_plugins = installed_data.get('plugins', []) + if self.plugin_dir in installed_plugins: + installed_plugins.remove(self.plugin_dir) + conf.save_installed_plugin(installed_plugins) + except Exception as e: + logger.error(f"更新已安装插件列表失败: {e}") + + InfoBar.success( + title='卸载成功', + content=f'插件 “{self.title}” 已卸载。请重启 Class Widgets 以完全移除。', + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.BOTTOM_RIGHT, + duration=5000, + parent=self.window() + ) + self.deleteLater() # 删除卡片 + else: + InfoBar.error( + title='卸载失败', + content=f'卸载插件 “{self.title}” 时出错,请查看日志获取详细信息。', + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.BOTTOM_RIGHT, + duration=5000, + parent=self.window() + ) + + +class TextFieldMessageBox(MessageBoxBase): + """ Custom message box """ + + def __init__( + self, parent=None, title='标题', text='请输入内容', default_text='', enable_check=False): + super().__init__(parent) + self.fail_color = (QColor('#c42b1c'), QColor('#ff99a4')) + self.success_color = (QColor('#0f7b0f'), QColor('#6ccb5f')) + self.check_list = enable_check + + self.titleLabel = SubtitleLabel() + self.titleLabel.setText(title) + self.subtitleLabel = BodyLabel() + self.subtitleLabel.setText(text) + self.textField = LineEdit() + self.tipsLabel = CaptionLabel() + self.tipsLabel.setText('') + self.yesButton.setText('确定') + + self.fieldLayout = QVBoxLayout() + self.textField.setPlaceholderText(default_text) + self.textField.setClearButtonEnabled(True) + if enable_check: + self.textField.textChanged.connect(self.check_text) + self.yesButton.setEnabled(False) + + # 将组件添加到布局中 + self.viewLayout.addWidget(self.titleLabel) + self.viewLayout.addWidget(self.subtitleLabel) + self.viewLayout.addLayout(self.fieldLayout) + self.fieldLayout.addWidget(self.textField) + self.fieldLayout.addWidget(self.tipsLabel) + + # 设置对话框的最小宽度 + self.widget.setMinimumWidth(350) + + def check_text(self): + self.tipsLabel.setTextColor(self.fail_color[0], self.fail_color[1]) + self.yesButton.setEnabled(False) + if self.textField.text() == '': + self.tipsLabel.setText('不能为空值啊 ( •̀ ω •́ )✧') + return + if f'{self.textField.text()}.json' in self.check_list: + self.tipsLabel.setText('不可以和之前的课程名重复哦 o(TヘTo)') + return + + self.yesButton.setEnabled(True) + self.tipsLabel.setTextColor(self.success_color[0], self.success_color[1]) + self.tipsLabel.setText('很好!就这样!ヾ(≧▽≦*)o') + + +class SettingsMenu(FluentWindow): + closed = pyqtSignal() + + def __init__(self): + super().__init__() + self.button_clear_log = None + self.version_thread = None + + # 创建子页面 + self.spInterface = uic.loadUi(f'{base_directory}/view/menu/preview.ui') # 预览 + self.spInterface.setObjectName("spInterface") + self.teInterface = uic.loadUi(f'{base_directory}/view/menu/timeline_edit.ui') # 时间线编辑 + self.teInterface.setObjectName("teInterface") + self.seInterface = uic.loadUi(f'{base_directory}/view/menu/schedule_edit.ui') # 课程表编辑 + self.seInterface.setObjectName("seInterface") + self.cdInterface = uic.loadUi(f'{base_directory}/view/menu/countdown_custom_edit.ui') # 倒计日编辑 + self.cdInterface.setObjectName("cdInterface") + self.adInterface = uic.loadUi(f'{base_directory}/view/menu/advance.ui') # 高级选项 + self.adInterface.setObjectName("adInterface") + self.ifInterface = uic.loadUi(f'{base_directory}/view/menu/about.ui') # 关于 + self.ifInterface.setObjectName("ifInterface") + self.ctInterface = uic.loadUi(f'{base_directory}/view/menu/custom.ui') # 自定义 + self.ctInterface.setObjectName("ctInterface") + self.cfInterface = uic.loadUi(f'{base_directory}/view/menu/configs.ui') # 配置文件 + self.cfInterface.setObjectName("cfInterface") + self.sdInterface = uic.loadUi(f'{base_directory}/view/menu/sound.ui') # 通知 + self.sdInterface.setObjectName("sdInterface") + self.hdInterface = uic.loadUi(f'{base_directory}/view/menu/help.ui') # 帮助 + self.hdInterface.setObjectName("hdInterface") + self.plInterface = uic.loadUi(f'{base_directory}/view/menu/plugin_mgr.ui') # 插件 + self.plInterface.setObjectName("plInterface") + + self.init_nav() + self.init_window() + + def init_font(self): # 设置字体 + self.setStyleSheet("""QLabel { + font-family: 'Microsoft YaHei'; + }""") + + def load_all_item(self): + self.setup_timeline_edit() + self.setup_schedule_edit() + self.setup_schedule_preview() + self.setup_advance_interface() + self.setup_about_interface() + self.setup_customization_interface() + self.setup_configs_interface() + self.setup_sound_interface() + self.setup_help_interface() + self.setup_plugin_mgr_interface() + self.setup_countdown_edit() + + # 初始化界面 + def setup_plugin_mgr_interface(self): + pm_scroll = self.findChild(SmoothScrollArea, 'pm_scroll') + QScroller.grabGesture(pm_scroll.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配 + + global plugin_dict, enabled_plugins + enabled_plugins = conf.load_plugin_config() # 加载启用的插件 + plugin_dict = (conf.load_plugins()) # 加载插件信息 + + open_pp = self.findChild(PushButton, 'open_plugin_plaza') + open_pp.clicked.connect(open_plaza) # 打开插件广场 + + open_pp2 = self.findChild(PushButton, 'open_plugin_plaza_2') + open_pp2.clicked.connect(open_plaza) # 打开插件广场 + + auto_delay = self.findChild(SpinBox, 'auto_delay') + auto_delay.setValue(int(config_center.read_conf('Plugin', 'auto_delay'))) + auto_delay.valueChanged.connect( + lambda: config_center.write_conf('Plugin', 'auto_delay', str(auto_delay.value()))) + # 设置自动化延迟 + + plugin_card_layout = self.findChild(QVBoxLayout, 'plugin_card_layout') + open_plugin_folder = self.findChild(PushButton, 'open_plugin_folder') + open_plugin_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, conf.PLUGINS_DIR))) # 打开插件目录 + + if not p_loader.plugins_settings: # 若插件设置为空 + p_loader.load_plugins() # 加载插件设置 + + for plugin in plugin_dict: + if (Path(conf.PLUGINS_DIR) / plugin / 'icon.png').exists(): # 若插件目录存在icon.png + icon_path = f'{base_directory}/plugins/{plugin}/icon.png' + else: + icon_path = f'{base_directory}/img/settings/plugin-icon.png' + card = PluginCard( + icon=icon_path, + title=plugin_dict[plugin]['name'], + version=plugin_dict[plugin]['version'], + author=plugin_dict[plugin]['author'], + plugin_dir=plugin, + content=plugin_dict[plugin]['description'], + enable_settings=plugin_dict[plugin]['settings'], + parent=self + ) + plugin_card_layout.addWidget(card) + + tips_plugin_empty = self.findChild(QLabel, 'tips_plugin_empty') + if plugin_dict: + tips_plugin_empty.hide() + + def setup_help_interface(self): + open_by_browser = self.findChild(PushButton, 'open_by_browser') + open_by_browser.setIcon(fIcon.LINK) + open_by_browser.clicked.connect(lambda: QDesktopServices.openUrl(QUrl( + 'https://classwidgets.rinlit.cn/docs-user/' + ))) + + def setup_sound_interface(self): + sd_scroll = self.findChild(SmoothScrollArea, 'sd_scroll') # 触摸屏适配 + QScroller.grabGesture(sd_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + switch_enable_toast = self.findChild(SwitchButton, 'switch_enable_attend') + switch_enable_toast.setChecked(int(config_center.read_conf('Toast', 'attend_class'))) + switch_enable_toast.checkedChanged.connect(lambda checked: switch_checked('Toast', 'attend_class', checked)) + # 上课提醒开关 + + switch_enable_finish = self.findChild(SwitchButton, 'switch_enable_finish') + switch_enable_finish.setChecked(int(config_center.read_conf('Toast', 'finish_class'))) + switch_enable_finish.checkedChanged.connect(lambda checked: switch_checked('Toast', 'finish_class', checked)) + # 下课提醒开关 + + switch_enable_finish = self.findChild(SwitchButton, 'switch_enable_schoolout') + switch_enable_finish.setChecked(int(config_center.read_conf('Toast', 'after_school'))) + switch_enable_finish.checkedChanged.connect(lambda checked: switch_checked('Toast', 'after_school', checked)) + # 放学提醒开关 + + switch_enable_prepare = self.findChild(SwitchButton, 'switch_enable_prepare') + switch_enable_prepare.setChecked(int(config_center.read_conf('Toast', 'prepare_class'))) + switch_enable_prepare.checkedChanged.connect(lambda checked: switch_checked('Toast', 'prepare_class', checked)) + # 预备铃开关 + + switch_enable_pin_toast = self.findChild(SwitchButton, 'switch_enable_pin_toast') + switch_enable_pin_toast.setChecked(int(config_center.read_conf('Toast', 'pin_on_top'))) + switch_enable_pin_toast.checkedChanged.connect(lambda checked: switch_checked('Toast', 'pin_on_top', checked)) + # 置顶开关 + + slider_volume = self.findChild(Slider, 'slider_volume') + slider_volume.setValue(int(config_center.read_conf('Audio', 'volume'))) + slider_volume.valueChanged.connect(self.save_volume) # 音量滑块 + + preview_toast_button = self.findChild(PrimaryDropDownPushButton, 'preview') + + pre_toast_menu = RoundMenu(parent=preview_toast_button) + pre_toast_menu.addActions([ + Action(fIcon.EDUCATION, '上课提醒', + triggered=lambda: tip_toast.push_notification(1, lesson_name='信息技术')), + Action(fIcon.CAFE, '下课提醒', + triggered=lambda: tip_toast.push_notification(0, lesson_name='信息技术')), + Action(fIcon.BOOK_SHELF, '预备提醒', + triggered=lambda: tip_toast.push_notification(3, lesson_name='信息技术')), + Action(fIcon.CODE, '其他提醒', + triggered=lambda: tip_toast.push_notification(4, title='通知', subtitle='测试通知示例', + content='这是一条测试通知ヾ(≧▽≦*)o')) + ]) + preview_toast_button.setMenu(pre_toast_menu) # 预览通知栏 + + switch_wave_effect = self.findChild(SwitchButton, 'switch_enable_wave') + switch_wave_effect.setChecked(int(config_center.read_conf('Toast', 'wave'))) + switch_wave_effect.checkedChanged.connect(lambda checked: switch_checked('Toast', 'wave', checked)) # 波纹开关 + + spin_prepare_time = self.findChild(SpinBox, 'spin_prepare_class') + spin_prepare_time.setValue(int(config_center.read_conf('Toast', 'prepare_minutes'))) + spin_prepare_time.valueChanged.connect(self.save_prepare_time) # 准备时间 + + def setup_configs_interface(self): # 配置界面 + cf_import_schedule = self.findChild(PushButton, 'im_schedule') + cf_import_schedule.clicked.connect(self.cf_import_schedule) # 导入课程表 + cf_export_schedule = self.findChild(PushButton, 'ex_schedule') + cf_export_schedule.clicked.connect(self.cf_export_schedule) # 导出课程表 + cf_open_schedule_folder = self.findChild(PushButton, 'open_schedule_folder') # 打开课程表文件夹 + cf_open_schedule_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, 'config/schedule'))) + + cf_import_schedule_cses = self.findChild(PushButton, 'im_schedule_cses') + cf_import_schedule_cses.clicked.connect(self.cf_import_schedule_cses) # 导入课程表(CSES) + cf_export_schedule_cses = self.findChild(PushButton, 'ex_schedule_cses') + cf_export_schedule_cses.clicked.connect(self.cf_export_schedule_cses) # 导出课程表(CSES) + cf_what_is_cses = self.findChild(HyperlinkButton, 'what_is') + cf_what_is_cses.setUrl(QUrl('https://github.com/CSES-org/CSES')) + + def setup_customization_interface(self): + ct_scroll = self.findChild(SmoothScrollArea, 'ct_scroll') # 触摸屏适配 + QScroller.grabGesture(ct_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + self.ct_update_preview() + + widgets_list_widgets = self.findChild(ListWidget, 'widgets_list') + widgets_list = [] + for key in list_.get_widget_config(): + try: + widgets_list.append(list_.widget_name[key]) + except KeyError: + logger.warning(f'未知的组件:{key}') + except Exception as e: + logger.error(f'获取组件名称时发生错误:{sys.exc_info()[0]}/{e}') + widgets_list_widgets.addItems(widgets_list) + widgets_list_widgets.sizePolicy().setVerticalPolicy(QSizePolicy.Policy.MinimumExpanding) + + save_config_button = self.findChild(PrimaryPushButton, 'save_config') + save_config_button.clicked.connect(self.ct_save_widget_config) + + set_ac_color = self.findChild(PushButton, 'set_ac_color') # 主题色 + set_ac_color.clicked.connect(self.ct_set_ac_color) + set_fc_color = self.findChild(PushButton, 'set_fc_color') + set_fc_color.clicked.connect(self.ct_set_fc_color) + set_floating_time_color = self.findChild(PushButton, 'set_fc_color_2') + set_floating_time_color.clicked.connect(self.ct_set_floating_time_color) + + open_theme_folder = self.findChild(HyperlinkLabel, 'open_theme_folder') # 打开主题文件夹 + open_theme_folder.clicked.connect(lambda: open_dir(os.path.join(base_directory, 'ui'))) + + select_theme_combo = self.findChild(ComboBox, 'combo_theme_select') # 主题选择 + select_theme_combo.addItems(list_.theme_names) + print(list_.theme_folder, list_.theme_names, get_theme_name()) + select_theme_combo.setCurrentIndex(list_.theme_folder.index(get_theme_name())) + select_theme_combo.currentIndexChanged.connect( + lambda: config_center.write_conf('General', 'theme', + list_.get_theme_ui_path(select_theme_combo.currentText()))) + + color_mode_combo = self.findChild(ComboBox, 'combo_color_mode') # 颜色模式选择 + color_mode_combo.addItems(list_.color_mode) + color_mode_combo.setCurrentIndex(int(config_center.read_conf('General', 'color_mode'))) + color_mode_combo.currentIndexChanged.connect(self.ct_change_color_mode) + + widgets_combo = self.findChild(ComboBox, 'widgets_combo') # 组件选择 + widgets_combo.addItems(list_.get_widget_names()) + + search_city_button = self.findChild(PushButton, 'select_city') # 查找城市 + search_city_button.clicked.connect(self.show_search_city) + + add_widget_button = self.findChild(PrimaryPushButton, 'add_widget') + add_widget_button.clicked.connect(self.ct_add_widget) + + remove_widget_button = self.findChild(PushButton, 'remove_widget') + remove_widget_button.clicked.connect(self.ct_remove_widget) + + slider_opacity = self.findChild(Slider, 'slider_opacity') + slider_opacity.setValue(int(config_center.read_conf('General', 'opacity'))) + slider_opacity.valueChanged.connect( + lambda: config_center.write_conf('General', 'opacity', str(slider_opacity.value())) + ) # 透明度 + + blur_countdown = self.findChild(SwitchButton, 'switch_blur_countdown') + blur_countdown.setChecked(int(config_center.read_conf('General', 'blur_countdown'))) + blur_countdown.checkedChanged.connect(lambda checked: switch_checked('General', 'blur_countdown', checked)) + # 模糊倒计时 + switch_blur_floating = self.findChild(SwitchButton, 'switch_blur_countdown_2') + switch_blur_floating.setChecked(int(config_center.read_conf('General', 'blur_floating_countdown'))) + switch_blur_floating.checkedChanged.connect( + lambda checked: config_center.write_conf('General', 'blur_floating_countdown', int(checked)) + ) + + select_weather_api = self.findChild(ComboBox, 'select_weather_api') # 天气API选择 + select_weather_api.addItems(weather_db.api_config['weather_api_list_zhCN']) + select_weather_api.setCurrentIndex(weather_db.api_config['weather_api_list'].index( + config_center.read_conf('Weather', 'api') + )) + select_weather_api.currentIndexChanged.connect( + lambda: config_center.write_conf('Weather', 'api', + weather_db.api_config['weather_api_list'][ + select_weather_api.currentIndex()]) + ) + + api_key_edit = self.findChild(LineEdit, 'api_key_edit') # API密钥 + api_key_edit.setText(config_center.read_conf('Weather', 'api_key')) + api_key_edit.textChanged.connect(lambda: config_center.write_conf('Weather', 'api_key', api_key_edit.text())) + + def setup_about_interface(self): + ab_scroll = self.findChild(SmoothScrollArea, 'ab_scroll') # 触摸屏适配 + QScroller.grabGesture(ab_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + self.version = self.findChild(BodyLabel, 'version') + + check_update_btn = self.findChild(PrimaryPushButton, 'check_update') + check_update_btn.setIcon(fIcon.SYNC) + check_update_btn.clicked.connect(self.check_update) + + self.auto_check_update = self.ifInterface.findChild(SwitchButton, 'auto_check_update') + self.auto_check_update.setChecked(int(config_center.read_conf("Other", "auto_check_update"))) + self.auto_check_update.checkedChanged.connect( + lambda checked: switch_checked("Other", "auto_check_update", checked) + ) # 自动检查更新 + + self.version_channel = self.findChild(ComboBox, 'version_channel') + self.version_channel.addItems(list_.version_channel) + self.version_channel.setCurrentIndex(int(config_center.read_conf("Other", "version_channel"))) + self.version_channel.currentIndexChanged.connect( + lambda: config_center.write_conf("Other", "version_channel", self.version_channel.currentIndex()) + ) # 版本更新通道 + + github_page = self.findChild(PushButton, "button_github") + github_page.clicked.connect(lambda: QDesktopServices.openUrl(QUrl( + 'https://github.com/RinLit-233-shiroko/Class-Widgets'))) + + bilibili_page = self.findChild(PushButton, 'button_bilibili') + bilibili_page.clicked.connect(lambda: QDesktopServices.openUrl(QUrl( + 'https://space.bilibili.com/569522843'))) + + license_button = self.findChild(PushButton, 'button_show_license') + license_button.clicked.connect(self.show_license) + + thanks_button = self.findChild(PushButton, 'button_thanks') + thanks_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl( + 'https://github.com/RinLit-233-shiroko/Class-Widgets?tab=readme-ov-file#致谢'))) + + self.check_update() + + def setup_advance_interface(self): + adv_scroll = self.adInterface.findChild(SmoothScrollArea, 'adv_scroll') # 触摸屏适配 + QScroller.grabGesture(adv_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + margin_spin = self.adInterface.findChild(SpinBox, 'margin_spin') + margin_spin.setValue(int(config_center.read_conf('General', 'margin'))) + margin_spin.valueChanged.connect( + lambda: config_center.write_conf('General', 'margin', str(margin_spin.value())) + ) # 保存边距设定 + + self.conf_combo = self.adInterface.findChild(ComboBox, 'conf_combo') + self.conf_combo.clear() + self.conf_combo.addItems(list_.get_schedule_config()) + self.conf_combo.setCurrentIndex( + list_.get_schedule_config().index(config_center.read_conf('General', 'schedule'))) + self.conf_combo.currentIndexChanged.connect(self.ad_change_file) # 切换配置文件 + + conf_name = self.adInterface.findChild(LineEdit, 'conf_name') + conf_name.setText(config_center.schedule_name[:-5]) + conf_name.textEdited.connect(self.ad_change_file_name) + + window_status_combo = self.adInterface.findChild(ComboBox, 'window_status_combo') + window_status_combo.addItems(list_.window_status) + window_status_combo.setCurrentIndex(int(config_center.read_conf('General', 'pin_on_top'))) + window_status_combo.currentIndexChanged.connect( + lambda: config_center.write_conf('General', 'pin_on_top', str(window_status_combo.currentIndex())) + ) # 窗口状态 + + switch_startup = self.adInterface.findChild(SwitchButton, 'switch_startup') + switch_startup.setChecked(int(config_center.read_conf('General', 'auto_startup'))) + switch_startup.checkedChanged.connect(lambda checked: switch_checked('General', 'auto_startup', checked)) + # 开机自启 + if os.name != 'nt': + switch_startup.setEnabled(False) + + hide_mode_combo = self.adInterface.findChild(ComboBox, 'hide_mode_combo') + hide_mode_combo.addItems(list_.hide_mode if os.name == 'nt' else list_.non_nt_hide_mode) + hide_mode_combo.setCurrentIndex(int(config_center.read_conf('General', 'hide'))) + hide_mode_combo.currentIndexChanged.connect( + lambda: config_center.write_conf('General', 'hide', str(hide_mode_combo.currentIndex())) + ) # 隐藏模式 + + hide_method_default = self.adInterface.findChild(RadioButton, 'hide_method_default') + hide_method_default.setChecked(config_center.read_conf('General', 'hide_method') == '0') + hide_method_default.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '0')) + if os.name != 'nt': + hide_method_default.setEnabled(False) + # 默认隐藏 + + hide_method_all = self.adInterface.findChild(RadioButton, 'hide_method_all') + hide_method_all.setChecked(config_center.read_conf('General', 'hide_method') == '1') + hide_method_all.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '1')) + # 单击全部隐藏 + + hide_method_floating = self.adInterface.findChild(RadioButton, 'hide_method_floating') + hide_method_floating.setChecked(config_center.read_conf('General', 'hide_method') == '2') + hide_method_floating.toggled.connect(lambda: config_center.write_conf('General', 'hide_method', '2')) + # 最小化为浮窗 + + switch_enable_exclude = self.adInterface.findChild(SwitchButton, 'switch_exclude_startup') + switch_enable_exclude.setChecked(int(config_center.read_conf('General', 'excluded_lesson'))) + switch_enable_exclude.checkedChanged.connect( + lambda checked: switch_checked('General', 'excluded_lesson', checked)) + # 允许排除课程 + + exclude_lesson = self.adInterface.findChild(LineEdit, 'excluded_lessons') + exclude_lesson.setText(config_center.read_conf('General', 'excluded_lessons')) + exclude_lesson.textChanged.connect( + lambda: config_center.write_conf('General', 'excluded_lessons', exclude_lesson.text())) + # 排除课程 + + switch_enable_click = self.adInterface.findChild(SwitchButton, 'switch_enable_click') + switch_enable_click.setChecked(int(config_center.read_conf('General', 'enable_click'))) + switch_enable_click.checkedChanged.connect(lambda checked: switch_checked('General', 'enable_click', checked)) + # 允许点击 + + switch_enable_alt_schedule = self.adInterface.findChild(SwitchButton, 'switch_enable_alt_schedule') + switch_enable_alt_schedule.setChecked(int(config_center.read_conf('General', 'enable_alt_schedule'))) + switch_enable_alt_schedule.checkedChanged.connect( + lambda checked: switch_checked('General', 'enable_alt_schedule', checked) + ) # 安全模式 + + switch_enable_safe_mode = self.adInterface.findChild(SwitchButton, 'switch_safe_mode') + switch_enable_safe_mode.setChecked(int(config_center.read_conf('Other', 'safe_mode'))) + switch_enable_safe_mode.checkedChanged.connect( + lambda checked: switch_checked('Other', 'safe_mode', checked) + ) + # 安全模式开关 + + switch_enable_multiple_programs = self.adInterface.findChild(SwitchButton, 'switch_multiple_programs') + switch_enable_multiple_programs.setChecked(int(config_center.read_conf('Other', 'multiple_programs'))) + switch_enable_multiple_programs.checkedChanged.connect( + lambda checked: switch_checked('Other', 'multiple_programs', checked) + ) # 多开程序 + + switch_disable_log = self.adInterface.findChild(SwitchButton, 'switch_disable_log') + switch_disable_log.setChecked(int(config_center.read_conf('Other', 'do_not_log'))) + switch_disable_log.checkedChanged.connect( + lambda checked: switch_checked('Other', 'do_not_log', checked) + ) # 禁用日志 + + button_clear_log = self.adInterface.findChild(PushButton, 'button_clear_log') + button_clear_log.clicked.connect(self.clear_log) # 清空日志 + + set_start_date = self.adInterface.findChild(CalendarPicker, 'set_start_date') # 日期 + if config_center.read_conf('Date', 'start_date') != '': + set_start_date.setDate(QDate.fromString(config_center.read_conf('Date', 'start_date'), 'yyyy-M-d')) + set_start_date.dateChanged.connect( + lambda: config_center.write_conf('Date', 'start_date', set_start_date.date.toString('yyyy-M-d'))) # 开学日期 + + offset_spin = self.adInterface.findChild(SpinBox, 'offset_spin') + offset_spin.setValue(int(config_center.read_conf('General', 'time_offset'))) + offset_spin.valueChanged.connect( + lambda: config_center.write_conf('General', 'time_offset', str(offset_spin.value())) + ) # 保存时差偏移 + + text_scale_factor = self.adInterface.findChild(LineEdit, 'text_scale_factor') + text_scale_factor.setText(str(float(config_center.read_conf('General', 'scale')) * 100) + '%') # 初始化缩放系数显示 + + slider_scale_factor = self.adInterface.findChild(Slider, 'slider_scale_factor') + slider_scale_factor.setValue(int(float(config_center.read_conf('General', 'scale')) * 100)) + slider_scale_factor.valueChanged.connect( + lambda: (config_center.write_conf('General', 'scale', str(slider_scale_factor.value() / 100)), + text_scale_factor.setText(str(slider_scale_factor.value()) + '%')) + ) # 保存缩放系数 + + what_is_hide_mode_3 = self.adInterface.findChild(HyperlinkLabel, 'what_is_hide_mode_3') + + def what_is_hide_mode_3_clicked(): + w = MessageBox('灵活模式', '灵活模式为上课时自动隐藏,可手动改变隐藏状态,当前课程状态(上课/课间)改变后会清除手动隐藏状态,重新转为自动隐藏。', self) + w.cancelButton.hide() + w.exec() + what_is_hide_mode_3.clicked.connect(what_is_hide_mode_3_clicked) + + def setup_schedule_edit(self): + se_load_item() + se_set_button = self.findChild(ToolButton, 'set_button') + se_set_button.setIcon(fIcon.EDIT) + se_set_button.setToolTip('编辑课程') + se_set_button.installEventFilter(ToolTipFilter(se_set_button, showDelay=300, position=ToolTipPosition.TOP)) + se_set_button.clicked.connect(self.se_edit_item) + + se_clear_button = self.findChild(ToolButton, 'clear_button') + se_clear_button.setIcon(fIcon.DELETE) + se_clear_button.setToolTip('清空课程') + se_clear_button.installEventFilter(ToolTipFilter(se_clear_button, showDelay=300, position=ToolTipPosition.TOP)) + se_clear_button.clicked.connect(self.se_delete_item) + + se_class_kind_combo = self.findChild(ComboBox, 'class_combo') # 课程类型 + se_class_kind_combo.addItems(list_.class_kind) + + se_week_combo = self.findChild(ComboBox, 'week_combo') # 星期 + se_week_combo.addItems(list_.week) + se_week_combo.currentIndexChanged.connect(self.se_upload_list) + + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + se_schedule_list.addItems(schedule_dict[str(current_week)]) + se_schedule_list.itemChanged.connect(self.se_upload_item) + QScroller.grabGesture(se_schedule_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配 + + se_save_button = self.findChild(PrimaryPushButton, 'save_schedule') + se_save_button.clicked.connect(self.se_save_item) + + se_week_type_combo = self.findChild(ComboBox, 'week_type_combo') + se_week_type_combo.addItems(list_.week_type) + se_week_type_combo.currentIndexChanged.connect(self.se_upload_list) + + se_copy_schedule_button = self.findChild(PushButton, 'copy_schedule') + se_copy_schedule_button.hide() + se_copy_schedule_button.clicked.connect(self.se_copy_odd_schedule) + + quick_set_schedule = self.findChild(ListWidget, 'subject_list') + quick_set_schedule.addItems(list_.class_kind[1:]) + quick_set_schedule.itemClicked.connect(self.se_quick_set_schedule) + + quick_select_week_button = self.findChild(PushButton, 'quick_select_week') + quick_select_week_button.clicked.connect(self.se_quick_select_week) + + def setup_timeline_edit(self): # 底层大改 + self.te_load_item() # 加载时段 + # teInterface + te_add_button = self.findChild(ToolButton, 'add_button') # 添加 + te_add_button.setIcon(fIcon.ADD) + te_add_button.setToolTip('添加时间线') # 增加提示 + te_add_button.installEventFilter(ToolTipFilter(te_add_button, showDelay=300, position=ToolTipPosition.TOP)) + te_add_button.clicked.connect(self.te_add_item) + te_add_button.clicked.connect(self.te_upload_item) + + te_add_part_button = self.findChild(ToolButton, 'add_part_button') # 添加节点 + te_add_part_button.setIcon(fIcon.ADD) + te_add_part_button.setToolTip('添加节点') + te_add_part_button.installEventFilter( + ToolTipFilter(te_add_part_button, showDelay=300, position=ToolTipPosition.TOP)) + te_add_part_button.clicked.connect(self.te_add_part) + + te_part_type_combo = self.findChild(ComboBox, 'part_type') # 节次类型 + te_part_type_combo.clear() + te_part_type_combo.addItems(list_.part_type) + + te_name_edit = self.findChild(EditableComboBox, 'name_part_combo') # 名称 + te_name_edit.addItems(list_.time) + + te_delete_part_button = self.findChild(ToolButton, 'delete_part_button') # 删除节点 + te_delete_part_button.setIcon(fIcon.DELETE) + te_delete_part_button.setToolTip('删除节点') + te_delete_part_button.installEventFilter( + ToolTipFilter(te_delete_part_button, showDelay=300, position=ToolTipPosition.TOP)) + te_delete_part_button.clicked.connect(self.te_delete_part) + + te_edit_button = self.findChild(ToolButton, 'edit_button') # 编辑 + te_edit_button.setIcon(fIcon.EDIT) + te_edit_button.setToolTip('编辑时间线') + te_edit_button.installEventFilter(ToolTipFilter(te_edit_button, showDelay=300, position=ToolTipPosition.TOP)) + te_edit_button.clicked.connect(self.te_edit_item) + + te_delete_button = self.findChild(ToolButton, 'delete_button') # 删除 + te_delete_button.setIcon(fIcon.DELETE) + te_delete_button.setToolTip('删除时间线') + te_delete_button.installEventFilter( + ToolTipFilter(te_delete_button, showDelay=300, position=ToolTipPosition.TOP)) + te_delete_button.clicked.connect(self.te_delete_item) + te_delete_button.clicked.connect(self.te_upload_item) + + te_class_activity_combo = self.findChild(ComboBox, 'class_activity') # 活动类型 + te_class_activity_combo.addItems(list_.class_activity) + te_class_activity_combo.setToolTip('选择活动类型(“课程”或“课间”)') + te_class_activity_combo.currentIndexChanged.connect(self.te_sync_time) + + te_select_timeline = self.findChild(ComboBox, 'select_timeline') # 选择时间线 + te_select_timeline.addItem('默认') + te_select_timeline.addItems(list_.week) + te_select_timeline.setToolTip('选择一周内的某一天的时间线') + te_select_timeline.currentIndexChanged.connect(self.te_upload_list) + + te_timeline_list = self.findChild(ListWidget, 'timeline_list') # 所选时间线列表 + te_timeline_list.addItems(timeline_dict['default']) + te_timeline_list.itemChanged.connect(self.te_upload_item) + + te_part_time = self.teInterface.findChild(TimeEdit, 'part_time') # 节次时间 + te_part_time.timeChanged.connect( + lambda: self.show_tip_flyout('重要提示', '请使用 24 小时制', te_part_time) + ) + + te_save_button = self.findChild(PrimaryPushButton, 'save') # 保存 + te_save_button.clicked.connect(self.te_save_item) + + part_list = self.findChild(ListWidget, 'part_list') + QScroller.grabGesture(te_timeline_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配 + QScroller.grabGesture(part_list.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配 + self.te_detect_item() + self.te_update_parts_name() # 修复在启动时无法添加时段到下拉框的问题 + + def setup_schedule_preview(self): + subtitle = self.findChild(SubtitleLabel, 'subtitle_file') + subtitle.setText(f'预览 - {config_center.schedule_name[:-5]}') + + schedule_view = self.findChild(TableWidget, 'schedule_view') + schedule_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) # 使列表自动等宽 + + sp_week_type_combo = self.findChild(ComboBox, 'pre_week_type_combo') + sp_week_type_combo.addItems(list_.week_type) + sp_week_type_combo.currentIndexChanged.connect(self.sp_fill_grid_row) + + # 设置表格 + schedule_view.setColumnCount(7) + schedule_view.setHorizontalHeaderLabels(list_.week[0:7]) + schedule_view.setBorderVisible(True) + schedule_view.verticalHeader().hide() + schedule_view.setBorderRadius(8) + QScroller.grabGesture(schedule_view.viewport(), QScroller.LeftMouseButtonGesture) # 触摸屏适配 + self.sp_fill_grid_row() + + def save_volume(self): + slider_volume = self.findChild(Slider, 'slider_volume') + config_center.write_conf('Audio', 'volume', str(slider_volume.value())) + + def show_search_city(self): + search_city_dialog = selectCity(self) + if search_city_dialog.exec(): + selected_city = search_city_dialog.city_list.selectedItems() + if selected_city: + config_center.write_conf('Weather', 'city', wd.search_code_by_name((selected_city[0].text(),''))) + + def show_license(self): + license_dialog = licenseDialog(self) + license_dialog.exec() + + def save_prepare_time(self): + prepare_time_spin = self.findChild(SpinBox, 'spin_prepare_class') + config_center.write_conf('Toast', 'prepare_minutes', str(prepare_time_spin.value())) + + def clear_log(self): # 清空日志 + def get_directory_size(path): # 计算目录大小 + total_size = 0 + for dir_path, dir_names, filenames in os.walk(path): + for file_name in filenames: + file_path = os.path.join(dir_path, file_name) + total_size += os.path.getsize(file_path) + total_size /= 1024 + return round(total_size, 2) + + self.button_clear_log = self.adInterface.findChild(PushButton, 'button_clear_log') + size = get_directory_size('log') + + try: + if os.path.exists('log'): + rmtree('log') + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='已清除日志', + content=f"已清空所有日志文件,约 {size} KB", + target=self.button_clear_log, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + else: + Flyout.create( + icon=InfoBarIcon.INFORMATION, + title='未找到日志', + content="日志目录下为空,已清理完成。", + target=self.button_clear_log, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + except OSError: # 遇到程序正在使用的log,忽略 + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='已清除日志', + content=f"已清空所有日志文件,约 {size} KB", + target=self.button_clear_log, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + except Exception as e: + Flyout.create( + icon=InfoBarIcon.ERROR, + title='清除日志失败!', + content=f"清除日志失败:{e}", + target=self.button_clear_log, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + def ct_change_color_mode(self): + color_mode_combo = self.findChild(ComboBox, 'combo_color_mode') + config_center.write_conf('General', 'color_mode', str(color_mode_combo.currentIndex())) + if color_mode_combo.currentIndex() == 0: + tg_theme = Theme.LIGHT + elif color_mode_combo.currentIndex() == 1: + tg_theme = Theme.DARK + else: + tg_theme = Theme.AUTO + setTheme(tg_theme) + self.ct_update_preview() + + def ct_add_widget(self): + widgets_list = self.findChild(ListWidget, 'widgets_list') + widgets_combo = self.findChild(ComboBox, 'widgets_combo') + if (not widgets_list.findItems(widgets_combo.currentText(), QtCore.Qt.MatchFlag.MatchExactly)) or widgets_combo.currentText() in list_.native_widget_name: + widgets_list.addItem(widgets_combo.currentText()) + self.ct_update_preview() + + def ct_remove_widget(self): + widgets_list = self.findChild(ListWidget, 'widgets_list') + if widgets_list.count() > 2: + widgets_list.takeItem(widgets_list.currentRow()) + self.ct_update_preview() + else: + w = MessageBox('无法删除', '至少需要保留两个小组件。', self) + w.cancelButton.hide() # 隐藏取消按钮 + w.buttonLayout.insertStretch(0, 1) + w.exec() + + def ct_set_ac_color(self): + current_color = QColor(f'#{config_center.read_conf("Color", "attend_class")}') + w = ColorDialog(current_color, "更改上课时主题色", self, enableAlpha=False) + w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'attend_class', color.name()[1:])) + w.exec() + + def ct_set_fc_color(self): + current_color = QColor(f'#{config_center.read_conf("Color", "finish_class")}') + w = ColorDialog(current_color, "更改课间时主题色", self, enableAlpha=False) + w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'finish_class', color.name()[1:])) + w.exec() + + def ct_set_floating_time_color(self): + current_color = QColor(f'#{config_center.read_conf("Color", "floating_time")}') + w = ColorDialog(current_color, "更改浮窗时间颜色", self, enableAlpha=False) + w.colorChanged.connect(lambda color: config_center.write_conf('Color', 'floating_time', color.name()[1:])) + w.exec() + self.ct_update_preview() + + def cf_export_schedule(self): # 导出课程表 + file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", config_center.schedule_name, + "Json 配置文件 (*.json)") + if file_path: + if list_.export_schedule(file_path, config_center.schedule_name): + alert = MessageBox('您已成功导出课程表配置文件', + f'文件将导出于{file_path}', self) + alert.cancelButton.hide() + alert.buttonLayout.insertStretch(0, 1) + if alert.exec(): + return 0 + else: + print('导出失败!') + alert = MessageBox('导出失败!', + '课程表文件导出失败,\n' + '可能为文件损坏,请将此情况反馈给开发者。', self) + alert.cancelButton.hide() + alert.buttonLayout.insertStretch(0, 1) + if alert.exec(): + return 0 + + def check_update(self): + self.version.setText(f'当前版本:{config_center.read_conf("Other", "version")}\n正在检查最新版本…') + self.version_thread = VersionThread() + self.version_thread.version_signal.connect(self.check_version) + self.version_thread.start() + + def check_version(self, version): # 检查更新 + if 'error' in version: + self.version.setText(f'当前版本:{config_center.read_conf("Other", "version")}\n{version["error"]}') + + if utils.tray_icon: + utils.tray_icon.push_error_notification( + "检查更新失败!", + f"检查更新失败!\n{version['error']}" + ) + return False + + channel = int(config_center.read_conf("Other", "version_channel")) + new_version = version['version_release' if channel == 0 else 'version_beta'] + local_version = config_center.read_conf("Other", "version") + + logger.debug(f"服务端版本: {Version(new_version)},本地版本: {Version(local_version)}") + if Version(new_version) <= Version(local_version): + self.version.setText(f'当前版本:{local_version}\n当前为最新版本') + else: + self.version.setText(f'当前版本:{local_version}\n最新版本:{new_version}') + + if utils.tray_icon: + utils.tray_icon.push_update_notification(f"新版本速递:{new_version}") + + def cf_import_schedule_cses(self): # 导入课程表(CSES) + file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "CSES 通用课程表交换文件 (*.yaml)") + if file_path: + file_name = file_path.split("/")[-1] + save_path = f"{base_directory}/config/schedule/{file_name.replace('.yaml', '.json')}" + + print(save_path) + importer = CSES_Converter(file_path) + importer.load_parser() + cw_data = importer.convert_to_cw() + if not cw_data: + alert = MessageBox('转换失败!', + '课程表文件转换失败!\n' + '可能为格式错误或文件损坏,请检查此文件是否为正确的 CSES 课程表文件。\n' + '详情请查看Log日志,日志位于./log/下。', self) + alert.cancelButton.hide() # 隐藏取消按钮 + alert.buttonLayout.insertStretch(0, 1) + alert.exec() + try: + with open(save_path, 'w', encoding='utf-8') as f: + json.dump(cw_data, f, ensure_ascii=False, indent=4) + self.conf_combo.addItem(file_name.replace('.yaml', '.json')) + alert = MessageBox('您已成功导入 CSES 课程表配置文件', + '请在“高级选项”中手动切换您的配置文件。', self) + alert.cancelButton.hide() + alert.buttonLayout.insertStretch(0, 1) + alert.exec() + except Exception as e: + logger.error(f'导入课程表时发生错误:{e}') + alert = MessageBox('导入失败!', + '课程表文件导入失败!\n' + '可能为格式错误或文件损坏,请检查此文件是否为正确的 CSES 课程表文件。\n' + '详情请查看Log日志,日志位于./log/下。', self) + alert.cancelButton.hide() # 隐藏取消按钮 + alert.buttonLayout.insertStretch(0, 1) + alert.exec() + + def cf_export_schedule_cses(self): # 导出课程表(CSES) + file_path, _ = QFileDialog.getSaveFileName( + self, "保存文件", config_center.schedule_name.replace('.json', '.yaml'), "CSES 通用课程表交换文件 (*.yaml)") + if file_path: + exporter = CSES_Converter(file_path) + exporter.load_generator() + if exporter.convert_to_cses(cw_path=f'{base_directory}/config/schedule/{config_center.schedule_name}'): + alert = MessageBox('您已成功导出课程表配置文件', + f'文件将导出于{file_path}', self) + alert.cancelButton.hide() + alert.buttonLayout.insertStretch(0, 1) + if alert.exec(): + return 0 + else: + print('导出失败!') + alert = MessageBox('导出失败!', + '课程表文件导出失败,\n' + '可能为文件损坏,请将此情况反馈给开发者。', self) + alert.cancelButton.hide() + alert.buttonLayout.insertStretch(0, 1) + if alert.exec(): + return 0 + + def cf_import_schedule(self): # 导入课程表 + file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "Json 配置文件 (*.json)") + if file_path: + file_name = file_path.split("/")[-1] + if list_.import_schedule(file_path, file_name): + self.conf_combo.addItem(file_name) + alert = MessageBox('您已成功导入课程表配置文件', + '请在“高级选项”中手动切换您的配置文件。', self) + alert.cancelButton.hide() # 隐藏取消按钮,必须重启 + alert.buttonLayout.insertStretch(0, 1) + else: + print('导入失败!') + alert = MessageBox('导入失败!', + '课程表文件导入失败!\n' + '可能为格式错误或文件损坏,请检查此文件是否为 Class Widgets 课程表文件。\n' + '详情请查看Log日志,日志位于./log/下。', self) + alert.cancelButton.hide() # 隐藏取消按钮 + alert.buttonLayout.insertStretch(0, 1) + if alert.exec(): + return 0 + + def ct_save_widget_config(self): + widgets_list = self.findChild(ListWidget, 'widgets_list') + widget_config = {'widgets': []} + for i in range(widgets_list.count()): + widget_config['widgets'].append(list_.widget_conf[widgets_list.item(i).text()]) + if conf.save_widget_conf_to_json(widget_config): + self.ct_update_preview() + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='保存成功', + content=f"已保存至 ./config/widget.json", + target=self.findChild(PrimaryPushButton, 'save_config'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + def ct_update_preview(self): + try: + widgets_preview = self.findChild(QHBoxLayout, 'widgets_preview') + # 获取配置列表 + widget_config = list_.get_widget_config() + while widgets_preview.count() > 0: # 清空预览界面 + item = widgets_preview.itemAt(0) + if item: + widget = item.widget() + if widget: + widget.deleteLater() + widgets_preview.removeItem(item) + + left_spacer = QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + widgets_preview.addItem(left_spacer) + + theme_folder = config_center.read_conf("General", "theme") + if not os.path.exists(f'{base_directory}/ui/{theme_folder}/theme.json'): + theme_folder = 'default' # 主题文件夹不存在,使用默认主题 + logger.warning(f'主题文件夹不存在,使用默认主题:{theme_folder}') + + for i in range(len(widget_config)): + widget_name = widget_config[i] + if isDarkTheme() and conf.load_theme_config(theme_folder)['support_dark_mode']: + if os.path.exists(f'{base_directory}/ui/{theme_folder}/dark/preview/{widget_name[:-3]}.png'): + path = f'{base_directory}/ui/{theme_folder}/dark/preview/{widget_name[:-3]}.png' + else: + path = f'{base_directory}/ui/{theme_folder}/dark/preview/widget-custom.png' + else: + if os.path.exists(f'ui/{theme_folder}/preview/{widget_name[:-3]}.png'): + path = f'{base_directory}/ui/{theme_folder}/preview/{widget_name[:-3]}.png' + else: + path = f'{base_directory}/ui/{theme_folder}/preview/widget-custom.png' + + label = ImageLabel() + label.setImage(path) + widgets_preview.addWidget(label) + widget_config[i] = label + right_spacer = QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + widgets_preview.addItem(right_spacer) + except Exception as e: + logger.error(f'更新预览界面时发生错误:{e}') + + def ad_change_file_name(self): + try: + conf_name = self.findChild(LineEdit, 'conf_name') + old_name = config_center.schedule_name + new_name = conf_name.text() + os.rename(f'{base_directory}/config/schedule/{old_name}', + f'{base_directory}/config/schedule/{new_name}.json') # 重命名 + config_center.write_conf('General', 'schedule', f'{new_name}.json') + config_center.schedule_name = new_name + '.json' + conf_combo = self.findChild(ComboBox, 'conf_combo') + conf_combo.clear() + conf_combo.addItems(list_.get_schedule_config()) + conf_combo.setCurrentIndex(list_.get_schedule_config().index(f'{new_name}.json')) + except Exception as e: + print(f'修改课程文件名称时发生错误:{e}') + logger.error(f'修改课程文件名称时发生错误:{e}') + + def ad_change_file(self): # 切换课程文件 + try: + conf_name = self.findChild(LineEdit, 'conf_name') + # 添加新课表 + if self.conf_combo.currentText() == '添加新课表': + self.conf_combo.setCurrentIndex(-1) # 取消 + # new_name = f'新课表 - {list.return_default_schedule_number() + 1}' + n2_dialog = TextFieldMessageBox( + self, '请输入新课表名称', + '请命名您的课程表计划:', '新课表 - 1', list_.get_schedule_config() + ) + if not n2_dialog.exec(): + return + + new_name = n2_dialog.textField.text() + list_.create_new_profile(f'{new_name}.json') + self.conf_combo.clear() + self.conf_combo.addItems(list_.get_schedule_config()) + config_center.write_conf('General', 'schedule', f'{new_name}.json') + self.conf_combo.setCurrentIndex( + list_.get_schedule_config().index(config_center.read_conf('General', 'schedule'))) + conf_name.setText(new_name) + update_tray_tooltip() + + elif self.conf_combo.currentText().endswith('.json'): + new_name = self.conf_combo.currentText() + config_center.write_conf('General', 'schedule', new_name) + conf_name.setText(new_name[:-5]) + update_tray_tooltip() + + else: + logger.error(f'切换课程文件时列表选择异常:{self.conf_combo.currentText()}') + Flyout.create( + icon=InfoBarIcon.ERROR, + title='错误!', + content=f"列表选项异常!{self.conf_combo.currentText()}", + target=self.conf_combo, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + return + global loaded_data + + config_center.schedule_name = config_center.read_conf('General', 'schedule') + schedule_center.update_schedule() + loaded_data = schedule_center.schedule_data + self.te_load_item() + self.te_upload_list() + self.te_update_parts_name() + se_load_item() + self.se_upload_list() + self.sp_fill_grid_row() + except Exception as e: + print(f'切换配置文件时发生错误:{e}') + logger.error(f'切换配置文件时发生错误:{e}') + + def sp_fill_grid_row(self): # 填充预览表格 + subtitle = self.findChild(SubtitleLabel, 'subtitle_file') + subtitle.setText(f'预览 - {config_center.schedule_name[:-5]}') + sp_week_type_combo = self.findChild(ComboBox, 'pre_week_type_combo') + schedule_view = self.findChild(TableWidget, 'schedule_view') + schedule_view.setRowCount(sp_get_class_num()) + if sp_week_type_combo.currentIndex() == 1: + schedule_dict_sp = schedule_even_dict + else: + schedule_dict_sp = schedule_dict + for i in range(len(schedule_dict_sp)): # 周数 + for j in range(len(schedule_dict_sp[str(i)])): # 一天内全部课程 + item_text = schedule_dict_sp[str(i)][j].split('-')[0] + if item_text != '未添加': + item = QTableWidgetItem(item_text) + else: + item = QTableWidgetItem('') + schedule_view.setItem(j, i, item) + item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) # 设置单元格文本居中对齐 + + # 加载时间线 + def te_load_item(self): + global morning_st, afternoon_st, loaded_data, timeline_dict + loaded_data = schedule_center.schedule_data + part = loaded_data.get('part') + part_name = loaded_data.get('part_name') + timeline = get_timeline() + # 找控件 + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + te_timeline_list.clear() + part_list = self.findChild(ListWidget, 'part_list') + part_list.clear() + + for part_num, part_time in part.items(): # 加载节点 + prefix = part_name[part_num] + time = QTime(int(part_time[0]), int(part_time[1])).toString('h:mm') + period = time + try: + part_type = part_time[2] + except IndexError: + part_type = 'part' + + part_type = list_.part_type[part_type == 'break'] + text = f'{prefix} - {period} - {part_type}' + part_list.addItem(text) + + for week, _ in timeline.items(): # 加载节点 + all_line = [] + for item_name, time in timeline[week].items(): # 加载时间线 + prefix = '' + item_time = f'{timeline[week][item_name]}分钟' + # 判断前缀和时段 + if item_name.startswith('a'): + prefix = '课程' + elif item_name.startswith('f'): + prefix = '课间' + period = part_name[item_name[1]] + + # 还原 item_text + item_text = f"{prefix} - {item_time} - {period}" + all_line.append(item_text) + timeline_dict[week] = all_line + + def se_copy_odd_schedule(self): + logger.info('复制单周课表') + global schedule_dict, schedule_even_dict + schedule_even_dict = deepcopy(schedule_dict) + self.se_upload_list() + + def te_upload_list(self): # 更新时间线到列表组件 + logger.info('更新列表:时间线编辑') + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + te_select_timeline = self.findChild(ComboBox, 'select_timeline') + try: + if te_select_timeline.currentIndex() == 0: + te_timeline_list.clear() + te_timeline_list.addItems(timeline_dict['default']) + else: + te_timeline_list.clear() + te_timeline_list.addItems(timeline_dict[str(te_select_timeline.currentIndex() - 1)]) + self.te_detect_item() + except Exception as e: + print(f'加载时间线时发生错误:{e}') + + def show_tip_flyout(self, title, content, target): + Flyout.create( + icon=InfoBarIcon.WARNING, + title=title, + content=content, + target=target, + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + # 上传课表到列表组件 + def se_upload_list(self): # 更新课表到列表组件 + logger.info('更新列表:课程表编辑') + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + se_schedule_list.clearSelection() + se_week_combo = self.findChild(ComboBox, 'week_combo') + se_week_type_combo = self.findChild(ComboBox, 'week_type_combo') + se_copy_schedule_button = self.findChild(PushButton, 'copy_schedule') + global current_week + try: + if se_week_type_combo.currentIndex() == 1: + se_copy_schedule_button.show() + current_week = se_week_combo.currentIndex() + se_schedule_list.clear() + se_schedule_list.addItems(schedule_even_dict[str(current_week)]) + else: + se_copy_schedule_button.hide() + current_week = se_week_combo.currentIndex() + se_schedule_list.clear() + se_schedule_list.addItems(schedule_dict[str(current_week)]) + except Exception as e: + print(f'加载课表时发生错误:{e}') + + def se_upload_item(self): # 保存列表内容到课表文件 + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + se_week_type_combo = self.findChild(ComboBox, 'week_type_combo') + if se_week_type_combo.currentIndex() == 1: + global schedule_even_dict + try: + cache_list = [] + for i in range(se_schedule_list.count()): + item_text = se_schedule_list.item(i).text() + cache_list.append(item_text) + schedule_even_dict[str(current_week)][:] = cache_list + except Exception as e: + print(f'加载双周课表时发生错误:{e}') + else: + global schedule_dict + cache_list = [] + for i in range(se_schedule_list.count()): + item_text = se_schedule_list.item(i).text() + cache_list.append(item_text) + schedule_dict[str(current_week)][:] = cache_list + + # 保存课程 + def se_save_item(self): + try: + data_dict = deepcopy(schedule_dict) + data_dict_even = deepcopy(schedule_even_dict) # 单双周保存 + + data_dict = convert_to_dict(data_dict) + data_dict_even = convert_to_dict(data_dict_even) + + # 写入 + data_dict_even = {"schedule_even": data_dict_even} + schedule_center.save_data(data_dict_even, config_center.schedule_name) + data_dict = {"schedule": data_dict} + schedule_center.save_data(data_dict, config_center.schedule_name) + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='保存成功', + content=f"已保存至 ./config/schedule/{config_center.schedule_name}", + target=self.findChild(PrimaryPushButton, 'save_schedule'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + self.sp_fill_grid_row() + except Exception as e: + logger.error(f'保存课表时发生错误: {e}') + + def te_upload_item(self): # 上传时间线到列表组件 + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + te_select_timeline = self.findChild(ComboBox, 'select_timeline') + global timeline_dict + cache_list = [] + for i in range(te_timeline_list.count()): + item_text = te_timeline_list.item(i).text() + cache_list.append(item_text) + if te_select_timeline.currentIndex() == 0: + timeline_dict['default'] = cache_list + else: + timeline_dict[str(te_select_timeline.currentIndex() - 1)] = cache_list + + # 保存时间线 + def te_save_item(self): + te_part_list = self.findChild(ListWidget, 'part_list') + data_dict = {"part": {}, "part_name": {}, "timeline": {'default': {}, **{str(w): {} for w in range(7)}}} + data_timeline_dict = deepcopy(timeline_dict) + # 逐条把列表里的信息整理保存 + for i in range(te_part_list.count()): + item_text = te_part_list.item(i).text() + item_info = item_text.split(' - ') + time_tostring = item_info[1].split(':') + if len(item_info) == 3: + part_type = ['part', 'break'][item_info[2] == '休息段'] + else: + part_type = 'part' + data_dict['part'][str(i)] = [int(time_tostring[0]), int(time_tostring[1]), part_type] + data_dict['part_name'][str(i)] = item_info[0] + + try: + for week, _ in data_timeline_dict.items(): + counter = [] # 初始化计数器 + for i in range(len(data_dict['part'])): + counter.append(0) + counter_key = 0 + lesson_num = 0 + for i in range(len(data_timeline_dict[week])): + item_text = data_timeline_dict[week][i] + item_info = item_text.split(' - ') + item_name = '' + if item_info[0] == '课程': + item_name += 'a' + lesson_num += 1 + if item_info[0] == '课间': + item_name += 'f' + + for key, value in data_dict['part_name'].items(): # 节点计数 + if value == item_info[2]: + item_name += str(key) # +节点序数 + counter_key = int(key) # 记录节点序数 + break + + if item_name.startswith('a'): + counter[counter_key] += 1 + + item_name += str(lesson_num - sum(counter[:counter_key])) # 课程序数 + item_time = item_info[1][0:len(item_info[1]) - 2] + data_dict['timeline'][str(week)][item_name] = item_time + + schedule_center.save_data(data_dict, config_center.schedule_name) + self.te_detect_item() + se_load_item() + self.se_upload_list() + self.se_upload_item() + self.te_upload_item() + self.sp_fill_grid_row() + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='保存成功', + content=f"已保存至 ./config/schedule/{config_center.schedule_name}", + target=self.findChild(PrimaryPushButton, 'save'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + except Exception as e: + logger.error(f'保存时间线时发生错误: {e}') + Flyout.create( + icon=InfoBarIcon.ERROR, + title='保存失败!', + content=f"{e}\n保存失败,请将 ./log/ 中的日志提交给开发者以反馈问题。", + target=self.findChild(PrimaryPushButton, 'save'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + def te_sync_time(self): + te_class_activity_combo = self.findChild(ComboBox, 'class_activity') + spin_time = self.findChild(SpinBox, 'spin_time') + if te_class_activity_combo.currentIndex() == 0: + spin_time.setValue(40) + if te_class_activity_combo.currentIndex() == 1: + spin_time.setValue(10) + + def te_detect_item(self): + timeline_list = self.findChild(ListWidget, 'timeline_list') + part_list = self.findChild(ListWidget, 'part_list') + tips = self.findChild(CaptionLabel, 'tips_2') + tips_part = self.findChild(CaptionLabel, 'tips_1') + if part_list.count() > 0: + tips_part.hide() + else: + tips_part.show() + if timeline_list.count() > 0: + tips.hide() + else: + tips.show() + + def te_add_item(self): + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + class_activity = self.findChild(ComboBox, 'class_activity') + spin_time = self.findChild(SpinBox, 'spin_time') + time_period = self.findChild(ComboBox, 'time_period') + if time_period.currentText() == "": # 时间段不能为空 修复 #184 + Flyout.create( + icon=InfoBarIcon.WARNING, + title='无法添加时间线 o(TヘTo)', + content='在添加时间线前,先任意添加一个节点', + target=self.findChild(ToolButton, 'add_button'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + return # 时间段不能为空 + te_timeline_list.addItem( + f'{class_activity.currentText()} - {spin_time.value()}分钟 - {time_period.currentText()}' + ) + self.te_detect_item() + + def te_add_part(self): + te_part_list = self.findChild(ListWidget, 'part_list') + te_name_part = self.findChild(EditableComboBox, 'name_part_combo') + te_part_time = self.findChild(TimeEdit, 'part_time') + te_part_type = self.findChild(ComboBox, 'part_type') + if te_part_list.count() < 10: + te_part_list.addItem( + f'{te_name_part.currentText()} - {te_part_time.time().toString("h:mm")} - {te_part_type.currentText()}' + ) + else: # 最多只能添加9个节点 + Flyout.create( + icon=InfoBarIcon.WARNING, + title='没办法继续添加了 o(TヘTo)', + content='Class Widgets 最多只能添加10个“节点”!', + target=self.findChild(ToolButton, 'add_part_button'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + self.te_detect_item() + self.te_update_parts_name() + + def te_delete_part(self): + alert = MessageBox("您确定要删除这个时段吗?", "删除该节点后,将一并删除该节点下所有课程安排,且无法恢复。", self) + alert.yesButton.setText('删除') + alert.yesButton.setStyleSheet(""" + PushButton{ + border-radius: 5px; + padding: 5px 12px 6px 12px; + outline: none; + } + PrimaryPushButton{ + color: white; + background-color: #FF6167; + border: 1px solid #FF8585; + border-bottom: 1px solid #943333; + } + PrimaryPushButton:hover{ + background-color: #FF7E83; + border: 1px solid #FF8084; + border-bottom: 1px solid #B13939; + } + PrimaryPushButton:pressed{ + color: rgba(255, 255, 255, 0.63); + background-color: #DB5359; + border: 1px solid #DB5359; + } + """) + alert.cancelButton.setText('取消') + if alert.exec(): + global timeline_dict, schedule_dict + te_part_list = self.findChild(ListWidget, 'part_list') + selected_items = te_part_list.selectedItems() + if not selected_items: + return + + deleted_part_name = selected_items[0].text().split(' - ')[0] + for item in selected_items: + te_part_list.takeItem(te_part_list.row(item)) + + # 修复了删除时段没能同步删除时间线的Bug #123 + for day in timeline_dict: # 删除时间线 + count = 0 + break_count = 0 + delete_schedule_list = [] + delete_schedule_even_list = [] + delete_part_list = [] + for i in range(len(timeline_dict[day])): + act = timeline_dict[day][i] + count += 1 + item_info = act.split(' - ') + + if item_info[0] == '课间': + break_count += 1 + + if item_info[2] == deleted_part_name: + delete_part_list.append(act) + if item_info[0] != '课间': + if day != 'default': + delete_schedule_list.append(schedule_dict[day][count - break_count - 1]) + delete_schedule_even_list.append(schedule_even_dict[day][count - break_count - 1]) + else: + for j in range(7): + try: + for item in schedule_dict[str(j)]: + if item.split('-')[1] == deleted_part_name: + delete_schedule_list.append( + schedule_dict[str(j)][count - break_count - 1]) + for item in schedule_even_dict[str(j)]: + if item.split('-')[1] == deleted_part_name: + delete_schedule_even_list.append( + schedule_dict[str(j)][count - break_count - 1]) + except Exception as e: + logger.warning(f'删除时段时发生错误:{e}') + + for item in delete_part_list: # 删除时间线 + timeline_dict[day].remove(item) + if day != 'default': # 删除课表 + for item in delete_schedule_list: + schedule_dict[day].remove(item) + + for day in range(7): # 删除默认课程表 + delete_schedule_list = [] + delete_schedule_even_list = [] + for item in schedule_dict[str(day)]: # 单周 + if item.split('-')[1] == deleted_part_name: + delete_schedule_list.append(item) + for item in delete_schedule_list: + schedule_dict[str(day)].remove(item) + + for item in schedule_even_dict[str(day)]: # 双周 + if item.split('-')[1] == deleted_part_name: + delete_schedule_even_list.append(item) + for item in delete_schedule_even_list: + schedule_even_dict[str(day)].remove(item) + + self.te_upload_list() + self.se_upload_list() + self.te_update_parts_name() + else: + return + + def te_update_parts_name(self): + rl = [] + te_time_combo = self.findChild(ComboBox, 'time_period') # 时段 + te_time_combo.clear() + part_list = self.findChild(ListWidget, 'part_list') + for i in range(part_list.count()): + info = part_list.item(i).text().split(' - ') + rl.append(info[0]) + te_time_combo.addItems(rl) + + def te_edit_item(self): + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + class_activity = self.findChild(ComboBox, 'class_activity') + spin_time = self.findChild(SpinBox, 'spin_time') + time_period = self.findChild(ComboBox, 'time_period') + selected_items = te_timeline_list.selectedItems() + + if selected_items: + selected_item = selected_items[0] # 取第一个选中的项目 + selected_item.setText( + f'{class_activity.currentText()} - {spin_time.value()}分钟 - {time_period.currentText()}' + ) + + def se_edit_item(self): + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + se_class_combo = self.findChild(ComboBox, 'class_combo') + se_custom_class_text = self.findChild(LineEdit, 'custom_class') + selected_items = se_schedule_list.selectedItems() + + if selected_items: + selected_item = selected_items[0] + name_list = selected_item.text().split('-') + if se_class_combo.currentIndex() != 0: + selected_item.setText( + f'{se_class_combo.currentText()}-{name_list[1]}' + ) + else: + if se_custom_class_text.text() != '': + selected_item.setText( + f'{se_custom_class_text.text()}-{name_list[1]}' + ) + se_class_combo.addItem(se_custom_class_text.text()) + + def se_quick_set_schedule(self): # 快速设置课表 + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + quick_set_schedule = self.findChild(ListWidget, 'subject_list') + selected_items = se_schedule_list.selectedItems() + selected_subject = quick_set_schedule.currentItem().text() + if se_schedule_list.count() > 0: + if not selected_items: + se_schedule_list.setCurrentRow(0) + + selected_row = se_schedule_list.currentRow() + selected_item = se_schedule_list.item(selected_row) + name_list = selected_item.text().split('-') + selected_item.setText( + f'{selected_subject}-{name_list[1]}' + ) + + if se_schedule_list.count() > selected_row + 1: # 选择下一行 + se_schedule_list.setCurrentRow(selected_row + 1) + + def se_quick_select_week(self): # 快速选择周 + se_week_combo = self.findChild(ComboBox, 'week_combo') + if se_week_combo.currentIndex() != 6: + se_week_combo.setCurrentIndex(se_week_combo.currentIndex() + 1) + + def te_delete_item(self): + te_timeline_list = self.findChild(ListWidget, 'timeline_list') + selected_items = te_timeline_list.selectedItems() + for item in selected_items: + te_timeline_list.takeItem(te_timeline_list.row(item)) + self.te_detect_item() + + def se_delete_item(self): + se_schedule_list = self.findChild(ListWidget, 'schedule_list') + selected_items = se_schedule_list.selectedItems() + if selected_items: + selected_item = selected_items[0] + name_list = selected_item.text().split('-') + selected_item.setText( + f'未添加-{name_list[1]}' + ) + + def cd_edit_item(self): + cd_countdown_list = self.findChild(ListWidget, 'countdown_list') + cd_text_cd = self.findChild(LineEdit, 'text_cd') + cd_set_countdown_date = self.findChild(CalendarPicker, 'set_countdown_date') + selected_items = cd_countdown_list.selectedItems() + if selected_items: + selected_item = selected_items[0] + selected_item.setText( + f"{cd_set_countdown_date.date.toString('yyyy-M-d')} - {cd_text_cd.text()}" + ) + + def cd_delete_item(self): + cd_countdown_list = self.findChild(ListWidget, 'countdown_list') + selected_items = cd_countdown_list.selectedItems() + if selected_items: + item = selected_items[0] + cd_countdown_list.takeItem(cd_countdown_list.row(item)) + + def cd_add_item(self): + cd_countdown_list = self.findChild(ListWidget, 'countdown_list') + cd_text_cd = self.findChild(LineEdit, 'text_cd') + cd_set_countdown_date = self.findChild(CalendarPicker, 'set_countdown_date') + cd_countdown_list.addItem( + f"{cd_set_countdown_date.date.toString('yyyy-M-d')} - {cd_text_cd.text()}" + ) + + def cd_save_item(self): + cd_countdown_list = self.findChild(ListWidget, 'countdown_list') + countdown_date = [] + cd_text_custom = [] + + for i in range(cd_countdown_list.count()): + item = cd_countdown_list.item(i) + text = item.text().split(' - ') + countdown_date.append(text[0]) + cd_text_custom.append(text[1]) + + Flyout.create( + icon=InfoBarIcon.SUCCESS, + title='保存成功', + content=f"已保存至 ./config.ini", + target=self.findChild(PrimaryPushButton, 'save_countdown'), + parent=self, + isClosable=True, + aniType=FlyoutAnimationType.PULL_UP + ) + + config_center.write_conf('Date', 'countdown_date', ','.join(countdown_date)) + config_center.write_conf('Date', 'cd_text_custom', ','.join(cd_text_custom)) + + def setup_countdown_edit(self): + cd_load_item() + logger.debug(f"{countdown_dict}") + cd_set_button = self.findChild(ToolButton, 'set_button_cd') + cd_set_button.setIcon(fIcon.EDIT) + cd_set_button.setToolTip('编辑倒计日') + cd_set_button.installEventFilter(ToolTipFilter(cd_set_button, showDelay=300, position=ToolTipPosition.TOP)) + cd_set_button.clicked.connect(self.cd_edit_item) + + cd_clear_button = self.findChild(ToolButton, 'clear_button_cd') + cd_clear_button.setIcon(fIcon.DELETE) + cd_clear_button.setToolTip('删除倒计日') + cd_clear_button.installEventFilter(ToolTipFilter(cd_clear_button, showDelay=300, position=ToolTipPosition.TOP)) + cd_clear_button.clicked.connect(self.cd_delete_item) + + cd_add_button = self.findChild(ToolButton, 'add_button_cd') + cd_add_button.setIcon(fIcon.ADD) + cd_add_button.setToolTip('添加倒计日') + cd_add_button.installEventFilter(ToolTipFilter(cd_add_button, showDelay=300, position=ToolTipPosition.TOP)) + cd_add_button.clicked.connect(self.cd_add_item) + + cd_schedule_list = self.findChild(ListWidget, 'countdown_list') + cd_schedule_list.addItems([f"{date} - {countdown_dict[date]}" for date in countdown_dict]) + + cd_save_button = self.findChild(PrimaryPushButton, 'save_countdown') + cd_save_button.clicked.connect(self.cd_save_item) + + cd_mode = self.findChild(ComboBox, 'countdown_mode') + cd_mode.addItems(list_.countdown_modes) + cd_mode.setCurrentIndex(int(config_center.read_conf('Date', 'countdown_custom_mode'))) + cd_mode.currentIndexChanged.connect( + lambda: config_center.write_conf('Date', 'countdown_custom_mode', str(cd_mode.currentIndex()))) + + cd_upd_cd = self.findChild(SpinBox, 'countdown_upd_cd') + cd_upd_cd.setValue(int(config_center.read_conf('Date', 'countdown_upd_cd'))) + cd_upd_cd.valueChanged.connect( + lambda: config_center.write_conf('Date', 'countdown_upd_cd', str(cd_upd_cd.value()))) + + def m_start_time_changed(self): + global morning_st + te_m_start_time = self.findChild(TimeEdit, 'morningStartTime') + unformatted_time = te_m_start_time.time() + h = unformatted_time.hour() + m = unformatted_time.minute() + morning_st = (h, m) + + def a_start_time_changed(self): + global afternoon_st + te_m_start_time = self.findChild(TimeEdit, 'afternoonStartTime') + unformatted_time = te_m_start_time.time() + h = unformatted_time.hour() + m = unformatted_time.minute() + afternoon_st = (h, m) + + def init_nav(self): + self.addSubInterface(self.spInterface, fIcon.HOME, '课表预览') + self.addSubInterface(self.teInterface, fIcon.DATE_TIME, '时间线编辑') + self.addSubInterface(self.seInterface, fIcon.EDUCATION, '课程表编辑') + self.addSubInterface(self.cdInterface, fIcon.CALENDAR, '倒计日编辑') + self.addSubInterface(self.cfInterface, fIcon.FOLDER, '配置文件') + self.navigationInterface.addSeparator() + self.addSubInterface(self.hdInterface, fIcon.QUESTION, '帮助') + self.addSubInterface(self.plInterface, fIcon.APPLICATION, '插件', NavigationItemPosition.BOTTOM) + self.navigationInterface.addSeparator(NavigationItemPosition.BOTTOM) + self.addSubInterface(self.ctInterface, fIcon.BRUSH, '自定义', NavigationItemPosition.BOTTOM) + self.addSubInterface(self.sdInterface, fIcon.RINGER, '提醒', NavigationItemPosition.BOTTOM) + self.addSubInterface(self.adInterface, fIcon.SETTING, '高级选项', NavigationItemPosition.BOTTOM) + self.addSubInterface(self.ifInterface, fIcon.INFO, '关于本产品', NavigationItemPosition.BOTTOM) + + def init_window(self): + self.stackedWidget.setCurrentIndex(0) # 设置初始页面 + self.load_all_item() + self.setMinimumWidth(700) + self.setMinimumHeight(400) + self.navigationInterface.setExpandWidth(250) + self.navigationInterface.setCollapsible(False) + self.setMicaEffectEnabled(True) + + # 修复设置窗口在各个屏幕分辨率DPI下的窗口大小 + screen_geometry = QApplication.primaryScreen().geometry() + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + width = int(screen_width * 0.6) + height = int(screen_height * 0.7) + + self.move(int(screen_width / 2 - width / 2), 150) + self.resize(width, height) + + self.setWindowTitle('Class Widgets - 设置') + self.setWindowIcon(QIcon(f'{base_directory}/img/logo/favicon-settings.ico')) + + self.init_font() # 设置字体 + + def closeEvent(self, event): + self.closed.emit() + event.accept() + + +def sp_get_class_num(): # 获取当前周课程数(未完成) + highest_count = 0 + for timeline_ in get_timeline().keys(): + timeline = get_timeline()[timeline_] + count = 0 + for item_name, item_time in timeline.items(): + if item_name.startswith('a'): + count += 1 + if count > highest_count: + highest_count = count + return highest_count + + +if __name__ == '__main__': + app = QApplication(sys.argv) + settings = SettingsMenu() + settings.show() + # settings.setMicaEffectEnabled(True) + sys.exit(app.exec()) diff --git a/network_thread.py b/network_thread.py new file mode 100644 index 0000000..6968a10 --- /dev/null +++ b/network_thread.py @@ -0,0 +1,483 @@ +import json +import os +import shutil +import zipfile # 解压插件zip +from datetime import datetime + +import requests +from PyQt5.QtCore import QThread, pyqtSignal, QEventLoop +from loguru import logger +from packaging.version import Version + +import conf +import utils +import weather_db as db +from conf import base_directory +from file import config_center + +headers = {"User-Agent": "Mozilla/5.0", "Cache-Control": "no-cache"} # 设置请求头 +# proxies = {"http": "http://127.0.0.1:10809", "https": "http://127.0.0.1:10809"} # 加速访问 +proxies = {"http": None, "https": None} + +MIRROR_PATH = f"{base_directory}/config/mirror.json" +PLAZA_REPO_URL = "https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/" +PLAZA_REPO_DIR = "https://api.github.com/repos/Class-Widgets/plugin-plaza/contents/" +threads = [] + +# 读取镜像配置 +mirror_list = [] +try: + with open(MIRROR_PATH, 'r', encoding='utf-8') as file: + mirror_dict = json.load(file).get('gh_mirror') +except Exception as e: + logger.error(f"读取镜像配置失败: {e}") + +for name in mirror_dict: + mirror_list.append(name) + +if config_center.read_conf('Plugin', 'mirror') not in mirror_list: # 如果当前配置不在镜像列表中,则设置为默认镜像 + logger.warning(f"当前配置不在镜像列表中,设置为默认镜像: {mirror_list[0]}") + config_center.write_conf('Plugin', 'mirror', mirror_list[0]) + + +class getRepoFileList(QThread): # 获取仓库文件目录 + repo_signal = pyqtSignal(dict) + + def __init__( + self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/banner.json' + ): + super().__init__() + self.download_url = url + + def run(self): + try: + plugin_info_data = self.get_plugin_info() + self.repo_signal.emit(plugin_info_data) + except Exception as e: + logger.error(f"触发banner信息失败: {e}") + + def get_plugin_info(self): + try: + mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + url = f"{mirror_url}{self.download_url}" + response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理 + if response.status_code == 200: + data = response.json() + return data + else: + logger.error(f"获取banner信息失败:{response.status_code}") + return {"error": response.status_code} + except Exception as e: + logger.error(f"获取banner信息失败:{e}") + return {"error": e} + + +class getPluginInfo(QThread): # 获取插件信息(json) + repo_signal = pyqtSignal(dict) + + def __init__( + self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Plugins/plugin_list.json' + ): + super().__init__() + self.download_url = url + + def run(self): + try: + plugin_info_data = self.get_plugin_info() + self.repo_signal.emit(plugin_info_data) + except Exception as e: + logger.error(f"触发插件信息失败: {e}") + + def get_plugin_info(self): + try: + mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + url = f"{mirror_url}{self.download_url}" + response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理 + if response.status_code == 200: + data = response.json() + return data + else: + logger.error(f"获取插件信息失败:{response.status_code}") + return {} + except Exception as e: + logger.error(f"获取插件信息失败:{e}") + return {} + + +class getTags(QThread): # 获取插件标签(json) + repo_signal = pyqtSignal(dict) + + def __init__( + self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Plugins/plaza_detail.json' + ): + super().__init__() + self.download_url = url + + def run(self): + try: + plugin_info_data = self.get_plugin_info() + self.repo_signal.emit(plugin_info_data) + except Exception as e: + logger.error(f"触发Tag信息失败: {e}") + + def get_plugin_info(self): + try: + mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + url = f"{mirror_url}{self.download_url}" + response = requests.get(url, proxies=proxies, headers=headers) # 禁用代理 + if response.status_code == 200: + data = response.json() + return data + else: + logger.error(f"获取Tag信息失败:{response.status_code}") + return {} + except Exception as e: + logger.error(f"获取Tag信息失败:{e}") + return {} + + +class getImg(QThread): # 获取图片 + repo_signal = pyqtSignal(bytes) + + def __init__(self, url='https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/banner_1.png'): + super().__init__() + self.download_url = url + + def run(self): + try: + banner_data = self.get_banner() + if banner_data is not None: + self.repo_signal.emit(banner_data) + else: + with open(f"{base_directory}/img/plaza/banner_pre.png", 'rb') as default_img: # 读取默认图片 + self.repo_signal.emit(default_img.read()) + except Exception as e: + logger.error(f"触发图片失败: {e}") + + def get_banner(self): + try: + mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + url = f"{mirror_url}{self.download_url}" + response = requests.get(url, proxies=proxies, headers=headers) + if response.status_code == 200: + return response.content + else: + logger.error(f"获取图片失败:{response.status_code}") + return None + except Exception as e: + logger.error(f"获取图片失败:{e}") + return None + + +class getReadme(QThread): # 获取README + html_signal = pyqtSignal(str) + + def __init__(self, url='https://raw.githubusercontent.com/Class-Widgets/Class-Widgets/main/README.md'): + super().__init__() + self.download_url = url + + def run(self): + try: + readme_data = self.get_readme() + self.html_signal.emit(readme_data) + except Exception as e: + logger.error(f"触发README失败: {e}") + + def get_readme(self): + try: + mirror_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + url = f"{mirror_url}{self.download_url}" + # print(url) + response = requests.get(url, proxies=proxies) + if response.status_code == 200: + return response.text + else: + logger.error(f"获取README失败:{response.status_code}") + return '' + except Exception as e: + logger.error(f"获取README失败:{e}") + return '' + +class getCity(QThread): + + def __init__(self, url='https://qifu-api.baidubce.com/ip/local/geo/v1/district'): + super().__init__() + self.download_url = url + + def run(self): + try: + city_data = self.get_city() + config_center.write_conf('Weather', 'city', db.search_code_by_name(city_data)) + except Exception as e: + logger.error(f"获取城市失败: {e}") + + def get_city(self): + try: + req = requests.get(self.download_url, proxies=proxies) + if req.status_code == 200: + data = req.json() + # {"code":"Success","data":{"continent":"","country":"中国","zipcode":"","owner":"","isp":"","adcode":"","prov":"","city":"","district":""},"ip":"45.192.96.246"} + if data['code'] == 'Success': + data = data['data'] + logger.info(f"获取城市成功:{data['city']}, {data['district']}") + return (data['city'], data['district']) + else: + logger.error(f"获取城市失败:{data['message']}") + return ('', '') + else: + logger.error(f"获取城市失败:{req.status_code}") + return ('', '') + + except Exception as e: + logger.error(f"获取城市失败:{e}") + return ('', '') + +class VersionThread(QThread): # 获取最新版本号 + version_signal = pyqtSignal(dict) + _instance_running = False + + def __init__(self): + super().__init__() + def run(self): + version = self.get_latest_version() + self.version_signal.emit(version) + + @classmethod + def is_running(cls): + return cls._instance_running + + @staticmethod + def get_latest_version(): + url = "https://classwidgets.rinlit.cn/version.json" + try: + logger.info(f"正在获取版本信息") + response = requests.get(url, proxies=proxies, timeout=30) + logger.debug(f"更新请求响应: {response.status_code}") + if response.status_code == 200: + data = response.json() + return data + else: + logger.error(f"无法获取版本信息 错误代码:{response.status_code},响应内容: {response.text}") + return {'error': f"请求失败,错误代码:{response.status_code}"} + except requests.exceptions.RequestException as e: + logger.error(f"请求失败,错误详情:{str(e)}") + return {"error": f"请求失败\n{str(e)}"} + + +class getDownloadUrl(QThread): + # 定义信号,通知下载进度或完成 + geturl_signal = pyqtSignal(str) + + def __init__(self, username, repo): + super().__init__() + self.username = username + self.repo = repo + + def run(self): + try: + url = f"https://api.github.com/repos/{self.username}/{self.repo}/releases/latest" + response = requests.get(url, proxies=proxies) + if response.status_code == 200: + data = response.json() + for asset in data['assets']: # 遍历下载链接 + if isinstance(asset, dict) and 'browser_download_url' in asset: + asset_url = asset['browser_download_url'] + self.geturl_signal.emit(asset_url) + elif response.status_code == 403: # 触发API限制 + logger.warning("到达Github API限制,请稍后再试") + response = requests.get('https://api.github.com/users/octocat', proxies=proxies) + reset_time = response.headers.get('X-RateLimit-Reset') + reset_time = datetime.fromtimestamp(int(reset_time)) + self.geturl_signal.emit(f"ERROR: 由于请求次数过多,到达Github API限制,请在{reset_time.minute}分钟后再试") + else: + logger.error(f"网络连接错误:{response.status_code}") + except Exception as e: + logger.error(f"获取下载链接错误: {e}") + self.geturl_signal.emit(f"获取下载链接错误: {e}") + + +class DownloadAndExtract(QThread): # 下载并解压插件 + progress_signal = pyqtSignal(float) # 进度 + status_signal = pyqtSignal(str) # 状态 + + def __init__(self, url, plugin_name='test_114'): + super().__init__() + self.download_url = url + print(self.download_url) + self.cache_dir = "cache" + self.plugin_name = plugin_name + self.extract_dir = conf.PLUGINS_DIR # 插件目录 + + def run(self): + try: + enabled_plugins = conf.load_plugin_config() # 加载启用的插件 + + os.makedirs(self.cache_dir, exist_ok=True) + os.makedirs(self.extract_dir, exist_ok=True) + + zip_path = os.path.join(self.cache_dir, f'{self.plugin_name}.zip') + + self.status_signal.emit("DOWNLOADING") + self.download_file(zip_path) + self.status_signal.emit("EXTRACTING") + self.extract_zip(zip_path) + os.remove(zip_path) + print(enabled_plugins) + + if ( + self.plugin_name not in enabled_plugins['enabled_plugins'] + and config_center.read_conf('Plugin', 'auto_enable_plugin') == '1' + ): + logger.info(f"自动启用插件: {self.plugin_name}") + enabled_plugins['enabled_plugins'].append(self.plugin_name) + conf.save_plugin_config(enabled_plugins) + + self.status_signal.emit("DONE") + except Exception as e: + self.status_signal.emit(f"错误: {e}") + logger.error(f"插件下载/解压失败: {e}") + + def stop(self): + self._running = False + self.terminate() + + def download_file(self, file_path): + # time.sleep(555) # 模拟下载时间 + try: + self.download_url = mirror_dict[config_center.read_conf('Plugin', 'mirror')] + self.download_url + print(self.download_url) + response = requests.get(self.download_url, stream=True, proxies=proxies) + if response.status_code != 200: + logger.error(f"插件下载失败,错误代码: {response.status_code}") + self.status_signal.emit(f'ERROR: 网络连接错误:{response.status_code}') + return + + total_size = int(response.headers.get('content-length', 0)) + downloaded_size = 0 + + with open(file_path, 'wb') as file: + for chunk in response.iter_content(1024): + file.write(chunk) + downloaded_size += len(chunk) + progress = (downloaded_size / total_size) * 100 if total_size > 0 else 0 # 计算进度 + self.progress_signal.emit(progress) + except Exception as e: + self.status_signal.emit(f'ERROR: {e}') + logger.error(f"插件下载错误: {e}") + + def extract_zip(self, zip_path): + try: + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(self.extract_dir) + + for p_dir in os.listdir(self.extract_dir): + if p_dir.startswith(self.plugin_name) and len(p_dir) > len(self.plugin_name): + new_name = p_dir.rsplit('-', 1)[0] + if os.path.exists(os.path.join(self.extract_dir, new_name)): + shutil.copytree( + os.path.join(self.extract_dir, p_dir), os.path.join(self.extract_dir, new_name), + dirs_exist_ok=True) + shutil.rmtree(os.path.join(self.extract_dir, p_dir)) + else: + os.rename(os.path.join(self.extract_dir, p_dir), os.path.join(self.extract_dir, new_name)) + except Exception as e: + logger.error(f"解压失败: {e}") + + +def check_update(): + global threads + + if VersionThread.is_running(): + logger.debug("已存在版本检查线程在运行,跳过本检查") + return + + # 清理已终止的线程 + threads = [t for t in threads if t.isRunning()] + + # 创建新的版本检查线程 + version_thread = VersionThread() + threads.append(version_thread) + version_thread.version_signal.connect(check_version) + version_thread.start() + + +def check_version(version): # 检查更新 + global threads + for thread in threads: + thread.terminate() + threads = [] + if 'error' in version: + utils.tray_icon.push_error_notification( + "检查更新失败!", + f"检查更新失败!\n{version['error']}" + ) + return False + + channel = int(config_center.read_conf("Other", "version_channel")) + server_version = version['version_release' if channel == 0 else 'version_beta'] + local_version = config_center.read_conf("Other", "version") + logger.debug(f"服务端版本: {Version(server_version)},本地版本: {Version(local_version)}") + if Version(server_version) > Version(local_version): + utils.tray_icon.push_update_notification(f"新版本速递:{server_version}\n请在“设置”中了解更多。") + +class weatherReportThread(QThread): # 获取最新天气信息 + weather_signal = pyqtSignal(dict) + + def __init__(self): + super().__init__() + + def run(self): + try: + weather_data = self.get_weather_data() + self.weather_signal.emit(weather_data) + except Exception as e: + logger.error(f"触发天气信息失败: {e}") + finally: + self.deleteLater() + + @staticmethod + def get_weather_data(): + location_key = config_center.read_conf('Weather', 'city') + if location_key == '0': + city_thread = getCity() + loop = QEventLoop() + city_thread.finished.connect(loop.quit) + city_thread.start() + loop.exec_() # 阻塞到完成 + location_key = config_center.read_conf('Weather', 'city') + if location_key == '0' or not location_key: + location_key = 101010100 + days = 1 + key = config_center.read_conf('Weather', 'api_key') + url = db.get_weather_url().format(location_key=location_key, days=days, key=key) + alert_url = db.get_weather_alert_url() + try: + data_group = {'now': {}, 'alert': {}} + response_now = requests.get(url, proxies=proxies) # 禁用代理 + if alert_url == 'NotSupported': + logger.warning(f"当前API不支持天气预警信息") + elif alert_url is None: + logger.warning(f"无单独天气预警信息API") + else: + alert_url = alert_url.format(location_key=location_key, key=key) + response_alert = requests.get(alert_url, proxies=proxies) + + if response_alert.status_code == 200: + data_alert = response_alert.json() + data_group['alert'] = data_alert + else: + logger.error(f"获取天气预警信息失败:{response_alert.status_code}") + + if response_now.status_code == 200: + data = response_now.json() + data_group['now'] = data + return data_group + else: + logger.error(f"获取天气信息失败:{response_now.status_code}") + return {'error': {'info': {'value': '错误', 'unit': response_now.status_code}}} + except requests.exceptions.RequestException as e: # 请求失败 + logger.error(f"获取天气信息失败:{e}") + return {'error': {'info': {'value': '错误', 'unit': ''}}} + except Exception as e: + logger.error(f"获取天气信息失败:{e}") + return {'error': {'info': {'value': '错误', 'unit': ''}}} diff --git a/play_audio.py b/play_audio.py new file mode 100644 index 0000000..2d8e1b3 --- /dev/null +++ b/play_audio.py @@ -0,0 +1,99 @@ +import os +import time + +import pygame +import pygame.mixer +from PyQt5.QtCore import QThread, pyqtSignal +from loguru import logger + +import conf +from file import config_center +from generate_speech import TTSEngine + +sound_cache = {} + + +class PlayAudio(QThread): + play_back_signal = pyqtSignal(bool) + + def __init__(self, file_path: str, tts_delete_after: bool = False): + super().__init__() + self.file_path = file_path + self.tts_delete_after = tts_delete_after + + def run(self): + play_audio(self.file_path, self.tts_delete_after) + self.play_back_signal.emit(True) + + +def play_audio(file_path: str, tts_delete_after: bool = False): + # global sound # Removed global sound variable + sound = None # Use local variable + channel = None # Initialize channel + try: + if not os.path.exists(file_path): + raise FileNotFoundError(f"音频文件不存在: {file_path}") + + if not pygame.mixer.get_init(): + try: + pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) + except pygame.error: + logger.warning("标准 Mixer 初始化失败,尝试兼容模式...") + try: + pygame.mixer.init(frequency=22050, size=-16, channels=1, buffer=1024) + logger.info("使用兼容设置成功初始化 Mixer") + except pygame.error as e_fallback: + logger.error(f"Pygame mixer 初始化失败: {e_fallback}") + return + + # 检查文件是否可读 + if os.path.getsize(file_path) <= 0: + start_time = time.time() + while time.time() - start_time < 4: + if os.path.getsize(file_path) > 0: + break + time.sleep(0.1) + else: + raise IOError("音频文件写入超时") + + try: + if file_path in sound_cache: + sound = sound_cache[file_path] + logger.debug(f'使用缓存音频: {file_path}') + else: + sound = pygame.mixer.Sound(file_path) + sound_cache[file_path] = sound + logger.debug(f'缓存音频: {file_path}') + except pygame.error as e_load: + logger.error(f"加载音频文件失败: {file_path} | 错误: {e_load}") + return + + volume = int(config_center.read_conf('Audio', 'volume')) / 100 + sound.set_volume(volume) # 设置Sound对象的音量 + channel = sound.play() + if channel: + channel.set_volume(volume) # 设置Channel对象的音量 + while channel.get_busy(): + pygame.time.wait(100) + else: + logger.error(f"无法获取播放通道: {file_path}") + + logger.debug(f'成功播放音频: {file_path}') + + if tts_delete_after: + tts = TTSEngine() + tts.delete_audio_file(file_path) + + except FileNotFoundError as e: + logger.error(f'音频文件未找到 | 路径: {file_path} | 错误: {str(e)}') + except IOError as e: + logger.error(f'音频文件读取错误或超时 | 路径: {file_path} | 错误: {str(e)}') + except pygame.error as e: + logger.error(f'Pygame 播放错误 | 路径: {file_path} | 错误: {str(e)}') + except Exception as e: + logger.error(f'未知播放失败 | 路径: {file_path} | 错误: {str(e)}') + finally: + if channel: + channel.stop() + if sound: + sound.stop() diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..aca1ea7 --- /dev/null +++ b/plugin.py @@ -0,0 +1,124 @@ +import importlib +import json +from pathlib import Path +import shutil + +from loguru import logger + +import conf + + +class PluginLoader: # 插件加载器 + def __init__(self, p_mgr=None): + self.plugins_settings = {} + self.plugins_name = [] + self.plugins_dict = {} + self.manager = p_mgr + + def set_manager(self, p_mgr): + self.manager = p_mgr + + def load_plugins(self): + for folder in Path(conf.PLUGINS_DIR).iterdir(): + if folder.is_dir() and (folder / 'plugin.json').exists(): + self.plugins_name.append(folder.name) # 检测所有插件 + + if folder.name not in conf.load_plugin_config()['enabled_plugins']: + continue + relative_path = conf.PLUGINS_DIR.name + module_name = f"{relative_path}.{folder.name}" + try: + module = importlib.import_module(module_name) + + if hasattr(module, 'Settings'): # 设置页 + plugin_class = getattr(module, "Settings") # 获取 Plugin 类 + # 实例化插件 + self.plugins_settings[folder.name] = plugin_class(f'{conf.PLUGINS_DIR}/{folder.name}') + + if self.manager and hasattr(module, 'Plugin'): # 插件入口 + plugin_class = getattr(module, "Plugin") # 获取 Plugin 类 + # 实例化插件 + self.plugins_dict[folder.name] = plugin_class( + self.manager.get_app_contexts(folder.name), self.manager.method + ) + + logger.success(f"加载插件成功:{module_name}") + except (ImportError, FileNotFoundError) as e: + logger.warning(f"加载插件 {folder.name} 失败: {e}. 可能缺少文件或依赖项。将禁用此插件。") + plugin_config = conf.load_plugin_config() + if folder.name in plugin_config['enabled_plugins']: + plugin_config['enabled_plugins'].remove(folder.name) + conf.save_plugin_config(plugin_config) + if folder.name in self.plugins_name: + self.plugins_name.remove(folder.name) + continue + except Exception as e: + logger.error(f"加载插件 {folder.name} 时发生未知错误: {e}") + # 大部分情况一般不会影响运行 + continue + return self.plugins_name + + def run_plugins(self): + for plugin in self.plugins_dict.values(): + plugin.execute() + + def update_plugins(self): + for plugin in self.plugins_dict.values(): + if hasattr(plugin, 'update'): + plugin.update(self.manager.get_app_contexts()) + + def delete_plugin(self, plugin_name): + plugin_dir = Path(conf.PLUGINS_DIR) / plugin_name + if not plugin_dir.is_dir(): + logger.warning(f"插件目录 {plugin_dir} 不存在,无法删除。") + return False + widgets_to_remove = [] + if widgets_to_remove: + try: + widget_config_path = Path(conf.base_directory) / 'config' / 'widget.json' + if widget_config_path.exists(): + with open(widget_config_path, 'r', encoding='utf-8') as f: + widget_config = json.load(f) + + original_widgets = widget_config.get('widgets', []) + # 过滤掉要移除的组件 + widget_config['widgets'] = [w for w in original_widgets if w not in widgets_to_remove] + + with open(widget_config_path, 'w', encoding='utf-8') as f: + json.dump(widget_config, f, ensure_ascii=False, indent=4) + logger.info(f"已从 config/widget.json 中移除插件 {plugin_name} 的关联组件: {widgets_to_remove}") + else: + logger.warning(f"主配置文件 config/widget.json 不存在,无法移除插件组件。") + except Exception as e: + logger.error(f"更新 config/widget.json 失败: {e}") + + if plugin_name in self.plugins_dict: + del self.plugins_dict[plugin_name] + logger.info(f"已移除正在运行的插件实例: {plugin_name}") + if plugin_name in self.plugins_settings: + del self.plugins_settings[plugin_name] + logger.info(f"已移除插件设置实例: {plugin_name}") + + plugin_config = conf.load_plugin_config() + if plugin_name in plugin_config.get('enabled_plugins', []): + plugin_config['enabled_plugins'].remove(plugin_name) + conf.save_plugin_config(plugin_config) + logger.info(f"已从启用插件列表中移除: {plugin_name}") + + if plugin_name in self.plugins_name: + self.plugins_name.remove(plugin_name) + + try: + shutil.rmtree(plugin_dir) + logger.success(f"插件 {plugin_name} 已成功删除。") + return True + except Exception as e: + logger.error(f"删除插件目录 {plugin_dir} 失败: {e}") + return False + +p_loader = PluginLoader() + + +if __name__ == '__main__': + p_loader.load_plugins() + p_loader.run_plugins() diff --git a/plugin_plaza.py b/plugin_plaza.py new file mode 100644 index 0000000..fcf8822 --- /dev/null +++ b/plugin_plaza.py @@ -0,0 +1,801 @@ +import json +import sys +from datetime import datetime +from random import shuffle + +from PyQt5 import uic +from PyQt5.QtCore import QSize, Qt, QTimer, QUrl, QStringListModel, pyqtSignal +from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices +from PyQt5.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QGridLayout, QSpacerItem, QSizePolicy, QWidget, \ + QScroller, QCompleter +from loguru import logger +from qfluentwidgets import MSFluentWindow, FluentIcon as fIcon, NavigationItemPosition, TitleLabel, \ + ImageLabel, StrongBodyLabel, HyperlinkLabel, CaptionLabel, PrimaryPushButton, HorizontalFlipView, \ + InfoBar, InfoBarPosition, SplashScreen, MessageBoxBase, TransparentToolButton, BodyLabel, \ + PrimarySplitPushButton, RoundMenu, Action, PipsPager, TextBrowser, CardWidget, \ + IndeterminateProgressRing, ComboBox, ProgressBar, SmoothScrollArea, SearchLineEdit, HyperlinkButton, \ + MessageBox, SwitchButton, SubtitleLabel + +import conf +import list_ as l +import network_thread as nt +from conf import base_directory +from file import config_center +from plugin import p_loader +from utils import restart, calculate_size +import platform +from loguru import logger + +# 适配高DPI缩放 +if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: + QApplication.setHighDpiScaleFactorRoundingPolicy( + Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) +else: + logger.warning('不兼容的系统,跳过高DPI标识') + +CONF_PATH = f"{base_directory}/plugins/plugins_from_pp.json" +PLAZA_REPO_URL = "https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/" +PLAZA_REPO_DIR = "https://api.github.com/repos/Class-Widgets/plugin-plaza/contents/Plugins" +TEST_DOWNLOAD_LINK = "https://dldir1.qq.com/qqfile/qq/PCQQ9.7.17/QQ9.7.17.29225.exe" + +restart_tips_flag = False # 重启提示 +plugins_data = {} # 仓库插件信息 +local_plugins_version = {} # 本地插件版本 +download_progress = [] # 下载线程 + +installed_plugins = [] # 已安装插件(通过PluginPlaza获取) +tags = ['示例', '信息展示', '学习', '测试', '工具', '自动化'] # 测试用TAG +recommend_plugins = ['cw-example-plugin'] # 推荐插件(通过PluginPlaza获取) +search_items = [] +SELF_PLUGIN_VERSION = config_center.read_conf('Plugin', 'version') # 自身版本号 +SEARCH_FIELDS = ["name", "description", "tag", "author"] # 搜索字段 + + +class TagLink(HyperlinkButton): # 标签链接 + def __init__(self, text, parent=None): + super().__init__(parent) + self.parent = parent + self.tag = text + self.setText(text) + self.setIcon(fIcon.SEARCH) + + self.setFixedHeight(30) + self.clicked.connect(self.search_tag) + + def search_tag(self): + self.parent.search_plugin.setText(self.tag) + self.parent.search_plugin.searchSignal.emit(self.tag) # 发射搜索信号 + + +class downloadProgressBar(InfoBar): # 下载进度条(创建下载进程) + def __init__(self, url=TEST_DOWNLOAD_LINK, branch='main', name="Test", parent=None): + global download_progress + self.p_name = url.split('/')[4] # repo + # user = url.split('/')[3] + self.name = name + self.url = f'{url}/archive/refs/heads/{branch}.zip' + + super().__init__(icon=fIcon.DOWNLOAD, + title='', + content=f"正在下载 {name} (~ ̄▽ ̄)~)", + orient=Qt.Horizontal, + isClosable=False, + position=InfoBarPosition.TOP, + duration=-1, + parent=parent + ) + self.setCustomBackgroundColor('white', '#202020') + self.bar = ProgressBar() + self.bar.setFixedWidth(300) + self.cancelBtn = HyperlinkLabel() + self.cancelBtn.setText("取消") + self.cancelBtn.clicked.connect(self.cancelDownload) + self.addWidget(self.bar) + self.addWidget(self.cancelBtn) + + # 开始下载 + + download_progress.append(self.p_name) + self.download(self.url) + + def download(self, url): # 接受下载连接并开始任务 + self.download_thread = nt.DownloadAndExtract(url, self.p_name) + # self.download_thread = nt.DownloadAndExtract(TEST_DOWNLOAD_LINK, self.p_name) + self.download_thread.progress_signal.connect(lambda progress: self.bar.setValue(int(progress))) # 下载进度 + self.download_thread.status_signal.connect(self.detect_status) # 判断状态 + self.download_thread.start() + + def cancelDownload(self): + global download_progress + download_progress.remove(self.p_name) + self.download_thread.stop() + self.download_thread.deleteLater() + self.close() + + def detect_status(self, status): + if status == "DOWNLOADING": + self.content = f"正在下载 {self.name} (~ ̄▽ ̄)~)" + elif status == "EXTRACTING": + self.content = f"正在解压 {self.name} ( •̀ ω •́ )✧)" + elif status == "DONE": + self.download_finished() + elif status.startswith("ERROR"): + self.download_error(status[6:]) + else: + pass + + def download_finished(self): + global download_progress + download_progress.remove(self.p_name) + add2save_plugin(self.p_name) # 保存到配置 + self.download_thread.finished.emit() + self.download_thread.deleteLater() + + InfoBar.success( + title='下载成功!', + content=f"下载 {self.name} 成功!", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self.parent() + ) + if not restart_tips_flag: # 重启提示 + self.parent().restart_tips() + self.close() + + def download_error(self, error_info): + global download_progress + download_progress.remove(self.p_name) + InfoBar.error( + title='下载失败(っ °Д °;)っ', + content=f"{error_info}", + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=5000, + parent=self.parent() + ) + self.close() + + +def install_plugin(parent, p_name, data): + plugin_ver = str(data.get('plugin_ver')) + if plugin_ver != SELF_PLUGIN_VERSION: # 插件版本不匹配 + if plugin_ver > SELF_PLUGIN_VERSION: + content = (f'此插件版本({plugin_ver})高于当前设备中 Class Widgets 兼容的插件版本({SELF_PLUGIN_VERSION});\n' + f'请更新 Class Widgets 后再尝试安装此插件。') + else: + content = (f'此插件版本({plugin_ver})低于当前设备中 Class Widgets 兼容的插件版本({SELF_PLUGIN_VERSION});\n' + f'可能是插件缺乏维护,请联系插件作者更新插件,或在社区(GitHub、QQ群)中提出问题。') + + cc = MessageBox( + "本插件不兼容当前版本的 Class Widgets", + f"{content}\n\n不建议安装此插件,否则将出现不可预料(包括崩溃、闪退等故障)的问题。", + parent + ) # 兼容性检查窗口 + cc.yesButton.setText("取消安装") + cc.cancelButton.setText("强制安装(不建议)") + if cc.exec(): # 取消安装 + return False + + if p_name not in download_progress: # 如果正在下载 + url = data.get("url") + branch = data.get("branch") + title = data.get("name") + + di = downloadProgressBar( + url=f"{url}", + branch=branch, + name=title, + parent=parent + ) + di.show() + return True + return False + + +class PluginDetailPage(MessageBoxBase): # 插件详情页面 + def __init__(self, icon, title, content, tag, version, author, url, data=None, parent=None): + super().__init__(parent) + self.data = data + self.branch = data.get("branch") + self.title = title + self.parent = parent + self.url = url + self.p_name = url.split('/')[-1] # repo + author_url = '/'.join(url.rsplit('/', 2)[:-1]) + self.init_ui() + self.download_readme() + scroll_area_widget = self.findChild(QVBoxLayout, 'verticalLayout_9') + + self.iconWidget = self.findChild(ImageLabel, 'pluginIcon') + self.iconWidget.setImage(icon) + self.iconWidget.setFixedSize(100, 100) + self.iconWidget.setBorderRadius(8, 8, 8, 8) + + self.titleLabel = self.findChild(TitleLabel, 'titleLabel') # 标题 + self.titleLabel.setText(title) + + self.contentLabel = self.findChild(CaptionLabel, 'descLabel') # 描述 + self.contentLabel.setText(content) + + self.tagLabel = self.findChild(HyperlinkLabel, 'tagButton') # tag + self.tagLabel.setText(tag) + + self.versionLabel = self.findChild(BodyLabel, 'versionLabel') # 版本 + self.versionLabel.setText(version) + + self.authorLabel = self.findChild(HyperlinkLabel, 'authorButton') # 作者 + self.authorLabel.setText(author) + self.authorLabel.setUrl(author_url) + + self.openGitHub = self.findChild(TransparentToolButton, 'openGitHub') # 打开连接 + self.openGitHub.setIcon(fIcon.LINK) + self.openGitHub.setIconSize(QSize(18, 18)) + self.openGitHub.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url))) + + self.installButton = self.findChild(PrimarySplitPushButton, 'installButton') + self.installButton.setText(" 安装 ") + self.installButton.setIcon(fIcon.DOWNLOAD) + self.installButton.clicked.connect(self.install) + + if self.p_name in download_progress: # 如果正在下载 + self.installButton.setText(" 安装中 ") + self.installButton.setEnabled(False) + if self.p_name in installed_plugins: # 如果已安装 + self.installButton.setText(" 已安装 ") + self.installButton.setEnabled(False) + + if self.p_name in local_plugins_version: # 如果本地版本低于仓库版本 + print(local_plugins_version[self.p_name], version) + if local_plugins_version[self.p_name] < version: + self.installButton.setText("更新") + self.installButton.setIcon(fIcon.SYNC) + self.installButton.setEnabled(True) + + menu = RoundMenu(parent=self.installButton) + menu.addActions([ + Action(fIcon.DOWNLOAD, "为 Class Widgets 安装", triggered=self.install), + Action(fIcon.LINK, "下载到本地", + triggered=lambda: QDesktopServices.openUrl(QUrl(f"{url}/releases/latest"))) + ]) + self.installButton.setFlyout(menu) + + self.readmePage = TextBrowser(self) + self.readmePage.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.readmePage.setReadOnly(True) + scroll_area_widget.addWidget(self.readmePage) + + def install(self): + if install_plugin(self.parent, self.p_name, self.data): + self.installButton.setText(" 安装中 ") + self.installButton.setEnabled(False) + + def download_readme(self): + def display_readme(markdown_text): + self.readmePage.setMarkdown(markdown_text) + + if self.data is None: + self.download_thread = nt.getReadme(f"{replace_to_file_server(self.url)}/README.md") + else: + self.download_thread = nt.getReadme(f"{replace_to_file_server(self.url, self.data['branch'])}/README.md") + self.download_thread.html_signal.connect(display_readme) + self.download_thread.start() + + def init_ui(self): + # 加载ui文件 + self.temp_widget = QWidget() + uic.loadUi(f'{base_directory}/view/pp/plugin_detail.ui', self.temp_widget) + self.viewLayout.addWidget(self.temp_widget) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + # 隐藏原有按钮 + self.yesButton.hide() + self.cancelButton.hide() + self.buttonGroup.hide() + + # 自定关闭按钮 + self.closeButton = self.findChild(TransparentToolButton, 'closeButton') + self.closeButton.setIcon(fIcon.CLOSE) + self.closeButton.clicked.connect(self.close) + + self.widget.setMinimumWidth(875) + self.widget.setMinimumHeight(625) + + +class PluginCard_Horizontal(CardWidget): # 插件卡片(横向) + def __init__( + self, icon='img/plaza/plugin_pre.png', title='Plugin Name', content='Description...', tag='Unknown', + version='1.0.0', author="CW Support", + url="https://github.com/RinLit-233-shiroko/cw-example-plugin", data=None, parent=None): + super().__init__(parent) + self.icon = icon + self.title = title + self.plugin_ver = data.get('plugin_ver') + self.parent = parent + self.tag = tag + self.branch = data.get("branch") + self.url = url + self.p_name = url.split('/')[-1] # repo + self.data = data + author_url = '/'.join(self.url.rsplit('/', 2)[:-1]) + + self.iconWidget = ImageLabel(icon) # 插件图标 + self.titleLabel = StrongBodyLabel(title, self) # 插件名 + self.versionLabel = CaptionLabel(version, self) # 插件版本 + self.authorLabel = HyperlinkLabel() # 插件作者 + self.contentLabel = CaptionLabel(content, self) # 插件描述 + self.installButton = PrimaryPushButton() + + # layout + self.hBoxLayout = QHBoxLayout() + self.hBoxLayout_Title = QHBoxLayout() + self.hBoxLayout_Author = QHBoxLayout() + self.vBoxLayout = QVBoxLayout() + + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.setFixedHeight(110) + self.setMinimumWidth(250) + self.authorLabel.setText(author) + self.authorLabel.setUrl(author_url) + self.authorLabel.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) + self.iconWidget.setFixedSize(84, 84) + self.iconWidget.setBorderRadius(5, 5, 5, 5) # 圆角 + self.contentLabel.setTextColor("#606060", "#d2d2d2") + self.contentLabel.setWordWrap(True) + self.versionLabel.setTextColor("#999999", "#999999") + self.titleLabel.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) + + self.installButton.setText("安装") + self.installButton.setMaximumSize(100, 36) + self.installButton.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + self.installButton.setIcon(fIcon.DOWNLOAD) + self.installButton.clicked.connect(self.install) + + if self.p_name in installed_plugins: # 如果已安装 + self.installButton.setText("已安装") + self.installButton.setEnabled(False) + + if self.p_name in local_plugins_version: # 如果本地版本低于仓库版本 + print(local_plugins_version[self.p_name], version) + if local_plugins_version[self.p_name] < version: + self.installButton.setText("更新") + self.installButton.setIcon(fIcon.SYNC) + self.installButton.setEnabled(True) + + self.hBoxLayout.setContentsMargins(20, 11, 11, 11) + self.hBoxLayout.setSpacing(15) + self.hBoxLayout.addWidget(self.iconWidget) + + self.blank = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.vBoxLayout.setContentsMargins(0, 5, 0, 5) + self.vBoxLayout.setSpacing(0) + self.vBoxLayout.addLayout(self.hBoxLayout_Title) + self.vBoxLayout.addLayout(self.hBoxLayout_Author) + self.vBoxLayout.addItem(self.blank) + self.vBoxLayout.addWidget(self.contentLabel, 0, Qt.AlignmentFlag.AlignTop) + self.vBoxLayout.setAlignment(Qt.AlignmentFlag.AlignVCenter) + + self.hBoxLayout.addLayout(self.vBoxLayout) + self.hBoxLayout.addWidget(self.installButton) + self.setLayout(self.hBoxLayout) + + self.hBoxLayout_Title.setSpacing(12) + self.hBoxLayout_Title.addWidget(self.titleLabel, 0, Qt.AlignmentFlag.AlignVCenter) + self.hBoxLayout_Title.addWidget(self.versionLabel, 0, Qt.AlignmentFlag.AlignVCenter) + + self.hBoxLayout_Author.addWidget(self.authorLabel, 0, Qt.AlignmentFlag.AlignLeft) + + def install(self): + install_plugin(self.parent, self.p_name, self.data) + + def set_img(self, img): + try: + self.icon = img + self.iconWidget.setImage(img) + self.iconWidget.setFixedSize(84, 84) + except Exception as e: + logger.error(f"设置插件图片失败: {e}") + + def show_detail(self): + w = PluginDetailPage( + icon=self.icon, title=self.title, content=self.contentLabel.text(), + tag=self.tag, version=self.versionLabel.text(), author=self.authorLabel.text(), + url=self.url, data=self.data, parent=self.parent + ) + w.exec() + + +class PluginPlaza(MSFluentWindow): + closed = pyqtSignal() + + def __init__(self): + super().__init__() + self.splashScreen = None + global installed_plugins + try: + with open(CONF_PATH, 'r', encoding='utf-8') as file: + installed_plugins = json.load(file).get('plugins') + # 校验 + for plugin in installed_plugins: + if plugin not in p_loader.plugins_name: + logger.warning(f"已在插件广场安装的插件 {plugin} 未找到,可能已遭删除") + installed_plugins.remove(plugin) + except Exception as e: + logger.error(f"读取已安装的插件失败: {e}") + try: + self.homeInterface = uic.loadUi(f'{base_directory}/view/pp/home.ui') # 首页 + self.homeInterface.setObjectName("homeInterface") + self.latestsInterface = uic.loadUi(f'{base_directory}/view/pp/latests.ui') # 最新更新 + self.latestsInterface.setObjectName("latestInterface") + self.settingsInterface = uic.loadUi(f'{base_directory}/view/pp/settings.ui') # 设置 + self.settingsInterface.setObjectName("settingsInterface") + self.searchInterface = uic.loadUi(f'{base_directory}/view/pp/search.ui') # 搜索 + self.searchInterface.setObjectName("searchInterface") + + load_local_plugins_version() # 加载本地插件版本 + self.init_nav() + self.init_window() + self.get_pp_data() + self.get_banner_img() + except Exception as e: + logger.error(f'初始化插件广场时发生错误:{e}') + + def load_all_interface(self): + self.setup_homeInterface() + self.setup_latestInterface() + self.setup_settingsInterface() + self.setup_searchInterface() + + def setup_latestInterface(self): # 初始化最新更新 + latest_scroll = self.latestsInterface.findChild(SmoothScrollArea, 'latest_scroll') + QScroller.grabGesture(latest_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + def setup_searchInterface(self): # 初始化搜索 + search_scroll = self.searchInterface.findChild(SmoothScrollArea, 'search_scroll') + + def search(keyword): # 搜索 + if keyword == '/all': + return plugins_data + + result = {} + for key, value in plugins_data.items(): + if any(keyword.lower() in str(value.get(field, "")).lower() for field in SEARCH_FIELDS): + result[key] = value + return result + + def clear_results(): + for i in reversed(range(self.search_plugin_grid.count())): + widget = self.search_plugin_grid.itemAt(i).widget() + if widget: + widget.setParent(None) # 移除控件 + widget.destroy() # 销毁控件 + + def search_plugins(): # 搜索插件 + if not plugins_data: + return + clear_results() + + if self.search_plugin.text(): + def set_plugin_image(plugin_card, data): + pixmap = QPixmap() + pixmap.loadFromData(data) + plugin_card.set_img(pixmap) + + keyword = self.search_plugin.text() + print(f'结果:{search(keyword)}') # 结果 + plugin_num = 0 # 计数 + for key, data in search(keyword).items(): + plugin_card = PluginCard_Horizontal(title=data['name'], content=data['description'], + tag=data['tag'], version=data['version'], url=data['url'], + author=data['author'], data=data, parent=self) + plugin_card.clicked.connect(plugin_card.show_detail) # 点击事件 + + # 启动线程加载图片 + image_thread = nt.getImg(f"{replace_to_file_server(data['url'], data['branch'])}/icon.png") + image_thread.repo_signal.connect( + lambda img_data, card=plugin_card: set_plugin_image(card, img_data)) + image_thread.start() + + self.search_plugin_grid.addWidget(plugin_card, plugin_num // 2, plugin_num % 2) # 排列 + plugin_num += 1 + + self.search_plugin_grid = self.searchInterface.findChild(QGridLayout, 'search_plugin_grid') # 插件表格 + self.tags_layout = self.searchInterface.findChild(QGridLayout, 'tags_layout') # tag 布局 + self.search_plugin = self.searchInterface.findChild(SearchLineEdit, 'search_plugin') + self.search_plugin.searchSignal.connect(search_plugins) + self.search_plugin.returnPressed.connect(search_plugins) + self.search_plugin.clearSignal.connect(clear_results) + self.search_completer = QCompleter(search_items, self.search_plugin) + # 设置显示的选项数 + self.search_completer.setMaxVisibleItems(10) + self.search_completer.setFilterMode(Qt.MatchContains) # 内容匹配 + self.search_completer.setCaseSensitivity(Qt.CaseInsensitive) # 不区分大小写 + self.search_completer.activated.connect(search_plugins) + self.search_plugin.setCompleter(self.search_completer) + + QScroller.grabGesture(search_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + def setup_settingsInterface(self): # 初始化设置 + # 选择代理 + select_mirror = self.settingsInterface.findChild(ComboBox, 'select_proxy') + select_mirror.addItems(nt.mirror_list) + select_mirror.setCurrentIndex(nt.mirror_list.index(config_center.read_conf('Plugin', 'mirror'))) + select_mirror.currentIndexChanged.connect( + lambda: config_center.write_conf('Plugin', 'mirror', select_mirror.currentText())) + + # 开关自动启用插件 + auto_enable_plugin = self.settingsInterface.findChild(SwitchButton, 'auto_enable_plugin') + auto_enable_plugin.setChecked(int(config_center.read_conf('Plugin', 'auto_enable_plugin'))) + auto_enable_plugin.checkedChanged.connect( + lambda: config_center.write_conf('Plugin', 'auto_enable_plugin', int(auto_enable_plugin.isChecked())) + ) + + def setup_homeInterface(self): # 初始化首页 + # 标题和副标题 + home_scroll = self.homeInterface.findChild(SmoothScrollArea, 'home_scroll') + time_today_label = self.homeInterface.findChild(TitleLabel, 'time_today_label') + time_today_label.setText(f"{datetime.now().month}月{datetime.now().day}日 {l.week[datetime.now().weekday()]}") + + # Banner + self.banner_view = self.homeInterface.findChild(HorizontalFlipView, 'banner_view') + self.banner_view.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio) + self.banner_view.setItemSize(QSize(900, 450)) # 设置图片大小(banner图片尺寸比) + self.banner_view.setBorderRadius(8) + self.banner_view.setSpacing(5) + self.banner_view.clicked.connect(self.open_banner_link) + + self.auto_play_timer = QTimer(self) # 自动轮播 + self.auto_play_timer.timeout.connect(lambda: self.switch_banners()) + self.auto_play_timer.setInterval(2500) + + # 翻页 + self.banner_pager = self.homeInterface.findChild(PipsPager, 'banner_pager') + self.banner_pager.setVisibleNumber(5) + self.banner_pager.currentIndexChanged.connect( + lambda: (self.banner_view.scrollToIndex(self.banner_pager.currentIndex()), + self.auto_play_timer.stop(), + self.auto_play_timer.start(2500)) + ) + QScroller.grabGesture(home_scroll.viewport(), QScroller.LeftMouseButtonGesture) + + def open_banner_link(self): + if not hasattr(self, 'img_list'): + QDesktopServices.openUrl(QUrl( + 'https://www.yuque.com/rinlit/class-widgets_help/ez4vv7tv8wikxc0s#Se2Bb' + )) + return False # 没有图片 + + if self.img_list[self.banner_view.currentIndex()] in self.banners_data: + if not self.banners_data[self.img_list[self.banner_view.currentIndex()]]['link']: + return False # 无链接 + QDesktopServices.openUrl(QUrl( + self.banners_data[self.img_list[self.banner_view.currentIndex()]]['link'] + )) + + def set_tags_data(self, data): + global tags, search_items, recommend_plugins + rec_data = {} + if data: + tags = data.get('tags') + recommend_plugins = data.get('recommend_plugin') + shuffle(tags) # 随机 + for tag in tags: + search_items.append(tag) + self.search_completer.setModel(QStringListModel(search_items)) # 设置搜索提示 + tag_num = 0 # 计数 + for tag in tags[:6]: + tag_link = TagLink(tag, self) + self.tags_layout.addWidget(tag_link, tag_num // 3, tag_num % 3) # 排列 + tag_num += 1 + + for key, data in plugins_data.items(): + if key in recommend_plugins: + rec_data[key] = data + self.load_plugins(rec_data, 'home') + + def load_plugins(self, p_data, page): + global search_items + + for plugin in p_data.values(): # 遍历插件数据 + search_items.append(plugin['name']) + if plugin['author'] not in search_items: + search_items.append(plugin['author']) + self.search_completer.setModel(QStringListModel(search_items)) # 设置搜索提示 + + def set_plugin_image(plugin_card, data): + pixmap = QPixmap() + pixmap.loadFromData(data) + plugin_card.set_img(pixmap) + + if page == 'latest': + self.plugin_grid = self.latestsInterface.findChild(QGridLayout, 'all_plugin_grid') # 插件表格 + elif page == 'home': + self.plugin_grid = self.homeInterface.findChild(QGridLayout, 'rec_plugin_grid') # 插件表格 + else: + self.plugin_grid = self.latestsInterface.findChild(QGridLayout, 'all_plugin_grid') # 插件表格 + plugin_num = 0 # 计数 + + for plugin, data in p_data.items(): # 遍历插件数据 + plugin_card = PluginCard_Horizontal(title=data['name'], content=data['description'], + tag=data['tag'], version=data['version'], url=data['url'], + author=data['author'], data=data, parent=self) + plugin_card.clicked.connect(plugin_card.show_detail) # 点击事件 + + # 启动线程加载图片 + image_thread = nt.getImg(f"{replace_to_file_server(data['url'], data['branch'])}/icon.png") + image_thread.repo_signal.connect(lambda img_data, card=plugin_card: set_plugin_image(card, img_data)) + image_thread.start() + + self.plugin_grid.addWidget(plugin_card, plugin_num // 2, plugin_num % 2) # 排列 + plugin_num += 1 + + self.homeInterface.findChild(IndeterminateProgressRing, 'load_plugin_progress').hide() + + def get_banner_img(self): + def display_banner(data, index=0): + if index == 0: + self.auto_play_timer.start() + if data: + pixmap = QPixmap() + pixmap.loadFromData(data) + self.banner_view.setItemImage(index, pixmap) + self.splashScreen.hide() + + def get_banner(data=dict): + try: + if 'error' not in data: + self.banners_data = data + self.img_list = self.img_links = list(data.keys()) + self.img_links = [f'https://raw.githubusercontent.com/Class-Widgets/plugin-plaza/main/Banner/' + f'{img}.png' for img in self.img_links] + self.banner_pager.setPageNumber(len(data)) + banner_placeholders = ["img/plaza/banner_pre.png" for _ in range(len(data))] + self.banner_view.addImages(banner_placeholders) + else: + error_info = data.get("error", "未知错误") + logger.error(f'PluginPlaza 无法联网,错误:{error_info}') + self.findChild(BodyLabel, 'tips').setText(f'错误原因:{error_info}') + self.banner_view.addImage("img/plaza/banner_network-failed.png") + self.banner_view.addImage("img/plaza/banner_network-failed.png") + self.splashScreen.hide() + self.homeInterface.findChild(SubtitleLabel, 'SubtitleLabel_3').hide() # 隐藏副标题 + return + + # 定义一个内部函数来启动下一个线程 + def start_next_banner(index): + if index < len(data): + self.banner_thread = nt.getImg(self.img_links[index]) + self.banner_thread.repo_signal.connect(lambda data: display_banner(data, index)) + self.banner_thread.repo_signal.connect(lambda: start_next_banner(index + 1)) # 连接完成信号 + self.banner_thread.start() + + start_next_banner(0) # 启动第一个线程 + + except Exception as e: + logger.error(f"获取Banner失败:{e}") + + self.banner_list_thread = nt.getRepoFileList() + self.banner_list_thread.repo_signal.connect(get_banner) + self.banner_list_thread.start() + + def restart_tips(self): + global restart_tips_flag + restart_tips_flag = True + w = InfoBar.info( + title='需要重启', + content='若要应用插件配置,需重启 Class Widgets', + orient=Qt.Horizontal, + isClosable=True, + position=InfoBarPosition.TOP, + duration=-1, + parent=self + ) + restart_btn = HyperlinkLabel('现在重启') + restart_btn.clicked.connect(restart) + w.addWidget(restart_btn) + w.show() + + def get_pp_data(self): + global plugins_data + def callback(data): + global plugins_data + plugins_data = data # 保存插件数据 + self.load_plugins(data, 'latest') + self.get_tags_data() + + self.get_plugin_list_thread = nt.getPluginInfo() + self.get_plugin_list_thread.repo_signal.connect(callback) + self.get_plugin_list_thread.start() + + def get_tags_data(self): + self.get_tags_list_thread = nt.getTags() + self.get_tags_list_thread.repo_signal.connect(self.set_tags_data) + self.get_tags_list_thread.start() + + def switch_banners(self): # 切换Banner + if self.banner_view.currentIndex() == len(self.img_list) - 1: + self.banner_view.scrollToIndex(0) + self.banner_pager.setCurrentIndex(0) + else: + self.banner_view.scrollNext() + self.banner_pager.setCurrentIndex(self.banner_view.currentIndex()) + + def init_nav(self): + self.addSubInterface(self.homeInterface, fIcon.HOME, '首页', fIcon.HOME_FILL) + self.addSubInterface(self.latestsInterface, fIcon.LIBRARY, '分类', fIcon.LIBRARY_FILL) + self.addSubInterface( + self.searchInterface, fIcon.SEARCH, '搜索', position=NavigationItemPosition.BOTTOM + ) + self.addSubInterface( + self.settingsInterface, fIcon.SETTING, '设置', fIcon.SETTING, position=NavigationItemPosition.BOTTOM + ) + + def init_window(self): + self.load_all_interface() + self.init_font() + + self.setMinimumWidth(850) + self.setMinimumHeight(500) + self.setWindowTitle('插件广场') + self.setWindowIcon(QIcon(f'{base_directory}/img/pp_favicon.png')) + + # 设置窗口大小 + size, pos = calculate_size() + + self.move(pos[0], pos[1]) + self.resize(size[0], size[1]) + + # 启动屏幕 + self.splashScreen = SplashScreen(self.windowIcon(), self) + self.splashScreen.setIconSize(QSize(102, 102)) + self.show() + + def init_font(self): # 设置字体 + self.setStyleSheet("""QLabel { + font-family: 'Microsoft YaHei'; + }""") + + def closeEvent(self, event): + self.closed.emit() + event.accept() + + +def add2save_plugin(p_name): # 保存已安装插件 + global installed_plugins + installed_plugins.append(p_name) + try: + with open(CONF_PATH, 'r+', encoding='utf-8') as f: + if p_name not in json.load(f)['plugins']: + f.seek(0) # 指针指向开头 + json.dump({"plugins": installed_plugins}, f, ensure_ascii=False, indent=4) + f.truncate() # 截断文件 + except Exception as e: + logger.error(f"保存已安装插件失败:{e}") + + +def replace_to_file_server(url, branch='main'): + return (f'{url.replace("https://github.com/", "https://raw.githubusercontent.com/")}' + f'/{branch}') + + +def load_local_plugins_version(): + global local_plugins_version + for plugin in installed_plugins: + try: + with open(f"plugins/{plugin}/plugin.json", 'r', encoding='utf-8') as f: + data = json.load(f) + local_plugins_version[plugin] = data['version'] + except Exception as e: + logger.error(f"加载本地插件版本失败:{e}") + print(local_plugins_version) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + pp = PluginPlaza() + pp.show() + sys.exit(app.exec()) diff --git a/plugins/plugins_from_pp.json b/plugins/plugins_from_pp.json new file mode 100644 index 0000000..80a6988 --- /dev/null +++ b/plugins/plugins_from_pp.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + + ] +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2fa5cbf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +# --------------------------------- +# System platform value +# --------------------------------- +# Windows "win32" +# --------------------------------- +certifi==2024.8.30 +charset-normalizer==3.3.2 +colorama==0.4.6 +configparser~=7.1.0 +darkdetect==0.8.0 +idna==3.10 +loguru~=0.7.2 +PyQt5~=5.15.11 +PyQt-Fluent-Widgets~=1.8.1 +pyqt5-frameless-window==0.4.3 +pyqt5-qt5~=5.15.2 +pyqt5-sip==12.15.0 +pyqt5-stubs==5.15.6.0 +pywin32==306; platform_system == "Windows" +psutil==5.9.5 +requests==2.32.3 +urllib3==2.2.3 +win32-setctime==1.1.0 +PyQtWebEngine~=5.15.7 +Markdown~=3.7 +pygame~=2.6.1 +packaging~=24.2 +PyGetWindow~=0.0.9 +edge-tts~=7.0.0 +pyttsx3==2.98 +python-dateutil==2.8.2 +git+https://github.com/SmartTeachCN/pycses.git diff --git a/tip_toast.py b/tip_toast.py new file mode 100644 index 0000000..df9d6a8 --- /dev/null +++ b/tip_toast.py @@ -0,0 +1,460 @@ +import sys + +import os +from PyQt5 import uic +from PyQt5.QtCore import Qt, QPropertyAnimation, QRect, QEasingCurve, QTimer, QPoint, pyqtProperty, QThread +from PyQt5.QtGui import QColor, QPainter, QBrush, QPixmap +from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QFrame, QGraphicsBlurEffect +from loguru import logger +from qfluentwidgets import setThemeColor + +import conf +from conf import base_directory +import list_ +from file import config_center +from play_audio import PlayAudio +import platform + +# 适配高DPI缩放 +if platform.system() == 'Windows' and platform.release() not in ['7', 'XP', 'Vista']: + QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) +else: + logger.warning('不兼容的系统,跳过高DPI标识') + +prepare_class = config_center.read_conf('Audio', 'prepare_class') +attend_class = config_center.read_conf('Audio', 'attend_class') +finish_class = config_center.read_conf('Audio', 'finish_class') + +pushed_notification = False +notification_contents = {"state": None, "lesson_name": None, "title": None, "subtitle": None, "content": None} + +# 波纹效果 +normal_color = '#56CFD8' + +window_list = [] # 窗口列表 +active_windows = [] + + +class tip_toast(QWidget): + def __init__(self, pos, width, state=1, lesson_name=None, title=None, subtitle=None, content=None, icon=None, duration=2000): + super().__init__() + for w in active_windows[:]: + w.close() + active_windows.append(self) + self.audio_thread = None + uic.loadUi(f"{base_directory}/view/widget-toast-bar.ui", self) + + try: + dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() + except AttributeError: + dpr = QApplication.primaryScreen().devicePixelRatio() + dpr = max(1.0, dpr) + + # 窗口位置 + if config_center.read_conf('Toast', 'pin_on_top') == '1': + self.setWindowFlags( + Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint | + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + else: + self.setWindowFlags( + Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.move(pos[0], pos[1]) + self.resize(width, height) + + # 标题 + title_label = self.findChild(QLabel, 'title') + backgnd = self.findChild(QFrame, 'backgnd') + lesson = self.findChild(QLabel, 'lesson') + subtitle_label = self.findChild(QLabel, 'subtitle') + icon_label = self.findChild(QLabel, 'icon') + + sound_to_play = None + if icon: + pixmap = QPixmap(icon) + icon_size = int(48 * dpr) + pixmap = pixmap.scaled(icon_size, icon_size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + icon_label.setPixmap(pixmap) + icon_label.setFixedSize(icon_size, icon_size) + + if state == 1: + logger.info('上课铃声显示') + title_label.setText('活动开始') # 修正文本,以适应不同场景 + subtitle_label.setText('当前课程') + lesson.setText(lesson_name) # 课程名 + sound_to_play = attend_class + setThemeColor(f"#{config_center.read_conf('Color', 'attend_class')}") # 主题色 + elif state == 0: + logger.info('下课铃声显示') + title_label.setText('下课') + if lesson_name: + subtitle_label.setText('即将进行') + else: + subtitle_label.hide() + lesson.setText(lesson_name) # 课程名 + sound_to_play = finish_class + setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") + elif state == 2: + logger.info('放学铃声显示') + title_label.setText('放学') + subtitle_label.setText('当前课程已结束') + lesson.setText('') # 课程名 + sound_to_play = finish_class + setThemeColor(f"#{config_center.read_conf('Color', 'finish_class')}") + elif state == 3: + logger.info('预备铃声显示') + title_label.setText('即将开始') # 同上 + subtitle_label.setText('下一节') + lesson.setText(lesson_name) + sound_to_play = prepare_class + setThemeColor(f"#{config_center.read_conf('Color', 'prepare_class')}") + elif state == 4: + logger.info(f'通知显示: {title}') + title_label.setText(title) + subtitle_label.setText(subtitle) + lesson.setText(content) + sound_to_play = prepare_class + + # 设置样式表 + if state == 1: # 上课铃声 + bg_color = [ # 1为正常、2为渐变亮色部分、3为渐变暗色部分 + generate_gradient_color(attend_class_color)[0], + generate_gradient_color(attend_class_color)[1], + generate_gradient_color(attend_class_color)[2] + ] + elif state == 0 or state == 2: # 下课铃声 + bg_color = [ + generate_gradient_color(finish_class_color)[0], + generate_gradient_color(finish_class_color)[1], + generate_gradient_color(finish_class_color)[2] + ] + elif state == 3: # 预备铃声 + bg_color = [ + generate_gradient_color(prepare_class_color)[0], + generate_gradient_color(prepare_class_color)[1], + generate_gradient_color(prepare_class_color)[2] + ] + elif state == 4: # 通知铃声 + bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)'] + else: + bg_color = ['rgba(110, 190, 210, 255)', 'rgba(110, 190, 210, 255)', 'rgba(90, 210, 215, 255)'] + + backgnd.setStyleSheet(f'font-weight: bold; border-radius: {radius}; ' + 'background-color: qlineargradient(' + 'spread:pad, x1:0, y1:0, x2:1, y2:1,' + f' stop:0 {bg_color[1]}, stop:0.5 {bg_color[0]}, stop:1 {bg_color[2]}' + ');' + ) + + # 模糊效果 + self.blur_effect = QGraphicsBlurEffect(self) + if config_center.read_conf('Toast', 'wave') == '1': + backgnd.setGraphicsEffect(self.blur_effect) + + mini_size_x = 150 / dpr + mini_size_y = 50 / dpr + + self.timer = QTimer(self) + self.timer.setSingleShot(True) + self.timer.setInterval(duration) + self.timer.timeout.connect(self.close_window) + + # 放大效果 + self.geometry_animation = QPropertyAnimation(self, b"geometry") + self.geometry_animation.setDuration(750) # 动画持续时间 + start_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2), + int(total_width - mini_size_x), int(height - mini_size_y)) + self.geometry_animation.setStartValue(start_rect) + self.geometry_animation.setEndValue(QRect(start_x, start_y, total_width, height)) + self.geometry_animation.setEasingCurve(QEasingCurve.Type.OutCirc) + self.geometry_animation.finished.connect(self.timer.start) + + self.blur_animation = QPropertyAnimation(self.blur_effect, b"blurRadius") + self.blur_animation.setDuration(550) + self.blur_animation.setStartValue(25) + self.blur_animation.setEndValue(0) + + # 渐显 + self.opacity_animation = QPropertyAnimation(self, b"windowOpacity") + self.opacity_animation.setDuration(450) + self.opacity_animation.setStartValue(0) + self.opacity_animation.setEndValue(1) + self.opacity_animation.setEasingCurve(QEasingCurve.Type.InOutQuad) + + if sound_to_play: + self.playsound(sound_to_play) + + self.geometry_animation.start() + self.opacity_animation.start() + self.blur_animation.start() + + def close_window(self): + try: + dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() + except AttributeError: + dpr = QApplication.primaryScreen().devicePixelRatio() + dpr = max(1.0, dpr) + mini_size_x = 120 / dpr + mini_size_y = 20 / dpr + + # 放大效果 + self.geometry_animation_close = QPropertyAnimation(self, b"geometry") + self.geometry_animation_close.setDuration(500) # 动画持续时间 + self.geometry_animation_close.setStartValue(QRect(start_x, start_y, total_width, height)) + end_rect = QRect(int(start_x + mini_size_x / 2), int(start_y + mini_size_y / 2), + int(total_width - mini_size_x), int(height - mini_size_y)) + self.geometry_animation_close.setEndValue(end_rect) + self.geometry_animation_close.setEasingCurve(QEasingCurve.Type.InOutQuad) + + self.blur_animation_close = QPropertyAnimation(self.blur_effect, b"blurRadius") + self.blur_animation_close.setDuration(500) + self.blur_animation_close.setStartValue(0) + self.blur_animation_close.setEndValue(30) + + self.opacity_animation_close = QPropertyAnimation(self, b"windowOpacity") + self.opacity_animation_close.setDuration(500) + self.opacity_animation_close.setStartValue(1) + self.opacity_animation_close.setEndValue(0) + + self.geometry_animation_close.start() + self.opacity_animation_close.start() + self.blur_animation_close.start() + self.opacity_animation_close.finished.connect(self.close) + + def closeEvent(self, event): + if self in active_windows: + active_windows.remove(self) + global window_list + # window_list.remove(self) + self.hide() + self.deleteLater() + event.ignore() + + def playsound(self, filename): + try: + file_path = os.path.join(base_directory, 'audio', filename) + if self.audio_thread and self.audio_thread.isRunning(): + self.audio_thread.quit() + self.audio_thread.wait() + self.audio_thread = PlayAudio(str(file_path)) + self.audio_thread.start() + self.audio_thread.setPriority(QThread.Priority.HighestPriority) # 设置优先级 + except Exception as e: + logger.error(f'播放音频文件失败:{e}') + + +class wave_Effect(QWidget): + def __init__(self, state=1): + super().__init__() + + if config_center.read_conf('Toast', 'pin_on_top') == '1': + self.setWindowFlags( + Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.FramelessWindowHint | + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + else: + self.setWindowFlags( + Qt.WindowType.WindowStaysOnBottomHint | Qt.WindowType.FramelessWindowHint | + Qt.X11BypassWindowManagerHint # 绕过窗口管理器以在全屏显示通知 + ) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + self._radius = 0 + self.duration = 1200 + + if state == 1: + self.color = QColor(attend_class_color) + elif state == 0 or state == 2: + self.color = QColor(finish_class_color) + elif state == 3: + self.color = QColor(prepare_class_color) + elif state == 4: + self.color = QColor(normal_color) + else: + self.color = QColor(normal_color) + + screen_geometry = QApplication.primaryScreen().geometry() + self.setGeometry(screen_geometry) + + self.timer = QTimer(self) + self.timer.setSingleShot(True) + self.timer.setInterval(275) + self.timer.timeout.connect(self.showAnimation) + self.timer.start() + + @pyqtProperty(int) + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + self._radius = value + self.update() + + def showAnimation(self): + self.animation = QPropertyAnimation(self, b'radius') + self.animation.setDuration(self.duration) + self.animation.setStartValue(50) + try: + dpr = self.screen().devicePixelRatio() if self.screen() else QApplication.primaryScreen().devicePixelRatio() + except AttributeError: + dpr = QApplication.primaryScreen().devicePixelRatio() + dpr = max(1.0, dpr) + fixed_end_radius = 1000 * dpr # 动画效果值 + self.animation.setEndValue(fixed_end_radius) + self.animation.setEasingCurve(QEasingCurve.Type.InOutQuad) + self.animation.start() + + self.fade_animation = QPropertyAnimation(self, b'windowOpacity') + self.fade_animation.setDuration(self.duration - 150) + + self.fade_animation.setKeyValues([ # 关键帧 + (0, 0), + (0.06, 0.9), + (1, 0) + ]) + + self.fade_animation.setEasingCurve(QEasingCurve.Type.InOutQuad) + self.fade_animation.finished.connect(self.close) + self.fade_animation.start() + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setBrush(QBrush(self.color)) + painter.setPen(Qt.PenStyle.NoPen) + center = self.rect().center() + loc = QPoint(center.x(), self.rect().top() + start_y + 50) + painter.drawEllipse(loc, self._radius, self._radius) + + def closeEvent(self, event): + if self in active_windows: + active_windows.remove(self) + global window_list + # window_list.remove(self) + self.deleteLater() + self.hide() + event.ignore() + + +def generate_gradient_color(theme_color): # 计算渐变色 + def adjust_color(color, factor): + r = max(0, min(255, int(color.red() * (1 + factor)))) + g = max(0, min(255, int(color.green() * (1 + factor)))) + b = max(0, min(255, int(color.blue() * (1 + factor)))) + # return QColor(r, g, b) + return f'rgba({r}, {g}, {b}, 255)' + + color = QColor(theme_color) + gradient = [adjust_color(color, 0), adjust_color(color, 0.24), adjust_color(color, -0.11)] + return gradient + + +def main(state=1, lesson_name='', title='通知示例', subtitle='副标题', + content='这是一条通知示例', icon=None, duration=2000): # 0:下课铃声 1:上课铃声 2:放学铃声 3:预备铃 4:其他 + if detect_enable_toast(state): + return + + global start_x, start_y, total_width, height, radius, attend_class_color, finish_class_color, prepare_class_color + + widgets = list_.get_widget_config() + for widget in widgets: # 检查组件 + if widget not in list_.widget_name: + widgets.remove(widget) # 移除不存在的组件(确保移除插件后不会出错) + + attend_class_color = f"#{config_center.read_conf('Color', 'attend_class')}" + finish_class_color = f"#{config_center.read_conf('Color', 'finish_class')}" + prepare_class_color = f"#{config_center.read_conf('Color', 'prepare_class')}" + + theme = config_center.read_conf('General', 'theme') + height = conf.load_theme_config(theme)['height'] + radius = conf.load_theme_config(theme)['radius'] + + screen_geometry = QApplication.primaryScreen().geometry() + screen_width = screen_geometry.width() + spacing = conf.load_theme_config(theme)['spacing'] + try: + dpr = QApplication.primaryScreen().devicePixelRatio() + except AttributeError: + dpr = 1.0 + dpr = max(1.0, dpr) + + widgets_width = 0 + for widget in widgets: # 计算总宽度(兼容插件) + try: + widgets_width += conf.load_theme_width(theme)[widget] + except KeyError: + widgets_width += list_.widget_width[widget] + except: + widgets_width += 0 + + total_width = widgets_width + spacing * (len(widgets) - 1) + + start_x = int((screen_width - total_width) / 2) + margin_base = int(config_center.read_conf('General', 'margin')) + start_y = int(margin_base * dpr) + + if state != 4: + window = tip_toast((start_x, start_y), total_width, state, lesson_name, duration=duration) + else: + window = tip_toast( + (start_x, start_y), + total_width, state, + '', + title, + subtitle, + content, + icon, + duration=duration + ) + + window.show() + window_list.append(window) + + if config_center.read_conf('Toast', 'wave') == '1': + wave = wave_Effect(state) + wave.show() + window_list.append(wave) + + +def detect_enable_toast(state=0): + if config_center.read_conf('Toast', 'attend_class') != '1' and state == 1: + return True + if (config_center.read_conf('Toast', 'finish_class') != '1') and (state in [0, 2]): + return True + if config_center.read_conf('Toast', 'prepare_class') != '1' and state == 3: + return True + else: + return False + + +def push_notification(state=1, lesson_name='', title=None, subtitle=None, + content=None): # 推送通知 + global pushed_notification, notification_contents + pushed_notification = True + notification_contents = { + "state": state, + "lesson_name": lesson_name, + "title": title, + "subtitle": subtitle, + "content": content + } + main(state, lesson_name, title, subtitle, content) + return notification_contents + + +if __name__ == '__main__': + app = QApplication(sys.argv) + main( + state=4, # 自定义通知 + title='天气预报', + subtitle='', + content='1°~-3° | 3°~-3° | 9°~1°', + icon='img/favicon.ico', + duration=2000 + ) + sys.exit(app.exec()) diff --git a/ui/default/dark/preview/widget-countdown-day.png b/ui/default/dark/preview/widget-countdown-day.png new file mode 100644 index 0000000..6f3db4b Binary files /dev/null and b/ui/default/dark/preview/widget-countdown-day.png differ diff --git a/ui/default/dark/preview/widget-countdown.png b/ui/default/dark/preview/widget-countdown.png new file mode 100644 index 0000000..46514c1 Binary files /dev/null and b/ui/default/dark/preview/widget-countdown.png differ diff --git a/ui/default/dark/preview/widget-current-activity.png b/ui/default/dark/preview/widget-current-activity.png new file mode 100644 index 0000000..9797024 Binary files /dev/null and b/ui/default/dark/preview/widget-current-activity.png differ diff --git a/ui/default/dark/preview/widget-custom.png b/ui/default/dark/preview/widget-custom.png new file mode 100644 index 0000000..c714428 Binary files /dev/null and b/ui/default/dark/preview/widget-custom.png differ diff --git a/ui/default/dark/preview/widget-next-activity.png b/ui/default/dark/preview/widget-next-activity.png new file mode 100644 index 0000000..42ede81 Binary files /dev/null and b/ui/default/dark/preview/widget-next-activity.png differ diff --git a/ui/default/dark/preview/widget-time.png b/ui/default/dark/preview/widget-time.png new file mode 100644 index 0000000..8748127 Binary files /dev/null and b/ui/default/dark/preview/widget-time.png differ diff --git a/ui/default/dark/preview/widget-weather.png b/ui/default/dark/preview/widget-weather.png new file mode 100644 index 0000000..02db73d Binary files /dev/null and b/ui/default/dark/preview/widget-weather.png differ diff --git a/ui/default/dark/toast-open_dialog.ui b/ui/default/dark/toast-open_dialog.ui new file mode 100644 index 0000000..aa33d18 --- /dev/null +++ b/ui/default/dark/toast-open_dialog.ui @@ -0,0 +1,247 @@ + + + Form + + + + 0 + 0 + 281 + 112 + + + + + 200 + 0 + + + + Form + + + + 8 + + + 8 + + + 8 + + + 22 + + + + + background-color: rgba(10, 10, 15, 245); +border-radius: 38px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + 12 + + + + + + 0 + 0 + + + + + 58 + 58 + + + + + 58 + 58 + + + + + Microsoft YaHei + 12 + 75 + true + + + + font-weight: bold; + + + false + + + %p + + + 0.000000000000000 + + + 7 + + + + + + + 0 + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 12 + 75 + true + + + + color: rgba(188, 188, 188, 200); +background-color: rgba(0,0,0,0); +font-weight: bold; + + + 即将打开 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 17 + 75 + true + + + + color: rgba(255, 255, 255, 255); +background-color: rgba(0,0,0,0); +font-weight: bold + + + 测试 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + 取消 + + + + + + + + + + + + + + + HyperlinkButton + PushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + ProgressBar + QProgressBar +
qfluentwidgets
+
+ + ProgressRing + ProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/dark/widget-base.ui b/ui/default/dark/widget-base.ui new file mode 100644 index 0000000..9be7802 --- /dev/null +++ b/ui/default/dark/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 20 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/default/dark/widget-countdown-day.ui b/ui/default/dark/widget-countdown-day.ui new file mode 100644 index 0000000..4848652 --- /dev/null +++ b/ui/default/dark/widget-countdown-day.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + backgnd + countdown_custom_title + custom_countdown + + + + diff --git a/ui/default/dark/widget-countdown.ui b/ui/default/dark/widget-countdown.ui new file mode 100644 index 0000000..9d99548 --- /dev/null +++ b/ui/default/dark/widget-countdown.ui @@ -0,0 +1,156 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + backgnd + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/dark/widget-current-activity.ui b/ui/default/dark/widget-current-activity.ui new file mode 100644 index 0000000..9cdcb40 --- /dev/null +++ b/ui/default/dark/widget-current-activity.ui @@ -0,0 +1,141 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + QPushButton { + qproperty-iconSize: 24px; + color: white; + border: none; + color: rgba(255, 255, 255, 255); + font-weight: bold + } + + + + 测试 + + + + ../../../img/it.svg../../../img/it.svg + + + + 24 + 24 + + + + + + + 170 + 40 + 35 + 35 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + backgnd + blurEffect + sub_title + subject + + + + diff --git a/ui/default/dark/widget-floating.ui b/ui/default/dark/widget-floating.ui new file mode 100644 index 0000000..ebd3c78 --- /dev/null +++ b/ui/default/dark/widget-floating.ui @@ -0,0 +1,252 @@ + + + Form + + + + 0 + 0 + 200 + 145 + + + + + 200 + 0 + + + + Form + + + + 8 + + + 8 + + + 8 + + + 22 + + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 16 + + + 4 + + + 16 + + + 10 + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 50 + 5 + + + + background-color: rgba(255, 255, 255, 100); +border-radius: 2px + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + + 12 + + + + + 0 + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 测试 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + <0分钟 + + + + + + + + + + 0 + 0 + + + + + 60 + 60 + + + + + 60 + 60 + + + + + Microsoft YaHei + 12 + 75 + true + + + + font-weight: bold; + + + true + + + %p% + + + 0.000000000000000 + + + 7 + + + + + + + + + + + + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+ + ProgressRing + ProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/dark/widget-next-activity.ui b/ui/default/dark/widget-next-activity.ui new file mode 100644 index 0000000..3708732 --- /dev/null +++ b/ui/default/dark/widget-next-activity.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + backgnd + next_subtitle + next_lesson_text + + + + diff --git a/ui/default/dark/widget-time.ui b/ui/default/dark/widget-time.ui new file mode 100644 index 0000000..73538f7 --- /dev/null +++ b/ui/default/dark/widget-time.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + backgnd + date_text + day_text + + + + diff --git a/ui/default/dark/widget-weather.ui b/ui/default/dark/widget-weather.ui new file mode 100644 index 0000000..d7e3f88 --- /dev/null +++ b/ui/default/dark/widget-weather.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + backgnd + current_city + horizontalLayoutWidget + + + + diff --git a/ui/default/preview/widget-countdown-day.png b/ui/default/preview/widget-countdown-day.png new file mode 100644 index 0000000..9e913a5 Binary files /dev/null and b/ui/default/preview/widget-countdown-day.png differ diff --git a/ui/default/preview/widget-countdown.png b/ui/default/preview/widget-countdown.png new file mode 100644 index 0000000..871b97c Binary files /dev/null and b/ui/default/preview/widget-countdown.png differ diff --git a/ui/default/preview/widget-current-activity.png b/ui/default/preview/widget-current-activity.png new file mode 100644 index 0000000..53f590b Binary files /dev/null and b/ui/default/preview/widget-current-activity.png differ diff --git a/ui/default/preview/widget-custom.png b/ui/default/preview/widget-custom.png new file mode 100644 index 0000000..827a24c Binary files /dev/null and b/ui/default/preview/widget-custom.png differ diff --git a/ui/default/preview/widget-next-activity.png b/ui/default/preview/widget-next-activity.png new file mode 100644 index 0000000..f53fe1a Binary files /dev/null and b/ui/default/preview/widget-next-activity.png differ diff --git a/ui/default/preview/widget-time.png b/ui/default/preview/widget-time.png new file mode 100644 index 0000000..5a5db31 Binary files /dev/null and b/ui/default/preview/widget-time.png differ diff --git a/ui/default/preview/widget-weather.png b/ui/default/preview/widget-weather.png new file mode 100644 index 0000000..74f0aed Binary files /dev/null and b/ui/default/preview/widget-weather.png differ diff --git a/ui/default/theme.json b/ui/default/theme.json new file mode 100644 index 0000000..9044a9c --- /dev/null +++ b/ui/default/theme.json @@ -0,0 +1,17 @@ +{ + "name": "默认", + "support_dark_mode": true, + "default_theme": null, + "radius": "8px", + "spacing": -5, + "shadow": true, + "height": 125, + "widget_width": { + "widget-time.ui": 210, + "widget-countdown.ui": 200, + "widget-current-activity.ui": 360, + "widget-next-activity.ui": 290, + "widget-countdown-day.ui": 200, + "widget-weather.ui": 200 + } +} diff --git a/ui/default/toast-open_dialog.ui b/ui/default/toast-open_dialog.ui new file mode 100644 index 0000000..3c111c4 --- /dev/null +++ b/ui/default/toast-open_dialog.ui @@ -0,0 +1,247 @@ + + + Form + + + + 0 + 0 + 281 + 112 + + + + + 200 + 0 + + + + Form + + + + 8 + + + 8 + + + 8 + + + 22 + + + + + background-color: rgba(245, 245, 255, 245); +border-radius: 38px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + 12 + + + + + + 0 + 0 + + + + + 58 + 58 + + + + + 58 + 58 + + + + + Microsoft YaHei + 12 + 75 + true + + + + font-weight: bold; + + + false + + + %p + + + 0.000000000000000 + + + 7 + + + + + + + 0 + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 12 + 75 + true + + + + color: rgba(0, 0, 0, 90); +background-color: rgba(0,0,0,0); +font-weight: bold; + + + 即将打开 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 17 + 75 + true + + + + color: rgba(37, 37, 37, 255); +background-color: rgba(0,0,0,0); +font-weight: bold + + + 测试 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + 取消 + + + + + + + + + + + + + + + HyperlinkButton + PushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + ProgressBar + QProgressBar +
qfluentwidgets
+
+ + ProgressRing + ProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/widget-base.ui b/ui/default/widget-base.ui new file mode 100644 index 0000000..eb8f7bc --- /dev/null +++ b/ui/default/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 22 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +background-color: rgba(255, 255, 255, 0); +font-weight: bold; + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/default/widget-countdown-day.ui b/ui/default/widget-countdown-day.ui new file mode 100644 index 0000000..5f3e17b --- /dev/null +++ b/ui/default/widget-countdown-day.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + countdown_custom_title + custom_countdown + + + + diff --git a/ui/default/widget-countdown.ui b/ui/default/widget-countdown.ui new file mode 100644 index 0000000..81d7e1f --- /dev/null +++ b/ui/default/widget-countdown.ui @@ -0,0 +1,156 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/widget-current-activity.ui b/ui/default/widget-current-activity.ui new file mode 100644 index 0000000..008c01e --- /dev/null +++ b/ui/default/widget-current-activity.ui @@ -0,0 +1,136 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; + + + 测试 + + + + ../../../../../img/it.svg../../../../../img/it.svg + + + + 36 + 26 + + + + + + + 155 + 35 + 45 + 45 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + blurEffect + sub_title + subject + + + + diff --git a/ui/default/widget-floating.ui b/ui/default/widget-floating.ui new file mode 100644 index 0000000..d5a5e9b --- /dev/null +++ b/ui/default/widget-floating.ui @@ -0,0 +1,239 @@ + + + Form + + + + 0 + 0 + 200 + 145 + + + + + 200 + 0 + + + + Form + + + + 8 + + + 8 + + + 8 + + + 22 + + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 4 + + + 16 + + + 4 + + + 16 + + + 10 + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 50 + 5 + + + + background-color:#503D3D3D; +border-radius: 2px + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + + + 12 + + + + + 0 + + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + color: rgba(37, 37, 37, 255); +font-weight: bold + + + 测试 + + + + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + <0分钟 + + + + + + + + + + 0 + 0 + + + + + 60 + 60 + + + + + 60 + 60 + + + + + Microsoft YaHei + 12 + 75 + true + + + + font-weight: bold; + + + true + + + %p% + + + 0.000000000000000 + + + 7 + + + + + + + + + + + + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+ + ProgressRing + ProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/default/widget-next-activity.ui b/ui/default/widget-next-activity.ui new file mode 100644 index 0000000..bdb0c53 --- /dev/null +++ b/ui/default/widget-next-activity.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + next_subtitle + next_lesson_text + + + + diff --git a/ui/default/widget-time.ui b/ui/default/widget-time.ui new file mode 100644 index 0000000..065f5bc --- /dev/null +++ b/ui/default/widget-time.ui @@ -0,0 +1,114 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + date_text + day_text + + + + diff --git a/ui/default/widget-weather.ui b/ui/default/widget-weather.ui new file mode 100644 index 0000000..2e03073 --- /dev/null +++ b/ui/default/widget-weather.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + backgnd + current_city + horizontalLayoutWidget + + + + diff --git a/ui/hoshino/dark/preview/widget-countdown-day.png b/ui/hoshino/dark/preview/widget-countdown-day.png new file mode 100644 index 0000000..6f3db4b Binary files /dev/null and b/ui/hoshino/dark/preview/widget-countdown-day.png differ diff --git a/ui/hoshino/dark/preview/widget-countdown.png b/ui/hoshino/dark/preview/widget-countdown.png new file mode 100644 index 0000000..46514c1 Binary files /dev/null and b/ui/hoshino/dark/preview/widget-countdown.png differ diff --git a/ui/hoshino/dark/preview/widget-current-activity.png b/ui/hoshino/dark/preview/widget-current-activity.png new file mode 100644 index 0000000..9797024 Binary files /dev/null and b/ui/hoshino/dark/preview/widget-current-activity.png differ diff --git a/ui/hoshino/dark/preview/widget-custom.png b/ui/hoshino/dark/preview/widget-custom.png new file mode 100644 index 0000000..c714428 Binary files /dev/null and b/ui/hoshino/dark/preview/widget-custom.png differ diff --git a/ui/hoshino/dark/preview/widget-next-activity.png b/ui/hoshino/dark/preview/widget-next-activity.png new file mode 100644 index 0000000..42ede81 Binary files /dev/null and b/ui/hoshino/dark/preview/widget-next-activity.png differ diff --git a/ui/hoshino/dark/preview/widget-time.png b/ui/hoshino/dark/preview/widget-time.png new file mode 100644 index 0000000..8748127 Binary files /dev/null and b/ui/hoshino/dark/preview/widget-time.png differ diff --git a/ui/hoshino/dark/preview/widget-weather.png b/ui/hoshino/dark/preview/widget-weather.png new file mode 100644 index 0000000..02db73d Binary files /dev/null and b/ui/hoshino/dark/preview/widget-weather.png differ diff --git a/ui/hoshino/dark/widget-base.ui b/ui/hoshino/dark/widget-base.ui new file mode 100644 index 0000000..378eb81 --- /dev/null +++ b/ui/hoshino/dark/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 20 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/hoshino/dark/widget-countdown-day.ui b/ui/hoshino/dark/widget-countdown-day.ui new file mode 100644 index 0000000..63a4869 --- /dev/null +++ b/ui/hoshino/dark/widget-countdown-day.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + ../img/2.png + + + true + + + backgnd + img + countdown_custom_title + custom_countdown + + + + diff --git a/ui/hoshino/dark/widget-countdown.ui b/ui/hoshino/dark/widget-countdown.ui new file mode 100644 index 0000000..130e961 --- /dev/null +++ b/ui/hoshino/dark/widget-countdown.ui @@ -0,0 +1,176 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + ../img/5.png + + + true + + + backgnd + img + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/hoshino/dark/widget-current-activity.ui b/ui/hoshino/dark/widget-current-activity.ui new file mode 100644 index 0000000..5d19263 --- /dev/null +++ b/ui/hoshino/dark/widget-current-activity.ui @@ -0,0 +1,161 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + QPushButton { + qproperty-iconSize: 24px; + color: white; + border: none; + color: rgba(255, 255, 255, 255); + font-weight: bold + } + + + + 测试 + + + + ../../../img/it.svg../../../img/it.svg + + + + 24 + 24 + + + + + + + 170 + 40 + 35 + 35 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 30 + 10 + 91 + 91 + + + + + + + ../img/1.png + + + true + + + backgnd + img + blurEffect + sub_title + subject + + + + diff --git a/ui/hoshino/dark/widget-next-activity.ui b/ui/hoshino/dark/widget-next-activity.ui new file mode 100644 index 0000000..e8cc344 --- /dev/null +++ b/ui/hoshino/dark/widget-next-activity.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 190 + 10 + 91 + 91 + + + + + + + ../img/4.png + + + true + + + backgnd + img + next_subtitle + next_lesson_text + + + + diff --git a/ui/hoshino/dark/widget-time.ui b/ui/hoshino/dark/widget-time.ui new file mode 100644 index 0000000..5aecfbe --- /dev/null +++ b/ui/hoshino/dark/widget-time.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(50, 25, 35, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + ../img/1.png + + + true + + + backgnd + img + date_text + day_text + + + + diff --git a/ui/hoshino/dark/widget-weather.ui b/ui/hoshino/dark/widget-weather.ui new file mode 100644 index 0000000..d96986a --- /dev/null +++ b/ui/hoshino/dark/widget-weather.ui @@ -0,0 +1,166 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + current_city + horizontalLayoutWidget + + + + diff --git a/ui/hoshino/img/1.png b/ui/hoshino/img/1.png new file mode 100644 index 0000000..024275d Binary files /dev/null and b/ui/hoshino/img/1.png differ diff --git a/ui/hoshino/img/2.png b/ui/hoshino/img/2.png new file mode 100644 index 0000000..5e8e8e3 Binary files /dev/null and b/ui/hoshino/img/2.png differ diff --git a/ui/hoshino/img/3.png b/ui/hoshino/img/3.png new file mode 100644 index 0000000..2d39423 Binary files /dev/null and b/ui/hoshino/img/3.png differ diff --git a/ui/hoshino/img/4.png b/ui/hoshino/img/4.png new file mode 100644 index 0000000..74af677 Binary files /dev/null and b/ui/hoshino/img/4.png differ diff --git a/ui/hoshino/img/5.png b/ui/hoshino/img/5.png new file mode 100644 index 0000000..494d566 Binary files /dev/null and b/ui/hoshino/img/5.png differ diff --git a/ui/hoshino/preview/widget-countdown-day.png b/ui/hoshino/preview/widget-countdown-day.png new file mode 100644 index 0000000..9e913a5 Binary files /dev/null and b/ui/hoshino/preview/widget-countdown-day.png differ diff --git a/ui/hoshino/preview/widget-countdown.png b/ui/hoshino/preview/widget-countdown.png new file mode 100644 index 0000000..871b97c Binary files /dev/null and b/ui/hoshino/preview/widget-countdown.png differ diff --git a/ui/hoshino/preview/widget-current-activity.png b/ui/hoshino/preview/widget-current-activity.png new file mode 100644 index 0000000..53f590b Binary files /dev/null and b/ui/hoshino/preview/widget-current-activity.png differ diff --git a/ui/hoshino/preview/widget-custom.png b/ui/hoshino/preview/widget-custom.png new file mode 100644 index 0000000..827a24c Binary files /dev/null and b/ui/hoshino/preview/widget-custom.png differ diff --git a/ui/hoshino/preview/widget-next-activity.png b/ui/hoshino/preview/widget-next-activity.png new file mode 100644 index 0000000..f53fe1a Binary files /dev/null and b/ui/hoshino/preview/widget-next-activity.png differ diff --git a/ui/hoshino/preview/widget-time.png b/ui/hoshino/preview/widget-time.png new file mode 100644 index 0000000..5a5db31 Binary files /dev/null and b/ui/hoshino/preview/widget-time.png differ diff --git a/ui/hoshino/preview/widget-weather.png b/ui/hoshino/preview/widget-weather.png new file mode 100644 index 0000000..74f0aed Binary files /dev/null and b/ui/hoshino/preview/widget-weather.png differ diff --git a/ui/hoshino/theme.json b/ui/hoshino/theme.json new file mode 100644 index 0000000..2351187 --- /dev/null +++ b/ui/hoshino/theme.json @@ -0,0 +1,17 @@ +{ + "name": "小鸟游星野", + "support_dark_mode": true, + "default_theme": null, + "radius": "8px", + "spacing": -5, + "shadow": true, + "height": 125, + "widget_width": { + "widget-time.ui": 210, + "widget-countdown.ui": 200, + "widget-current-activity.ui": 360, + "widget-next-activity.ui": 290, + "widget-countdown-day.ui": 200, + "widget-weather.ui": 200 + } +} diff --git a/ui/hoshino/widget-base.ui b/ui/hoshino/widget-base.ui new file mode 100644 index 0000000..1db5fe2 --- /dev/null +++ b/ui/hoshino/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 22 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +background-color: rgba(255, 255, 255, 0); +font-weight: bold; + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/hoshino/widget-countdown-day.ui b/ui/hoshino/widget-countdown-day.ui new file mode 100644 index 0000000..3c38442 --- /dev/null +++ b/ui/hoshino/widget-countdown-day.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/2.png + + + true + + + backgnd + img + countdown_custom_title + custom_countdown + + + + diff --git a/ui/hoshino/widget-countdown.ui b/ui/hoshino/widget-countdown.ui new file mode 100644 index 0000000..80d0050 --- /dev/null +++ b/ui/hoshino/widget-countdown.ui @@ -0,0 +1,176 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/3.png + + + true + + + backgnd + img + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/hoshino/widget-current-activity.ui b/ui/hoshino/widget-current-activity.ui new file mode 100644 index 0000000..39ab625 --- /dev/null +++ b/ui/hoshino/widget-current-activity.ui @@ -0,0 +1,156 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; + + + 测试 + + + + ../../../../../img/it.svg../../../../../img/it.svg + + + + 36 + 26 + + + + + + + 155 + 35 + 45 + 45 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 30 + 10 + 91 + 91 + + + + + + + img/1.png + + + true + + + backgnd + img + blurEffect + sub_title + subject + + + + diff --git a/ui/hoshino/widget-next-activity.ui b/ui/hoshino/widget-next-activity.ui new file mode 100644 index 0000000..7207876 --- /dev/null +++ b/ui/hoshino/widget-next-activity.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 190 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + next_subtitle + next_lesson_text + + + + diff --git a/ui/hoshino/widget-time.ui b/ui/hoshino/widget-time.ui new file mode 100644 index 0000000..c73146b --- /dev/null +++ b/ui/hoshino/widget-time.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(255, 225, 245, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/1.png + + + true + + + backgnd + img + date_text + day_text + + + + diff --git a/ui/hoshino/widget-weather.ui b/ui/hoshino/widget-weather.ui new file mode 100644 index 0000000..d96986a --- /dev/null +++ b/ui/hoshino/widget-weather.ui @@ -0,0 +1,166 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + current_city + horizontalLayoutWidget + + + + diff --git a/ui/minimize/dark/preview/widget-countdown-day.png b/ui/minimize/dark/preview/widget-countdown-day.png new file mode 100644 index 0000000..5aab367 Binary files /dev/null and b/ui/minimize/dark/preview/widget-countdown-day.png differ diff --git a/ui/minimize/dark/preview/widget-countdown.png b/ui/minimize/dark/preview/widget-countdown.png new file mode 100644 index 0000000..2bdaabc Binary files /dev/null and b/ui/minimize/dark/preview/widget-countdown.png differ diff --git a/ui/minimize/dark/preview/widget-current-activity.png b/ui/minimize/dark/preview/widget-current-activity.png new file mode 100644 index 0000000..291dc43 Binary files /dev/null and b/ui/minimize/dark/preview/widget-current-activity.png differ diff --git a/ui/minimize/dark/preview/widget-custom.png b/ui/minimize/dark/preview/widget-custom.png new file mode 100644 index 0000000..5f054e2 Binary files /dev/null and b/ui/minimize/dark/preview/widget-custom.png differ diff --git a/ui/minimize/dark/preview/widget-next-activity.png b/ui/minimize/dark/preview/widget-next-activity.png new file mode 100644 index 0000000..c95f734 Binary files /dev/null and b/ui/minimize/dark/preview/widget-next-activity.png differ diff --git a/ui/minimize/dark/preview/widget-time.png b/ui/minimize/dark/preview/widget-time.png new file mode 100644 index 0000000..99bfd95 Binary files /dev/null and b/ui/minimize/dark/preview/widget-time.png differ diff --git a/ui/minimize/dark/preview/widget-weather.png b/ui/minimize/dark/preview/widget-weather.png new file mode 100644 index 0000000..c85ce87 Binary files /dev/null and b/ui/minimize/dark/preview/widget-weather.png differ diff --git a/ui/minimize/dark/widget-base.ui b/ui/minimize/dark/widget-base.ui new file mode 100644 index 0000000..96d9df7 --- /dev/null +++ b/ui/minimize/dark/widget-base.ui @@ -0,0 +1,147 @@ + + + Form + + + + 0 + 0 + 160 + 117 + + + + + 0 + 0 + + + + + 16777215 + 125 + + + + 基本组件 + + + + 22 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 12 + + + 12 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/minimize/dark/widget-countdown-day.ui b/ui/minimize/dark/widget-countdown-day.ui new file mode 100644 index 0000000..e0afb7b --- /dev/null +++ b/ui/minimize/dark/widget-countdown-day.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 180 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 10 + 10 + 161 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 141 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 161 + 81 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + + backgnd + countdown_custom_title + custom_countdown + + + + diff --git a/ui/minimize/dark/widget-countdown.ui b/ui/minimize/dark/widget-countdown.ui new file mode 100644 index 0000000..59652f1 --- /dev/null +++ b/ui/minimize/dark/widget-countdown.ui @@ -0,0 +1,166 @@ + + + Form + + + + 0 + 0 + 170 + 114 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 80 + 131 + 6 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + 75 + + + false + + + Qt::Horizontal + + + QProgressBar::BottomToTop + + + true + + + 75.000000000000000 + + + + + + 10 + 10 + 151 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 131 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 150 + 81 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + + backgnd + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/minimize/dark/widget-current-activity.ui b/ui/minimize/dark/widget-current-activity.ui new file mode 100644 index 0000000..33a8008 --- /dev/null +++ b/ui/minimize/dark/widget-current-activity.ui @@ -0,0 +1,137 @@ + + + Form + + + + 0 + 0 + 290 + 110 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 20 + 30 + 251 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 测试 + + + + ../../../../../img/it.svg../../../../../img/it.svg + + + + 36 + 26 + + + + + + + 130 + 40 + 25 + 25 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 10 + 231 + 51 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 271 + 81 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + + backgnd + blurEffect + sub_title + subject + + + + diff --git a/ui/minimize/dark/widget-next-activity.ui b/ui/minimize/dark/widget-next-activity.ui new file mode 100644 index 0000000..a81261e --- /dev/null +++ b/ui/minimize/dark/widget-next-activity.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 240 + 110 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 20 + 10 + 201 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 201 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 一 二 三 四 五 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 221 + 81 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + + backgnd + next_subtitle + next_lesson_text + + + + diff --git a/ui/minimize/dark/widget-time.ui b/ui/minimize/dark/widget-time.ui new file mode 100644 index 0000000..a7001f6 --- /dev/null +++ b/ui/minimize/dark/widget-time.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 170 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 10 + 10 + 151 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 131 + 61 + + + + + Microsoft YaHei UI + 18 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 151 + 81 + + + + background-color: rgba(15, 18, 22, 255); +border-radius: 8px + + + + + + + backgnd + date_text + day_text + + + + diff --git a/ui/minimize/dark/widget-weather.ui b/ui/minimize/dark/widget-weather.ui new file mode 100644 index 0000000..3926088 --- /dev/null +++ b/ui/minimize/dark/widget-weather.ui @@ -0,0 +1,147 @@ + + + Form + + + + 0 + 0 + 180 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 10 + 10 + 161 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 161 + 81 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + + 30 + 30 + 121 + 61 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + backgnd + current_city + horizontalLayoutWidget + + + + diff --git a/ui/minimize/preview/widget-countdown-day.png b/ui/minimize/preview/widget-countdown-day.png new file mode 100644 index 0000000..926a7bc Binary files /dev/null and b/ui/minimize/preview/widget-countdown-day.png differ diff --git a/ui/minimize/preview/widget-countdown.png b/ui/minimize/preview/widget-countdown.png new file mode 100644 index 0000000..4311e13 Binary files /dev/null and b/ui/minimize/preview/widget-countdown.png differ diff --git a/ui/minimize/preview/widget-current-activity.png b/ui/minimize/preview/widget-current-activity.png new file mode 100644 index 0000000..6cbd734 Binary files /dev/null and b/ui/minimize/preview/widget-current-activity.png differ diff --git a/ui/minimize/preview/widget-custom.png b/ui/minimize/preview/widget-custom.png new file mode 100644 index 0000000..b98d32d Binary files /dev/null and b/ui/minimize/preview/widget-custom.png differ diff --git a/ui/minimize/preview/widget-next-activity.png b/ui/minimize/preview/widget-next-activity.png new file mode 100644 index 0000000..331dbfc Binary files /dev/null and b/ui/minimize/preview/widget-next-activity.png differ diff --git a/ui/minimize/preview/widget-time.png b/ui/minimize/preview/widget-time.png new file mode 100644 index 0000000..2152f61 Binary files /dev/null and b/ui/minimize/preview/widget-time.png differ diff --git a/ui/minimize/preview/widget-weather.png b/ui/minimize/preview/widget-weather.png new file mode 100644 index 0000000..92941fe Binary files /dev/null and b/ui/minimize/preview/widget-weather.png differ diff --git a/ui/minimize/theme.json b/ui/minimize/theme.json new file mode 100644 index 0000000..619e619 --- /dev/null +++ b/ui/minimize/theme.json @@ -0,0 +1,17 @@ +{ + "name": "小尺寸", + "support_dark_mode": true, + "default_theme": null, + "radius": "8px", + "spacing": -9, + "height": 110, + "shadow": true, + "widget_width": { + "widget-time.ui": 170, + "widget-countdown.ui": 170, + "widget-current-activity.ui": 290, + "widget-next-activity.ui": 240, + "widget-countdown-day.ui": 180, + "widget-weather.ui": 180 + } +} diff --git a/ui/minimize/widget-base.ui b/ui/minimize/widget-base.ui new file mode 100644 index 0000000..dc39f72 --- /dev/null +++ b/ui/minimize/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 160 + 110 + + + + + 0 + 0 + + + + + 16777215 + 125 + + + + 基本组件 + + + + 22 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 12 + + + 12 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +background-color: rgba(255, 255, 255, 0); +font-weight: bold; + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/minimize/widget-countdown-day.ui b/ui/minimize/widget-countdown-day.ui new file mode 100644 index 0000000..aab27d0 --- /dev/null +++ b/ui/minimize/widget-countdown-day.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 180 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 10 + 10 + 161 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 141 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 161 + 81 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + + backgnd + countdown_custom_title + custom_countdown + + + + diff --git a/ui/minimize/widget-countdown.ui b/ui/minimize/widget-countdown.ui new file mode 100644 index 0000000..eaa38f9 --- /dev/null +++ b/ui/minimize/widget-countdown.ui @@ -0,0 +1,165 @@ + + + Form + + + + 0 + 0 + 170 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 80 + 131 + 6 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + 75 + + + false + + + Qt::Horizontal + + + QProgressBar::BottomToTop + + + true + + + 75.000000000000000 + + + + + + 10 + 10 + 151 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 131 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 151 + 81 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + backgnd + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/minimize/widget-current-activity.ui b/ui/minimize/widget-current-activity.ui new file mode 100644 index 0000000..8f34ddf --- /dev/null +++ b/ui/minimize/widget-current-activity.ui @@ -0,0 +1,137 @@ + + + Form + + + + 0 + 0 + 290 + 110 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 20 + 30 + 251 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; + + + 测试 + + + + ../../../../../img/it.svg../../../../../img/it.svg + + + + 36 + 26 + + + + + + + 130 + 35 + 25 + 25 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 10 + 231 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 271 + 81 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + + backgnd + blurEffect + sub_title + subject + + + + diff --git a/ui/minimize/widget-next-activity.ui b/ui/minimize/widget-next-activity.ui new file mode 100644 index 0000000..2d7a01a --- /dev/null +++ b/ui/minimize/widget-next-activity.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 240 + 110 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 20 + 10 + 201 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 201 + 61 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 一 二 三 四 五 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 221 + 81 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + + backgnd + next_subtitle + next_lesson_text + + + + diff --git a/ui/minimize/widget-time.ui b/ui/minimize/widget-time.ui new file mode 100644 index 0000000..5cda201 --- /dev/null +++ b/ui/minimize/widget-time.ui @@ -0,0 +1,115 @@ + + + Form + + + + 0 + 0 + 170 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 10 + 10 + 151 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 131 + 61 + + + + + Microsoft YaHei UI + 18 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 151 + 81 + + + + background-color: rgba(242, 243, 245, 255); +border-radius: 8px + + + + + + + backgnd + date_text + day_text + + + + diff --git a/ui/minimize/widget-weather.ui b/ui/minimize/widget-weather.ui new file mode 100644 index 0000000..3926088 --- /dev/null +++ b/ui/minimize/widget-weather.ui @@ -0,0 +1,147 @@ + + + Form + + + + 0 + 0 + 180 + 110 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 10 + 10 + 161 + 41 + + + + + Microsoft YaHei UI + 13 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 161 + 81 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + + 30 + 30 + 121 + 61 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 19 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + backgnd + current_city + horizontalLayoutWidget + + + + diff --git a/ui/shiroko/dark/preview/widget-countdown-day.png b/ui/shiroko/dark/preview/widget-countdown-day.png new file mode 100644 index 0000000..6f3db4b Binary files /dev/null and b/ui/shiroko/dark/preview/widget-countdown-day.png differ diff --git a/ui/shiroko/dark/preview/widget-countdown.png b/ui/shiroko/dark/preview/widget-countdown.png new file mode 100644 index 0000000..46514c1 Binary files /dev/null and b/ui/shiroko/dark/preview/widget-countdown.png differ diff --git a/ui/shiroko/dark/preview/widget-current-activity.png b/ui/shiroko/dark/preview/widget-current-activity.png new file mode 100644 index 0000000..9797024 Binary files /dev/null and b/ui/shiroko/dark/preview/widget-current-activity.png differ diff --git a/ui/shiroko/dark/preview/widget-custom.png b/ui/shiroko/dark/preview/widget-custom.png new file mode 100644 index 0000000..c714428 Binary files /dev/null and b/ui/shiroko/dark/preview/widget-custom.png differ diff --git a/ui/shiroko/dark/preview/widget-next-activity.png b/ui/shiroko/dark/preview/widget-next-activity.png new file mode 100644 index 0000000..42ede81 Binary files /dev/null and b/ui/shiroko/dark/preview/widget-next-activity.png differ diff --git a/ui/shiroko/dark/preview/widget-time.png b/ui/shiroko/dark/preview/widget-time.png new file mode 100644 index 0000000..8748127 Binary files /dev/null and b/ui/shiroko/dark/preview/widget-time.png differ diff --git a/ui/shiroko/dark/preview/widget-weather.png b/ui/shiroko/dark/preview/widget-weather.png new file mode 100644 index 0000000..02db73d Binary files /dev/null and b/ui/shiroko/dark/preview/widget-weather.png differ diff --git a/ui/shiroko/dark/widget-base.ui b/ui/shiroko/dark/widget-base.ui new file mode 100644 index 0000000..957bf43 --- /dev/null +++ b/ui/shiroko/dark/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 20 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/shiroko/dark/widget-countdown-day.ui b/ui/shiroko/dark/widget-countdown-day.ui new file mode 100644 index 0000000..1ba06bb --- /dev/null +++ b/ui/shiroko/dark/widget-countdown-day.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + ../img/2.png + + + true + + + backgnd + img + countdown_custom_title + custom_countdown + + + + diff --git a/ui/shiroko/dark/widget-countdown.ui b/ui/shiroko/dark/widget-countdown.ui new file mode 100644 index 0000000..35eaa02 --- /dev/null +++ b/ui/shiroko/dark/widget-countdown.ui @@ -0,0 +1,176 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + ../img/5.png + + + true + + + backgnd + img + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/shiroko/dark/widget-current-activity.ui b/ui/shiroko/dark/widget-current-activity.ui new file mode 100644 index 0000000..f753021 --- /dev/null +++ b/ui/shiroko/dark/widget-current-activity.ui @@ -0,0 +1,161 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + QPushButton { + qproperty-iconSize: 24px; + color: white; + border: none; + color: rgba(255, 255, 255, 255); + font-weight: bold + } + + + + 测试 + + + + ../../../img/it.svg../../../img/it.svg + + + + 24 + 24 + + + + + + + 170 + 40 + 35 + 35 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 30 + 10 + 91 + 91 + + + + + + + ../img/1.png + + + true + + + backgnd + img + blurEffect + sub_title + subject + + + + diff --git a/ui/shiroko/dark/widget-next-activity.ui b/ui/shiroko/dark/widget-next-activity.ui new file mode 100644 index 0000000..12257b6 --- /dev/null +++ b/ui/shiroko/dark/widget-next-activity.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 190 + 10 + 91 + 91 + + + + + + + ../img/4.png + + + true + + + backgnd + img + next_subtitle + next_lesson_text + + + + diff --git a/ui/shiroko/dark/widget-time.ui b/ui/shiroko/dark/widget-time.ui new file mode 100644 index 0000000..8a24023 --- /dev/null +++ b/ui/shiroko/dark/widget-time.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 150); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(0, 30, 50, 255), stop:1 rgba(15, 18, 22, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + ../img/1.png + + + true + + + backgnd + img + date_text + day_text + + + + diff --git a/ui/shiroko/dark/widget-weather.ui b/ui/shiroko/dark/widget-weather.ui new file mode 100644 index 0000000..d96986a --- /dev/null +++ b/ui/shiroko/dark/widget-weather.ui @@ -0,0 +1,166 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + current_city + horizontalLayoutWidget + + + + diff --git a/ui/shiroko/img/1.png b/ui/shiroko/img/1.png new file mode 100644 index 0000000..50344e0 Binary files /dev/null and b/ui/shiroko/img/1.png differ diff --git a/ui/shiroko/img/2.png b/ui/shiroko/img/2.png new file mode 100644 index 0000000..8ae3a7d Binary files /dev/null and b/ui/shiroko/img/2.png differ diff --git a/ui/shiroko/img/3.png b/ui/shiroko/img/3.png new file mode 100644 index 0000000..fad35ea Binary files /dev/null and b/ui/shiroko/img/3.png differ diff --git a/ui/shiroko/img/4.png b/ui/shiroko/img/4.png new file mode 100644 index 0000000..9a5e22a Binary files /dev/null and b/ui/shiroko/img/4.png differ diff --git a/ui/shiroko/img/5.png b/ui/shiroko/img/5.png new file mode 100644 index 0000000..c5a5a7e Binary files /dev/null and b/ui/shiroko/img/5.png differ diff --git a/ui/shiroko/preview/widget-countdown-day.png b/ui/shiroko/preview/widget-countdown-day.png new file mode 100644 index 0000000..9e913a5 Binary files /dev/null and b/ui/shiroko/preview/widget-countdown-day.png differ diff --git a/ui/shiroko/preview/widget-countdown.png b/ui/shiroko/preview/widget-countdown.png new file mode 100644 index 0000000..871b97c Binary files /dev/null and b/ui/shiroko/preview/widget-countdown.png differ diff --git a/ui/shiroko/preview/widget-current-activity.png b/ui/shiroko/preview/widget-current-activity.png new file mode 100644 index 0000000..53f590b Binary files /dev/null and b/ui/shiroko/preview/widget-current-activity.png differ diff --git a/ui/shiroko/preview/widget-custom.png b/ui/shiroko/preview/widget-custom.png new file mode 100644 index 0000000..827a24c Binary files /dev/null and b/ui/shiroko/preview/widget-custom.png differ diff --git a/ui/shiroko/preview/widget-next-activity.png b/ui/shiroko/preview/widget-next-activity.png new file mode 100644 index 0000000..f53fe1a Binary files /dev/null and b/ui/shiroko/preview/widget-next-activity.png differ diff --git a/ui/shiroko/preview/widget-time.png b/ui/shiroko/preview/widget-time.png new file mode 100644 index 0000000..5a5db31 Binary files /dev/null and b/ui/shiroko/preview/widget-time.png differ diff --git a/ui/shiroko/preview/widget-weather.png b/ui/shiroko/preview/widget-weather.png new file mode 100644 index 0000000..74f0aed Binary files /dev/null and b/ui/shiroko/preview/widget-weather.png differ diff --git a/ui/shiroko/theme.json b/ui/shiroko/theme.json new file mode 100644 index 0000000..fb3a07a --- /dev/null +++ b/ui/shiroko/theme.json @@ -0,0 +1,17 @@ +{ + "name": "砂狼白子", + "support_dark_mode": true, + "default_theme": null, + "radius": "8px", + "spacing": -5, + "shadow": true, + "height": 125, + "widget_width": { + "widget-time.ui": 210, + "widget-countdown.ui": 200, + "widget-current-activity.ui": 360, + "widget-next-activity.ui": 290, + "widget-countdown-day.ui": 200, + "widget-weather.ui": 200 + } +} diff --git a/ui/shiroko/widget-base.ui b/ui/shiroko/widget-base.ui new file mode 100644 index 0000000..749f335 --- /dev/null +++ b/ui/shiroko/widget-base.ui @@ -0,0 +1,146 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 基本组件 + + + + 22 + + + 8 + + + 8 + + + 8 + + + 24 + + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 14 + + + 14 + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +background-color: rgba(255, 255, 255, 0); +font-weight: bold; + + + Title + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; +background-color: rgba(255, 255, 255, 0); + + + Content + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + + + + + + + + diff --git a/ui/shiroko/widget-countdown-day.ui b/ui/shiroko/widget-countdown-day.ui new file mode 100644 index 0000000..79d0b71 --- /dev/null +++ b/ui/shiroko/widget-countdown-day.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 倒计日 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 距离 中考 还有 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 30 + 161 + 71 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 300 天 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/2.png + + + true + + + backgnd + img + countdown_custom_title + custom_countdown + + + + diff --git a/ui/shiroko/widget-countdown.ui b/ui/shiroko/widget-countdown.ui new file mode 100644 index 0000000..8705468 --- /dev/null +++ b/ui/shiroko/widget-countdown.ui @@ -0,0 +1,176 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 活动倒计时 + + + + + 20 + 90 + 161 + 5 + + + + + 0 + 5 + + + + + 16777215 + 6 + + + + + + + Qt::Horizontal + + + true + + + 0.000000000000000 + + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 倒计时 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 20 + 40 + 161 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 00:00 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/3.png + + + true + + + backgnd + img + progressBar + activity_countdown_title + activity_countdown + + + + ProgressBar + QProgressBar +
qfluentwidgets
+
+
+ + +
diff --git a/ui/shiroko/widget-current-activity.ui b/ui/shiroko/widget-current-activity.ui new file mode 100644 index 0000000..46569cf --- /dev/null +++ b/ui/shiroko/widget-current-activity.ui @@ -0,0 +1,156 @@ + + + Form + + + + 0 + 0 + 360 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 当前活动 + + + + + 10 + 40 + 341 + 51 + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold; + + + 测试 + + + + ../../../../../img/it.svg../../../../../img/it.svg + + + + 36 + 26 + + + + + + + 155 + 35 + 45 + 45 + + + + background-color: rgb(0, 255, 127); +border-radius:20px + + + + + + + + + 30 + 0 + 301 + 71 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 当前活动 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 341 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 30 + 10 + 91 + 91 + + + + + + + img/1.png + + + true + + + backgnd + img + blurEffect + sub_title + subject + + + + diff --git a/ui/shiroko/widget-next-activity.ui b/ui/shiroko/widget-next-activity.ui new file mode 100644 index 0000000..be56055 --- /dev/null +++ b/ui/shiroko/widget-next-activity.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 290 + 125 + + + + + 0 + 100 + + + + + 16777215 + 65536 + + + + 更多活动 + + + + + 40 + 20 + 211 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 接下来 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 40 + 40 + 221 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 测试测试 + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 10 + 10 + 271 + 91 + + + + background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 180 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + next_subtitle + next_lesson_text + + + + diff --git a/ui/shiroko/widget-time.ui b/ui/shiroko/widget-time.ui new file mode 100644 index 0000000..20f7b63 --- /dev/null +++ b/ui/shiroko/widget-time.ui @@ -0,0 +1,134 @@ + + + Form + + + + 0 + 0 + 210 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 时间 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(0, 0, 0, 90); +font-weight: bold; + + + 2025 年 13 月 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 30 + 40 + 151 + 51 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(37, 37, 37, 255); +font-weight: bold + + + 32 日 周二 + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 10 + 10 + 191 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:1, x2:1, y2:0, stop:0 rgba(203, 239, 249, 255), stop:1 rgba(255, 255, 255, 255)); +border-radius: 8px + + + + + + + + + 10 + 10 + 91 + 91 + + + + + + + img/1.png + + + true + + + backgnd + img + date_text + day_text + + + + diff --git a/ui/shiroko/widget-weather.ui b/ui/shiroko/widget-weather.ui new file mode 100644 index 0000000..d96986a --- /dev/null +++ b/ui/shiroko/widget-weather.ui @@ -0,0 +1,166 @@ + + + Form + + + + 0 + 0 + 200 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + 天气 + + + + + 20 + 20 + 161 + 31 + + + + + Microsoft YaHei UI + 14 + 75 + true + + + + color: rgba(255, 255, 255, 185); +font-weight: bold; + + + 当前城市 + + + Qt::PlainText + + + Qt::AlignCenter + + + + + + 10 + 10 + 181 + 91 + + + + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 245), stop:1 rgba(75, 175, 245, 245)); +border-radius: 8px; +border-image: url(); + + + + + + + + + 30 + 10 + 141 + 111 + + + + + + + + 0 + 0 + + + + + + + ../../img/weather/0.svg + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + Microsoft YaHei UI + 21 + 75 + true + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold + + + 114℉ + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + 100 + 10 + 91 + 91 + + + + + + + img/4.png + + + true + + + backgnd + img + current_city + horizontalLayoutWidget + + + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..82749ac --- /dev/null +++ b/utils.py @@ -0,0 +1,266 @@ +import os +import sys +import psutil + +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QSystemTrayIcon, QApplication +from loguru import logger +from PyQt5.QtCore import QSharedMemory, QTimer, QObject, pyqtSignal +import darkdetect +import datetime as dt + +from file import base_directory, config_center +import signal + +share = QSharedMemory('ClassWidgets') +_stop_in_progress = False + +def restart(): + logger.debug('重启程序') + app = QApplication.instance() + if app: + try: + signal.signal(signal.SIGTERM, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) + except (AttributeError, ValueError): + pass + app.quit() + app.processEvents() + + if share.isAttached(): + share.detach() # 释放共享内存 + os.execl(sys.executable, sys.executable, *sys.argv) + +def stop(status=0): + global share, update_timer, _stop_in_progress + if _stop_in_progress: + return + _stop_in_progress = True + + logger.debug('退出程序...') + + if 'update_timer' in globals() and update_timer: + try: + update_timer.stop() + update_timer = None + except Exception as e: + logger.warning(f"停止全局更新定时器时出错: {e}") + + app = QApplication.instance() + if app: + try: + signal.signal(signal.SIGTERM, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.SIG_DFL) + except (AttributeError, ValueError): + pass + app.quit() + try: + current_pid = os.getpid() + parent = psutil.Process(current_pid) + children = parent.children(recursive=True) + if children: + logger.debug(f"尝试终止 {len(children)} 个子进程...") + for child in children: + try: + logger.debug(f"终止子进程 {child.pid}...") + child.terminate() + except psutil.NoSuchProcess: + logger.debug(f"子进程 {child.pid} 已不存在.") + continue + except psutil.AccessDenied: + logger.warning(f"无权限终止子进程 {child.pid}.") + continue + except Exception as e: + logger.warning(f"终止子进程 {child.pid} 时出错: {e}") + + gone, alive = psutil.wait_procs(children, timeout=1.5) + if alive: + logger.warning(f"{len(alive)} 个子进程未在规定时间内终止,将强制终止...") + for p in alive: + try: + logger.debug(f"强制终止子进程 {p.pid}...") + p.kill() + except psutil.NoSuchProcess: + logger.debug(f"子进程 {p.pid} 在强制终止前已消失.") + except Exception as e: + logger.error(f"强制终止子进程 {p.pid} 失败: {e}") + except psutil.NoSuchProcess: + logger.warning("无法获取当前进程信息,跳过子进程终止。") + except Exception as e: + logger.error(f"终止子进程时出现意外错误: {e}") + + if 'share' in globals() and share: + try: + if share.isAttached(): + share.detach() + logger.debug("共享内存已分离") + except Exception as e: + logger.error(f"分离共享内存时出错: {e}") + + logger.debug(f"程序退出({status})") + if not app: + os._exit(status) + +def calculate_size(p_w=0.6, p_h=0.7): # 计算尺寸 + screen_geometry = QApplication.primaryScreen().geometry() + screen_width = screen_geometry.width() + screen_height = screen_geometry.height() + + width = int(screen_width * p_w) + height = int(screen_height * p_h) + + return (width, height), (int(screen_width / 2 - width / 2), 150) + +def update_tray_tooltip(): + """更新托盘文字""" + if hasattr(sys.modules[__name__], 'tray_icon'): + tray_instance = getattr(sys.modules[__name__], 'tray_icon') + if tray_instance is not None: + schedule_name_from_conf = config_center.read_conf('General', 'schedule') + if schedule_name_from_conf: + try: + schedule_display_name = schedule_name_from_conf + if schedule_display_name.endswith('.json'): + schedule_display_name = schedule_display_name[:-5] + tray_instance.setToolTip(f'Class Widgets - "{schedule_display_name}"') + logger.info(f'托盘文字更新: "Class Widgets - {schedule_display_name}"') + except Exception as e: + logger.error(f"更新托盘提示时发生错误: {e}") + else: + tray_instance.setToolTip("Class Widgets - 未加载课表") + logger.info(f'托盘文字更新: "Class Widgets - 未加载课表"') + +class DarkModeWatcher(QObject): + darkModeChanged = pyqtSignal(bool) # 发出暗黑模式变化信号 + def __init__(self, interval=500, parent=None): + super().__init__(parent) + self._isDarkMode = darkdetect.isDark() # 初始状态 + self._timer = QTimer(self) + self._timer.timeout.connect(self._checkTheme) + self._timer.start(interval) # 轮询间隔(毫秒) + + def _checkTheme(self): + currentMode = darkdetect.isDark() + if currentMode != self._isDarkMode: + self._isDarkMode = currentMode + self.darkModeChanged.emit(currentMode) # 发出变化信号 + + def isDark(self): + """返回当前是否暗黑模式""" + return self._isDarkMode + + def stop(self): + """停止监听""" + self._timer.stop() + + def start(self, interval=None): + """开始监听""" + if interval: + self._timer.setInterval(interval) + self._timer.start() + + +class TrayIcon(QSystemTrayIcon): + def __init__(self, parent=None): + super().__init__(parent) + self.setIcon(QIcon(f"{base_directory}/img/logo/favicon.png")) + + def push_update_notification(self, text=''): + self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray + self.showMessage( + "发现 Class Widgets 新版本!", + text, + QIcon(f"{base_directory}/img/logo/favicon-update.png"), + 5000 + ) + + def push_error_notification(self, title='检查更新失败!', text=''): + self.setIcon(QIcon(f"{base_directory}/img/logo/favicon-update.png")) # tray + self.showMessage( + title, + text, + QIcon(f"{base_directory}/img/logo/favicon-error.ico"), + 5000 + ) + + +class UnionUpdateTimer(QObject): + """ + 统一更新计时器 + """ + + def __init__(self, parent=None): + super().__init__(parent) + self.timer = QTimer(self) + self.timer.timeout.connect(self._on_timeout) + self.callbacks = [] # 存储所有的回调函数 + self._is_running = False + + def _on_timeout(self): # 超时 + app = QApplication.instance() + if not app or app.closingDown(): + if self.timer.isActive(): + self.timer.stop() + return + + # 使用最初的备份列表,防止遍历时修改 + callbacks_copy = self.callbacks[:] + for callback in callbacks_copy: + if callback in self.callbacks: + try: + callback() + except RuntimeError as e: + logger.error(f"回调调用错误 (可能对象已删除): {e}") + try: + self.callbacks.remove(callback) + except ValueError: + pass + except Exception as e: + logger.error(f"执行回调时发生未知错误: {e}") + if self._is_running: + self._schedule_next() + + def _schedule_next(self): + now = dt.datetime.now() + next_tick = now.replace(microsecond=0) + dt.timedelta(seconds=1) + delay = max(0, int((next_tick - now).total_seconds() * 1000)) + self.timer.start(delay) + + def add_callback(self, callback): + if callback not in self.callbacks: + self.callbacks.append(callback) + if not self._is_running: + self.start() + + def remove_callback(self, callback): + try: + self.callbacks.remove(callback) + except ValueError: + pass + # if not self.callbacks and self._is_running: + # self.stop() # 删除定时器 + + def remove_all_callbacks(self): + self.callbacks = [] + # self.stop() # 删除定时器 + + def start(self): + if not self._is_running: + logger.debug("启动 UnionUpdateTimer...") + self._is_running = True + self._schedule_next() + + def stop(self): + self._is_running = False + if self.timer: + try: + if self.timer.isActive(): + self.timer.stop() + except RuntimeError as e: + logger.warning(f"停止 QTimer 时发生运行时错误: {e}") + except Exception as e: + logger.error(f"停止 QTimer 时发生未知错误: {e}") + + +tray_icon = None +update_timer = UnionUpdateTimer() diff --git a/view/extra_menu.ui b/view/extra_menu.ui new file mode 100644 index 0000000..8e53ece --- /dev/null +++ b/view/extra_menu.ui @@ -0,0 +1,372 @@ + + + Form + + + + 0 + 0 + 764 + 683 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 额外选项 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 716 + 525 + + + + + + + 调休 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 设置调休星期 + + + + + + + 将替换当前调休日的课程表为选定星期 + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + + + + + 换课 + + + + + + + 临时替换当天的课程,重启后失效 + + + + + + + + + + 0 + 0 + + + + + 0 + 275 + + + + QAbstractScrollArea::AdjustToContents + + + + + + + 5 + + + + + + 0 + 0 + + + + 课程/活动 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 自定义课程 + + + + + + + + 0 + 0 + + + + false + + + + + + + + + + + + + + + + 16 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + *所有更改在重启后重置 + + + + + + + 浏览更多设置 + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 保存 + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + HyperlinkButton + PushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + ToolButton + QToolButton +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + ListWidget + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/about.ui b/view/menu/about.ui new file mode 100644 index 0000000..025c7b7 --- /dev/null +++ b/view/menu/about.ui @@ -0,0 +1,593 @@ + + + Form + + + + 0 + 0 + 821 + 843 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 关于 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 773 + 716 + + + + + 14 + + + + + 48 + + + 0 + + + 48 + + + 0 + + + + + + + true + + + + 0 + 0 + + + + + 128 + 128 + + + + + + + + + + ../../img/Logo.png + + + true + + + Qt::AlignCenter + + + true + + + + + + + + + Class Widgets + + + Qt::AlignCenter + + + + + + + 版本:获取失败! + + + Qt::AlignCenter + + + true + + + + + + + Class Widgets 是一款能显示当前课程的桌面组件App。其提供了直观的图形化课程表编辑和美观的桌面组件。 + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 360 + 16777215 + + + + 此项目的 Github + + + false + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 360 + 16777215 + + + + 我的 哔哩哔哩 主页 + + + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 360 + 16777215 + + + + 查看开放源代码许可 + + + false + + + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 360 + 16777215 + + + + 鸣谢 + + + false + + + + + + + + + + + 3 + + + 12 + + + + + + + 更新 + + + + + + + + 0 + 0 + + + + 检查更新 + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 选择更新通道 + + + + + + + 将会获取选定更新通道的版本 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启动 Class Widgets 时自动检查更新 + + + + + + + 若启用,Class Widgets 将在启动时联网检查选定的更新通道中是否有最新版本更新。 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 106 + + + + + + + + + + + + Copyright © 2025 RinLit, All Rights Reversed. + + + Qt::AlignCenter + + + + 127 + 127 + 127 + + + + + 185 + 185 + 185 + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + SwitchButton + QWidget +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/advance.ui b/view/menu/advance.ui new file mode 100644 index 0000000..0e25181 --- /dev/null +++ b/view/menu/advance.ui @@ -0,0 +1,1701 @@ + + + Form + + + + 0 + 0 + 765 + 1331 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 0 + + + + + 高级选项 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 717 + 1812 + + + + + + + 3 + + + + + 课程 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 时差偏移 + + + + + + + 修正系统时间与学校铃声的时差,学校铃声慢于系统时间为正值,反之为负 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + -114514 + + + 114514 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 选择课程表配置 + + + + + + + 课程表配置将存储于 本软件根目录\config\schedule 下 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用 单/双 周课表 + + + + + + + 若要启用此选项,需设定开学日期以计算 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 关闭 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 选取开学日期 + + + + + + + 将用于计算单/双周,开学日期需设置为开学第一周第一天(即周一) + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 选取开学日期 + + + + + + + + + + + + + + 外观 + + + + + + + 隐藏方式 + + + + + + + 隐藏方式将会修改单击隐藏和自动隐藏的行为,可按需更改(重启后生效) + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + 6 + + + 12 + + + + + 9 + + + + + + 0 + 0 + + + + + 200 + 135 + + + + + 16777215 + 135 + + + + ../../img/settings/default.png + + + true + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + 默认 + + + + + + + + + 9 + + + + + + 0 + 0 + + + + + 200 + 135 + + + + + 16777215 + 135 + + + + ../../img/settings/hide_all.png + + + true + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + 全部隐藏 + + + + + + + + + 9 + + + + + + 0 + 0 + + + + + 200 + 135 + + + + + 16777215 + 135 + + + + ../../img/settings/floating.png + + + true + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + 最小化为浮窗(推荐) + + + + + + + + + + + 其他 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 自动隐藏 + + + + + + + 选择你需要的自动隐藏方式 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + 什么是灵活隐藏? + + + + + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 特定课程不自动隐藏 + + + + + + + 若启用,在遇到下方设置的特定课程时不会自动隐藏,以英文逗号分隔 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 不自动隐藏的课程 + + + + + + + 配合 特定课程不自动隐藏 使用 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 0 + + + + + + 0 + 0 + + + + 允许点击或触摸小组件 + + + + + + + 允许通过点击或触摸小组件方式控制小组件 +若启用,单击小组件可显示或隐藏小组件,右键小组件可打开额外选项 +若禁用,点击小组件等同于点击小组件后方的窗口 +* 重启后生效 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 0 + + + + + + 0 + 0 + + + + 自定义缩放 + + + + + + + 更改自定义缩放系数百分比(重启后生效) +*不建议使用 180% 以上的值,这可能会导致显示异常 + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + + + 0 + 0 + + + + 100 + + + 200 + + + 100 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 置顶/置底小组件 + + + + + + + + 0 + 0 + + + + 更改小组件的窗口状态(重启后生效) +*开启“置底”功能时,将会禁用“单击隐藏小组件” + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 边距大小 + + + + + + + 设定桌面组件离屏幕边缘的大小(单位:px) + + + false + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + 130 + 33 + + + + -15 + + + + + + + + + + + + + + 启动 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + + 0 + 0 + + + + 开机自启动 + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + + 3 + + + + + 其他 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 安全模式 + + + + + + + 若启用,Class Widgets 将在程序崩溃时自动忽略,并不再弹出窗口;以免影响教学任务。 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 禁用日志 + + + + + + + 若启用,应用将不再会保存日志到本地 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 清空日志 + + + + + + + 将会清空 软件根目录下log. 的所有内容 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 清空日志 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 允许程序多开 + + + + + + + 程序多开后可能出现未知的问题,请谨慎使用 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 不允许 + + + 允许 + + + 不允许 + + + + + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + SwitchButton + QWidget +
qfluentwidgets
+
+ + RadioButton + QRadioButton +
qfluentwidgets
+
+ + Slider + QSlider +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CalendarPicker + QPushButton +
qfluentwidgets
+
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + HyperlinkLabel + QPushButton +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + SpinBox + QSpinBox +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/configs.ui b/view/menu/configs.ui new file mode 100644 index 0000000..4a5c8d8 --- /dev/null +++ b/view/menu/configs.ui @@ -0,0 +1,540 @@ + + + Form + + + + 0 + 0 + 721 + 814 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 配置文件 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 673 + 709 + + + + + + + 3 + + + + + 课程表 + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 导入 Class Widgets 课程表 + + + + + + + 需导入从其他 Class Widgets 导出的课程表 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 导入课程表 + + + + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 导出 Class Widgets 课程表 + + + + + + + 将当前使用的课程表文件 (.json) 导出 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 导出课程表 + + + + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 管理 Class Widgets 课程表 + + + + + + + 打开 Class Widgets 课程表文件夹 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 使用“资源管理器”打开 + + + + + + + + + + + + 3 + + + + + 通用课程表交换格式(CSES) + + + + + + + + 0 + 0 + + + + 什么是 CSES? + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 导入 CSES 格式的课程表 + + + + + + + 需导入从其他支持的软件导出的 CSES 格式的课程表 +注意:由 CSES 格式转换的 Class Widgets 课程表可读性可能降低 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 导入 CSES 文件 + + + + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 导出 Class Widgets 课程表 + + + + + + + 将当前使用的课程表文件 (.yaml) 导出为 CSES 格式 +注意:生成的 CSES 课程表可读性可能降低 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 导出 CSES 文件 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + HyperlinkButton + PushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/countdown_custom_edit.ui b/view/menu/countdown_custom_edit.ui new file mode 100644 index 0000000..46f506c --- /dev/null +++ b/view/menu/countdown_custom_edit.ui @@ -0,0 +1,296 @@ + + + Form + + + + 0 + 0 + 648 + 627 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 自定义倒计时编辑 + + + + + + + + + 自定义倒计时 + + + + + + + + + 12 + + + + + + 0 + 0 + + + + Qt::IgnoreAction + + + true + + + + + + + + + 5 + + + + + + 0 + 0 + + + + 自定义文本 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 选择日期 + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 选定一个日期 + + + + + + + + + + + + + + + + + + 2 + + + + + + 0 + 0 + + + + 倒计时模式 + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 轮播间隔(秒) + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 保存 + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + ToolButton + QToolButton +
qfluentwidgets
+
+ + CalendarPicker + QPushButton +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + SpinBox + QSpinBox +
qfluentwidgets
+
+ + ListWidget + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/custom.ui b/view/menu/custom.ui new file mode 100644 index 0000000..9a32910 --- /dev/null +++ b/view/menu/custom.ui @@ -0,0 +1,1227 @@ + + + Form + + + + 0 + 0 + 706 + 1191 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 12 + + + + + 自定义 + + + + + + + 0 + + + QLayout::SetFixedSize + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 658 + 1036 + + + + + + + *对小组件的显示、隐藏和拖拽操作将在重启后生效。 + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::DragDrop + + + Qt::MoveAction + + + QListView::LeftToRight + + + true + + + QListView::Adjust + + + QListView::Batched + + + + + + + 10 + + + + + + 0 + 0 + + + + 选择小组件 + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 添加 + + + + + + + 移除 + + + + + + + + + 小组件 + + + + + + + 3 + + + + + 主题 + + + 18 + + + + + + + 打开“主题”文件夹 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 主题 + + + + + + + 将用于更改小组件的样式(重启后生效) + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 颜色模式 + + + + + + + 将改变应用的浅/深色外观 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 小组件透明度 + + + + + + + 更改小组件在屏幕上显示的透明度 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 200 + 16777215 + + + + Qt::Horizontal + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 上课时主题色 + + + + + + + 将用于设置窗口、进度条和提醒弹窗 (为了提醒弹窗可读性,请不要设置过浅的颜色) + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 更改颜色 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 下课时主题色 + + + + + + + 将用于设置窗口、进度条和提醒弹窗 (为了提醒弹窗可读性,请不要设置过浅的颜色) + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 更改颜色 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 浮窗时间颜色 + + + + + + + 将用于设置浮窗时间颜色 (为了时间的可读性,请不要设置过浅的颜色&过高的透明度) + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 更改颜色 + + + + + + + + + + + + 3 + + + + + 倒计时模糊 + + + 18 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 模糊主组件倒计时 + + + + + + + 将会以“< x 分钟”的形式模糊地显示倒计时 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 模糊浮窗倒计时 + + + + + + + 将会以“< x 分钟”的形式模糊地显示倒计时 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + 3 + + + + + 天气 + + + 18 + + + + + + + *在 高德天气/腾讯天气 和 小米天气/和风天气 间切换后,需要重新选择城市 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 选择城市 + + + + + + + 将会用于获得天气数据 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 选择一个城市 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 设置天气源 + + + + + + + 将会影响“天气”小组件的天气数据源 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 天气源 API Key + + + + + + + 部分天气源可能需要设置 Key 才能正常使用,可在“帮助”页找到各个天气源获得 Key 的方法。 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + 275 + 33 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 应用 + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + SwitchButton + QWidget +
qfluentwidgets
+
+ + Slider + QSlider +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + HyperlinkLabel + QPushButton +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + ListWidget + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/help.ui b/view/menu/help.ui new file mode 100644 index 0000000..b05a997 --- /dev/null +++ b/view/menu/help.ui @@ -0,0 +1,361 @@ + + + Form + + + + 0 + 0 + 721 + 817 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 6 + + + 6 + + + + + 3 + + + + + 帮助文档 + + + + + + + 需连接到互联网 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 在浏览器中浏览 + + + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 673 + 688 + + + + + 12 + + + 14 + + + 9 + + + 16 + + + + + 6 + + + + + 猜你想问 + + + + + + + 3 + + + + + + 0 + 0 + + + + 如何设置课程表? + + + + https://www.yuque.com/rinlit/class-widgets_help/swg86btkivirtnrl + + + + + + + + + 0 + 0 + + + + 如何切换主题? + + + + https://www.yuque.com/rinlit/class-widgets_help/lg0p91q2mg4yertn + + + + + + + + + 0 + 0 + + + + 软件的时间与铃声不符怎么办?如何设置时差偏移? + + + + https://www.yuque.com/rinlit/class-widgets_help/vlk3plggb8edvub4#mHfUX + + + + + + + + + 0 + 0 + + + + 怎么快速设置调休日和换课? + + + + https://www.yuque.com/rinlit/class-widgets_help/gc4epffu7g5bf9os + + + + + + + + + 0 + 0 + + + + 如何开发插件? + + + + https://www.yuque.com/rinlit/cw-docs-dev + + + + + + + + + + + + 6 + + + + + 社区 + + + + + + + + + + 0 + 0 + + + + 我们的Q群 + + + + + + + + 0 + 0 + + + + GitHub Discussion + + + + + + + + 0 + 0 + + + + Discord + + + + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + HyperlinkButton + PushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/plugin_mgr.ui b/view/menu/plugin_mgr.ui new file mode 100644 index 0000000..f817ff3 --- /dev/null +++ b/view/menu/plugin_mgr.ui @@ -0,0 +1,569 @@ + + + Form + + + + 0 + 0 + 791 + 1388 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 插件 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 743 + 1283 + + + + + + + 3 + + + + + + + 插件管理器 + + + + + + + *对插件的任意操作将在重启后生效。 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + + 96 + 96 + 96 + + + + + 210 + 210 + 210 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 在“插件广场”中检查更新 + + + + + + + 将跳转至“插件广场”以检查插件的更新状态 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 在“插件广场”检查 + + + + + + + + + + 3 + + + + + + 0 + 0 + + + + + 0 + 150 + + + + FluentLabelBase { + color: black; +} + +HyperlinkLabel { + color: #009faa; + border: none; + background-color: transparent; + text-align: left; + padding: 0; + margin: 0; +} + +HyperlinkLabel[underline=true] { + text-decoration: underline; +} + +HyperlinkLabel[underline=false] { + text-decoration: none; +} + +HyperlinkLabel:hover { + color: #007780; +} + +HyperlinkLabel:pressed { + color: #00a7b3; +} +FluentLabelBase{color:#7d000000} + + + 还未添加任何插件 + + + Qt::AlignCenter + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + 14 + + + + + + + + + + + 3 + + + + + 添加插件 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 管理插件文件夹 + + + + + + + 可在此文件夹添加、删除和修改您所安装的插件 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 使用“资源管理器”打开 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 在“插件广场”中寻找 + + + + + + + 将跳转至“插件广场” + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 打开“插件广场” + + + + + + + + + + + + 3 + + + + + 自动化 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 插件自动化执行延迟 + + + + + + + 当插件执行自动化操作时,需等待的时间(单位:秒) + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + PushButton + QPushButton +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + SpinBox + QSpinBox +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/preview.ui b/view/menu/preview.ui new file mode 100644 index 0000000..873a79e --- /dev/null +++ b/view/menu/preview.ui @@ -0,0 +1,221 @@ + + + Form + + + + 0 + 0 + 715 + 636 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 课程表 + + + + + + + + + + 0 + 0 + + + + 预览 + + + + + + + + + + + + + 0 + 0 + + + + QTableView { + background: transparent; + outline: none; + border: none; + /* font: 13px 'Segoe UI', 'Microsoft YaHei'; */ + selection-background-color: transparent; + alternate-background-color: transparent; +} + +QTableView[isBorderVisible=true] { + border: none; +} + +QTableView::item { + background: transparent; + border: 0px; + padding-left: 16px; + padding-right: 16px; + height: 35px; +} + + +QTableView::indicator { + width: 18px; + height: 18px; + border-radius: 5px; + border: none; + background-color: transparent; +} + + +QHeaderView { + background-color: transparent; +} + +QHeaderView::section { + background-color: transparent; + color: rgb(96, 96, 96); + padding-left: 5px; + padding-right: 5px; + border: 1px solid rgba(0, 0, 0, 15); + font: 13px 'Segoe UI', 'Microsoft YaHei', 'PingFang SC'; +} + +QHeaderView::section:horizontal { + border-left: none; + height: 33px; +} + +QTableView[isBorderVisible=true] QHeaderView::section:horizontal { + border-top: none; +} + +QHeaderView::section:horizontal:last { + border-right: none; +} + +QHeaderView::section:vertical { + border-top: none; +} + +QHeaderView::section:checked { + background-color: transparent; +} + +QHeaderView::down-arrow { + subcontrol-origin: padding; + subcontrol-position: center right; + margin-right: 6px; + image: url(:/qfluentwidgets/images/table_view/Down_black.svg); +} + +QHeaderView::up-arrow { + subcontrol-origin: padding; + subcontrol-position: center right; + margin-right: 6px; + image: url(:/qfluentwidgets/images/table_view/Up_black.svg); +} + +QTableCornerButton::section { + background-color: transparent; + border: 1px solid rgba(0, 0, 0, 15); +} + +QTableCornerButton::section:pressed { + background-color: rgba(0, 0, 0, 12); +} + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectItems + + + Qt::ElideMiddle + + + false + + + 50 + + + 50 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + TableWidget + QTableWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/schedule_edit.ui b/view/menu/schedule_edit.ui new file mode 100644 index 0000000..c07c1ca --- /dev/null +++ b/view/menu/schedule_edit.ui @@ -0,0 +1,463 @@ + + + Form + + + + 0 + 0 + 662 + 627 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 课程表编辑 + + + + + + + + + 课程表 + + + + + + + + + 3 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 选择星期 + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 60 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 选择单/双周课表 + + + + + + + 若要启用双周课表,请在“高级选项”中 启用单双周课表和设置开学日期 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + + + 复制单周课表 + + + + + + + + + + + + 12 + + + + + + 0 + 0 + + + + Qt::IgnoreAction + + + true + + + + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 快速添加课程 + + + + + + + + 0 + 0 + + + + false + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::IgnoreAction + + + false + + + QListView::Static + + + true + + + QListView::Adjust + + + QListView::SinglePass + + + QListView::IconMode + + + + + + + 下一天 + + + + + + + + + + + + 5 + + + + + + 0 + 0 + + + + 课程/活动 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 自定义课程 + + + + + + + + 0 + 0 + + + + false + + + + + + + + + + + + + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 保存 + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + ToolButton + QToolButton +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + ElevatedCardWidget + SimpleCardWidget +
qfluentwidgets
+ 1 +
+ + SimpleCardWidget + CardWidget +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + ListWidget + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/sound.ui b/view/menu/sound.ui new file mode 100644 index 0000000..5eb47c8 --- /dev/null +++ b/view/menu/sound.ui @@ -0,0 +1,837 @@ + + + Form + + + + 0 + 0 + 745 + 773 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 0 + + + + + 上下课提醒 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 697 + 823 + + + + + + + 3 + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用上课提醒 + + + + + + + 启用后将在上课时弹窗且发出提示音提醒 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用下课提醒 + + + + + + + 启用后将在下课时弹窗且发出提示音提醒 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用放学提醒 + + + + + + + 启用后将在放学时弹窗且发出提示音提醒 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用预备铃 + + + + + + + 启用后将在预备铃时弹窗且发出提示音提醒 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 预备铃 + + + + + + + 在正式上课前发出预备铃(输入提前的分钟数,若为0则禁用) + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 130 + 33 + + + + 9 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 音量 + + + + + + + 将调整提醒声音的音量大小 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + Qt::Horizontal + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 是否置顶 + + + + + + + 启用后将在提醒时置顶弹窗 + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + + 0 + 0 + + + + 启用强调特效 + + + + + + + 启用后弹出提醒弹窗同时会有水波强调及模糊淡入淡出效果 +*可能影响性能 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + 启用 + + + 禁用 + + + + + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + 预览 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + PrimaryDropDownPushButton + PrimaryPushButton +
qfluentwidgets
+
+ + SwitchButton + QWidget +
qfluentwidgets
+
+ + Slider + QSlider +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + SpinBox + QSpinBox +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/subject.ui b/view/menu/subject.ui new file mode 100644 index 0000000..9337725 --- /dev/null +++ b/view/menu/subject.ui @@ -0,0 +1,121 @@ + + + Form + + + + 0 + 0 + 662 + 627 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 学科编辑 + + + + + + + 12 + + + + + QAbstractScrollArea::AdjustToContents + + + Qt::SolidLine + + + + + + + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 保存 + + + + + + + + + + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + TableWidget + QTableWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/menu/timeline_edit.ui b/view/menu/timeline_edit.ui new file mode 100644 index 0000000..0c4cf22 --- /dev/null +++ b/view/menu/timeline_edit.ui @@ -0,0 +1,599 @@ + + + Form + + + + 0 + 0 + 733 + 658 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + 时间线编辑 + + + + + + + 12 + + + + + + + + 0 + 33 + + + + 节点 + + + + + + + + 0 + 0 + + + + FluentLabelBase { + color: black; +} + +HyperlinkLabel { + color: #009faa; + border: none; + background-color: transparent; + text-align: left; + padding: 0; + margin: 0; +} + +HyperlinkLabel[underline=true] { + text-decoration: underline; +} + +HyperlinkLabel[underline=false] { + text-decoration: none; +} + +HyperlinkLabel:hover { + color: #007780; +} + +HyperlinkLabel:pressed { + color: #00a7b3; +} +FluentLabelBase{color:#7d000000} + + + 还未添加任何节点 + + + Qt::AlignBottom|Qt::AlignHCenter + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + 14 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + true + + + + + + + 5 + + + + + + 0 + 0 + + + + 节点名称 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 类型 + + + + + + + + 0 + 0 + + + + + + + + + + 5 + + + + + + 0 + 0 + + + + 开始时间 + + + + + + + + 0 + 0 + + + + + 150 + 33 + + + + QDateTimeEdit::HourSection + + + h:mm + + + + + + + + + + + + + + + + + + + + + + + + + + + 时间线 + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + FluentLabelBase { + color: black; +} + +HyperlinkLabel { + color: #009faa; + border: none; + background-color: transparent; + text-align: left; + padding: 0; + margin: 0; +} + +HyperlinkLabel[underline=true] { + text-decoration: underline; +} + +HyperlinkLabel[underline=false] { + text-decoration: none; +} + +HyperlinkLabel:hover { + color: #007780; +} + +HyperlinkLabel:pressed { + color: #00a7b3; +} +FluentLabelBase{color:#7d000000} + + + 还未添加任何时间线 + + + Qt::AlignBottom|Qt::AlignHCenter + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + 14 + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + true + + + + + + + 5 + + + + + + 0 + 0 + + + + 活动类型 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + 时段 + + + + + + + + 0 + 0 + + + + + + + + + + 5 + + + + + + 0 + 0 + + + + 时长(分钟) + + + + + + + + 0 + 0 + + + + 999 + + + 5 + + + 40 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 保存 + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + EditableComboBox + LineEdit +
qfluentwidgets
+
+ + PushButton + QPushButton +
qfluentwidgets
+
+ + PrimaryPushButton + PushButton +
qfluentwidgets
+
+ + ToolButton + QToolButton +
qfluentwidgets
+
+ + VerticalSeparator + QWidget +
qfluentwidgets
+
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + TimeEdit + QTimeEdit +
qfluentwidgets
+
+ + SpinBox + QSpinBox +
qfluentwidgets
+
+ + ListWidget + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/pp/home.ui b/view/pp/home.ui new file mode 100644 index 0000000..876aa35 --- /dev/null +++ b/view/pp/home.ui @@ -0,0 +1,313 @@ + + + Form + + + + 0 + 0 + 688 + 777 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background: transparent; border: none + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 688 + 818 + + + + + 12 + + + 24 + + + 24 + + + 24 + + + + + + + 今天 + + + + + + + + HarmonyOS Sans SC + 18 + 75 + true + + + + 11月45日 周日 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + 93 + 93 + 93 + + + + + 209 + 209 + 209 + + + + + + + + + + + 480 + 450 + + + + + 16777215 + 450 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 推荐插件 + + + + + + + + + + 0 + + + 12 + + + 12 + + + + + + 45 + 45 + + + + + 45 + 45 + + + + + + + + + + 这就到底了吗……(っ °Д °;)っ + + + Qt::AlignCenter + + + true + + + + 125 + 125 + 125 + + + + + 211 + 211 + 211 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + + + + + + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + IndeterminateProgressRing + QProgressBar +
qfluentwidgets
+
+ + HorizontalFlipView + QListWidget +
qfluentwidgets
+
+ + HorizontalPipsPager + QListWidget +
qfluentwidgets
+
+
+ + +
diff --git a/view/pp/latests.ui b/view/pp/latests.ui new file mode 100644 index 0000000..0ee0883 --- /dev/null +++ b/view/pp/latests.ui @@ -0,0 +1,173 @@ + + + Form + + + + 0 + 0 + 688 + 713 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 688 + 713 + + + + + 24 + + + 24 + + + 24 + + + + + + + 分类 + + + + + + + + + 所有插件 + + + + + + + + + + Coming Soon~ + + + Qt::AlignCenter + + + + 125 + 125 + 125 + + + + + 211 + 211 + 211 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + + + + + + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/pp/plugin_detail.ui b/view/pp/plugin_detail.ui new file mode 100644 index 0000000..164e810 --- /dev/null +++ b/view/pp/plugin_detail.ui @@ -0,0 +1,405 @@ + + + Form + + + + 0 + 0 + 688 + 713 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 688 + 713 + + + + + 12 + + + 24 + + + 24 + + + 24 + + + 24 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + 24 + + + 12 + + + 12 + + + + + + 128 + 128 + + + + + 128 + 128 + + + + + + + + 0 + + + + + 12 + + + + + + 0 + 0 + + + + PluginName + + + false + + + + + + + 1.1.0 + + + + 153 + 153 + 153 + + + + + 153 + 153 + 153 + + + + + + + + + + 12 + + + + + + 0 + 0 + + + + Author + + + + + + + | + + + + 153 + 153 + 153 + + + + + 153 + 153 + 153 + + + + + + + + + 0 + 0 + + + + Tag + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 18 + + + 18 + + + + + 24 + + + + + + 425 + 16777215 + + + + Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description Plugin Description + + + true + + + + + + + + + Install + + + + .. + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + SplitPushButton + QWidget +
qfluentwidgets
+
+ + PrimarySplitPushButton + SplitPushButton +
qfluentwidgets
+
+ + ToolButton + QToolButton +
qfluentwidgets
+
+ + TransparentToolButton + ToolButton +
qfluentwidgets
+
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + BodyLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+ + HyperlinkLabel + QPushButton +
qfluentwidgets
+
+ + ImageLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/pp/search.ui b/view/pp/search.ui new file mode 100644 index 0000000..79b6caf --- /dev/null +++ b/view/pp/search.ui @@ -0,0 +1,171 @@ + + + Form + + + + 0 + 0 + 688 + 713 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + background: transparent; border: none + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 688 + 713 + + + + + 12 + + + 24 + + + 24 + + + 24 + + + + + 搜索你希望查找的插件、Tag等 + + + + + + + 3 + + + + + 探索更多 + + + + + + + 12 + + + 6 + + + 12 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + + + + + + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + LineEdit + QLineEdit +
qfluentwidgets
+
+ + SearchLineEdit + LineEdit +
qfluentwidgets
+
+
+ + +
diff --git a/view/pp/settings.ui b/view/pp/settings.ui new file mode 100644 index 0000000..bb87547 --- /dev/null +++ b/view/pp/settings.ui @@ -0,0 +1,326 @@ + + + Form + + + + 0 + 0 + 688 + 806 + + + + + 0 + 0 + + + + Form + + + + 18 + + + 24 + + + 24 + + + 24 + + + 0 + + + + + 设置 + + + + + + + background: transparent; border: none + + + true + + + + + 0 + 0 + 640 + 725 + + + + + + + 3 + + + + + 插件 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 下载后自动启用插件 + + + + + + + 在下载插件后,将为您自动启用插件以便您重启可以立即使用。 +但请确信您在“插件广场”中需要的插件是安全的。 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + 启用 + + + 禁用 + + + + + + + + + + + + 3 + + + + + 网络 + + + + + + + + 0 + 70 + + + + + 16 + + + 16 + + + 16 + + + 16 + + + + + 0 + + + + + 选择镜像源 + + + + + + + 若需要在中国大陆正常使用“插件广场”,最好为其设置一个镜像源。 + + + true + + + + 0 + 0 + 0 + + + + + 255 + 255 + 255 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 25 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + 5 + + + + + + + + + + + + ComboBox + QPushButton +
qfluentwidgets
+
+ + SwitchButton + QWidget +
qfluentwidgets
+
+ + CardWidget + QFrame +
qfluentwidgets
+ 1 +
+ + SmoothScrollArea + QScrollArea +
qfluentwidgets
+ 1 +
+ + CaptionLabel + QLabel +
qfluentwidgets
+
+ + StrongBodyLabel + QLabel +
qfluentwidgets
+
+ + SubtitleLabel + QLabel +
qfluentwidgets
+
+ + TitleLabel + QLabel +
qfluentwidgets
+
+
+ + +
diff --git a/view/widget-toast-bar.ui b/view/widget-toast-bar.ui new file mode 100644 index 0000000..fd79e12 --- /dev/null +++ b/view/widget-toast-bar.ui @@ -0,0 +1,201 @@ + + + Form + + + + 0 + 0 + 646 + 125 + + + + + 0 + 0 + + + + + 16777215 + 65536 + + + + Form + + + + 8 + + + 8 + + + 8 + + + 22 + + + + + border: none; +color: rgba(255, 255, 255, 255); +font-weight: bold; +border-radius: 8px; +background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(255, 200, 150, 255), stop:1 rgba(217, 147, 107, 255)); + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 52 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 23 + + + + + background: transparent + + + + + + ../img/attend_class.svg + + + + + + + + Microsoft YaHei UI + 22 + 75 + true + + + + color: rgb(255, 255, 255); +background: transparent + + + 上课 + + + + + + + + + 16 + + + + + + Microsoft YaHei UI + 14 + 75 + false + true + + + + background: transparent; +font-weight: bold; +color: rgba(255, 255, 255, 200); + + + 当前课程 + + + + + + + + Microsoft YaHei UI + 20 + 75 + true + + + + color: rgb(255, 255, 255); +background: transparent + + + 英语 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + diff --git a/weather_db.py b/weather_db.py new file mode 100644 index 0000000..badb665 --- /dev/null +++ b/weather_db.py @@ -0,0 +1,239 @@ +import datetime +import sqlite3 +import json +from loguru import logger + +from conf import base_directory +from file import config_center + +path = f'{base_directory}/config/data/xiaomi_weather.db' +api_config = json.load(open(f'{base_directory}/config/data/weather_api.json', encoding='utf-8')) + + +def update_path(): + global path + path = (f"{base_directory}/config/" + f"data/{api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['database']}") + + +def search_by_name(search_term): + update_path() + conn = sqlite3.connect(path) + cursor = conn.cursor() + + cursor.execute('SELECT * FROM citys WHERE name LIKE ?', ('%' + search_term + '%',)) # 模糊查询 + cities_results = cursor.fetchall() + conn.close() + result_list = [] + for city in cities_results: + result_list.append(city[2]) + # 返回两个表的搜索结果 + return result_list + + +def search_code_by_name(search_term): + if search_term == ('', ''): + return 101010100 + update_path() + conn = sqlite3.connect(path) + cursor = conn.cursor() + + logger.info(f"Searching for city: {search_term}") + search_term = (search_term[0].replace('市',''), search_term[1].replace('区','')) + + cursor.execute('SELECT * FROM citys WHERE name = ?', (f"{search_term[0]}.{search_term[1]}",)) + exact_results = cursor.fetchall() + + if not exact_results: + search_term = search_term[0] + cursor.execute('SELECT * FROM citys WHERE name LIKE ?', ('%' + f"{search_term}" + '%',)) + cities_results = cursor.fetchall() + else: + cities_results = exact_results + + conn.close() + + if cities_results: + # 多结果优先完全匹配,否则返回第一个 + for city in cities_results: + if city[2] == search_term or city[2] == search_term + '市' or city[2] + '市' == search_term: + logger.debug(f"找到城市: {city[2]},代码: {city[3]}") + return city[3] + result = cities_results[0][3] + logger.debug(f"模糊找到城市: {cities_results[0][2]},代码: {result}") + else: + result = "101010100" # 默认城市代码 + logger.warning(f'未找到城市: {search_term},使用默认城市代码') + + return result + + +def search_by_num(search_term): + update_path() + conn = sqlite3.connect(path) + cursor = conn.cursor() + + cursor.execute('SELECT * FROM citys WHERE city_num LIKE ?', ('%' + search_term + '%',)) # 模糊查询 + cities_results = cursor.fetchall() + + conn.close() + + if cities_results: + result = cities_results[0][2] + else: + result = '北京' # 默认城市 + # 返回两个表的搜索结果 + return result + + +def get_weather_by_code(code): # 用代码获取天气描述 + weather_status = json.load( + open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8")) + for weather in weather_status['weatherinfo']: + if str(weather['code']) == code: + return weather['wea'] + return '未知' + + +def get_weather_icon_by_code(code): # 用代码获取天气图标 + weather_status = json.load( + open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", + encoding="utf-8") + ) + weather_code = None + current_time = datetime.datetime.now() + # 遍历获取天气代码 + for weather in weather_status['weatherinfo']: + if str(weather['code']) == code: + original_code = weather.get('original_code') + if original_code is not None: + weather_code = str(weather['original_code']) + else: + weather_code = str(weather['code']) + break + if not weather_code: + logger.error(f'未找到天气代码 {code}') + return f'{base_directory}/img/weather/99.svg' + # 根据天气和时间获取天气图标 + if weather_code in ('0', '1', '3', '13'): # 晴、多云、阵雨、阵雪 + if current_time.hour < 6 or current_time.hour >= 18: # 如果是夜间 + return f'{base_directory}/img/weather/{weather_code}d.svg' + return f'{base_directory}/img/weather/{weather_code}.svg' + + +def get_weather_stylesheet(code): # 天气背景样式 + current_time = datetime.datetime.now() + weather_status = json.load( + open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8")) + weather_code = '99' + for weather in weather_status['weatherinfo']: + if str(weather['code']) == code: + original_code = weather.get('original_code') + if original_code is not None: + weather_code = str(weather['original_code']) + else: + weather_code = str(weather['code']) + break + if weather_code in ('0', '1', '3', '99', '900'): # 晴、多云、阵雨、未知 + if 6 <= current_time.hour < 18: # 如果是日间 + # return 'spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(40, 60, 110, 255), stop:1 rgba(75, 175, 245, 255)' + return 'img/weather/bkg/day.png' + else: # 如果是夜间 + return 'img/weather/bkg/night.png' + # return 'spread:pad, x1:0, y1:0, x2:1, y2:1, stop:0 rgba(20, 60, 90, 255), stop:1 rgba(10, 20, 29, 255)' + return 'img/weather/bkg/rain.png' + + +def get_weather_url(): + if config_center.read_conf('Weather', 'api') in api_config['weather_api_list']: + return api_config['weather_api'][config_center.read_conf('Weather', 'api')] + else: + return api_config['weather_api']['xiaomi_weather'] + + +def get_weather_alert_url(): + if not api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']: + return 'NotSupported' + if config_center.read_conf('Weather', 'api') in api_config['weather_api_list']: + return api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']['url'] + else: + return api_config['weather_api_parameters']['xiaomi_weather']['alerts']['url'] + + +def get_weather_code_by_description(value): + weather_status = json.load( + open(f"{base_directory}/config/data/{config_center.read_conf('Weather', 'api')}_status.json", encoding="utf-8")) + for weather in weather_status['weatherinfo']: + if str(weather['wea']) == value: + return str(weather['code']) + return '99' + + +def get_alert_image(alert_type): + alerts_list = api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']['types'] + return f'{base_directory}/img/weather/alerts/{alerts_list[alert_type]}' + + +def is_supported_alert(): + if not api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')]['alerts']: + return False + return True + + +def get_weather_data(key='temp', weather_data=None): # 获取天气数据 + if weather_data is None: + logger.error('weather_data is None!') + return None + ''' + 根据key值获取weather_data中的对应值 + key值可以为:temp、icon、alert_title + ''' + # 各个天气api的可访问值 + api_parameters = api_config['weather_api_parameters'][config_center.read_conf('Weather', 'api')] + if key == 'alert': + parameter = api_parameters['alerts']['type'].split('.') + elif key == 'alert_title': + if 'alerts' not in api_parameters or 'title' not in api_parameters['alerts']: + return None + parameter = api_parameters['alerts']['title'].split('.') + else: + parameter = api_parameters[key].split('.') + # 遍历获取值 + value = weather_data + if config_center.read_conf('Weather', 'api') == 'amap_weather': + value = weather_data['lives'][0][api_parameters[key]] + elif config_center.read_conf('Weather', 'api') == 'qq_weather': + value = str(weather_data['result']['realtime'][0]['infos'][api_parameters[key]]) + else: + for parameter in parameter: + if not value: + logger.warning(f'天气信息值{key}为空') + return None + if parameter == '0': + value = value[0] + continue + if parameter in value: + value = value[parameter] + else: + logger.error(f'获取天气参数失败,{parameter}不存在于{config_center.read_conf("Weather", "api")}中') + return '错误' + if key == 'temp': + value += '°' + elif key == 'icon': # 修复此代码影响其他天气源的问题 + if api_parameters['return_desc']: # 如果此api返回的是天气描述而不是代码 + value = get_weather_code_by_description(value) + return value + + +if __name__ == '__main__': + # 测试代码 + try: + num_results = search_by_num('101310101') # [2]城市名称 + print(num_results) + cities_results_ = search_by_name('上海') # [3]城市代码 + print(cities_results_) + cities_results_ = search_code_by_name('上海','') # [3]城市代码 + print(cities_results_) + get_weather_by_code(3) + except Exception as e: + print(e)