├── LICENSE ├── README.md ├── screenshots ├── autoupdate.png ├── browser.png ├── bulkOp.png ├── demo.gif ├── finder.png └── itemSize.png └── src ├── blitzkrieg ├── __init__.py ├── alt.py ├── const.py ├── forms │ ├── __init__.py │ └── findtreeitems.py ├── lib │ ├── __init__.py │ └── com │ │ ├── __init__.py │ │ └── lovac42 │ │ ├── __init__.py │ │ ├── anki │ │ ├── __init__.py │ │ ├── backend │ │ │ ├── __init__.py │ │ │ ├── collection.py │ │ │ ├── notes.py │ │ │ └── sound.py │ │ ├── others │ │ │ ├── __init__.py │ │ │ └── safety_first.py │ │ └── version.py │ │ └── config │ │ ├── __init__.py │ │ └── safety_first.py ├── main.py ├── patch_old_anki.py ├── patch_sidebar.py ├── sidebar21.py └── tree.py └── zip.bat /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Blitzkrieg II: Advanced Browser Sidebar 2 | 3 | ## About Blitzkrieg: 4 | Advanced Browser Sidebar -- add more features to the card browser sidebar. 5 | 6 | Advanced Browser Sidebar is an Anki add-on that aims to add useful features or enhance the usability of the card browser sidebar. Below is a screenshot of some of the features available. 7 | 8 | (Plagiarized text from Advanced Browser for the humor.) 9 | 10 | 11 | ## Quick Demo: 12 | 13 | 14 | 15 | 16 | ## Operations: 17 | Drag and drop items. 18 | Right-Click to show context menu. 19 | Shift+RClick to show alternative context menu. 20 | 21 | 22 | ### Highlights: 23 | Manual HLs are saved when you exit the browser, they are not saved if you exit Anki. 24 | 25 | 26 | ### Search: 27 | Search highlights are cleared on refresh or on exiting the browser. 28 | 29 | 30 | 31 | 32 | ### Batch Operations: 33 | Batch operations was added in Blitzkrieg II v0.1.0. Only some operatons * are supported. 34 | 35 | 36 | 37 | 38 | ### Others: 39 | 40 | 41 | 42 | 43 | 44 | ## Credits: 45 | Loosely based on Hierarchical Tags, by Patrice Neff: 46 | https://ankiweb.net/shared/download/1089921461 47 | 48 | -------------------------------------------------------------------------------- /screenshots/autoupdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/autoupdate.png -------------------------------------------------------------------------------- /screenshots/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/browser.png -------------------------------------------------------------------------------- /screenshots/bulkOp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/bulkOp.png -------------------------------------------------------------------------------- /screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/demo.gif -------------------------------------------------------------------------------- /screenshots/finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/finder.png -------------------------------------------------------------------------------- /screenshots/itemSize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/screenshots/itemSize.png -------------------------------------------------------------------------------- /src/blitzkrieg/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2014 Patrice Neff 4 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 5 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 6 | # Support: https://github.com/lovac42/Blitzkrieg 7 | 8 | 9 | from .lib.com.lovac42.anki.version import POINT_VERSION 10 | 11 | from .lib.com.lovac42.anki.others import safety_first 12 | 13 | if POINT_VERSION < 10: #exact version unknown 14 | print("\n\nYou can't expect Blitzkrieg to run on this old version an Anki?!\n\n") 15 | 16 | else: 17 | #Generated by build script 18 | -------------------------------------------------------------------------------- /src/blitzkrieg/alt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 4 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 5 | # Support: https://github.com/lovac42/Blitzkrieg 6 | 7 | 8 | # This is used for debugging and other stuff, yada yada. 9 | # Nothing to see here, just move along... 10 | 11 | from aqt import mw 12 | from aqt.qt import * 13 | from anki.lang import _ 14 | from anki.hooks import addHook 15 | from aqt.browser import Browser 16 | from aqt.tagedit import TagEdit 17 | 18 | from .sidebar21 import TagTreeWidget 19 | 20 | 21 | def replace_addTags(browser, tags=None, label=None, *args, **kwargs): 22 | nids = browser.selectedNotes() 23 | if not nids: 24 | showInfo("No card selected") 25 | return 26 | if label is None: 27 | label = _("Add Tags") 28 | if not tags: 29 | d = QDialog(browser) 30 | d.setObjectName("DeleteTags") 31 | d.setWindowTitle(label) 32 | d.resize(360, 340) 33 | tagTree = TagTreeWidget(browser,d) 34 | tagTree.addTags(nids) 35 | line = TagEdit(d) 36 | line.setCol(browser.col) 37 | layout = QVBoxLayout(d) 38 | layout.setContentsMargins(0, 0, 0, 0) 39 | layout.addWidget(QLabel(_("""\ 40 | Select tags and close dialog: \ 41 | Yellow is for existing tags. \ 42 | Green items will be added."""))) 43 | layout.addWidget(tagTree) 44 | layout.addWidget(QLabel(_("Add Extra Tags:"))) 45 | layout.addWidget(line) 46 | d.exec_() 47 | 48 | txt=line.text() 49 | tags=[txt] if txt else [] 50 | for k,v in tagTree.node.items(): 51 | if v: tags.append(k) 52 | tags=" ".join(tags) 53 | 54 | if tags: 55 | browser.mw.checkpoint(label) 56 | browser.model.beginReset() 57 | browser.col.tags.bulkAdd(nids,tags) 58 | browser.model.endReset() 59 | browser.mw.requireReset() 60 | 61 | 62 | def replace_deleteTags(browser, tags=None, label=None): 63 | nids = browser.selectedNotes() 64 | if not nids: 65 | showInfo("No card selected") 66 | return 67 | if label is None: 68 | label = _("Delete Tags") 69 | if not tags: 70 | d = QDialog(browser) 71 | d.setObjectName("DeleteTags") 72 | d.setWindowTitle(label) 73 | d.resize(360, 340) 74 | tagTree = TagTreeWidget(browser,d) 75 | tagTree.removeTags(nids) 76 | layout = QVBoxLayout(d) 77 | layout.setContentsMargins(0, 0, 0, 0) 78 | layout.addWidget(QLabel(_("""\ 79 | Select tags and close dialog. \ 80 | Red items will be deleted."""))) 81 | layout.addWidget(tagTree) 82 | d.exec_() 83 | 84 | tags=[] 85 | for k,v in tagTree.node.items(): 86 | if v: 87 | # tags.append(k+'::*') #inc subtags? 88 | tags.append(k) 89 | tags=" ".join(tags) 90 | 91 | if tags: 92 | browser.mw.checkpoint(label) 93 | browser.model.beginReset() 94 | browser.col.tags.bulkRem(nids,tags) 95 | browser.col.tags.registerNotes() 96 | browser.model.endReset() 97 | browser.mw.requireReset() 98 | 99 | 100 | 101 | def disabledDebugStuff(): 102 | if mw.pm.profile.get('Blitzkrieg.VFP',False): 103 | Browser.addTags = replace_addTags 104 | Browser.deleteTags = replace_deleteTags 105 | addHook('profileLoaded', disabledDebugStuff) 106 | -------------------------------------------------------------------------------- /src/blitzkrieg/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | # Support: https://github.com/lovac42/Blitzkrieg 5 | 6 | 7 | import os 8 | 9 | ADDON_PATH = os.path.dirname(__file__) 10 | 11 | ADDON_NAME = "Blitzkrieg" 12 | 13 | TARGET_STABLE_VERSION = 23 14 | -------------------------------------------------------------------------------- /src/blitzkrieg/forms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | # Support: https://github.com/lovac42/Blitzkrieg 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/blitzkrieg/forms/findtreeitems.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | # Support: https://github.com/lovac42/Blitzkrieg 5 | 6 | 7 | # Form implementation generated from reading ui file 'finder.ui' 8 | # 9 | # Created by: PyQt5 UI code generator 5.12.2 10 | # 11 | # WARNING! All changes made in this file will be lost! 12 | 13 | from anki.lang import _ 14 | # from PyQt4 import QtCore, QtGui as QtWidgets 15 | from PyQt5 import QtCore, QtGui, QtWidgets 16 | 17 | 18 | class Ui_Dialog(object): 19 | def setupUi(self, Dialog): 20 | Dialog.setObjectName("Dialog") 21 | Dialog.resize(410, 80) 22 | self.gridLayoutWidget = QtWidgets.QWidget(Dialog) 23 | self.gridLayoutWidget.setGeometry(QtCore.QRect(-20, 0, 421, 81)) 24 | self.gridLayoutWidget.setObjectName("gridLayoutWidget") 25 | self.gridLayout = QtWidgets.QGridLayout(self.gridLayoutWidget) 26 | self.gridLayout.setContentsMargins(0, 0, 0, 0) 27 | self.gridLayout.setObjectName("gridLayout") 28 | self.horizontalLayout = QtWidgets.QHBoxLayout() 29 | self.horizontalLayout.setObjectName("horizontalLayout") 30 | self.label = QtWidgets.QLabel(self.gridLayoutWidget) 31 | self.label.setTextFormat(QtCore.Qt.RichText) 32 | self.label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) 33 | self.label.setObjectName("label") 34 | self.horizontalLayout.addWidget(self.label) 35 | self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) 36 | self.btn_exactly = QtWidgets.QRadioButton(self.gridLayoutWidget) 37 | self.btn_exactly.setObjectName("btn_exactly") 38 | self.gridLayout.addWidget(self.btn_exactly, 2, 2, 1, 1) 39 | self.btn_endswith = QtWidgets.QRadioButton(self.gridLayoutWidget) 40 | self.btn_endswith.setObjectName("btn_endswith") 41 | self.gridLayout.addWidget(self.btn_endswith, 2, 3, 1, 1) 42 | self.btn_startswith = QtWidgets.QRadioButton(self.gridLayoutWidget) 43 | self.btn_startswith.setObjectName("btn_startswith") 44 | self.gridLayout.addWidget(self.btn_startswith, 1, 3, 1, 1) 45 | self.btn_contains = QtWidgets.QRadioButton(self.gridLayoutWidget) 46 | self.btn_contains.setChecked(True) 47 | self.btn_contains.setObjectName("btn_contains") 48 | self.gridLayout.addWidget(self.btn_contains, 1, 2, 1, 1) 49 | self.cb_case = QtWidgets.QCheckBox(self.gridLayoutWidget) 50 | self.cb_case.setEnabled(True) 51 | self.cb_case.setMaximumSize(QtCore.QSize(16777215, 16777215)) 52 | self.cb_case.setLayoutDirection(QtCore.Qt.LeftToRight) 53 | self.cb_case.setObjectName("cb_case") 54 | self.gridLayout.addWidget(self.cb_case, 1, 1, 1, 1) 55 | self.input = QtWidgets.QLineEdit(self.gridLayoutWidget) 56 | self.input.setObjectName("input") 57 | self.gridLayout.addWidget(self.input, 0, 1, 1, 4) 58 | self.btn_find = QtWidgets.QPushButton(self.gridLayoutWidget) 59 | self.btn_find.setObjectName("btn_find") 60 | self.gridLayout.addWidget(self.btn_find, 2, 4, 1, 1) 61 | self.btn_regexp = QtWidgets.QRadioButton(self.gridLayoutWidget) 62 | self.btn_regexp.setObjectName("btn_regexp") 63 | self.gridLayout.addWidget(self.btn_regexp, 2, 1, 1, 1) 64 | 65 | self.retranslateUi(Dialog) 66 | self.btn_find.clicked.connect(Dialog.accept) 67 | QtCore.QMetaObject.connectSlotsByName(Dialog) 68 | 69 | def retranslateUi(self, Dialog): 70 | _translate = QtCore.QCoreApplication.translate 71 | Dialog.setWindowTitle(_("Sidebar Item Finder")) 72 | self.label.setText(_("Search: ")) 73 | self.btn_exactly.setText(_("Exactly")) 74 | self.btn_endswith.setText(_("EndsWith")) 75 | self.btn_startswith.setText(_("StartsWith")) 76 | self.btn_contains.setText(_("Contains")) 77 | self.cb_case.setText(_("Case Sensitive")) 78 | self.btn_find.setText(_("Find")) 79 | self.btn_regexp.setText(_("RegExp")) 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/com/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/com/lovac42/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | 6 | __author__ = "lovac42" 7 | 8 | __version__ = "0.0.2" 9 | 10 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/com/lovac42/anki/backend/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/backend/collection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | 6 | from aqt import mw 7 | 8 | 9 | def getConfigGetterMethod(): 10 | try: 11 | return mw.col.get_config 12 | except AttributeError: 13 | return mw.col.conf.get 14 | 15 | def getConfigSetterMethod(): 16 | try: 17 | return mw.col.set_config 18 | except AttributeError: 19 | return _dictSetter 20 | 21 | def _dictSetter(key, value): 22 | mw.col.conf[key] = value 23 | 24 | 25 | 26 | 27 | def getFindCards(): 28 | try: 29 | return mw.col.find_cards 30 | except AttributeError: 31 | import anki.find 32 | return anki.find.Finder(mw.col).findCards 33 | 34 | 35 | def getFindNotes(): 36 | try: 37 | return mw.col.find_notes 38 | except AttributeError: 39 | import anki.find 40 | return anki.find.Finder(mw.col).findNotes 41 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/backend/notes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | 6 | from aqt import mw 7 | from anki.utils import ids2str 8 | 9 | 10 | def fieldNamesForNotes(nids): 11 | fields = set() 12 | mids = mw.col.db.list("select distinct mid from notes where id in %s" % ids2str(nids)) 13 | for mid in mids: 14 | model = mw.col.models.get(mid) 15 | for name in mw.col.models.fieldNames(model): 16 | if name not in fields: #slower w/o 17 | fields.add(name) 18 | return sorted(fields, key=lambda x: x.lower()) 19 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/backend/sound.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | 6 | import re 7 | from aqt import mw 8 | 9 | _soundReg = r"\[sound:(.*?)\]" 10 | 11 | def stripSounds(text): 12 | try: 13 | return mw.col.backend.strip_av_tags(text) 14 | except AttributeError: 15 | return re.sub(_soundReg, "", text) 16 | 17 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/others/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/com/lovac42/anki/others/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/others/safety_first.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (C) 2020 Lovac42 3 | # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html 4 | 5 | 6 | import anki 7 | from aqt import mw 8 | from anki.hooks import addHook, runHook 9 | 10 | from ..version import POINT_VERSION 11 | from ......const import ( 12 | ADDON_PATH, ADDON_NAME, 13 | TARGET_STABLE_VERSION 14 | ) 15 | try: 16 | from ...config.safety_first import AUTHOR_HOOK, AUTHOR_MESSAGE 17 | except ImportError: 18 | AUTHOR_HOOK = "BabyOnBoard" 19 | AUTHOR_MESSAGE = "%s" 20 | 21 | 22 | def ankiVersionCompatibilityChecker(addon_name, stable_version): 23 | try: 24 | import os 25 | import time 26 | 27 | meta = mw.addonManager.addonMeta(ADDON_PATH) 28 | mod = meta.get("mod", 0) 29 | warn_mod = meta.get("warn_time", -1) 30 | warn_ver = meta.get("warn_pt_ver", stable_version) 31 | 32 | if warn_mod < mod or warn_ver < POINT_VERSION: 33 | if not mod: 34 | mod = int(time.time()) 35 | meta["mod"] = mod 36 | meta["warn_time"] = mod 37 | meta["warn_pt_ver"] = POINT_VERSION 38 | mw.addonManager.writeAddonMeta(ADDON_PATH, meta) 39 | 40 | runHook(AUTHOR_HOOK, addon_name, stable_version) 41 | except: 42 | print("Can not print version compatibility warning due to an error.") 43 | 44 | 45 | 46 | _timer = None 47 | _to_warn = {} 48 | 49 | 50 | def tryToWarn(addon_name, stable_version): 51 | global _timer, _to_warn 52 | try: 53 | if _timer: 54 | _timer.stop() 55 | _to_warn[addon_name] = stable_version 56 | _timer = mw.progress.timer(3000,warn,False) 57 | except: pass 58 | 59 | 60 | def warn(): 61 | addons = message = "" 62 | try: 63 | from aqt.utils import showWarning 64 | from ...config.safety_first import getMessageFromAuthor 65 | 66 | for k,v in _to_warn.items(): 67 | addons += "%s was last tested to work on Anki v2.1.%d.\n"%(k,v) 68 | 69 | try: 70 | from anki.lang import currentLang 71 | message = getMessageFromAuthor(currentLang) % addons 72 | except: 73 | message = AUTHOR_MESSAGE % addons 74 | 75 | showWarning( 76 | text=message, 77 | parent=mw, 78 | title="Version Warnings" 79 | ) 80 | except: 81 | print(message) 82 | 83 | 84 | def onProfileLoaded(): 85 | try: 86 | if AUTHOR_HOOK not in anki.hooks._hooks: 87 | addHook(AUTHOR_HOOK, tryToWarn) 88 | 89 | ankiVersionCompatibilityChecker( 90 | ADDON_NAME, TARGET_STABLE_VERSION) 91 | except: pass 92 | 93 | 94 | if TARGET_STABLE_VERSION < POINT_VERSION: 95 | addHook('profileLoaded', onProfileLoaded) 96 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/anki/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020 Lovac42 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | 6 | from anki import version 7 | 8 | ANKI20 = version.startswith("2.0.") 9 | 10 | CCBC = version.endswith("ccbc") 11 | 12 | ANKI21 = not CCBC and version.startswith("2.1.") 13 | 14 | VERSION = version.split('_')[0] #rm ccbc 15 | m,n,p = VERSION.split('.') 16 | 17 | MAJOR_VERSION = int(m) 18 | MINOR_VERSION = int(n) 19 | PATCH_VERSION = int(p) 20 | POINT_VERSION = 0 if ANKI20 else PATCH_VERSION 21 | 22 | -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovac42/Blitzkrieg/b4e13cab811479be012b7d6a5068286a8ffa82bb/src/blitzkrieg/lib/com/lovac42/config/__init__.py -------------------------------------------------------------------------------- /src/blitzkrieg/lib/com/lovac42/config/safety_first.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: (C) 2020 Lovac42 3 | # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html 4 | 5 | 6 | import re 7 | 8 | 9 | AUTHOR_HOOK = "L42.BabyOnBoard" 10 | 11 | AUTHOR_MESSAGE = """\ 12 | The addon(s) listed below were not made for your fancy new version 13 | 14 | of Anki. You may continue to use it on this platform unsupported. 15 | 16 | But no whining or complaining should any problems occur, mkay? 17 | 18 | —L42 (╯°□°)╯︵ ┻━┻ 19 | _________________________________________________________ 20 | %s""" 21 | 22 | 23 | def getMessageFromAuthor(lang): 24 | lang2 = lang[:2] 25 | 26 | if lang2 == "ja": 27 | return """\ 28 | 下記のアドオンは、Ankiの新しいバージョン用ではありません。 29 | 30 | サポートされていないこのプラットフォームで引き続き使用できます。 31 | 32 | しかし、何かがうまくいかなくても、怒ったり不満を言ったりしてはいけません。 33 | 34 | —L42 (╯°□°)╯︵ ┻━┻ 35 | _________________________________________________________ 36 | %s""" 37 | 38 | 39 | if lang2 == "fr": 40 | return """\ 41 | Les add-ons suivants ne sont pas destinés aux nouvelles versions d'Anki. Vous 42 | 43 | pouvez continuer à l'utiliser sur cette plate-forme non prise en charge. Mais si 44 | 45 | quelque chose ne fonctionne pas, ne vous fâchez pas et ne vous plaignez pas. 46 | 47 | —L42 (╯°□°)╯︵ ┻━┻ 48 | _________________________________________________________ 49 | %s""" 50 | 51 | 52 | if lang2 == "es": 53 | return """\ 54 | Los siguientes complementos no están pensados para las nuevas 55 | 56 | versiones de Anki. Puedes continuar usándolo en esta plataforma 57 | 58 | no soportada. Pero si algo no funciona, no te enfades o te quejes. 59 | 60 | —L42 (╯°□°)╯︵ ┻━┻ 61 | _________________________________________________________ 62 | %s""" 63 | 64 | 65 | if lang2 == "gl": 66 | return """\ 67 | Die folgenden Add-ons sind nicht für neue Versionen von Anki vorgesehen. 68 | 69 | Sie können sie weiterhin auf dieser nicht unterstützten Plattform verwenden. 70 | 71 | Aber wenn etwas nicht funktioniert, ärgern oder beschweren Sie sich nicht. 72 | 73 | —L42 (╯°□°)╯︵ ┻━┻ 74 | _________________________________________________________ 75 | %s""" 76 | 77 | 78 | 79 | if lang2 == "it": 80 | return """\ 81 | I seguenti componenti aggiuntivi non sono destinati alle nuove versioni di 82 | 83 | Anki. È possibile continuare a utilizzarlo su questa piattaforma non supportata. 84 | 85 | Ma se qualcosa non funziona, non arrabbiatevi e non lamentatevi. 86 | 87 | —L42 (╯°□°)╯︵ ┻━┻ 88 | _________________________________________________________ 89 | %s""" 90 | 91 | 92 | 93 | 94 | if lang2 == "ru": 95 | return """\ 96 | Следующие дополнения не предназначены для новых версий Anki. 97 | 98 | Вы можете продолжать использовать его на этой неподдерживаемой 99 | 100 | платформе. Но если что-то не работает, не сердитесь и не жалуйтесь. 101 | 102 | —L42 (╯°□°)╯︵ ┻━┻ 103 | _________________________________________________________ 104 | %s""" 105 | 106 | 107 | 108 | lang = re.sub(r"[_-]", '', lang) 109 | 110 | 111 | if lang == "zhTW": 112 | return """\ 113 | 以下附加組件不適用於較新版本的Anki。 114 | 115 | 您可以在不受支持的平台上繼續使用它。 116 | 117 | 但是,如果出現問題,你不要生氣也不要抱怨。 118 | 119 | —L42 (╯°□°)╯︵ ┻━┻ 120 | _________________________________________________________ 121 | %s""" 122 | 123 | 124 | if lang == "zhCN": 125 | return """\ 126 | 以下附加组件不适用于较新版本的Anki。 127 | 128 | 您可以在不受支持的平台上继续使用它。 129 | 130 | 但是,如果出现问题,你不要生气也不要抱怨。 131 | 132 | —L42 (╯°□°)╯︵ ┻━┻ 133 | _________________________________________________________ 134 | %s""" 135 | 136 | return AUTHOR_MESSAGE 137 | -------------------------------------------------------------------------------- /src/blitzkrieg/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2014 Patrice Neff 4 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 5 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 6 | # Support: https://github.com/lovac42/Blitzkrieg 7 | 8 | 9 | import re 10 | from aqt import mw 11 | from aqt.qt import * 12 | from anki.hooks import addHook 13 | from aqt.browser import Browser 14 | from anki.lang import _ 15 | 16 | from .sidebar21 import SidebarTreeView 17 | from .tree import * 18 | from .alt import * 19 | 20 | 21 | try: #was fixed on 2.1.17beta3 22 | from aqt.browser import SidebarItem, SidebarModel 23 | from .patch_sidebar import SidebarItem as SBI, SidebarModel as SBM 24 | SidebarItem.__init__=SBI.__init__ 25 | SidebarModel.__init__=SBM.__init__ 26 | SidebarModel.data=SBM.data 27 | SidebarModel.flags=SBM.flags 28 | SidebarModel.supportedDropActions=SBM.supportedDropActions 29 | 30 | except: #SHOULD_PATCH 31 | from .patch_sidebar import SidebarItem, SidebarModel 32 | from .patch_old_anki import * 33 | Browser.maybeRefreshSidebar = bc_maybeRefreshSidebar 34 | Browser.setupSidebar = bc_setupSidebar 35 | Browser.onSidebarVisChanged = bc_onSidebarVisChanged 36 | # print("patched browser code for addon:Blitzkrieg") 37 | 38 | 39 | 40 | browserInstance=None 41 | 42 | def replace_buildTree(self): 43 | global browserInstance 44 | browserInstance = self 45 | self.sidebarTree.browser = self 46 | 47 | root = SidebarItem("", "") 48 | 49 | try: #addons compatibility 50 | self._stdTree(root) #2.1.17++ 51 | except TypeError: 52 | stdTree(self,root) #2.1.16-- 53 | 54 | favTree(self,root) 55 | decksTree(self,root) 56 | modelTree(self,root) 57 | userTagTree(self,root) 58 | return root 59 | 60 | 61 | 62 | Browser.SidebarTreeView = SidebarTreeView 63 | Browser.buildTree = replace_buildTree 64 | 65 | 66 | def onProfileLoaded(): 67 | if browserInstance: 68 | browserInstance.sidebarTree.clear() 69 | addHook('profileLoaded', onProfileLoaded) 70 | 71 | 72 | 73 | def onRevertedState(stateName): 74 | if browserInstance: 75 | tok=stateName.split()[-1] 76 | if tok=='deck' or \ 77 | tok in browserInstance.sidebarTree.node_state.keys(): 78 | browserInstance.sidebarTree.refresh() 79 | 80 | addHook("revertedState", onRevertedState) 81 | 82 | -------------------------------------------------------------------------------- /src/blitzkrieg/patch_old_anki.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2014 Patrice Neff 4 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 5 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 6 | # Support: https://github.com/lovac42/Blitzkrieg 7 | 8 | 9 | from aqt.qt import * 10 | from anki.lang import _ 11 | from aqt.browser import Browser #, SidebarItem 12 | from anki.hooks import addHook 13 | 14 | from .patch_sidebar import SidebarItem, SidebarModel 15 | from .sidebar21 import SidebarTreeView 16 | 17 | 18 | #compatible with new nightmode, for colorizing tags on 2.1.15 19 | NM_CONFIG = None 20 | def nightModeChanged(config): 21 | global NM_CONFIG 22 | NM_CONFIG = config 23 | addHook("night_mode_config_loaded", nightModeChanged) 24 | 25 | 26 | 27 | #backwards compatible 28 | def bc_maybeRefreshSidebar(self): 29 | if self.sidebarDockWidget.isVisible(): 30 | # add slight delay to allow browser window to appear first 31 | def deferredDisplay(): 32 | root = self.buildTree() 33 | model = SidebarModel(root) 34 | try: 35 | model.nightmode = NM_CONFIG.state_on.value 36 | except: pass 37 | self.sidebarTree.setModel(model) 38 | model.expandWhereNeccessary(self.sidebarTree) 39 | self.mw.progress.timer(10, deferredDisplay, False) 40 | 41 | 42 | #backwards compatible 43 | def bc_setupSidebar(self): 44 | def onSidebarItemExpanded(idx): 45 | item = idx.internalPointer() 46 | #item.on 47 | 48 | dw = self.sidebarDockWidget = QDockWidget(_("Sidebar"), self) 49 | dw.setFeatures(QDockWidget.DockWidgetClosable) 50 | dw.setObjectName("Sidebar") 51 | dw.setAllowedAreas(Qt.LeftDockWidgetArea) 52 | self.sidebarTree = self.SidebarTreeView() 53 | self.sidebarTree.mw = self.mw 54 | self.sidebarTree.browser = self 55 | self.sidebarTree.setUniformRowHeights(True) 56 | self.sidebarTree.setHeaderHidden(True) 57 | self.sidebarTree.setIndentation(15) 58 | self.sidebarTree.expanded.connect(onSidebarItemExpanded) # type: ignore 59 | dw.setWidget(self.sidebarTree) 60 | p = QPalette() 61 | p.setColor(QPalette.Base, p.window().color()) 62 | self.sidebarTree.setPalette(p) 63 | self.sidebarDockWidget.setFloating(False) 64 | self.sidebarDockWidget.visibilityChanged.connect(self.onSidebarVisChanged) # type: ignore 65 | self.sidebarDockWidget.setTitleBarWidget(QWidget()) 66 | self.addDockWidget(Qt.LeftDockWidgetArea, dw) 67 | 68 | 69 | #backwards compatible 70 | def bc_onSidebarVisChanged(self, _visible): 71 | self.maybeRefreshSidebar() 72 | -------------------------------------------------------------------------------- /src/blitzkrieg/patch_sidebar.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 4 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 5 | # Support: https://github.com/lovac42/Blitzkrieg 6 | 7 | 8 | import aqt #TODO: RM later, for 2.1.15 9 | from aqt.qt import * 10 | from anki.lang import _ 11 | 12 | 13 | class SidebarItem: 14 | def __init__(self, name, icon, onClick=None, onExpanded=None, expanded=False): 15 | self.name = name 16 | self.icon = icon 17 | self.onClick = onClick 18 | self.onExpanded = onExpanded 19 | self.expanded = expanded 20 | self.children = [] # List["SidebarItem"] 21 | self.parentItem = None # Optional[SidebarItem] 22 | 23 | self.tooltip = None 24 | self.foreground = None 25 | self.background = None 26 | 27 | def addChild(self, cb): # cb=SidebarItem 28 | self.children.append(cb) 29 | cb.parentItem = self 30 | 31 | def rowForChild(self, child): # -> Optional[int], child=SidebarItem 32 | try: 33 | return self.children.index(child) 34 | except ValueError: 35 | return None 36 | 37 | 38 | 39 | class SidebarModel(QAbstractItemModel): 40 | nightmode = False #TODO: RM later, for 2.1.15 41 | 42 | def __init__(self, root): # root=SidebarItem 43 | QAbstractItemModel.__init__(self) 44 | self.root = root 45 | self.iconCache = {} # Dict[str, QIcon] 46 | 47 | try: 48 | from aqt.theme import theme_manager 49 | self._getIcon = theme_manager.icon_from_resources 50 | except ImportError: 51 | self._getIcon = self.iconFromRef 52 | 53 | 54 | # Qt API 55 | ###################################################################### 56 | 57 | def rowCount(self, parent=QModelIndex()): 58 | if not parent.isValid(): 59 | return len(self.root.children) 60 | else: 61 | item: SidebarItem = parent.internalPointer() 62 | return len(item.children) 63 | 64 | def columnCount(self, parent=QModelIndex()): 65 | return 1 66 | 67 | def index(self, row, column, parent=QModelIndex()): 68 | if not self.hasIndex(row, column, parent): 69 | return QModelIndex() 70 | 71 | parentItem: SidebarItem 72 | if not parent.isValid(): 73 | parentItem = self.root 74 | else: 75 | parentItem = parent.internalPointer() 76 | 77 | item = parentItem.children[row] 78 | return self.createIndex(row, column, item) 79 | 80 | def parent(self, child): # type: ignore 81 | if not child.isValid(): 82 | return QModelIndex() 83 | 84 | childItem = child.internalPointer() 85 | parentItem = childItem.parentItem 86 | 87 | if parentItem is None or parentItem == self.root: 88 | return QModelIndex() 89 | 90 | row = parentItem.rowForChild(childItem) 91 | if row is None: 92 | return QModelIndex() 93 | 94 | return self.createIndex(row, 0, parentItem) 95 | 96 | def data(self, index, role=Qt.DisplayRole): 97 | if not index.isValid(): 98 | return QVariant() 99 | 100 | item: SidebarItem = index.internalPointer() 101 | if role == Qt.DisplayRole: 102 | return QVariant(item.name) 103 | elif role == Qt.DecorationRole: 104 | return QVariant(self._getIcon(item.icon)) 105 | elif role == Qt.BackgroundRole: 106 | return QVariant(item.background) 107 | elif role == Qt.ForegroundRole: 108 | return QVariant(item.foreground) 109 | elif role == Qt.ToolTipRole: 110 | return QVariant(item.tooltip) 111 | else: 112 | return QVariant() 113 | 114 | # Helpers 115 | ###################################################################### 116 | 117 | #DEPRECATION WARNING: This method has been deprecated in anki 2.1.20 118 | def iconFromRef(self, iconRef): 119 | icon = self.iconCache.get(iconRef) 120 | if icon is None: 121 | icon = QIcon(iconRef) 122 | 123 | if self.nightmode: #TODO: RM later, for 2.1.15 124 | pixmap = icon.pixmap(32, 32) 125 | image = pixmap.toImage() 126 | image.invertPixels() 127 | icon = aqt.QIcon(QPixmap.fromImage(image)) 128 | 129 | self.iconCache[iconRef] = icon 130 | return icon 131 | 132 | def expandWhereNeccessary(self, tree): 133 | for row, child in enumerate(self.root.children): 134 | if child.expanded: 135 | idx = self.index(row, 0, QModelIndex()) 136 | self._expandWhereNeccessary(idx, tree) 137 | 138 | def _expandWhereNeccessary(self, parent, tree): 139 | parentItem: SidebarItem 140 | if not parent.isValid(): 141 | parentItem = self.root 142 | else: 143 | parentItem = parent.internalPointer() 144 | 145 | # nothing to do? 146 | if not parentItem.expanded: 147 | return 148 | 149 | # expand children 150 | for row, child in enumerate(parentItem.children): 151 | if not child.expanded: 152 | continue 153 | childIdx = self.index(row, 0, parent) 154 | self._expandWhereNeccessary(childIdx, tree) 155 | 156 | # then ourselves 157 | tree.setExpanded(parent, True) 158 | 159 | 160 | # Drag and drop support 161 | ###################################################################### 162 | 163 | def supportedDropActions(self): 164 | return Qt.MoveAction | Qt.CopyAction 165 | 166 | def flags(self, index): 167 | f = Qt.ItemIsEnabled | Qt.ItemIsSelectable 168 | if index.isValid(): 169 | f |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled 170 | return f 171 | -------------------------------------------------------------------------------- /src/blitzkrieg/sidebar21.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 4 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 5 | # Support: https://github.com/lovac42/Blitzkrieg 6 | 7 | 8 | import re 9 | import aqt 10 | import unicodedata 11 | from aqt import mw 12 | from anki.lang import ngettext, _ 13 | from aqt.qt import * 14 | from aqt.utils import getOnlyText, askUser, showWarning, showInfo 15 | from anki.utils import intTime, ids2str 16 | from anki.errors import DeckRenameError, AnkiError 17 | from anki.hooks import runHook 18 | 19 | from .lib.com.lovac42.anki.backend import collection 20 | 21 | 22 | class SidebarTreeView(QTreeView): 23 | node_state = { # True for open, False for closed 24 | #Decks are handled per deck settings 25 | 'group': {}, 'tag': {}, 'fav': {}, 'pinDeck': {}, 'pinDyn': {}, 26 | 'model': {}, 'dyn': {}, 'pinTag': {}, 'pin': {}, 27 | 'deck': None, 'Deck': None, 28 | } 29 | 30 | finder = {} # saved gui options 31 | 32 | marked = { 33 | 'group': {}, 'tag': {}, 'fav': {}, 'pinDeck': {}, 'pinDyn': {}, 34 | 'model': {}, 'dyn': {}, 'deck': {}, 'pinTag': {}, 'pin': {}, 35 | } 36 | 37 | 38 | def __init__(self): 39 | super().__init__() 40 | self.expanded.connect(self.onExpansion) 41 | self.collapsed.connect(self.onCollapse) 42 | 43 | self.found = {} 44 | self.browser = None 45 | self.timer = None 46 | 47 | self.setDragEnabled(True) 48 | self.setAcceptDrops(True) 49 | self.setDragDropMode(QAbstractItemView.InternalMove) 50 | self.setSelectionMode(QAbstractItemView.ExtendedSelection) 51 | self.setDropIndicatorShown(True) 52 | 53 | self.setContextMenuPolicy(Qt.CustomContextMenu) 54 | self.customContextMenuRequested.connect(self.onTreeMenu) 55 | self.setupContextMenuItems() 56 | 57 | mw.col.tags.registerNotes() # clear unused tags to prevent lockup 58 | 59 | self.getConf = collection.getConfigGetterMethod() 60 | self.setConf = collection.getConfigSetterMethod() 61 | self.findNotes = collection.getFindNotes() 62 | self.findCards = collection.getFindCards() 63 | 64 | 65 | def clear(self): 66 | self.finder.clear() 67 | for k in self.node_state: 68 | try: 69 | self.node_state[k].clear() 70 | except AttributeError: 71 | pass 72 | for k in self.marked: 73 | try: 74 | self.marked[k].clear() 75 | except AttributeError: 76 | pass 77 | 78 | 79 | def keyPressEvent(self, evt): 80 | if evt.key() in (Qt.Key_Return, Qt.Key_Enter): 81 | self.onClickCurrent() 82 | elif evt.key() in (Qt.Key_Down, Qt.Key_Up): 83 | super().keyPressEvent(evt) 84 | self.onClickCurrent() 85 | else: 86 | super().keyPressEvent(evt) 87 | 88 | 89 | def onClickCurrent(self): 90 | idx = self.currentIndex() 91 | if idx.isValid(): 92 | item = idx.internalPointer() 93 | if item.onClick: 94 | #filter out right mouse clicks 95 | self.timer=mw.progress.timer( 96 | 25, lambda:self._timedItemClick(item, True), False 97 | ) 98 | 99 | def _timedItemClick(self, item, fromTimer=False): 100 | item.onClick() 101 | try: 102 | type=item.type 103 | except AttributeError: 104 | return 105 | 106 | # if type=='tag': 107 | # showConf = self.getConf('Blitzkrieg.showAllTags', True) 108 | # if (showConf and fromTimer) or \ 109 | # (not showConf and not fromTimer): 110 | #show all subtags option 111 | # el = self.browser.form.searchEdit.lineEdit() 112 | # el.setText(el.text()+"*") 113 | 114 | if type=='deck': 115 | #Auto update overview summary deck 116 | up = self.getConf('Blitzkrieg.updateOV', False) 117 | if up and item.type in ('deck','dyn','pinDeck','pinDyn') \ 118 | and mw.state == 'overview': 119 | d = mw.col.decks.byName(item.fullname) 120 | mw.col.decks.select(d["id"]) 121 | mw.moveToState("overview") 122 | 123 | 124 | def mouseReleaseEvent(self, event): 125 | super().mouseReleaseEvent(event) 126 | self.onClickCurrent() 127 | 128 | def onExpansion(self, idx): 129 | self._onExpansionChange(idx, True) 130 | 131 | def onCollapse(self, idx): 132 | self._onExpansionChange(idx, False) 133 | 134 | def _onExpansionChange(self, idx, expanded): 135 | item = idx.internalPointer() 136 | try: 137 | self.node_state[item.type][item.fullname] = expanded 138 | except TypeError: 139 | pass 140 | except (AttributeError,KeyError): 141 | return # addon: Customize Sidebar, favTree errors 142 | 143 | if item.expanded != expanded: 144 | item.expanded = expanded 145 | if item.onExpanded: 146 | item.onExpanded(expanded) 147 | for c in item.children: 148 | if c.onExpanded: 149 | c.onExpanded(c.expanded) 150 | 151 | #highlight parent items 152 | if not self.marked[item.type].get(item.fullname): 153 | if item.expanded and item.type in ('tag','deck','model') \ 154 | and len(item.children) and '::' not in item.fullname: 155 | item.background = QBrush(QColor(0,0,10,10)) 156 | else: 157 | item.background = None 158 | 159 | 160 | def setupContextMenuItems(self): 161 | # Type, Item Title, Action Name, Callback 162 | # type -1: separator 163 | # type 0: normal 164 | # type 1: non-folder path, actual item 165 | # type 2: type 0 and multi selected 166 | # type 3: type 1 and multi selected 167 | self.MENU_ITEMS = { 168 | "pin":((-1,),), 169 | "pinDyn":( 170 | (1,"Empty","Empty",self._onTreeDeckEmpty), 171 | (1,"Rebuild","Rebuild",self._onTreeDeckRebuild), 172 | (1,"Options","Options",self._onTreeDeckOptions), 173 | (1,"Export","Export",self._onTreeDeckExport), 174 | (-1,), 175 | (1,"Rename","Rename",self._onTreeFavRename), 176 | (3,"Unpin*",None,self._onTreePinDelete), 177 | ), 178 | "pinDeck":( 179 | (1,"Add Notes",None,self._onTreeDeckAddCard), 180 | (1,"Options","Options",self._onTreeDeckOptions), 181 | (1,"Export","Export",self._onTreeDeckExport), 182 | (-1,), 183 | (1,"Rename","Rename",self._onTreeFavRename), 184 | (3,"Unpin*",None,self._onTreePinDelete), 185 | ), 186 | "pinTag":( 187 | (1,"Show All/one",None,self._timedItemClick), 188 | (3,"Add Notes*",None,self._onTreeTagAddCard), 189 | (1,"Tag Selected","Tag",self._onTreeTag), 190 | (1,"Untag Selected","Untag",self._onTreeUnTag), 191 | (-1,), 192 | (1,"Rename","Rename",self._onTreeFavRename), 193 | (3,"Unpin*",None,self._onTreePinDelete), 194 | ), 195 | "tag":( 196 | (0,"Show All/one",None,self._timedItemClick), 197 | (2,"Add Notes*",None,self._onTreeTagAddCard), 198 | (0,"Rename Leaf","Rename",self._onTreeTagRenameLeaf), 199 | (0,"Rename Branch","Rename",self._onTreeTagRenameBranch), 200 | (0,"Tag Selected","Tag",self._onTreeTag), 201 | (0,"Untag Selected","Untag",self._onTreeUnTag), 202 | (2,"Delete*","Delete",self._onTreeTagDelete), 203 | (-1,), 204 | (0,"Convert to decks","Convert",self._onTreeTag2Deck), 205 | ), 206 | "deck":( 207 | (0,"Rename Leaf","Rename",self._onTreeDeckRenameLeaf), 208 | (0,"Rename Branch","Rename",self._onTreeDeckRename), 209 | (0,"Add Notes",None,self._onTreeDeckAddCard), 210 | (0,"Add Subdeck","Add",self._onTreeDeckAdd), 211 | (0,"Options","Options",self._onTreeDeckOptions), 212 | (0,"Export","Export",self._onTreeDeckExport), 213 | (0,"Delete","Delete",self._onTreeDeckDelete), 214 | (-1,), 215 | (0,"Convert to tags","Convert",self._onTreeDeck2Tag), 216 | ), 217 | "dyn":( 218 | (0,"Rename Leaf","Rename",self._onTreeDeckRenameLeaf), 219 | (0,"Rename Branch","Rename",self._onTreeDeckRename), 220 | (0,"Empty","Empty",self._onTreeDeckEmpty), 221 | (0,"Rebuild","Rebuild",self._onTreeDeckRebuild), 222 | (0,"Options","Options",self._onTreeDeckOptions), 223 | (0,"Export","Export",self._onTreeDeckExport), 224 | (0,"Delete","Delete",self._onTreeDeckDelete), 225 | ), 226 | "fav":( 227 | (1,"Rename","Rename",self._onTreeFavRename), 228 | (1,"Modify","Modify",self._onTreeFavModify), 229 | (3,"Delete*","Delete",self._onTreeFavDelete), 230 | ), 231 | "model":( 232 | (0,"Rename Leaf","Rename",self._onTreeModelRenameLeaf), 233 | (0,"Rename Branch","Rename",self._onTreeModelRenameBranch), 234 | (0,"Add Model","Add",self._onTreeModelAdd), 235 | (1,"Edit Fields","Edit",self.onTreeModelFields), 236 | (1,"LaTeX Options","Edit",self.onTreeModelOptions), 237 | (1,"Delete","Delete",self._onTreeModelDelete), 238 | ), 239 | } 240 | 241 | 242 | 243 | def dropEvent(self, event): 244 | super().dropEvent(event) 245 | 246 | index = self.indexAt(event.pos()) 247 | if not index.isValid(): 248 | return 249 | dropItem = index.internalPointer() 250 | 251 | try: #quick patch for addon compatibility 252 | dropItem.type 253 | except AttributeError: 254 | dropItem.type=None 255 | 256 | if dropItem and isinstance(dropItem.type, str): 257 | if dropItem.type=='group': 258 | #clear item to allow dropping to groups 259 | dropItem = None 260 | else: 261 | return #not drop-able 262 | 263 | type = None 264 | dragItems = [] 265 | for sel in event.source().selectedIndexes(): 266 | item = sel.internalPointer() 267 | dgType = item.type 268 | if not isinstance(dgType, str): 269 | continue 270 | if dgType not in self.node_state: 271 | continue 272 | if not dropItem or \ 273 | dropItem.type == dgType or \ 274 | dropItem.type == dgType[:3]: #pin 275 | if not type: 276 | if dgType in ("deck", "dyn"): 277 | type = "deck" 278 | elif dgType == "tag": 279 | type = "tag" 280 | elif dgType == "model": 281 | type = "model" 282 | elif dgType[:3] in ("fav","pin"): 283 | type = "fav" 284 | else: 285 | continue 286 | elif type!=dgType: 287 | #first item and subsequent items must match the same type 288 | continue 289 | dragItems.append(item) 290 | 291 | if not type or not dragItems: 292 | return 293 | self.mw.progress.timer(10, 294 | lambda:self.dropEventHandler(type,dragItems,dropItem), 295 | False 296 | ) 297 | 298 | def dropEventHandler(self, type, dragItems, dropItem): 299 | mw.checkpoint("Dragged %s"%type) 300 | self.browser._lastSearchTxt="" 301 | self.mw.progress.start(label=_("Processing...")) #doesn't always show up 302 | # prevent child elements being moved if selected 303 | dragItems = sorted(dragItems, key=lambda t: t.fullname) 304 | try: 305 | if type=="deck": 306 | self._deckDropEvent(dragItems, dropItem) 307 | elif type=="tag": 308 | self._tagDropEvent(dragItems, dropItem) 309 | elif type=="model": 310 | self._modelDropEvent(dragItems, dropItem) 311 | elif type=="fav": 312 | self._favDropEvent(dragItems, dropItem) 313 | finally: 314 | self.mw.progress.finish() 315 | mw.col.setMod() 316 | self.browser.maybeRefreshSidebar() 317 | if type=="deck": 318 | mw.reset() 319 | 320 | 321 | def _getItemNames(self, item, dropItem): 322 | try: #type fav or pin 323 | itemName = item.favname 324 | try: 325 | dropName = dropItem.favname 326 | except AttributeError: 327 | dropName = None #no parent 328 | except AttributeError: 329 | itemName = item.fullname 330 | try: 331 | dropName = dropItem.fullname 332 | except AttributeError: 333 | dropName = None #no parent 334 | if not dropName and item.type[:3] == "pin": 335 | dropName="Pinned" 336 | return itemName,dropName 337 | 338 | 339 | def _strDropEvent(self, dragItem, dropItem, type, callback): 340 | parse=mw.col.decks #used for parsing '::' separators 341 | dragName,dropName = self._getItemNames(dragItem, dropItem) 342 | if dragName and not dropName: 343 | if len(parse._path(dragName)) > 1: 344 | newName = parse._basename(dragName) 345 | callback(dragName, newName, dragItem, dropItem) 346 | elif parse._canDragAndDrop(dragName, dropName): 347 | assert dropName.strip() 348 | newName = dropName + "::" + parse._basename(dragName) 349 | callback(dragName, newName, dragItem, dropItem) 350 | self.node_state[type][dropName] = True 351 | 352 | 353 | def _deckDropEvent(self, dragItems, dropItem): 354 | parse = mw.col.decks #used for parsing '::' separators 355 | for item in dragItems: 356 | mw.progress.update(label=item.name) 357 | dropDid = None 358 | dragName,dropName = self._getItemNames(item, dropItem) 359 | dragDeck = parse.byName(dragName) 360 | if not dragDeck: #parent was moved first 361 | continue 362 | try: 363 | _,newName = dragName.rsplit('::',1) 364 | except ValueError: 365 | newName = None 366 | if dropName: 367 | dropDeck = parse.byName(dropName) 368 | dropDid = dropDeck["id"] 369 | newName = dropDeck["name"]+"::"+(newName or dragName) 370 | try: 371 | parse.renameForDragAndDrop(dragDeck["id"], dropDid) 372 | except DeckRenameError as e: 373 | showWarning(e.description) 374 | continue 375 | #deck type not used 376 | # self.node_state[item.type][dropName] = True 377 | mw.col.decks.get(dropDid or 1)['browserCollapsed'] = False 378 | if newName: 379 | self._swapHighlight(item.type,dragName,newName) 380 | #Adding HL here gets really annoying 381 | # self.highlight(item.type,dragDeck['name']) 382 | 383 | 384 | def _favDropEvent(self, dragItems, dropItem): 385 | for item in dragItems: 386 | mw.progress.update(label=item.name) 387 | self._strDropEvent(item,dropItem,item.type,self._moveFav) 388 | 389 | def _moveFav(self, dragName, newName="", dragItem=None, dropItem=None): 390 | try: 391 | type = dropItem.type or "fav" 392 | except AttributeError: 393 | type = "fav" 394 | savedFilters = self.getConf('savedFilters', {}) 395 | for fav in list(savedFilters): 396 | act = savedFilters.get(fav) 397 | if fav.startswith(dragName + "::"): 398 | nn = fav.replace(dragName+"::", newName+"::", 1) 399 | savedFilters[nn] = act 400 | del(savedFilters[fav]) 401 | self.node_state[type][nn] = True 402 | self._swapHighlight(type,dragName,newName) 403 | elif fav == dragName: 404 | savedFilters[newName] = act 405 | del(savedFilters[dragName]) 406 | self.node_state[type][newName] = True 407 | self._swapHighlight(type,dragName,newName) 408 | self.setConf('savedFilters', savedFilters) 409 | 410 | def _modelDropEvent(self, dragItems, dropItem): 411 | if len(dragItems)>1: 412 | self.mw.progress._showWin() #never appears if not forced 413 | self.browser.editor.saveNow(self.hideEditor) 414 | for item in dragItems: 415 | mw.progress.update(label=item.name) 416 | self._strDropEvent(item,dropItem,item.type,self._moveModel) 417 | mw.col.models.flush() 418 | self.browser.model.reset() 419 | 420 | def moveModel(self, dragName, newName, dragItem): 421 | "Rename or Delete models" 422 | self.browser.editor.saveNow(self.hideEditor) 423 | self._moveModel(dragName,newName,dragItem) 424 | mw.col.models.flush() 425 | self.browser.model.reset() 426 | 427 | def _moveModel(self, dragName, newName="", dragItem=None, dropItem=None): 428 | "Rename or Delete models" 429 | model = mw.col.models.get(dragItem.mid) 430 | modelName=model['name'] 431 | if modelName.startswith(dragName + "::"): 432 | model['name'] = modelName.replace(dragName+"::", newName+"::", 1) 433 | elif modelName == dragName: 434 | model['name'] = newName 435 | self.node_state['model'][newName] = True 436 | self._swapHighlight('model',dragName,newName) 437 | mw.col.models.save(model) 438 | 439 | def _tagDropEvent(self, dragItems, dropItem): 440 | self.browser.editor.saveNow(self.hideEditor) 441 | mw.col.tags.registerNotes() # clearn unused tags to prevent lockup 442 | for item in dragItems: 443 | mw.progress.update(label=item.name) 444 | self._strDropEvent(item,dropItem,item.type,self._moveTag) 445 | self._saveTags() 446 | mw.col.tags.registerNotes() 447 | 448 | def moveTag(self, dragName, newName): 449 | self.browser.editor.saveNow(self.hideEditor) 450 | mw.col.tags.registerNotes() # clearn unused tags to prevent lockup 451 | self._moveTag(dragName,newName) 452 | self._saveTags() 453 | mw.col.tags.registerNotes() 454 | 455 | def _moveTag(self, dragName, newName, dragItem=None, dropItem=None): 456 | "Rename tag" 457 | # rename children 458 | for tag in mw.col.tags.all(): 459 | if tag.startswith(dragName + "::"): 460 | ids = self.findNotes('"tag:%s"'%tag) 461 | nn = tag.replace(dragName+"::", newName+"::", 1) 462 | mw.col.tags.bulkRem(ids,tag) 463 | mw.col.tags.bulkAdd(ids,nn) 464 | self.node_state['tag'][nn]=True 465 | self._swapHighlight('tag',tag,nn) 466 | # rename parent 467 | ids = self.findNotes('"tag:%s"'%dragName) 468 | mw.col.tags.bulkRem(ids,dragName) 469 | mw.col.tags.bulkAdd(ids,newName) 470 | self.node_state['tag'][newName] = True 471 | self._swapHighlight('tag',dragName,newName) 472 | 473 | 474 | def hideEditor(self): 475 | self.browser.editor.setNote(None) 476 | self.browser.singleCard=False 477 | 478 | 479 | def onTreeMenu(self, pos): 480 | try: 481 | #stop timer for, auto update overview summary deck, during right clicks 482 | self.timer.stop() 483 | except: pass 484 | 485 | index=self.indexAt(pos) 486 | if not index.isValid(): 487 | return 488 | item=index.internalPointer() 489 | if not item: 490 | return 491 | 492 | try: #quick patch for addon compatibility 493 | item.type 494 | except AttributeError: 495 | item.type=None 496 | 497 | m = QMenu(self) 498 | if not isinstance(item.type, str) or item.type == "sys": 499 | # 2.1 does not patch _systemTagTree. 500 | # So I am using this to readjust item.type 501 | item.type = "sys" 502 | 503 | #TODO: Rewrite menu for each type + modifier keys 504 | elif mw.app.keyboardModifiers()==Qt.ShiftModifier: 505 | if item.type != "group": 506 | if item.type == "tag": 507 | act = m.addAction("Create Filtered Tag*") 508 | act.triggered.connect(lambda:self._onTreeCramTags(index)) 509 | m.addSeparator() 510 | #TODO: add support for custom study from deck list 511 | 512 | act = m.addAction("Mark/Unmark Item*") 513 | act.triggered.connect(lambda:self._onTreeMark(index)) 514 | if item.type in ("deck","tag"): 515 | act = m.addAction("Pin Item*") 516 | act.triggered.connect(lambda:self._onTreePin(index)) 517 | if item.type in ("pin","fav"): 518 | ico = self.getConf('Blitzkrieg.icon_fav', True) 519 | act = m.addAction("Show icon for paths") 520 | act.setCheckable(True) 521 | act.setChecked(ico) 522 | act.triggered.connect(lambda:self._toggleIconOption(item)) 523 | 524 | act = m.addAction("Refresh") 525 | act.triggered.connect(self.refresh) 526 | if item.type == "group": 527 | if item.fullname in ("tag","deck","model"): 528 | sort = self.getConf('Blitzkrieg.sort_'+item.fullname, False) 529 | act = m.addAction("Sort by A-a-B-b") 530 | act.setCheckable(True) 531 | act.setChecked(sort) 532 | act.triggered.connect(lambda:self._toggleSortOption(item)) 533 | if item.fullname in ("tag","model"): 534 | ico = self.getConf('Blitzkrieg.icon_'+item.fullname, True) 535 | act = m.addAction("Show icon for paths") 536 | act.setCheckable(True) 537 | act.setChecked(ico) 538 | act.triggered.connect(lambda:self._toggleIconOption(item)) 539 | if item.fullname == "deck": 540 | up = self.getConf('Blitzkrieg.updateOV', False) 541 | act = m.addAction("Auto Update Overview") 542 | act.setCheckable(True) 543 | act.setChecked(up) 544 | act.triggered.connect(self._toggleMWUpdate) 545 | elif item.fullname == "tag": 546 | sa = self.getConf('Blitzkrieg.showAllTags', True) 547 | act = m.addAction("Auto Show Subtags") 548 | act.setCheckable(True) 549 | act.setChecked(sa) 550 | act.triggered.connect(self._toggleShowSubtags) 551 | 552 | if len(item.children): 553 | m.addSeparator() 554 | act = m.addAction("Collapse All*") 555 | act.triggered.connect(lambda:self.expandAllChildren(index)) 556 | act = m.addAction("Expand All*") 557 | act.triggered.connect(lambda:self.expandAllChildren(index,True)) 558 | 559 | elif item.type == "group": 560 | if item.fullname == "tag": 561 | act = m.addAction("Refresh") 562 | act.triggered.connect(self.refresh) 563 | elif item.fullname == "deck": 564 | act = m.addAction("Add Deck") 565 | act.triggered.connect(self._onTreeDeckAdd) 566 | act = m.addAction("Empty All Filters") 567 | act.triggered.connect(self.onEmptyAll) 568 | act = m.addAction("Rebuild All Filters") 569 | act.triggered.connect(self.onRebuildAll) 570 | elif item.fullname == "model": 571 | act = m.addAction("Manage Model") 572 | act.triggered.connect(self.onManageModel) 573 | 574 | m.addSeparator() 575 | act = m.addAction("Find...") 576 | act.triggered.connect(lambda:self.findRecursive(index)) 577 | act = m.addAction("Collapse All*") 578 | act.triggered.connect(lambda:self.expandAllChildren(index)) 579 | act = m.addAction("Expand All*") 580 | act.triggered.connect(lambda:self.expandAllChildren(index,True)) 581 | 582 | elif len(self.selectedIndexes())>1: 583 | #Multi sel items 584 | for itm in self.MENU_ITEMS[item.type]: 585 | if itm[0] < 0: 586 | m.addSeparator() 587 | elif itm[0]==2 or (itm[0]==3 and self.hasValue(item)): 588 | act = m.addAction(itm[1]) 589 | act.triggered.connect( 590 | lambda b, item=item, itm=itm: 591 | self._onTreeItemAction(item,itm[2],itm[3]) 592 | ) 593 | else: 594 | #Single selected itms 595 | for itm in self.MENU_ITEMS[item.type]: 596 | if itm[0] < 0: 597 | m.addSeparator() 598 | elif itm[0] in (0,2) or self.hasValue(item): 599 | act = m.addAction(itm[1]) 600 | act.triggered.connect( 601 | lambda b, item=item, itm=itm: 602 | self._onTreeItemAction(item,itm[2],itm[3]) 603 | ) 604 | 605 | runHook("Blitzkrieg.treeMenu", self, item, m) 606 | if not m.isEmpty(): 607 | m.popup(QCursor.pos()) 608 | 609 | 610 | 611 | def _onTreeItemAction(self, item, action, callback): 612 | self.browser.editor.saveNow(self.hideEditor) 613 | if action: 614 | mw.checkpoint(action+" "+item.type) 615 | try: 616 | callback(item) 617 | finally: 618 | mw.col.setMod() 619 | self.browser.onReset() 620 | self.browser.maybeRefreshSidebar() 621 | 622 | 623 | def _onTreeDeckEmpty(self, item): 624 | self.browser._lastSearchTxt="" 625 | sel = mw.col.decks.byName(item.fullname) 626 | mw.col.sched.emptyDyn(sel['id']) 627 | mw.reset() 628 | 629 | def _onTreeDeckRebuild(self, item): 630 | sel = mw.col.decks.byName(item.fullname) 631 | mw.col.sched.rebuildDyn(sel['id']) 632 | mw.reset() 633 | 634 | def _onTreeDeckOptions(self, item): 635 | deck = mw.col.decks.byName(item.fullname) 636 | try: 637 | if deck['dyn']: 638 | import aqt.dyndeckconf 639 | aqt.dyndeckconf.DeckConf(self.mw, deck=deck, parent=self.browser) 640 | else: 641 | import aqt.deckconf 642 | aqt.deckconf.DeckConf(self.mw, deck, self.browser) 643 | except TypeError: 644 | mw.onDeckConf(deck) 645 | mw.reset(True) 646 | 647 | def _onTreeDeckExport(self, item): 648 | deck = mw.col.decks.byName(item.fullname) 649 | try: 650 | import aqt.exporting 651 | aqt.exporting.ExportDialog(self.mw, deck['id'], self.browser) 652 | except TypeError: 653 | mw.onExport(did=deck['id']) 654 | mw.reset(True) 655 | 656 | def _onTreeDeckAdd(self, item=None): 657 | parent=item.fullname+"::" if item and item.type=='deck' else '' 658 | subdeck = getOnlyText(_("Name for deck/subdeck:")) 659 | if subdeck: 660 | mw.col.decks.id(parent+subdeck) 661 | self._saveDecks() 662 | mw.reset(True) 663 | 664 | def _onTreeDeckDelete(self, item): 665 | self.browser._lastSearchTxt="" 666 | sel=mw.col.decks.byName(item.fullname) 667 | mw.deckBrowser._delete(sel['id']) 668 | self._saveDecks() 669 | mw.reset(True) 670 | 671 | def _onTreeDeckRenameLeaf(self, item): 672 | mw.checkpoint(_("Rename Deck")) 673 | from aqt.utils import showWarning 674 | from anki.errors import DeckRenameError 675 | 676 | self.browser._lastSearchTxt="" 677 | sel = mw.col.decks.byName(item.fullname) 678 | try: 679 | path,leaf = item.fullname.rsplit('::',1) 680 | newName = path+'::'+ getOnlyText(_("New deck name:"), default=leaf) 681 | except ValueError: 682 | newName = getOnlyText(_("New deck name:"), default=item.fullname) 683 | newName = newName.replace('"', "") 684 | if not newName or newName == item.fullname: 685 | return 686 | 687 | newName = unicodedata.normalize("NFC", newName) 688 | deck = mw.col.decks.get(sel["id"]) 689 | try: 690 | mw.col.decks.rename(deck, newName) 691 | except DeckRenameError as e: 692 | return showWarning(e.description) 693 | self._swapHighlight(item.type,item.fullname,newName) 694 | self._saveDecks() 695 | # self.highlight('deck',newName) 696 | mw.show() 697 | mw.reset(True) 698 | 699 | def _onTreeDeckRename(self, item): 700 | self.browser._lastSearchTxt="" 701 | sel=mw.col.decks.byName(item.fullname) 702 | mw.deckBrowser._rename(sel['id']) 703 | if item.fullname != sel['name']: 704 | # TODO: current version of anki api does not normalize on renames. 705 | # Remove this line once it does. 706 | sel['name'] = unicodedata.normalize("NFC", sel['name']) 707 | 708 | self._swapHighlight(item.type,item.fullname,sel['name']) 709 | self._saveDecks() 710 | # self.highlight('deck',sel['name']) 711 | mw.reset(True) 712 | 713 | def _onTreeDeckAddCard(self, item): 714 | from aqt import addcards 715 | d = mw.col.decks.byName(item.fullname) 716 | mw.col.decks.select(d["id"]) 717 | diag = aqt.dialogs.open("AddCards", self.mw) 718 | 719 | def _onTreeTagAddCard(self, item): 720 | from aqt import addcards 721 | tags=[] 722 | items=self.selectedIndexes() 723 | for i in items: 724 | itm=i.internalPointer() 725 | tags.append(itm.fullname) 726 | diag = aqt.dialogs.open("AddCards", self.mw) 727 | diag.editor.tags.setText(" ".join(tags)) 728 | 729 | def _onTreeTagRenameLeaf(self, item): 730 | oldNameArr = item.fullname.split("::") 731 | newName = getOnlyText(_("New tag name:"),default=oldNameArr[-1]) 732 | newName = unicodedata.normalize("NFC", newName) 733 | newName = newName.replace('"', "") 734 | if not newName or newName == oldNameArr[-1]: 735 | return 736 | oldNameArr[-1] = newName 737 | newName = "::".join(oldNameArr) 738 | self.moveTag(item.fullname,newName) 739 | # self.highlight('tag',newName) 740 | 741 | def _onTreeTagRenameBranch(self, item): 742 | newName = getOnlyText(_("New tag name:"),default=item.fullname) 743 | newName = unicodedata.normalize("NFC", newName) 744 | newName = newName.replace('"', "") 745 | if not newName or newName == item.fullname: 746 | return 747 | self.moveTag(item.fullname,newName) 748 | # self.highlight('tag',newName) 749 | 750 | def _onTreeTagDelete(self, item): 751 | "allows del of multi selected tags" 752 | self.browser.editor.saveNow(self.hideEditor) 753 | items=self.selectedIndexes() 754 | for i in items: 755 | itm=i.internalPointer() 756 | self._massDelTag(itm.fullname) 757 | self._saveTags() 758 | mw.col.tags.registerNotes() 759 | 760 | def _massDelTag(self, dragName): 761 | # rename children 762 | for tag in mw.col.tags.all(): 763 | if tag.startswith(dragName + "::"): 764 | ids = self.findNotes('"tag:%s"'%tag) 765 | self._swapHighlight('tag',tag,"",False) 766 | mw.col.tags.bulkRem(ids,tag) 767 | # rename parent 768 | ids = self.findNotes('"tag:%s"'%dragName) 769 | mw.col.tags.bulkRem(ids,dragName) 770 | self._swapHighlight('tag',dragName,"",False) 771 | 772 | def _onTreeTag(self, item, add=True): 773 | sel = self.browser.selectedNotes() 774 | tag = item.fullname 775 | self.browser.model.beginReset() 776 | if add: 777 | mw.col.tags.bulkAdd(sel,tag) 778 | else: 779 | mw.col.tags.bulkRem(sel,tag) 780 | self.browser.model.endReset() 781 | mw.requireReset() 782 | 783 | def _onTreeUnTag(self, item): 784 | self._onTreeTag(item,False) 785 | self.refresh() 786 | 787 | def _onTreeDeck2Tag(self, item): 788 | msg = _("Convert all notes in deck/subdecks to tags?") 789 | if not askUser(msg, parent=self, defaultno=True): 790 | return 791 | 792 | mw.progress.start( 793 | label=_("Converting decks to tags")) 794 | try: 795 | self.browser._lastSearchTxt="" 796 | parentDid = mw.col.decks.byName(item.fullname)["id"] 797 | actv = mw.col.decks.children(parentDid) 798 | actv = sorted(actv, key=lambda t: t[0]) 799 | actv.insert(0,(item.fullname,parentDid)) 800 | 801 | found = False 802 | for name,did in actv: 803 | mw.progress.update(label=name) 804 | #add subdeck tree structure as tags 805 | nids = self.findNotes('''"deck:%s" -"deck:%s::*"'''%(name,name)) 806 | if nids: 807 | found = True 808 | tagName = re.sub(r"\s*(::)\s*","\g<1>",name) 809 | tagName = re.sub(r"\s+","_",tagName) 810 | tagName = unicodedata.normalize("NFC", tagName) 811 | mw.col.tags.bulkAdd(nids, tagName) 812 | #skip parent or dyn decks 813 | if did == parentDid or mw.col.decks.get(did)['dyn']: 814 | continue 815 | #collapse subdecks into one 816 | mw.col.sched.emptyDyn(None, "odid=%d"%did) 817 | mw.col.db.execute( 818 | "update cards set usn=?, mod=?, did=? where did=?", 819 | mw.col.usn(), intTime(), parentDid, did 820 | ) 821 | mw.col.decks.rem(did,childrenToo=False) 822 | finally: 823 | mw.progress.finish() 824 | if not found: 825 | showInfo("No Cards in deck") 826 | return 827 | self._saveDecks() 828 | self._saveTags() 829 | # self.highlight('tag',item.fullname) 830 | mw.col.tags.registerNotes() 831 | mw.requireReset() 832 | 833 | 834 | def _onTreeTag2Deck(self, item): 835 | def tag2Deck(tag): 836 | did = mw.col.decks.id(tag) 837 | cids = self.findCards('"tag:%s"'%tag) 838 | if not cids: 839 | return 840 | mw.col.sched.remFromDyn(cids) 841 | mw.col.db.execute( 842 | "update cards set usn=?, mod=?, did=? where id in %s"%ids2str(cids), 843 | mw.col.usn(), intTime(), did 844 | ) 845 | nids = self.findNotes('"tag:%s"'%tag) 846 | mw.col.tags.bulkRem(nids,tag) 847 | 848 | msg = _("Convert all tags to deck structure?") 849 | if not askUser(msg, parent=self, defaultno=True): 850 | return 851 | 852 | mw.progress.start( 853 | label=_("Converting tags to decks")) 854 | 855 | try: 856 | self.browser._lastSearchTxt="" 857 | parent = unicodedata.normalize("NFC", item.fullname) 858 | tag2Deck(parent) 859 | for tag in mw.col.tags.all(): 860 | mw.progress.update(label=tag) 861 | if tag.startswith(parent + "::"): 862 | tag2Deck(tag) 863 | finally: 864 | mw.progress.finish() 865 | self._saveDecks() 866 | self._saveTags() 867 | # self.highlight('deck',item.fullname) 868 | mw.col.tags.registerNotes() 869 | mw.requireReset() 870 | 871 | 872 | def _onTreePinDelete(self, item): 873 | savedFilters = self.getConf('savedFilters', {}) 874 | for idx in self.selectedIndexes(): 875 | itm = idx.internalPointer() 876 | if savedFilters.get(itm.favname): 877 | del savedFilters[itm.favname] 878 | self.setConf('savedFilters', savedFilters) 879 | 880 | def _onTreeFavDelete(self, item): 881 | savedFilters = self.getConf('savedFilters', {}) 882 | for idx in self.selectedIndexes(): 883 | itm = idx.internalPointer() 884 | if savedFilters.get(itm.favname) and \ 885 | askUser(_("Remove %s from your saved searches?") % itm.favname): 886 | del savedFilters[itm.favname] 887 | self.setConf('savedFilters', savedFilters) 888 | 889 | def _onTreeFavRename(self, item): 890 | savedFilters = self.getConf('savedFilters', {}) 891 | act = savedFilters.get(item.favname) 892 | if not act: return 893 | s=item.favname 894 | p=False 895 | if item.type.startswith("pin"): 896 | s=re.sub(r"^Pinned::","",s) 897 | p=True 898 | newName = getOnlyText(_("New search name:"),default=s) 899 | newName = re.sub(r"^Pinned::","",newName) 900 | if newName: 901 | if p: newName="Pinned::"+newName 902 | del(savedFilters[item.favname]) 903 | savedFilters[newName] = act 904 | self.setConf('savedFilters', savedFilters) 905 | 906 | def _onTreeFavModify(self, item): 907 | savedFilters = self.getConf('savedFilters', {}) 908 | act = savedFilters.get(item.fullname) 909 | if not act: return 910 | act=getOnlyText(_("New Search:"),default=act) 911 | if act: 912 | savedFilters[item.fullname]=act 913 | self.setConf('savedFilters', savedFilters) 914 | 915 | def _onTreeModelRenameLeaf(self, item): 916 | self.browser._lastSearchTxt="" 917 | oldNameArr = item.fullname.split("::") 918 | newName = getOnlyText(_("New model name:"),default=oldNameArr[-1]) 919 | newName = newName.replace('"', "") 920 | if not newName or newName == oldNameArr[-1]: 921 | return 922 | oldNameArr[-1] = unicodedata.normalize("NFC", newName) 923 | newName = "::".join(oldNameArr) 924 | self.moveModel(item.fullname,newName,item) 925 | # self.highlight('model',newName) 926 | 927 | def _onTreeModelRenameBranch(self, item): 928 | self.browser._lastSearchTxt="" 929 | newName = getOnlyText(_("New model name:"),default=item.fullname) 930 | newName = newName.replace('"', "") 931 | if not newName or newName == item.fullname: 932 | return 933 | newName = unicodedata.normalize("NFC", newName) 934 | self.moveModel(item.fullname,newName,item) 935 | # self.highlight('model',newName) 936 | 937 | def _onTreeModelDelete(self, item): 938 | self.browser._lastSearchTxt="" 939 | model = mw.col.models.get(item.mid) 940 | if not model: 941 | return 942 | if mw.col.models.useCount(model): 943 | msg = _("Delete this note type and all its cards?") 944 | else: 945 | msg = _("Delete this unused note type?") 946 | if askUser(msg, parent=self, defaultno=True): 947 | try: 948 | mw.col.models.rem(model) 949 | except AnkiError: 950 | #user says no to full sync requirement 951 | return 952 | self._saveModels() 953 | self.browser.setupTable() 954 | self.browser.model.reset() 955 | 956 | def _onTreeModelAdd(self, item): 957 | from aqt.models import AddModel 958 | self.browser.form.searchEdit.lineEdit().setText("") 959 | m = AddModel(self.mw, self.browser).get() 960 | if m: 961 | #model is already created 962 | txt = getOnlyText(_("Name:"), default=item.fullname+'::') 963 | if txt: 964 | m['name'] = txt 965 | mw.col.models.ensureNameUnique(m) 966 | mw.col.models.save(m) 967 | 968 | def onTreeModelFields(self, item): 969 | from aqt.fields import FieldDialog 970 | model = mw.col.models.get(item.mid) 971 | mw.col.models.setCurrent(model) 972 | n = mw.col.newNote(forDeck=False) 973 | for name in list(n.keys()): 974 | n[name] = "("+name+")" 975 | try: 976 | if "{{cloze:Text}}" in model['tmpls'][0]['qfmt']: 977 | n['Text'] = _("This is a {{c1::sample}} cloze deletion.") 978 | except: 979 | # invalid cloze 980 | pass 981 | FieldDialog(self.mw, n, parent=self.browser) 982 | 983 | def onTreeModelOptions(self, item): 984 | from aqt.forms import modelopts 985 | model = mw.col.models.get(item.mid) 986 | d = QDialog(self) 987 | frm = modelopts.Ui_Dialog() 988 | frm.setupUi(d) 989 | frm.latexHeader.setText(model['latexPre']) 990 | frm.latexFooter.setText(model['latexPost']) 991 | d.setWindowTitle(_("Options for %s") % model['name']) 992 | d.exec_() 993 | model['latexPre'] = str(frm.latexHeader.toPlainText()) 994 | model['latexPost'] = str(frm.latexFooter.toPlainText()) 995 | self._saveModels() 996 | 997 | def onManageModel(self): 998 | self.browser.editor.saveNow(self.hideEditor) 999 | mw.checkpoint("Manage model") 1000 | import aqt.models 1001 | aqt.models.Models(self.mw, self.browser) 1002 | mw.col.setMod() 1003 | self.browser.onReset() 1004 | self.browser.maybeRefreshSidebar() 1005 | 1006 | 1007 | def _onTreeCramTags(self, index): 1008 | line = self.browser.form.searchEdit.lineEdit() 1009 | self.clearSelection() 1010 | mw.onCram(line.text()) 1011 | 1012 | 1013 | def _onTreeMark(self, index): 1014 | indexes=self.selectedIndexes() 1015 | if index not in indexes: 1016 | indexes.append(index) 1017 | for idx in indexes: 1018 | item = idx.internalPointer() 1019 | tf=not self.marked[item.type].get(item.fullname, False) 1020 | self.marked[item.type][item.fullname]=tf 1021 | color=QBrush(Qt.yellow) if tf else None 1022 | item.background=color 1023 | self.clearSelection() 1024 | 1025 | def _onTreePin(self, index): 1026 | savedFilters = self.getConf('savedFilters', {}) 1027 | indexes=self.selectedIndexes() 1028 | if index not in indexes: 1029 | indexes.append(index) 1030 | for idx in indexes: 1031 | item = idx.internalPointer() 1032 | name = "Pinned::%s"%( 1033 | item.fullname.split("::")[-1]) 1034 | search = '"%s:%s"'%(item.type,item.fullname) 1035 | savedFilters[name] = search 1036 | self.setConf('savedFilters', savedFilters) 1037 | self.browser.maybeRefreshSidebar() 1038 | 1039 | def onEmptyAll(self): 1040 | for d in mw.col.decks.all(): 1041 | if d['dyn']: 1042 | mw.col.sched.emptyDyn(d['id']) 1043 | self.browser.onReset() 1044 | 1045 | def onRebuildAll(self): 1046 | for d in mw.col.decks.all(): 1047 | if d['dyn']: 1048 | mw.col.sched.rebuildDyn(d['id']) 1049 | self.browser.onReset() 1050 | 1051 | def hasValue(self, item): 1052 | if item.type == "model": 1053 | return mw.col.models.byName(item.fullname) 1054 | if item.type == "fav": 1055 | savedFilters = self.getConf('savedFilters', {}) 1056 | return savedFilters.get(item.fullname) 1057 | if item.type in ("pinTag","pinDeck","pinDyn"): 1058 | savedFilters = self.getConf('savedFilters', {}) 1059 | return savedFilters.get(item.favname) 1060 | return False 1061 | 1062 | def _toggleMWUpdate(self): 1063 | up = self.getConf('Blitzkrieg.updateOV', False) 1064 | self.setConf('Blitzkrieg.updateOV', not up) 1065 | 1066 | def _toggleShowSubtags(self): 1067 | sa = self.getConf('Blitzkrieg.showAllTags', True) 1068 | self.setConf('Blitzkrieg.showAllTags', not sa) 1069 | self.refresh() 1070 | 1071 | def _toggleSortOption(self, item): 1072 | sort = not self.getConf('Blitzkrieg.sort_'+item.fullname,False) 1073 | self.setConf('Blitzkrieg.sort_'+item.fullname, sort) 1074 | self.browser.maybeRefreshSidebar() 1075 | 1076 | def _toggleIconOption(self, item): 1077 | TYPE='fav' if item.type in ("pin","fav") else item.fullname 1078 | ico = not self.getConf('Blitzkrieg.icon_'+TYPE,True) 1079 | self.setConf('Blitzkrieg.icon_'+TYPE, ico) 1080 | self.browser.maybeRefreshSidebar() 1081 | 1082 | 1083 | def expandAllChildren(self, index, expanded=False): 1084 | self._expandAllChildren(index, expanded) 1085 | for idx in self.selectedIndexes(): 1086 | self._expandAllChildren(idx, expanded) 1087 | 1088 | def _expandAllChildren(self, parentIdx, expanded=False): 1089 | parentItem=parentIdx.internalPointer() 1090 | parentItem.expanded=expanded 1091 | for row, child in enumerate(parentItem.children): 1092 | childIdx = self.model().index(row, 0, parentIdx) 1093 | self._expandAllChildren(childIdx, expanded) 1094 | 1095 | self.setExpanded(parentIdx, expanded) 1096 | try: #no deck type 1097 | self.node_state[parentItem.type][parentItem.fullname]=expanded 1098 | except TypeError: pass 1099 | 1100 | 1101 | def findRecursive(self, index): 1102 | from .forms import findtreeitems 1103 | item=index.internalPointer() 1104 | TAG_TYPE = item.fullname 1105 | self.found = {} 1106 | self.found[TAG_TYPE] = {} 1107 | d = QDialog(self.browser) 1108 | frm = findtreeitems.Ui_Dialog() 1109 | frm.setupUi(d) 1110 | 1111 | # Restore btn states 1112 | frm.input.setText(self.finder.get('txt','')) 1113 | frm.input.setFocus() 1114 | frm.cb_case.setChecked(self.finder.get('case',0)) 1115 | for idx,func in enumerate(( 1116 | frm.btn_contains, frm.btn_exactly, 1117 | frm.btn_startswith, frm.btn_endswith, 1118 | frm.btn_regexp 1119 | )): 1120 | func.setChecked(0) 1121 | if self.finder.get('radio',0)==idx: 1122 | func.setChecked(2) 1123 | 1124 | if not d.exec_(): 1125 | return 1126 | 1127 | txt = frm.input.text() 1128 | if not txt: 1129 | return 1130 | txt = unicodedata.normalize("NFC", txt) 1131 | options = Qt.MatchRecursive 1132 | if txt=='vote for pedro': 1133 | mw.pm.profile['Blitzkrieg.VFP']=True 1134 | from .alt import disabledDebugStuff 1135 | disabledDebugStuff() 1136 | self.finder['txt'] = txt 1137 | self.finder['case'] = frm.cb_case.isChecked() 1138 | if self.finder['case']: 1139 | options |= Qt.MatchCaseSensitive 1140 | 1141 | if frm.btn_exactly.isChecked(): 1142 | options |= Qt.MatchExactly 1143 | self.finder['radio'] = 1 1144 | elif frm.btn_startswith.isChecked(): 1145 | options |= Qt.MatchStartsWith 1146 | self.finder['radio'] = 2 1147 | elif frm.btn_endswith.isChecked(): 1148 | options |= Qt.MatchEndsWith 1149 | self.finder['radio'] = 3 1150 | elif frm.btn_regexp.isChecked(): 1151 | options |= Qt.MatchRegExp 1152 | self.finder['radio'] = 4 1153 | else: 1154 | options |= Qt.MatchContains 1155 | self.finder['radio'] = 0 1156 | 1157 | self.expandAllChildren(index,True) 1158 | 1159 | for idx in self.findItems(txt,options): 1160 | itm = idx.internalPointer() 1161 | if itm.type == TAG_TYPE: 1162 | itm.background=QBrush(Qt.cyan) 1163 | self.found[TAG_TYPE][itm.fullname] = True 1164 | 1165 | if not self.found[TAG_TYPE]: 1166 | showInfo("Found nothing, nada, zilch!") 1167 | 1168 | def findItems(self, txt, options): 1169 | model=self.model() 1170 | return model.match( 1171 | self.currentIndex(), Qt.DisplayRole, 1172 | QVariant(txt), -1, options 1173 | ) 1174 | 1175 | def refresh(self): 1176 | self.found = {} 1177 | mw.col.tags.registerNotes() #calls "newTag" hook which invokes maybeRefreshSidebar 1178 | #Clear to create a smooth UX 1179 | self.marked['group'] = {} 1180 | self.marked['pinDeck'] = {} 1181 | self.marked['pinDyn'] = {} 1182 | self.marked['pinTag'] = {} 1183 | 1184 | def _swapHighlight(self, type, oName, nName, swap=True): 1185 | if swap and self.marked[type].get(oName, False): 1186 | self.marked[type][nName] = True 1187 | try: 1188 | del(self.marked[type][oName]) 1189 | except KeyError: pass 1190 | 1191 | 1192 | def _saveTags(self): 1193 | # for anki 2.1.24beta4 and below 1194 | try: 1195 | mw.col.tags.save() 1196 | mw.col.tags.flush() 1197 | except AttributeError: pass 1198 | 1199 | def _saveDecks(self): 1200 | try: 1201 | mw.col.decks.save() 1202 | mw.col.decks.flush() 1203 | except AttributeError: pass 1204 | 1205 | def _saveModels(self): 1206 | try: 1207 | mw.col.models.save() 1208 | mw.col.models.flush() 1209 | except AttributeError: pass 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | class TagTreeWidget(QTreeWidget): 1217 | def __init__(self, browser, parent): 1218 | QTreeWidget.__init__(self, parent) 1219 | self.setHeaderHidden(True) 1220 | self.browser = browser 1221 | self.col = browser.col 1222 | self.node = {} 1223 | self.addMode = False 1224 | self.color = Qt.red 1225 | 1226 | self.itemClicked.connect(self.onClick) 1227 | self.itemExpanded.connect(self.onCollapse) 1228 | self.itemCollapsed.connect(self.onCollapse) 1229 | 1230 | # self.setSelectionMode(QAbstractItemView.ExtendedSelection) 1231 | 1232 | def onClick(self, item, col): 1233 | item.setSelected(False) 1234 | if self.addMode or item.type=="tag": 1235 | s = not self.node.get(item.fullname,False) 1236 | self.node[item.fullname] = s 1237 | color = self.color if s else Qt.transparent 1238 | item.setBackground(0, QBrush(color)) 1239 | 1240 | def onCollapse(self, item): 1241 | try: 1242 | s = self.node.get(item.fullname,False) 1243 | color = self.color if s else Qt.transparent 1244 | item.setBackground(0, QBrush(color)) 1245 | except AttributeError: pass 1246 | 1247 | def removeTags(self, nids): 1248 | self.addMode = False 1249 | self.color = Qt.red 1250 | SORT = self.col.conf.get('Blitzkrieg.sort_tag',False) 1251 | tags = self.col.db.list(""" 1252 | select tags from notes where id in %s""" % ids2str(nids)) 1253 | tags = sorted(" ".join(tags).split(), 1254 | key=lambda t: t.lower() if SORT else t) 1255 | self._setTags(tags) 1256 | 1257 | def addTags(self, nids): 1258 | self.addMode = True 1259 | self.color = Qt.green 1260 | SORT = self.col.conf.get('Blitzkrieg.sort_tag',False) 1261 | allTags = sorted(self.col.tags.all(), 1262 | key=lambda t: t.lower() if SORT else t) 1263 | tags = self.col.db.list(""" 1264 | select tags from notes where id in %s""" % ids2str(nids)) 1265 | tags = set(" ".join(tags).split()) 1266 | self._setTags(allTags,tags) 1267 | 1268 | def _setTags(self, allTags, curTags=""): 1269 | tags_tree = {} 1270 | for t in allTags: 1271 | if self.addMode and t.lower() in ("marked","leech"): 1272 | continue 1273 | node = t.split('::') 1274 | for idx, name in enumerate(node): 1275 | leaf_tag = '::'.join(node[0:idx + 1]) 1276 | if not tags_tree.get(leaf_tag): 1277 | parent = tags_tree['::'.join(node[0:idx])] if idx else self 1278 | item = QTreeWidgetItem(parent,[name]) 1279 | item.fullname = leaf_tag 1280 | item.setExpanded(True) 1281 | tags_tree[leaf_tag] = item 1282 | if leaf_tag in curTags: 1283 | item.setBackground(0, QBrush(Qt.yellow)) 1284 | try: 1285 | item.type = "tag" 1286 | item.setIcon(0, QIcon(":/icons/tag.svg")) 1287 | except AttributeError: pass 1288 | -------------------------------------------------------------------------------- /src/blitzkrieg/tree.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2019-2020 Lovac42 3 | # Copyright 2014 Patrice Neff 4 | # Copyright 2006-2019 Ankitects Pty Ltd and contributors 5 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 6 | # Support: https://github.com/lovac42/Blitzkrieg 7 | 8 | 9 | from aqt import mw 10 | from aqt.qt import * 11 | from anki.lang import ngettext, _ 12 | from operator import itemgetter 13 | 14 | from .lib.com.lovac42.anki.backend.collection import getConfigGetterMethod 15 | 16 | 17 | try: 18 | from aqt.browser import SidebarItem 19 | except: #SHOULD_PATCH 20 | from .patch_sidebar import SidebarItem 21 | 22 | 23 | 24 | def stdTree(browser, root): 25 | for name, filt, icon in [[_("Whole Collection"), "", "collection"], 26 | [_("Current Deck"), "deck:current", "deck"]]: 27 | item = SidebarItem( 28 | name, ":/icons/{}.svg".format(icon), browser._filterFunc(filt)) 29 | item.type=None 30 | root.addChild(item) 31 | 32 | 33 | 34 | def favTree(browser, root): 35 | assert browser.col 36 | tree = browser.sidebarTree 37 | ico = ":/icons/heart.svg" 38 | 39 | getConfig = getConfigGetterMethod() 40 | 41 | icoOpt = getConfig('Blitzkrieg.icon_fav',True) 42 | 43 | saved = getConfig('savedFilters', {}) 44 | if not saved: 45 | return 46 | favs_tree = {} 47 | for fav, filt in sorted(saved.items()): 48 | node = fav.split('::') 49 | lstIdx = len(node)-1 50 | ico = "heart.svg" 51 | type = "fav" 52 | fname = None 53 | for idx, name in enumerate(node): 54 | if node[0]=='Pinned': 55 | if idx==0: 56 | type = "pin" 57 | elif filt.startswith('"tag:'): 58 | type = "pinTag" 59 | ico = "tag.svg" 60 | fname = filt[5:-1] 61 | 62 | # TODO: fix tags with () chars 63 | 64 | elif filt.startswith('"deck:'): 65 | type = "pinDeck" 66 | ico = "deck.svg" 67 | fname = filt[6:-1] 68 | elif filt.startswith('"dyn:'): 69 | type = "pinDyn" 70 | ico = "deck.svg" 71 | fname = filt[5:-1] 72 | filt='"deck'+filt[4:] 73 | 74 | item = None 75 | leaf_tag = '::'.join(node[0:idx + 1]) 76 | if not favs_tree.get(leaf_tag): 77 | parent = favs_tree['::'.join(node[0:idx])] if idx else root 78 | exp = tree.node_state.get(type).get(leaf_tag,False) 79 | item = SidebarItem( 80 | name, 81 | (":/icons/"+ico) if icoOpt or not idx or idx==lstIdx else None, 82 | browser._filterFunc(filt), 83 | expanded=exp 84 | ) 85 | parent.addChild(item) 86 | 87 | item.type = type 88 | item.fullname = fname or leaf_tag 89 | item.favname = leaf_tag 90 | if tree.marked[type].get(leaf_tag, False): 91 | item.background=QBrush(Qt.yellow) 92 | favs_tree[leaf_tag] = item 93 | 94 | 95 | 96 | 97 | def userTagTree(browser, root): 98 | assert browser.col 99 | tree=browser.sidebarTree 100 | ico = ":/icons/tag.svg" 101 | getConfig = getConfigGetterMethod() 102 | 103 | showConf = getConfig('Blitzkrieg.showAllTags', True) 104 | 105 | icoOpt = getConfig('Blitzkrieg.icon_tag',True) 106 | rootNode = SidebarItem( 107 | "Tags", ico, 108 | expanded=tree.node_state.get("group").get('tag',True) 109 | ) 110 | rootNode.type = "group" 111 | rootNode.fullname = "tag" 112 | root.addChild(rootNode) 113 | 114 | tags_tree = {} 115 | SORT = getConfig('Blitzkrieg.sort_tag',False) 116 | TAGS = sorted(browser.col.tags.all(), 117 | key=lambda t: t.lower() if SORT else t) 118 | for t in TAGS: 119 | if t.lower() == "marked" or t.lower() == "leech": 120 | continue 121 | node = t.split('::') 122 | lstIdx = len(node)-1 123 | for idx, name in enumerate(node): 124 | leaf_tag = '::'.join(node[0:idx + 1]) 125 | if not tags_tree.get(leaf_tag): 126 | parent = tags_tree['::'.join(node[0:idx])] if idx else rootNode 127 | exp = tree.node_state.get('tag').get(leaf_tag,False) 128 | 129 | if showConf: 130 | fil = f'("tag:{leaf_tag}" or "tag:{leaf_tag}::*")' 131 | click = browser._filterFunc(fil) 132 | else: 133 | click = browser._filterFunc("tag", leaf_tag) 134 | 135 | item = SidebarItem( 136 | name, ico if icoOpt or idx==lstIdx else None, 137 | click, expanded=exp 138 | ) 139 | parent.addChild(item) 140 | 141 | item.type = "tag" 142 | item.fullname = leaf_tag 143 | if tree.found.get(item.type,{}).get(leaf_tag, False): 144 | item.background=QBrush(Qt.cyan) 145 | elif tree.marked['tag'].get(leaf_tag, False): 146 | item.background=QBrush(Qt.yellow) 147 | elif exp and '::' not in leaf_tag: 148 | item.background=QBrush(QColor(0,0,10,10)) 149 | tags_tree[leaf_tag] = item 150 | 151 | tag_cnt = len(TAGS) 152 | rootNode.tooltip = f"Total: {tag_cnt} tags" 153 | 154 | 155 | 156 | 157 | 158 | def decksTree(browser, root): 159 | assert browser.col 160 | tree=browser.sidebarTree 161 | ico = ":/icons/deck.svg" 162 | rootNode = SidebarItem( 163 | _("Decks"), ico, 164 | expanded=tree.node_state.get("group").get('deck',True) 165 | ) 166 | rootNode.type = "group" 167 | rootNode.fullname = "deck" 168 | root.addChild(rootNode) 169 | 170 | getConfig = getConfigGetterMethod() 171 | 172 | SORT = getConfig('Blitzkrieg.sort_deck',False) 173 | grps = sorted(browser.col.sched.deckDueTree(), 174 | key=lambda g: g[0].lower() if SORT else g[0]) 175 | def fillGroups(rootNode, grps, head=""): 176 | for g in grps: 177 | item = SidebarItem( 178 | g[0], ico, 179 | lambda g=g: browser.setFilter("deck", head+g[0]), 180 | lambda expanded, g=g: browser.mw.col.decks.collapseBrowser(g[1]), 181 | not browser.mw.col.decks.get(g[1]).get('browserCollapsed', False)) 182 | rootNode.addChild(item) 183 | item.fullname = head + g[0] #name 184 | if mw.col.decks.isDyn(g[1]): #id 185 | item.foreground = QBrush(Qt.blue) 186 | item.type = "dyn" 187 | else: 188 | if g[1]==1: #default deck 189 | item.foreground = QBrush(Qt.darkRed) 190 | item.type = "deck" 191 | if tree.found.get(item.type,{}).get(item.fullname, False): 192 | item.background=QBrush(Qt.cyan) 193 | elif tree.marked[item.type].get(item.fullname, False): 194 | item.background=QBrush(Qt.yellow) 195 | newhead = head + g[0]+"::" 196 | fillGroups(item, g[5], newhead) 197 | 198 | fillGroups(rootNode, grps) 199 | 200 | deck_cnt = len(browser.col.decks.all()) 201 | rootNode.tooltip = f"Total: {deck_cnt} decks" 202 | 203 | 204 | 205 | 206 | 207 | def modelTree(browser, root): 208 | assert browser.col 209 | tree=browser.sidebarTree 210 | ico = ":/icons/notetype.svg" 211 | 212 | getConfig = getConfigGetterMethod() 213 | 214 | icoOpt = getConfig('Blitzkrieg.icon_model',True) 215 | rootNode = SidebarItem( 216 | _("Models"), ico, 217 | expanded=tree.node_state.get("group").get('model',False) 218 | ) 219 | rootNode.type = "group" 220 | rootNode.fullname = "model" 221 | root.addChild(rootNode) 222 | 223 | models_tree = {} 224 | SORT = getConfig('Blitzkrieg.sort_model',False) 225 | MODELS = sorted(browser.col.models.all(), 226 | key=lambda m: m["name"].lower() if SORT else m["name"]) 227 | for m in MODELS: 228 | item = None 229 | mid=str(m['id']) 230 | node = m['name'].split('::') 231 | lstIdx = len(node)-1 232 | for idx, name in enumerate(node): 233 | leaf_model = '::'.join(node[0:idx + 1]) 234 | if not models_tree.get(leaf_model) or idx==lstIdx: #last element, model names are not unique 235 | parent = models_tree['::'.join(node[0:idx])] if idx else rootNode 236 | exp = tree.node_state.get('model').get(leaf_model,False) 237 | 238 | item = SidebarItem( 239 | name, ico if icoOpt or idx==lstIdx else None, 240 | browser._filterFunc("mid", str(m['id'])), 241 | expanded=exp 242 | ) 243 | parent.addChild(item) 244 | item.type = "model" 245 | item.fullname = leaf_model 246 | item.mid = mid 247 | 248 | if tree.found.get(item.type,{}).get(leaf_model, False): 249 | item.background=QBrush(Qt.cyan) 250 | elif tree.marked['model'].get(leaf_model, False): 251 | item.background=QBrush(Qt.yellow) 252 | models_tree[leaf_model] = item 253 | 254 | model_cnt = len(MODELS) 255 | rootNode.tooltip = f"Total: {model_cnt} models" 256 | 257 | -------------------------------------------------------------------------------- /src/zip.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set ZIP=C:\PROGRA~1\7-Zip\7z.exe a -tzip -y -r 3 | set REPO=blitzkrieg 4 | set VERSION=0.4.0 5 | 6 | fsum -r -jm -md5 -d%REPO% * > checksum.md5 7 | move checksum.md5 %REPO%/checksum.md5 8 | 9 | echo from .main import * >>%REPO%/__init__.py 10 | 11 | quick_manifest.exe "Blitzkrieg II - Advanced Browser Sidebar" "564851917" >%REPO%/manifest.json 12 | 13 | echo %VERSION% >%REPO%/VERSION 14 | 15 | cd %REPO% 16 | %ZIP% ../%REPO%_v%VERSION%_Anki21.ankiaddon * 17 | --------------------------------------------------------------------------------