Track your Anki study sessions directly into Yōten.

chore(init): initial commit

Signed-off-by: brookjeynes <me@brookjeynes.dev>

+1602
+12
.gitignore
··· 1 + __pycache__/ 2 + *.pyc 3 + *.pyo 4 + .DS_Store 5 + .vscode/ 6 + *.ankiaddon 7 + user_files/ 8 + meta.json 9 + venv/ 10 + libs/ 11 + *.egg-info/ 12 + .pytest_cache/
+661
LICENSE
··· 1 + GNU AFFERO GENERAL PUBLIC LICENSE 2 + Version 3, 19 November 2007 3 + 4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 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 + <one line to give the program's name and a brief idea of what it does.> 633 + Copyright (C) <year> <name of author> 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 <https://www.gnu.org/licenses/>. 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 + <https://www.gnu.org/licenses/>.
+15
README.md
··· 1 + # Yōten Anki Tracker addon 2 + 3 + **[yoten.app](https://yoten.app)** 4 + **[apps.ankiweb.net](https://apps.ankiweb.net/)** 5 + 6 + Track your Anki study sessions directly into Yōten. 7 + 8 + Read our [devlogs](https://yoten.leaflet.pub/). 9 + Join the [Discord](https://discord.gg/XAUWY3zvD4). 10 + 11 + Find your focus and turn daily practice into visible results. 12 + 13 + ## docs 14 + - [contributing guide - **please read before opening a PR**](./docs/contributing.md) 15 + - [hacking on yōten anki addon](./docs/hacking.md)
+71
__init__.py
··· 1 + from typing import Optional 2 + 3 + from aqt import mw, gui_hooks 4 + from aqt.utils import openLink, showWarning 5 + 6 + from .session_tracker import SessionTracker, StudySession 7 + from .ui_dialog import ShareDialog, SettingsDialog 8 + from .utils import build_yoten_url, get_config_value, get_current_config 9 + 10 + tracker: Optional[SessionTracker] = None 11 + 12 + 13 + def init_addon(): 14 + global tracker 15 + 16 + config = get_current_config(__name__) 17 + 18 + min_cards = get_config_value(config, 'min_cards_threshold') 19 + min_duration = get_config_value(config, 'min_duration_threshold') 20 + tracker = SessionTracker(min_cards=min_cards, min_duration=min_duration) 21 + 22 + gui_hooks.reviewer_did_show_question.append(tracker.on_card_shown) 23 + gui_hooks.state_did_change.append(tracker.on_state_change) 24 + 25 + mw.addonManager.setConfigAction(__name__, on_config_button_clicked) 26 + 27 + print("Yōten: Addon initialized successfully") 28 + 29 + 30 + def show_post_dialog(session: StudySession): 31 + config = get_current_config(__name__) 32 + dialog = ShareDialog(session, config, mw) 33 + 34 + # User clicked "Share to Yōten" 35 + if dialog.exec(): 36 + language_code = dialog.get_language_code() 37 + open_yoten_url(session, language_code) 38 + 39 + 40 + def open_yoten_url(session: StudySession, language_code: str): 41 + config = get_current_config(__name__) 42 + url = build_yoten_url(session, language_code, config) 43 + openLink(url) 44 + print(f"Yōten: Opened URL: {url}") 45 + 46 + 47 + def on_config_button_clicked(): 48 + config = get_current_config(__name__) 49 + dialog = SettingsDialog(config, mw) 50 + 51 + # Config was saved, reload it 52 + if dialog.exec(): 53 + config = get_current_config(__name__) 54 + 55 + if tracker: 56 + tracker.update_thresholds( 57 + min_cards=get_config_value(config, 'min_cards_threshold'), 58 + min_duration=get_config_value(config, 'min_duration_threshold') 59 + ) 60 + 61 + 62 + try: 63 + init_addon() 64 + except Exception as e: 65 + showWarning( 66 + f"Yōten Addon failed to initialize:\n\n{str(e)}\n\n" 67 + f"Please check your addon installation." 68 + ) 69 + print(f"Yōten initialization error: {e}") 70 + import traceback 71 + traceback.print_exc()
+7
config.json
··· 1 + { 2 + "yoten_url": "https://yoten.app/session/new", 3 + "activity_id": "81", 4 + "default_language": "en", 5 + "min_cards_threshold": 5, 6 + "min_duration_threshold": 30 7 + }
+116
docs/contributing.md
··· 1 + # Contributing guide 2 + 3 + ## Commit guidelines 4 + 5 + We follow a commit style similar to the Go project. Please keep commits: 6 + 7 + - **Atomic**: Each commit should represent one logical change. 8 + - **Descriptive**: The commit message should clearly describe what the change 9 + does and why it's needed. 10 + 11 + ### Message format 12 + 13 + ``` 14 + <type>(affected system): <short summary of change> 15 + 16 + 17 + Optional longer description, if necessary. Explain what the change does and 18 + why. Reference relevant issues or PRs when applicable via links. 19 + ``` 20 + 21 + Examples 22 + 23 + ``` 24 + fix(xp): update xp when a study session is deleted 25 + ``` 26 + 27 + ``` 28 + refactor(views/edit-activity): remove page handler in favor of direct invocation 29 + ``` 30 + 31 + ### General notes 32 + 33 + - PRs get merged "as-is" (fast-forward) - like applying a patch-series using 34 + `git am`. There is no squashing so please author your commits as they would 35 + appear on `master`, following the above guidelines. 36 + - Keep commits lowercased with no trailing period. 37 + - Use the imperative mood in the summary line (e.g., "fix bug" not "fixed bug" 38 + or "fixes bug"). 39 + - Try to keep the summary line under 72 characters. 40 + - Follow the same formatting for PR titles if filled manually. 41 + - Don't include unrelated changes in the same commit. 42 + - Avoid noisy commit messages like "wip" or "final fix". Rewrite history before 43 + submitting if necessary. 44 + 45 + ## Proposals for bigger changes 46 + 47 + Small fixes like typos, minor bugs, or trivial refactors can be submitted 48 + directly as PRs. 49 + 50 + For larger changes—especially those introducing new features, significant 51 + refactoring, or altering system behavior—please open a proposal first. This 52 + helps us evaluate the scope, design, and potential impact before 53 + implementation. 54 + 55 + ### Proposal format 56 + 57 + Create a new issue titled: 58 + 59 + ``` 60 + proposal: <affected scope>: <summary of change> 61 + ``` 62 + 63 + In the description, explain: 64 + 65 + - What the change is 66 + - Why it's needed 67 + - How you plan to implement it (roughly) 68 + - Any open questions or tradeoffs 69 + 70 + We'll use the issue thread to discuss and refine the idea before moving 71 + forward. 72 + 73 + ## Developer certificate of origin (DCO) 74 + 75 + We require all contributors to certify that they have the right to submit the 76 + code they're contributing. To do this, we follow the [Developer Certificate of 77 + Origin (DCO)](https://developercertificate.org/). 78 + 79 + By signing your commits, you're stating that the contribution is your own work, 80 + or that you have the right to submit it under the project's license. This helps 81 + us keep things clean and legally sound. 82 + 83 + To sign your commit, just add the `-s` flag when committing: 84 + 85 + ```sh 86 + git commit -s -m "your commit message" 87 + ``` 88 + 89 + This appends a line like: 90 + 91 + ``` 92 + Signed-off-by: Your Name <your.email@example.com> 93 + ``` 94 + 95 + We won't merge commits if they aren't signed off. If you forget, you can amend 96 + the last commit like this: 97 + 98 + ```sh 99 + git commit --amend -s 100 + ``` 101 + 102 + If you're submitting a PR with multiple commits, make sure each one is signed. 103 + 104 + ## Commit Authoring 105 + 106 + To ensure a consistent and clean project history, we use a `.mailmap` file to 107 + consolidate contributions from different email addresses. 108 + 109 + If you have committed to this project with multiple emails in the past, or plan 110 + to in the future, please add an entry to the `.mailmap` file in the root of the 111 + repository. This will ensure all your work is correctly attributed to a single 112 + identity. 113 + 114 + The format is: `Preferred Name <preferred@email.com> <old@email.com>` 115 + 116 + Please include this change in your first pull request.
+14
docs/hacking.md
··· 1 + # Hacking guide 2 + 3 + ## Required tools 4 + - Python 3.9+ 5 + - Anki 2.1.50+ 6 + 7 + ## Developing 8 + 9 + When developing locally, symlink this project to your Anki addons folder. After 10 + making local changes, Anki must be restarted. 11 + 12 + ```bash 13 + ln -s $(pwd) $HOME/.local/share/Anki2/addons21/yoten-anki-tracker 14 + ```
+169
language_codes.py
··· 1 + # An ISO 639-1 two-letter language code (e.g., 'en', 'es', 'ko') 2 + # Pulled from https://tangled.org/yoten.app/yoten/blob/master/internal/db/language.go 3 + LANGUAGE_CODES = { 4 + "aa": "Afar", 5 + "ab": "Abkhazian", 6 + "af": "Afrikaans", 7 + "am": "Amharic", 8 + "ar": "Arabic", 9 + "as": "Assamese", 10 + "ay": "Aymara", 11 + "az": "Azerbaijani", 12 + "ba": "Bashkir", 13 + "be": "Byelorussian", 14 + "bg": "Bulgarian", 15 + "bh": "Bihari", 16 + "bi": "Bislama", 17 + "bn": "Bengali", 18 + "bo": "Tibetan", 19 + "br": "Breton", 20 + "ca": "Catalan", 21 + "co": "Corsican", 22 + "cs": "Czech", 23 + "cy": "Welsh", 24 + "da": "Danish", 25 + "de": "German", 26 + "dz": "Bhutani", 27 + "el": "Greek", 28 + "en": "English", 29 + "eo": "Esperanto", 30 + "es": "Spanish", 31 + "et": "Estonian", 32 + "eu": "Basque", 33 + "fa": "Persian", 34 + "fi": "Finnish", 35 + "fj": "Fiji", 36 + "fo": "Faroese", 37 + "fr": "French", 38 + "fy": "Frisian", 39 + "ga": "Irish", 40 + "gd": "Gaelic (Scottish)", 41 + "gl": "Galician", 42 + "gn": "Guarani", 43 + "gu": "Gujarati", 44 + "ha": "Hausa", 45 + "he": "Hebrew", 46 + "hi": "Hindi", 47 + "hr": "Croatian", 48 + "hu": "Hungarian", 49 + "hy": "Armenian", 50 + "ia": "Interlingua", 51 + "id": "Indonesian", 52 + "ie": "Interlingue", 53 + "ik": "Inupiak", 54 + "is": "Icelandic", 55 + "it": "Italian", 56 + "iu": "Inuktitut", 57 + "ja": "Japanese", 58 + "jw": "Javanese", 59 + "ka": "Georgian", 60 + "kk": "Kazakh", 61 + "kl": "Greenlandic", 62 + "km": "Cambodian", 63 + "kn": "Kannada", 64 + "ko": "Korean", 65 + "ks": "Kashmiri", 66 + "ku": "Kurdish", 67 + "ky": "Kirghiz", 68 + "la": "Latin", 69 + "ln": "Lingala", 70 + "lo": "Laothian", 71 + "lt": "Lithuanian", 72 + "lv": "Latvian", 73 + "mg": "Malagasy", 74 + "mi": "Maori", 75 + "mk": "Macedonian", 76 + "ml": "Malayalam", 77 + "mn": "Mongolian", 78 + "mo": "Moldavian", 79 + "mr": "Marathi", 80 + "ms": "Malay", 81 + "mt": "Maltese", 82 + "my": "Burmese", 83 + "na": "Nauru", 84 + "ne": "Nepali", 85 + "nl": "Dutch", 86 + "no": "Norwegian", 87 + "oc": "Occitan", 88 + "om": "Oromo (Afan)", 89 + "or": "Oriya", 90 + "pa": "Punjabi", 91 + "pl": "Polish", 92 + "ps": "Pashto (Pushto)", 93 + "pt": "Portuguese", 94 + "qu": "Quechua", 95 + "rm": "Rhaeto-Romance", 96 + "rn": "Kirundi", 97 + "ro": "Romanian", 98 + "ru": "Russian", 99 + "rw": "Kinyarwanda", 100 + "sa": "Sanskrit", 101 + "sd": "Sindhi", 102 + "sg": "Sangro", 103 + "sh": "Serbo-Croatian", 104 + "si": "Singhalese", 105 + "sk": "Slovak", 106 + "sl": "Slovenian", 107 + "sm": "Samoan", 108 + "sn": "Shona", 109 + "so": "Somali", 110 + "sq": "Albanian", 111 + "sr": "Serbian", 112 + "ss": "Siswati", 113 + "st": "Sesotho", 114 + "su": "Sundanese", 115 + "sv": "Swedish", 116 + "sw": "Swahili", 117 + "ta": "Tamil", 118 + "te": "Tegulu", 119 + "tg": "Tajik", 120 + "th": "Thai", 121 + "ti": "Tigrinya", 122 + "tk": "Turkmen", 123 + "tl": "Tagalog", 124 + "tn": "Setswana", 125 + "to": "Tonga", 126 + "tr": "Turkish", 127 + "ts": "Tsonga", 128 + "tt": "Tatar", 129 + "tw": "Twi", 130 + "ug": "Uigur", 131 + "uk": "Ukrainian", 132 + "ur": "Urdu", 133 + "uz": "Uzbek", 134 + "vi": "Vietnamese", 135 + "vo": "Volapuk", 136 + "wo": "Wolof", 137 + "xh": "Xhosa", 138 + "yi": "Yiddish", 139 + "yo": "Yoruba", 140 + "za": "Zhuang", 141 + "zh": "Chinese", 142 + "zu": "Zulu", 143 + } 144 + 145 + 146 + def get_common_languages(): 147 + common_codes = [ 148 + "en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh", 149 + "ar", "hi", "nl", "pl", "tr", "vi", "th", "sv", "he", "id" 150 + ] 151 + 152 + common = [(code, LANGUAGE_CODES[code]) 153 + for code in common_codes if code in LANGUAGE_CODES] 154 + 155 + remaining_codes = set(LANGUAGE_CODES.keys()) - set(common_codes) 156 + remaining = sorted( 157 + [(code, LANGUAGE_CODES[code]) for code in remaining_codes], 158 + key=lambda x: x[1] # Sort by language name 159 + ) 160 + 161 + return common + [("---", "─── Other Languages ───")] + remaining 162 + 163 + 164 + def get_language_name(code: str) -> str: 165 + return LANGUAGE_CODES.get(code, code.upper()) 166 + 167 + 168 + def is_valid_language_code(code: str) -> bool: 169 + return code in LANGUAGE_CODES
+10
manifest.json
··· 1 + { 2 + "package": "yoten-anki-tracker", 3 + "name": "Yōten Anki Tracker", 4 + "author": "Yōten", 5 + "version": "1.0.0", 6 + "homepage": "https://yoten.app", 7 + "conflicts": [], 8 + "min_point_version": 50, 9 + "max_point_version": 0 10 + }
+32
scripts/package.sh
··· 1 + #!/bin/bash 2 + 3 + set -e 4 + 5 + ADDON_NAME="yoten-anki-tracker" 6 + OUTPUT_FILE="${ADDON_NAME}.ankiaddon" 7 + 8 + echo "Packaging ${ADDON_NAME}..." 9 + 10 + if [ -f "$OUTPUT_FILE" ]; then 11 + echo "Removing old package..." 12 + rm "$OUTPUT_FILE" 13 + fi 14 + 15 + echo "Creating package..." 16 + zip -r "$OUTPUT_FILE" \ 17 + __init__.py \ 18 + session_tracker.py \ 19 + ui_dialog.py \ 20 + language_codes.py \ 21 + utils.py \ 22 + config.json \ 23 + manifest.json \ 24 + -x "*.pyc" "*.pyo" "*__pycache__*" "*.DS_Store" 25 + 26 + echo "Done! Package created: $OUTPUT_FILE" 27 + echo "Size: $(du -sh $OUTPUT_FILE | cut -f1)" 28 + echo "" 29 + echo "To install:" 30 + echo "1. Open Anki" 31 + echo "2. Go to Tools → Add-ons → Install from file" 32 + echo "3. Select $OUTPUT_FILE"
+120
session_tracker.py
··· 1 + import time 2 + from dataclasses import dataclass 3 + from datetime import datetime 4 + from typing import Optional 5 + 6 + 7 + @dataclass 8 + class StudySession: 9 + deck_name: str 10 + cards_studied: int 11 + duration_seconds: int 12 + session_end: datetime 13 + 14 + @property 15 + def description(self) -> str: 16 + return f"Anki: {self.deck_name} - {self.cards_studied} cards" 17 + 18 + @property 19 + def formatted_duration(self) -> str: 20 + mins, secs = divmod(self.duration_seconds, 60) 21 + if mins > 0: 22 + return f"{mins}m {secs}s" 23 + return f"{secs}s" 24 + 25 + @property 26 + def duration_parts(self) -> tuple[int, int, int]: 27 + hours = self.duration_seconds // 3600 28 + minutes = (self.duration_seconds % 3600) // 60 29 + seconds = self.duration_seconds % 60 30 + return (hours, minutes, seconds) 31 + 32 + 33 + class SessionTracker: 34 + def __init__(self, min_cards: int = 5, min_duration: int = 30): 35 + self.min_cards = min_cards 36 + self.min_duration = min_duration 37 + self.reset() 38 + 39 + def reset(self): 40 + self.session_active = False 41 + self.session_start: Optional[float] = None 42 + self.cards_count = 0 43 + self.deck_name: Optional[str] = None 44 + self.deck_id: Optional[int] = None 45 + 46 + def _start_new_session(self, deck_id: int, deck_name: str) -> None: 47 + self.session_active = True 48 + self.session_start = time.time() 49 + self.cards_count = 1 50 + self.deck_name = deck_name 51 + self.deck_id = deck_id 52 + 53 + def on_card_shown(self, card): 54 + from aqt import mw 55 + 56 + current_deck = mw.col.decks.current() 57 + current_deck_id = current_deck['id'] 58 + current_deck_name = current_deck['name'] 59 + 60 + if not self.session_active: 61 + # Start new session 62 + self._start_new_session(current_deck_id, current_deck_name) 63 + elif self.deck_id != current_deck_id: 64 + # Deck changed - end current session and start new one 65 + if self.cards_count > 0: 66 + self.end_session() 67 + self._start_new_session(current_deck_id, current_deck_name) 68 + else: 69 + # Same deck, increment counter 70 + self.cards_count += 1 71 + 72 + def on_state_change(self, new_state: str, old_state: str): 73 + if old_state == "review" and self.session_active: 74 + if self.cards_count > 0: 75 + self.end_session() 76 + 77 + def end_session(self): 78 + if not self.session_active or self.session_start is None: 79 + return 80 + 81 + duration = int(time.time() - self.session_start) 82 + 83 + # Too few cards - don't post 84 + if self.cards_count < self.min_cards: 85 + self.reset() 86 + return 87 + 88 + # Too short - don't post 89 + if duration < self.min_duration: 90 + self.reset() 91 + return 92 + 93 + session = StudySession( 94 + deck_name=self.deck_name, 95 + cards_studied=self.cards_count, 96 + duration_seconds=duration, 97 + session_end=datetime.utcnow() 98 + ) 99 + 100 + self.reset() 101 + 102 + # Imported here to avoid circular import 103 + from aqt import mw 104 + 105 + # Schedule on main thread to ensure UI updates properly 106 + mw.taskman.run_on_main( 107 + lambda: self._show_dialog(session) 108 + ) 109 + 110 + def _show_dialog(self, session: StudySession): 111 + # Imported here to avoid circular import 112 + from . import show_post_dialog 113 + show_post_dialog(session) 114 + 115 + def update_thresholds(self, min_cards: Optional[int] = None, 116 + min_duration: Optional[int] = None): 117 + if min_cards is not None: 118 + self.min_cards = min_cards 119 + if min_duration is not None: 120 + self.min_duration = min_duration
+307
ui_dialog.py
··· 1 + from PyQt6.QtWidgets import ( 2 + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, 3 + QComboBox, QTextEdit, QLineEdit, QMessageBox, QSpinBox 4 + ) 5 + from PyQt6.QtCore import Qt 6 + 7 + from .session_tracker import StudySession 8 + from .utils import ( 9 + build_yoten_url, 10 + populate_language_combo, 11 + get_default_config, 12 + get_config_value, 13 + get_current_config 14 + ) 15 + 16 + 17 + class ShareDialog(QDialog): 18 + def __init__(self, session: StudySession, config: dict, parent=None): 19 + super().__init__(parent) 20 + self.session = session 21 + self.config = config 22 + self.setup_ui() 23 + 24 + def setup_ui(self): 25 + self.setWindowTitle("Share to Yōten") 26 + self.setModal(True) 27 + 28 + layout = QVBoxLayout() 29 + 30 + header_label = QLabel("<h2>Study Session Complete!</h2>") 31 + header_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 32 + layout.addWidget(header_label) 33 + 34 + stats_text = ( 35 + f"<b>Deck:</b> {self.session.deck_name}<br>" 36 + f"<b>Cards Studied:</b> {self.session.cards_studied}<br>" 37 + f"<b>Duration:</b> {self.session.formatted_duration}" 38 + ) 39 + stats_label = QLabel(stats_text) 40 + stats_label.setStyleSheet( 41 + "padding: 10px; background-color: #f0f0f0; border-radius: 5px;") 42 + layout.addWidget(stats_label) 43 + 44 + lang_layout = QHBoxLayout() 45 + lang_layout.addWidget(QLabel("<b>Language:</b>")) 46 + 47 + self.lang_combo = QComboBox() 48 + self.lang_combo.setMaxVisibleItems(12) 49 + default_lang = get_config_value(self.config, 'default_language') 50 + populate_language_combo(self.lang_combo, default_lang) 51 + self.lang_combo.currentIndexChanged.connect(self.update_preview) 52 + lang_layout.addWidget(self.lang_combo) 53 + 54 + layout.addLayout(lang_layout) 55 + 56 + preview_label = QLabel("<b>URL Preview:</b>") 57 + layout.addWidget(preview_label) 58 + 59 + self.preview_text = QTextEdit() 60 + self.preview_text.setReadOnly(True) 61 + self.preview_text.setMaximumHeight(100) 62 + self.preview_text.setStyleSheet( 63 + "font-family: monospace; font-size: 10px;") 64 + self.update_preview() 65 + layout.addWidget(self.preview_text) 66 + 67 + info_label = QLabel( 68 + "<i>Clicking 'Share to Yōten' will open your browser where you can review and post your session.</i>" 69 + ) 70 + info_label.setWordWrap(True) 71 + info_label.setStyleSheet( 72 + "color: #666; font-size: 11px; margin-top: 10px;") 73 + layout.addWidget(info_label) 74 + 75 + button_layout = QHBoxLayout() 76 + 77 + share_btn = QPushButton("Share to Yōten") 78 + share_btn.setStyleSheet( 79 + "background-color: #4CAF50; color: white; padding: 8px; font-weight: bold;") 80 + share_btn.clicked.connect(self.accept) 81 + share_btn.setDefault(True) 82 + 83 + skip_btn = QPushButton("Skip") 84 + skip_btn.clicked.connect(self.reject) 85 + 86 + settings_btn = QPushButton("⚙ Settings") 87 + settings_btn.clicked.connect(self.open_settings) 88 + 89 + button_layout.addWidget(share_btn) 90 + button_layout.addWidget(skip_btn) 91 + button_layout.addWidget(settings_btn) 92 + 93 + layout.addLayout(button_layout) 94 + 95 + self.setLayout(layout) 96 + self.resize(550, 400) 97 + 98 + def update_preview(self): 99 + lang_code = self.lang_combo.currentData() 100 + url = build_yoten_url(self.session, lang_code, self.config) 101 + self.preview_text.setPlainText(url) 102 + 103 + def get_language_code(self) -> str: 104 + return self.lang_combo.currentData() 105 + 106 + def open_settings(self): 107 + dialog = SettingsDialog(self.config, self) 108 + if dialog.exec(): 109 + # Config was saved, refresh our copy 110 + self.config = get_current_config(__name__) 111 + self.update_preview() 112 + 113 + 114 + class SettingsDialog(QDialog): 115 + def __init__(self, config: dict, parent=None): 116 + super().__init__(parent) 117 + self.config = config.copy() # Work with a copy 118 + self.setup_ui() 119 + 120 + def setup_ui(self): 121 + self.setWindowTitle("Yōten Addon Settings") 122 + self.setModal(True) 123 + 124 + layout = QVBoxLayout() 125 + 126 + header = QLabel("<h2>Yōten Settings</h2>") 127 + header.setAlignment(Qt.AlignmentFlag.AlignCenter) 128 + layout.addWidget(header) 129 + 130 + instructions = QLabel( 131 + "Configure how your study sessions are shared with Yōten." 132 + ) 133 + instructions.setWordWrap(True) 134 + instructions.setStyleSheet("color: #666; margin-bottom: 10px;") 135 + layout.addWidget(instructions) 136 + 137 + url_layout = QHBoxLayout() 138 + url_layout.addWidget(QLabel("Yōten URL:")) 139 + self.url_input = QLineEdit(get_config_value(self.config, 'yoten_url')) 140 + self.url_input.setPlaceholderText("https://yoten.app/session/new") 141 + url_layout.addWidget(self.url_input) 142 + layout.addLayout(url_layout) 143 + 144 + url_help = QLabel( 145 + "<small>The URL where session data will be sent</small>") 146 + url_help.setStyleSheet( 147 + "color: #666; margin-left: 20px; margin-bottom: 10px;") 148 + layout.addWidget(url_help) 149 + 150 + activity_layout = QHBoxLayout() 151 + activity_layout.addWidget(QLabel("Activity ID:")) 152 + self.activity_input = QLineEdit( 153 + get_config_value(self.config, 'activity_id')) 154 + self.activity_input.setPlaceholderText("81") 155 + activity_layout.addWidget(self.activity_input) 156 + layout.addLayout(activity_layout) 157 + 158 + activity_help = QLabel( 159 + "<small>The type of study activity (e.g., 'Flashcard Revision')</small>") 160 + activity_help.setStyleSheet( 161 + "color: #666; margin-left: 20px; margin-bottom: 10px;") 162 + layout.addWidget(activity_help) 163 + 164 + separator = QLabel() 165 + separator.setStyleSheet( 166 + "border-bottom: 1px solid #ccc; margin: 15px 0;") 167 + layout.addWidget(separator) 168 + 169 + default_lang_layout = QHBoxLayout() 170 + default_lang_layout.addWidget(QLabel("Default Language:")) 171 + self.default_lang_combo = QComboBox() 172 + self.default_lang_combo.setMaxVisibleItems(15) 173 + current_default = get_config_value(self.config, 'default_language') 174 + populate_language_combo(self.default_lang_combo, current_default) 175 + default_lang_layout.addWidget(self.default_lang_combo) 176 + layout.addLayout(default_lang_layout) 177 + 178 + threshold_header = QLabel("<b>Session Thresholds:</b>") 179 + threshold_header.setStyleSheet("margin-top: 10px; margin-bottom: 5px;") 180 + layout.addWidget(threshold_header) 181 + 182 + min_cards_layout = QHBoxLayout() 183 + min_cards_layout.addWidget(QLabel("Minimum Cards:")) 184 + self.min_cards_spin = QSpinBox() 185 + self.min_cards_spin.setMinimum(1) 186 + self.min_cards_spin.setMaximum(999) # Reasonable max, but flexible 187 + self.min_cards_spin.setValue(get_config_value( 188 + self.config, 'min_cards_threshold')) 189 + self.min_cards_spin.setToolTip( 190 + "Minimum number of cards to trigger the share dialog") 191 + min_cards_layout.addWidget(self.min_cards_spin) 192 + min_cards_layout.addStretch() 193 + layout.addLayout(min_cards_layout) 194 + 195 + min_cards_help = QLabel( 196 + "<small>Only show dialog after studying this many cards</small>") 197 + min_cards_help.setStyleSheet( 198 + "color: #666; margin-left: 20px; margin-bottom: 5px;") 199 + layout.addWidget(min_cards_help) 200 + 201 + min_duration_layout = QHBoxLayout() 202 + min_duration_layout.addWidget(QLabel("Minimum Duration:")) 203 + self.min_duration_spin = QSpinBox() 204 + self.min_duration_spin.setMinimum(0) 205 + self.min_duration_spin.setMaximum(999999) # No practical limit 206 + self.min_duration_spin.setValue(get_config_value( 207 + self.config, 'min_duration_threshold')) 208 + self.min_duration_spin.setSuffix(" seconds") 209 + self.min_duration_spin.setToolTip( 210 + "Minimum session duration in seconds to trigger the share dialog") 211 + min_duration_layout.addWidget(self.min_duration_spin) 212 + min_duration_layout.addStretch() 213 + layout.addLayout(min_duration_layout) 214 + 215 + min_duration_help = QLabel( 216 + "<small>Only show dialog after studying for this long</small>") 217 + min_duration_help.setStyleSheet( 218 + "color: #666; margin-left: 20px; margin-bottom: 10px;") 219 + layout.addWidget(min_duration_help) 220 + 221 + button_layout = QHBoxLayout() 222 + 223 + reset_btn = QPushButton("Reset to Defaults") 224 + reset_btn.clicked.connect(self.reset_to_defaults) 225 + 226 + save_btn = QPushButton("Save") 227 + save_btn.setStyleSheet( 228 + "background-color: #4CAF50; color: white; padding: 8px;") 229 + save_btn.clicked.connect(self.save_config) 230 + save_btn.setDefault(True) 231 + 232 + cancel_btn = QPushButton("Cancel") 233 + cancel_btn.clicked.connect(self.reject) 234 + 235 + button_layout.addWidget(reset_btn) 236 + button_layout.addStretch() 237 + button_layout.addWidget(save_btn) 238 + button_layout.addWidget(cancel_btn) 239 + 240 + layout.addLayout(button_layout) 241 + 242 + self.setLayout(layout) 243 + self.resize(500, 400) 244 + 245 + def _validate_field(self, condition: bool, message: str) -> bool: 246 + if not condition: 247 + QMessageBox.warning(self, "Invalid Configuration", message) 248 + return False 249 + return True 250 + 251 + def save_config(self): 252 + from aqt import mw 253 + 254 + self.config['yoten_url'] = self.url_input.text().strip() 255 + self.config['activity_id'] = self.activity_input.text().strip() 256 + self.config['default_language'] = self.default_lang_combo.currentData() 257 + self.config['min_cards_threshold'] = self.min_cards_spin.value() 258 + self.config['min_duration_threshold'] = self.min_duration_spin.value() 259 + 260 + validations = [ 261 + (bool(self.config['yoten_url']), 262 + "Please enter a valid Yōten URL."), 263 + (bool(self.config['activity_id']), "Please enter an activity ID."), 264 + (self.config['min_cards_threshold'] >= 1, 265 + "Minimum cards threshold must be at least 1."), 266 + (self.config['min_duration_threshold'] >= 0, 267 + "Minimum duration threshold cannot be negative."), 268 + ] 269 + 270 + for condition, message in validations: 271 + if not self._validate_field(condition, message): 272 + return 273 + 274 + mw.addonManager.writeConfig(__name__, self.config) 275 + QMessageBox.information( 276 + self, 277 + "Settings Saved", 278 + "Your settings have been saved successfully!" 279 + ) 280 + self.accept() 281 + 282 + def reset_to_defaults(self): 283 + result = QMessageBox.question( 284 + self, 285 + "Reset to Defaults", 286 + "Are you sure you want to reset all settings to their default values?", 287 + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No 288 + ) 289 + 290 + if result == QMessageBox.StandardButton.Yes: 291 + defaults = get_default_config() 292 + 293 + self.url_input.setText(defaults['yoten_url']) 294 + self.activity_input.setText(defaults['activity_id']) 295 + self.min_cards_spin.setValue(defaults['min_cards_threshold']) 296 + self.min_duration_spin.setValue(defaults['min_duration_threshold']) 297 + 298 + index = self.default_lang_combo.findData( 299 + defaults['default_language']) 300 + if index >= 0: 301 + self.default_lang_combo.setCurrentIndex(index) 302 + 303 + QMessageBox.information( 304 + self, 305 + "Defaults Restored", 306 + "All settings have been reset to default values.\nClick 'Save' to apply the changes." 307 + )
+68
utils.py
··· 1 + import urllib.parse 2 + from typing import TYPE_CHECKING, Any 3 + 4 + if TYPE_CHECKING: 5 + from PyQt6.QtWidgets import QComboBox 6 + from .session_tracker import StudySession 7 + 8 + 9 + def get_default_config() -> dict: 10 + return { 11 + 'yoten_url': 'https://yoten.app/session/new', 12 + 'activity_id': '81', 13 + 'default_language': 'en', 14 + 'min_cards_threshold': 5, 15 + 'min_duration_threshold': 30 16 + } 17 + 18 + 19 + def get_config_value(config: dict, key: str) -> Any: 20 + defaults = get_default_config() 21 + return config.get(key, defaults.get(key)) 22 + 23 + 24 + def get_current_config(module_name: str) -> dict: 25 + from aqt import mw 26 + return mw.addonManager.getConfig(module_name) 27 + 28 + 29 + def build_yoten_url(session: 'StudySession', language_code: str, config: dict) -> str: 30 + base_url = get_config_value(config, 'yoten_url') 31 + activity_id = get_config_value(config, 'activity_id') 32 + 33 + hours, minutes, seconds = session.duration_parts 34 + 35 + params = { 36 + 'language_code': language_code, 37 + 'duration_hours': str(hours), 38 + 'duration_minutes': str(minutes), 39 + 'duration_seconds': str(seconds), 40 + 'description': session.description, 41 + 'activity_id': activity_id 42 + } 43 + 44 + query_string = urllib.parse.urlencode(params) 45 + return f"{base_url}?{query_string}" 46 + 47 + 48 + def populate_language_combo(combo_box: 'QComboBox', default_code: str = 'en') -> None: 49 + from .language_codes import get_common_languages 50 + 51 + common_languages = get_common_languages() 52 + 53 + for idx, (code, name) in enumerate(common_languages): 54 + # Handle separator item 55 + if code == "---": 56 + combo_box.addItem(name, code) 57 + # Make separator item disabled (not selectable) 58 + model = combo_box.model() 59 + item = model.item(idx) 60 + if item: 61 + item.setEnabled(False) 62 + else: 63 + combo_box.addItem(f"{name} ({code})", code) 64 + 65 + # Set default selection 66 + index = combo_box.findData(default_code) 67 + if index >= 0: 68 + combo_box.setCurrentIndex(index)