diff --git a/Freewood-OctoWoW-Mail.toc b/Freewood-OctoWoW-Mail.toc new file mode 100644 index 0000000..6f36fb5 --- /dev/null +++ b/Freewood-OctoWoW-Mail.toc @@ -0,0 +1,12 @@ +## Interface: 11200 +## Title: Freewood |cffc89b3cMail|r +## Notes: Own mailbox window: open all or selected mails safely, recent recipients, mailing lists with automatic sending, multi-item mails, gold surplus and cash on delivery. +## Notes-deDE: Eigenes Briefkasten-Fenster: alles oder Ausgewähltes sicher öffnen, letzte Empfänger, Mailinglisten mit Automatik, mehrere Items, Gold-Überschuss und Nachnahme. +## Author: Lacustat +## Version: 0.1.152 +## Dependencies: Freewood-OctoWoW-Core +## OptionalDeps: Freewood-OctoWoW-Creator, Freewood-OctoWoW-Brett + +Locales.lua +Mail.lua +MailUI.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Locales.lua b/Locales.lua new file mode 100644 index 0000000..a9aaa3c --- /dev/null +++ b/Locales.lua @@ -0,0 +1,205 @@ +-- Freewood-OctoWoW-Mail: Texte +local ADDON, NS = ... + +local FW = _G.Freewood +if not FW then + return +end + +FW:AddLocale("enUS", { + MAIL_TITLE = "Mailbox", + MAIL_DESC = "Own mailbox window: open all or selected mails safely, recent recipients, mailing lists, several items per mail, gold surplus and cash on delivery.", + MAIL_WINDOW_TITLE = "Freewood Mailbox", + MAIL_TAB_INBOX = "Inbox", + MAIL_TAB_SEND = "Send", + MAIL_TAB_LISTS = "Mailing lists", + MAIL_CLOSE = "Close mailbox", + + MAIL_INBOX_COUNT = "%d mails in the inbox", + MAIL_OPEN_ALL = "Open all", + MAIL_OPEN_SELECTED = "Open selected only (%d)", + MAIL_STOP = "Stop", + MAIL_SELECT_ALL = "Select all", + MAIL_SELECT_NONE = "Select none", + MAIL_INBOX_EMPTY = "The inbox is empty.", + MAIL_ROW_GOLD = "Gold: %s", + MAIL_ROW_COD = "Cash on delivery: %s (not taken)", + MAIL_ROW_LETTER = "Letter only (not touched)", + MAIL_ROW_DAYS = "%d d", + MAIL_ROW_FROM = "From %s", + MAIL_NOTE_SAFE = "Mails are never deleted or returned. Cash-on-delivery mails and plain letters are skipped. One bag slot always stays free.", + MAIL_RUN_DONE = "Mailbox: %d gold mails and %d items taken%s.", + MAIL_RUN_SKIP_FULL = ", %d items left because the bags are nearly full", + MAIL_RUN_STOPPED = "Mailbox: stopped. %d gold mails and %d items taken.", + MAIL_RUN_PROGRESS = "Taking mail... %d items, %d gold mails", + MAIL_RUN_FULL = "Bags nearly full: items stay in the mailbox (one slot stays free).", + + MAIL_TO = "Recipient", + MAIL_TO_RECENT = "Last recipients", + MAIL_TO_NONE = "No recipients yet", + MAIL_SUBJECT = "Subject", + MAIL_BODY = "Text", + MAIL_ITEMS = "Items", + MAIL_ITEMS_ADD = "Add items...", + MAIL_ITEMS_NONE = "No items. Without items only gold (if any) is sent.", + MAIL_ITEMS_REMOVE = "Remove", + MAIL_GOLD = "Gold", + MAIL_SILVER = "Silver", + MAIL_COPPER = "Copper", + MAIL_COD = "Cash on delivery (the recipient pays this amount for every mail)", + MAIL_SEND = "Send", + MAIL_SEND_COUNT = "%d mails will be sent one after another.", + MAIL_SEND_COUNT1 = "1 mail will be sent.", + MAIL_SEND_NOTHING = "Nothing to send: add items or gold.", + MAIL_SEND_NO_TO = "Enter a recipient.", + MAIL_SEND_SELF = "You cannot mail yourself.", + MAIL_SEND_COD_NEEDS_ITEM = "Cash on delivery needs at least one item.", + MAIL_BOARD_BUSY = "A board mission is preparing a mail. Finish it in the mission window first; this window waits.", + MAIL_BUSY = "Another action is running. Please wait.", + + MAIL_PICK_TITLE = "Choose items", + MAIL_PICK_LIST_TITLE = "Items for %s", + MAIL_PICK_DONE = "Done", + MAIL_PICK_BAGS = "In your bags", + MAIL_PICK_EXTRA = "Not in your bags", + MAIL_PICK_EMPTY = "Nothing to choose in your bags.", + MAIL_PICK_HINT_LIST = "All stacks of a chosen item are sent. Click again to remove.", + MAIL_PICK_HINT_SEND = "Every chosen stack is one mail. Click again to remove.", + + MAIL_LISTS_HEADER = "Mailing lists", + MAIL_LISTS_HINT = "Create a name, choose its items, and tick the box to send automatically whenever the mailbox opens.", + MAIL_LISTS_ADD = "Add", + MAIL_LISTS_NAME_BOX = "Character name", + MAIL_LISTS_EMPTY = "No mailing list yet.", + MAIL_LISTS_ITEMS = "Items (%d)", + MAIL_LISTS_AUTO = "Send automatically when the mailbox opens", + MAIL_LISTS_GOLD = "Send gold above", + MAIL_LISTS_GOLD_G = "gold (keep this much)", + MAIL_LISTS_NOW = "Send now", + MAIL_LISTS_REMOVE = "Remove list", + MAIL_LISTS_SELF = "This is you; it is skipped on this character.", + MAIL_LISTS_RESERVED = "%d items reserved for board missions", + MAIL_LISTS_ALL_NOW = "Send all lists now", + + MAIL_SUBJECT_STAMP = "%s", + MAIL_SUBJECT_N = "%s - %d of %d", + MAIL_SUBJECT_ITEM_N = "%s (%d of %d)", + MAIL_SUBJECT_DEFAULT = "Freewood", + + MAIL_SEND_PROGRESS = "Sending mail %d of %d to %s...", + MAIL_SEND_DONE = "Mail: %d of %d mails sent%s.", + MAIL_SEND_DONE_FAIL = ", %d skipped", + MAIL_SEND_STOPPED = "Mail: sending stopped after %d of %d mails.", + MAIL_SEND_NOTHING_LIST = "Mail: nothing to send for %s.", + MAIL_SEND_FAIL_ATTACH = "Mail: %s could not be attached (soulbound or locked?). Skipped.", + MAIL_SEND_FAIL_TIMEOUT = "Mail: no answer from the server. Sending stopped.", + MAIL_SEND_FAIL_ERROR = "Mail: the server refused a mail. Sending stopped.", + MAIL_AUTO_WAIT_BOARD = "Mail: automatic sending waits for the board mission mail.", + + MAIL_SET_HEADER = "Mailbox", + MAIL_SET_REPLACE = "Replace the normal mailbox window with the Freewood mailbox", + MAIL_SET_CHAT = "Report results in chat", + MAIL_SET_NOTE = "Off by default. With the replacement on, the normal window stays open but invisible, so board missions keep working. /fwmail off switches back at any time.", + MAIL_CMD_ON = "Freewood mailbox window: on", + MAIL_CMD_OFF = "Freewood mailbox window: off", + MAIL_CMD_CLOSED = "Open a mailbox first.", +}) + +FW:AddLocale("deDE", { + MAIL_TITLE = "Briefkasten", + MAIL_DESC = "Eigenes Briefkasten-Fenster: alles oder Ausgewähltes sicher öffnen, letzte Empfänger, Mailinglisten, mehrere Items pro Versand, Gold-Überschuss und Nachnahme.", + MAIL_WINDOW_TITLE = "Freewood Briefkasten", + MAIL_TAB_INBOX = "Posteingang", + MAIL_TAB_SEND = "Senden", + MAIL_TAB_LISTS = "Mailinglisten", + MAIL_CLOSE = "Briefkasten schließen", + + MAIL_INBOX_COUNT = "%d Briefe im Posteingang", + MAIL_OPEN_ALL = "Alle öffnen", + MAIL_OPEN_SELECTED = "Nur Ausgewählte öffnen (%d)", + MAIL_STOP = "Stopp", + MAIL_SELECT_ALL = "Alle auswählen", + MAIL_SELECT_NONE = "Keine auswählen", + MAIL_INBOX_EMPTY = "Der Posteingang ist leer.", + MAIL_ROW_GOLD = "Gold: %s", + MAIL_ROW_COD = "Nachnahme: %s (wird nicht genommen)", + MAIL_ROW_LETTER = "Nur ein Brief (bleibt unberührt)", + MAIL_ROW_DAYS = "%d T", + MAIL_ROW_FROM = "Von %s", + MAIL_NOTE_SAFE = "Briefe werden nie gelöscht oder zurückgeschickt. Nachnahme-Briefe und reine Briefe werden übersprungen. Ein Taschenplatz bleibt immer frei.", + MAIL_RUN_DONE = "Briefkasten: %d Gold-Briefe und %d Items genommen%s.", + MAIL_RUN_SKIP_FULL = ", %d Items blieben liegen, weil die Taschen fast voll sind", + MAIL_RUN_STOPPED = "Briefkasten: gestoppt. %d Gold-Briefe und %d Items genommen.", + MAIL_RUN_PROGRESS = "Nehme Post... %d Items, %d Gold-Briefe", + MAIL_RUN_FULL = "Taschen fast voll: Items bleiben im Briefkasten (ein Platz bleibt frei).", + + MAIL_TO = "Empfänger", + MAIL_TO_RECENT = "Letzte Empfänger", + MAIL_TO_NONE = "Noch keine Empfänger", + MAIL_SUBJECT = "Betreff", + MAIL_BODY = "Text", + MAIL_ITEMS = "Items", + MAIL_ITEMS_ADD = "Items hinzufügen...", + MAIL_ITEMS_NONE = "Keine Items. Ohne Items wird nur Gold (falls angegeben) gesendet.", + MAIL_ITEMS_REMOVE = "Entfernen", + MAIL_GOLD = "Gold", + MAIL_SILVER = "Silber", + MAIL_COPPER = "Kupfer", + MAIL_COD = "Nachnahme (der Empfänger zahlt diesen Betrag für jeden Brief)", + MAIL_SEND = "Senden", + MAIL_SEND_COUNT = "%d Briefe werden nacheinander gesendet.", + MAIL_SEND_COUNT1 = "1 Brief wird gesendet.", + MAIL_SEND_NOTHING = "Nichts zu senden: Items oder Gold angeben.", + MAIL_SEND_NO_TO = "Empfänger eingeben.", + MAIL_SEND_SELF = "Du kannst dir selbst nichts schicken.", + MAIL_SEND_COD_NEEDS_ITEM = "Nachnahme braucht mindestens ein Item.", + MAIL_BOARD_BUSY = "Ein Brett-Auftrag bereitet gerade einen Brief vor. Erst im Auftragsfenster abschließen; dieses Fenster wartet.", + MAIL_BUSY = "Es läuft gerade eine andere Aktion. Bitte warten.", + + MAIL_PICK_TITLE = "Items auswählen", + MAIL_PICK_LIST_TITLE = "Items für %s", + MAIL_PICK_DONE = "Fertig", + MAIL_PICK_BAGS = "In deinen Taschen", + MAIL_PICK_EXTRA = "Nicht in den Taschen", + MAIL_PICK_EMPTY = "In deinen Taschen gibt es nichts zum Auswählen.", + MAIL_PICK_HINT_LIST = "Alle Stapel eines gewählten Items werden gesendet. Nochmal klicken entfernt es.", + MAIL_PICK_HINT_SEND = "Jeder gewählte Stapel ist ein Brief. Nochmal klicken entfernt ihn.", + + MAIL_LISTS_HEADER = "Mailinglisten", + MAIL_LISTS_HINT = "Namen anlegen, Items festlegen und mit dem Haken automatisch senden, sobald der Briefkasten aufgeht.", + MAIL_LISTS_ADD = "Anlegen", + MAIL_LISTS_NAME_BOX = "Charaktername", + MAIL_LISTS_EMPTY = "Noch keine Mailingliste.", + MAIL_LISTS_ITEMS = "Items (%d)", + MAIL_LISTS_AUTO = "Automatisch senden, sobald der Briefkasten aufgeht", + MAIL_LISTS_GOLD = "Gold senden über", + MAIL_LISTS_GOLD_G = "Gold (so viel behalten)", + MAIL_LISTS_NOW = "Jetzt senden", + MAIL_LISTS_REMOVE = "Liste entfernen", + MAIL_LISTS_SELF = "Das bist du; auf diesem Charakter wird sie übersprungen.", + MAIL_LISTS_RESERVED = "%d Items sind für Brett-Aufträge reserviert", + MAIL_LISTS_ALL_NOW = "Alle Listen jetzt senden", + + MAIL_SUBJECT_STAMP = "%s", + MAIL_SUBJECT_N = "%s - %d von %d", + MAIL_SUBJECT_ITEM_N = "%s (%d von %d)", + MAIL_SUBJECT_DEFAULT = "Freewood", + + MAIL_SEND_PROGRESS = "Sende Brief %d von %d an %s...", + MAIL_SEND_DONE = "Briefkasten: %d von %d Briefen gesendet%s.", + MAIL_SEND_DONE_FAIL = ", %d übersprungen", + MAIL_SEND_STOPPED = "Briefkasten: Senden nach %d von %d Briefen gestoppt.", + MAIL_SEND_NOTHING_LIST = "Briefkasten: Für %s gibt es nichts zu senden.", + MAIL_SEND_FAIL_ATTACH = "Briefkasten: %s ließ sich nicht anhängen (seelengebunden oder gesperrt?). Übersprungen.", + MAIL_SEND_FAIL_TIMEOUT = "Briefkasten: keine Antwort vom Server. Senden gestoppt.", + MAIL_SEND_FAIL_ERROR = "Briefkasten: Der Server hat einen Brief abgelehnt. Senden gestoppt.", + MAIL_AUTO_WAIT_BOARD = "Briefkasten: Das automatische Senden wartet auf den Brief des Brett-Auftrags.", + + MAIL_SET_HEADER = "Briefkasten", + MAIL_SET_REPLACE = "Normales Briefkasten-Fenster durch das Freewood-Briefkasten ersetzen", + MAIL_SET_CHAT = "Ergebnisse im Chat melden", + MAIL_SET_NOTE = "Standardmäßig aus. Mit Ersetzen bleibt das normale Fenster geöffnet, aber unsichtbar, damit Brett-Aufträge weiter funktionieren. /fwmail off schaltet jederzeit zurück.", + MAIL_CMD_ON = "Freewood-Briefkasten-Fenster: an", + MAIL_CMD_OFF = "Freewood-Briefkasten-Fenster: aus", + MAIL_CMD_CLOSED = "Öffne zuerst einen Briefkasten.", +}) diff --git a/Mail.lua b/Mail.lua new file mode 100644 index 0000000..a2f0b52 --- /dev/null +++ b/Mail.lua @@ -0,0 +1,800 @@ +-- Freewood-OctoWoW-Mail: eigenes Briefkasten (Logik). +-- +-- Posteingang "Alle oeffnen" / "Nur Ausgewaehlte oeffnen": nimmt Gold und Items, ein Schritt nach dem anderen. +-- - loescht und retourniert nie einen Brief; Nachnahme-Briefe und reine Briefe bleiben unberuehrt +-- - ein Taschenplatz bleibt immer frei (Items bleiben sonst im Briefkasten) +-- - nach jedem Schritt wird der Posteingang neu gelesen (die Nummern verschieben sich) +-- Senden Empfaenger mit den letzten 5, mehrere Items (je Item ein Brief, nacheinander), Gold, Nachnahme +-- Mailinglisten Namen mit Items, optional Gold-Ueberschuss und Automatik beim Oeffnen des Briefkastens +-- +-- Alle Sendungen laufen ueber dieselbe Warteschlange (Mail.send): ein Brief, dann auf MAIL_SEND_SUCCESS warten, +-- dann Pause, dann der naechste. Sie laufen nie, solange ein Brett-Auftrag einen Brief vorbereitet (BoardBusy). +-- +-- Gespeichert: Account-weit Mail.db = { settings, recipients, lists }. + +local ADDON, NS = ... + +local FW = _G.Freewood +if not FW or not FW:RequireAPI(1, ADDON) then + return +end + +local Util = FW.Util +local L = FW.L + +local MAX_RECENT = 5 +local MAX_LISTS = 30 +local MAX_ITEMS_PER_LIST = 60 +local MAX_JOBS = 120 +local STEP = 0.1 -- Takt des Treibers +local INBOX_GAP = 0.5 -- Pause nach dem Nehmen +local INBOX_TIMEOUT = 2.5 -- so lange auf die Antwort des Servers warten +local SEND_GAP = 1.2 -- Pause nach jedem gesendeten Brief +local SEND_TIMEOUT = 8 +local POSTAGE = 30 -- Kupfer je Brief (Schaetzung fuer den Gold-Ueberschuss) +local AUTO_DELAY = 2.5 +local KEEP_FREE = 1 -- so viele Taschenplaetze bleiben beim Oeffnen immer frei + +local function GetMeta(field) + local fn = (C_AddOns and C_AddOns.GetAddOnMetadata) or GetAddOnMetadata + if fn then + local ok, value = pcall(fn, ADDON, field) + if ok then + return value + end + end + return nil +end + +local Mail = FW:NewModule("Mail", { + version = GetMeta("Version") or "dev", + title = L.MAIL_TITLE, + description = L.MAIL_DESC, + defaults = { + settings = { replace = false, chat = true }, + recipients = {}, + lists = {}, + }, +}) +NS.Mail = Mail +FW.Mail = Mail + +Mail.open = false -- ein Briefkasten ist offen +Mail.busy = nil -- nil | "inbox" | "send" +Mail.gen = 0 -- zaehlt die Briefkasten-Oeffnungen + +local function Now() return GetTime and GetTime() or 0 end +local function After(t, fn) + if C_Timer and C_Timer.After then C_Timer.After(t, fn) else pcall(fn) end +end + +local function Trace(msg) + local C = FW.Creator + if C and C.Trace then pcall(C.Trace, "Mail: " .. tostring(msg)) end +end +Mail.Trace = Trace + +local function Say(msg) + if Mail.db and Mail.db.settings.chat and FW.Print then FW:Print(msg) end +end + +function Mail.Money(c) + if Util.FormatMoney then return Util.FormatMoney(c) end + return tostring(c) .. "c" +end + +local function Trim(s) + return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", "")) +end + +-- Charaktername: Leerzeichen weg, erster Buchstabe gross +function Mail.CleanName(s) + s = Trim(s):gsub("%s+", "") + if s == "" then return "" end + return (s:sub(1, 1):upper() .. s:sub(2)) +end + +local function IsSelf(name) + local me = UnitName and UnitName("player") + return me and name and name:lower() == me:lower() +end +Mail.IsSelf = IsSelf + +------------------------------------------------------------------------ +-- Taschen +------------------------------------------------------------------------ + +local function NumSlots(bag) + local fn = (C_Container and C_Container.GetContainerNumSlots) or GetContainerNumSlots + if type(fn) ~= "function" then return 0 end + local ok, n = pcall(fn, bag) + return (ok and tonumber(n)) or 0 +end + +local function SlotInfo(bag, slot) + if C_Container and C_Container.GetContainerItemInfo then + local ok, info = pcall(C_Container.GetContainerItemInfo, bag, slot) + if ok and type(info) == "table" then + return tonumber(info.itemID), tonumber(info.stackCount) or 1, info.isLocked and true or false + end + end + if GetContainerItemInfo then + local ok, _, count, locked, _, _, _, link, _, _, itemID = pcall(GetContainerItemInfo, bag, slot) + if ok then + local id = tonumber(itemID) or (type(link) == "string" and tonumber(link:match("item:(%d+)"))) or nil + if not id and GetContainerItemLink then + local ok2, l2 = pcall(GetContainerItemLink, bag, slot) + if ok2 and type(l2) == "string" then id = tonumber(l2:match("item:(%d+)")) end + end + return id, tonumber(count) or 1, locked and true or false + end + end + return nil +end + +local function BagCount() return tonumber(_G.NUM_BAG_SLOTS) or 4 end + +-- Alle Staepel in den normalen Taschen: { bag, slot, id, count, locked } +function Mail.BagStacks() + local out = {} + for bag = 0, BagCount() do + for slot = 1, NumSlots(bag) do + local id, count, locked = SlotInfo(bag, slot) + if id then out[#out + 1] = { bag = bag, slot = slot, id = id, count = count, locked = locked } end + end + end + return out +end + +-- freie Plaetze in Taschen ohne Spezialtyp (Koecher, Seelentasche ... zaehlen nicht) +function Mail.FreeSlots() + local fn = (C_Container and C_Container.GetContainerNumFreeSlots) or GetContainerNumFreeSlots + if type(fn) ~= "function" then return 0 end + local n = 0 + for bag = 0, BagCount() do + local ok, free, kind = pcall(fn, bag) + if ok and tonumber(free) and (kind == nil or kind == 0) then n = n + free end + end + return n +end + +------------------------------------------------------------------------ +-- Zusammenspiel mit den Brett-Auftraegen (Creator) +------------------------------------------------------------------------ + +-- Ein Brett-Auftrag bereitet gerade einen Brief vor oder fragt danach: dann fassen wir den Brief nicht an. +function Mail.BoardBusy() + local C = FW.Creator + if not C then return false end + if C.mailChoice or C.asking then return true end + if C.askQueue and #C.askQueue > 0 then return true end + if C.GuardState then + local ok, st = pcall(C.GuardState) + if ok and st and st ~= "off" then return true end + end + return false +end + +-- Items, die ein offener Brett-Auftrag noch verschicken muss: set[itemID] = true +function Mail.Reserved() + local set, n = {}, 0 + local C = FW.Creator + if C and C.ActiveList and C.db and C.ObjProgress then + local ok = pcall(function() + for _, id in ipairs(C.ActiveList()) do + local q = C.db.quests[id] + for i, o in ipairs(q and q.objs or {}) do + if o.type == "mail" and o.item then + local have, need = C.ObjProgress(id, i) + if have < need and not set[o.item] then + set[o.item] = true + n = n + 1 + end + end + end + end + end) + if not ok then Trace("Reservierung konnte nicht gelesen werden") end + end + return set, n +end + +------------------------------------------------------------------------ +-- Empfaenger (die letzten 5) +------------------------------------------------------------------------ + +function Mail:AddRecipient(name) + name = Mail.CleanName(name) + if name == "" or IsSelf(name) then return end + local list = self.db.recipients + for i = #list, 1, -1 do + if list[i]:lower() == name:lower() then table.remove(list, i) end + end + table.insert(list, 1, name) + while #list > MAX_RECENT do table.remove(list) end + FW:Fire("MAIL_RECIPIENTS_CHANGED") +end + +function Mail:Recipients() + return self.db.recipients +end + +------------------------------------------------------------------------ +-- Posteingang +------------------------------------------------------------------------ + +-- Liste der Briefe: { i, sender, subject, money, cod, days, hasItem, itemName, itemTex, itemCount, sig, key, take, why } +function Mail.ReadInbox() + local list = {} + if not GetInboxNumItems then return list end + local ok, n = pcall(GetInboxNumItems) + n = (ok and tonumber(n)) or 0 + local seen = {} + for i = 1, n do + local ok1, _, _, sender, subject, money, cod, days, hasItem, wasRead = pcall(GetInboxHeaderInfo, i) + if ok1 then + local m = { + i = i, sender = sender or "?", subject = subject or "", + money = tonumber(money) or 0, cod = tonumber(cod) or 0, days = tonumber(days), + hasItem = (hasItem and hasItem ~= 0) and true or false, read = wasRead and true or false, + } + if m.hasItem and GetInboxItem then + local ok2, name, tex, count = pcall(GetInboxItem, i) + if ok2 then m.itemName, m.itemTex, m.itemCount = name, tex, tonumber(count) or 1 end + end + m.sig = table.concat({ m.sender, m.subject, m.money, m.cod, tostring(m.itemName), tostring(m.itemCount) }, "|") + seen[m.sig] = (seen[m.sig] or 0) + 1 + m.key = m.sig .. "#" .. seen[m.sig] + if m.cod > 0 then + m.take, m.why = false, "cod" + elseif m.money > 0 or m.hasItem then + m.take = true + else + m.take, m.why = false, "letter" + end + list[#list + 1] = m + end + end + return list +end + +Mail.selected = {} -- key -> true + +function Mail:SelectedCount() + local n = 0 + for _ in pairs(self.selected) do n = n + 1 end + return n +end + +function Mail:ToggleSelect(key, on) + if on == nil then on = not self.selected[key] end + self.selected[key] = on and true or nil + FW:Fire("MAIL_SELECTION_CHANGED") +end + +function Mail:SelectAll(on) + self.selected = {} + if on then + for _, m in ipairs(Mail.ReadInbox()) do + if m.take then self.selected[m.key] = true end + end + end + FW:Fire("MAIL_SELECTION_CHANGED") +end + +-- onlySelected: nur angehakte Briefe +function Mail:StartOpen(onlySelected) + if self.busy or not self.open then return false end + local only + if onlySelected then + only = {} + for k in pairs(self.selected) do only[k] = true end + if next(only) == nil then return false end + end + self.run = { only = only, tries = {}, money = 0, items = 0, pending = nil } + self.busy = "inbox" + self.driver:Show() + Trace("Posteingang: Start" .. (only and " (nur Ausgewaehlte)" or " (alle)")) + FW:Fire("MAIL_STATE") + return true +end + +local function FinishOpen(self, reason) + local run = self.run + self.run, self.busy = nil, nil + if not run then return end + local skipped = 0 + if run.full and reason == "done" then + for _, m in ipairs(Mail.ReadInbox()) do + if m.take and m.hasItem and (not run.only or run.only[m.key]) then skipped = skipped + 1 end + end + end + Trace(string.format("Posteingang: Ende (%s), %d Gold-Briefe, %d Items, %d Items liegen geblieben", reason, run.money, run.items, skipped)) + if reason == "stopped" or reason == "closed" then + Say(string.format(FW.L.MAIL_RUN_STOPPED, run.money, run.items)) + else + Say(string.format(FW.L.MAIL_RUN_DONE, run.money, run.items, skipped > 0 and string.format(FW.L.MAIL_RUN_SKIP_FULL, skipped) or "")) + end + self.lastRun = { money = run.money, items = run.items, skipped = skipped } + self.selected = {} + FW:Fire("MAIL_STATE") + FW:Fire("MAIL_SELECTION_CHANGED") + FW:Fire("MAIL_INBOX_CHANGED") +end + +function Mail:Stop() + if self.busy == "inbox" and self.run then FinishOpen(self, "stopped") end + if self.busy == "send" and self.send then self:FinishSend("stopped") end +end + +function Mail:StepOpen() + local run = self.run + local now = Now() + if not self.open then return FinishOpen(self, "closed") end + if run.pending then + -- auf die Antwort des Servers warten (oder den Ablauf der Zeit) + if not ((run.updated and now >= run.pending + INBOX_GAP) or now >= run.pending + INBOX_TIMEOUT) then return end + run.pending = nil + end + local list = Mail.ReadInbox() + local free = Mail.FreeSlots() + for idx = #list, 1, -1 do + local m = list[idx] + if m.take and (not run.only or run.only[m.key]) and (run.tries[m.sig] or 0) < 3 then + if m.money > 0 then + run.tries[m.sig] = (run.tries[m.sig] or 0) + 1 + run.money = run.money + 1 + run.pending, run.updated = now, false + pcall(TakeInboxMoney, m.i) + Trace(string.format("Brief %d von %s: Gold %d genommen", m.i, m.sender, m.money)) + FW:Fire("MAIL_STATE") + return + elseif m.hasItem then + if free <= KEEP_FREE then + run.full = true + else + run.tries[m.sig] = (run.tries[m.sig] or 0) + 1 + run.items = run.items + 1 + run.pending, run.updated = now, false + pcall(TakeInboxItem, m.i) + Trace(string.format("Brief %d von %s: Item %s genommen (frei vorher %d)", m.i, m.sender, tostring(m.itemName), free)) + FW:Fire("MAIL_STATE") + return + end + end + end + end + FinishOpen(self, "done") +end + +------------------------------------------------------------------------ +-- Senden (gemeinsame Warteschlange) +------------------------------------------------------------------------ + +local function AttachedName() + if not GetSendMailItem then return nil end + local ok, name = pcall(GetSendMailItem, 1) + if ok and name and name ~= "" then return name end + return nil +end + +-- Anhang-Platz leeren (zurueck in die Tasche) +local function ClearAttachment() + if AttachedName() and ClickSendMailItemButton then + pcall(ClickSendMailItemButton) + if CursorHasItem and CursorHasItem() then pcall(ClearCursor) end + end +end + +local function FindStack(item) + local fallback + for _, s in ipairs(Mail.BagStacks()) do + if s.id == item.id and not s.locked then + if s.count == item.count then return s end + fallback = fallback or s + end + end + return fallback +end + +-- Bereitet einen Brief vor. Rueckgabe: true oder false, Grund +local function Prepare(job) + if CursorHasItem and CursorHasItem() then return false, "cursor" end + ClearAttachment() + if job.item then + local s = FindStack(job.item) + if not s then return false, "gone" end + pcall(ClearCursor) + pcall(PickupContainerItem, s.bag, s.slot) + if not (CursorHasItem and CursorHasItem()) then return false, "pickup" end + if not ClickSendMailItemButton then pcall(ClearCursor) return false, "noapi" end + pcall(ClickSendMailItemButton) + if not AttachedName() then + pcall(ClearCursor) + return false, "attach" + end + end + if SetSendMailMoney then pcall(SetSendMailMoney, (job.cod or 0) > 0 and 0 or (job.money or 0)) end + if SetSendMailCOD then pcall(SetSendMailCOD, job.cod or 0) end + return true +end + +local function JobLabel(job) + if job.item then + local info = Util.GetItem and Util.GetItem(job.item.id) + return (info and info.name) or ("#" .. tostring(job.item.id)) + end + return Mail.Money(job.money or 0) +end + +-- jobs: { to, subject, body, item = {id,count}, money, cod } +function Mail:StartSend(jobs) + if self.busy then return false, "busy" end + if not self.open then return false, "closed" end + if Mail.BoardBusy() then return false, "board" end + if not jobs or #jobs == 0 then return false, "empty" end + self.send = { jobs = jobs, i = 0, sent = 0, failed = 0, stage = "next", nextAt = 0 } + self.busy = "send" + self.driver:Show() + Trace(string.format("Senden: Start mit %d Briefen", #jobs)) + FW:Fire("MAIL_STATE") + return true +end + +function Mail:FinishSend(reason) + local s = self.send + self.send, self.busy = nil, nil + if not s then return end + pcall(ClearCursor) + if reason ~= "done" then ClearAttachment() end + Trace(string.format("Senden: Ende (%s), %d von %d gesendet, %d uebersprungen", reason, s.sent, #s.jobs, s.failed)) + if reason == "done" then + Say(string.format(FW.L.MAIL_SEND_DONE, s.sent, #s.jobs, s.failed > 0 and string.format(FW.L.MAIL_SEND_DONE_FAIL, s.failed) or "")) + elseif reason == "timeout" then + Say(FW.L.MAIL_SEND_FAIL_TIMEOUT) + elseif reason == "error" then + Say(FW.L.MAIL_SEND_FAIL_ERROR) + else + Say(string.format(FW.L.MAIL_SEND_STOPPED, s.sent, #s.jobs)) + end + FW:Fire("MAIL_STATE") + FW:Fire("MAIL_SENT_DONE", s.sent, #s.jobs) +end + +function Mail:StepSend() + local s = self.send + local now = Now() + if not self.open then return self:FinishSend("closed") end + if Mail.BoardBusy() then return self:FinishSend("board") end + if s.stage == "wait" then + if s.ok then + s.stage, s.nextAt = "next", now + SEND_GAP + elseif s.err then + return self:FinishSend("error") + elseif now > s.deadline then + return self:FinishSend("timeout") + end + return + end + if now < s.nextAt then return end + if CursorHasItem and CursorHasItem() then return end + s.i = s.i + 1 + local job = s.jobs[s.i] + if not job then return self:FinishSend("done") end + s.current = job + local ok, why = Prepare(job) + if not ok then + s.failed = s.failed + 1 + s.nextAt = now + 0.2 + Trace(string.format("Brief %d (%s) nicht vorbereitet: %s", s.i, JobLabel(job), tostring(why))) + if why == "attach" or why == "gone" or why == "pickup" then + Say(string.format(FW.L.MAIL_SEND_FAIL_ATTACH, JobLabel(job))) + end + FW:Fire("MAIL_STATE") + return + end + s.ok, s.err, s.deadline, s.stage = false, false, now + SEND_TIMEOUT, "wait" + Trace(string.format("Brief %d von %d an %s: %s (Betreff '%s', Nachnahme %d)", s.i, #s.jobs, job.to, JobLabel(job), tostring(job.subject), job.cod or 0)) + FW:Fire("MAIL_STATE") + local okc = pcall(SendMail, job.to, job.subject or "", job.body or "") + if not okc then s.err = true end +end + +-- Fortschritt fuer die Anzeige: i, n, an wen +function Mail:SendProgress() + local s = self.send + if not s then return nil end + local job = s.current or s.jobs[1] + return math.min(s.i, #s.jobs), #s.jobs, job and job.to or "?" +end + +------------------------------------------------------------------------ +-- Briefe zusammenstellen +------------------------------------------------------------------------ + +local function Stamp() return date and date("%d.%m. %H:%M") or "" end + +-- Eingabe des Senden-Reiters: form = { to, subject, body, picks = { {id,count}... }, money, cod = bool } +function Mail:BuildCompose(form) + local to = Mail.CleanName(form.to) + if to == "" then return nil, "to" end + if IsSelf(to) then return nil, "self" end + local picks = form.picks or {} + local money = math.floor(tonumber(form.money) or 0) + if #picks == 0 and money <= 0 then return nil, "nothing" end + if form.cod and #picks == 0 then return nil, "codItem" end + if form.cod and money <= 0 then return nil, "codMoney" end + local base = Trim(form.subject) + local jobs = {} + local n = math.max(#picks, 1) + for i = 1, n do + local pick = picks[i] + local subject = base + if subject == "" then + local info = pick and Util.GetItem and Util.GetItem(pick.id) + subject = (info and info.name) or FW.L.MAIL_SUBJECT_DEFAULT + end + if n > 1 then subject = string.format(FW.L.MAIL_SUBJECT_ITEM_N, subject, i, n) end + local job = { to = to, subject = subject, body = form.body or "", item = pick } + if form.cod then + job.cod = money + elseif i == 1 and money > 0 then + job.money = money + end + jobs[#jobs + 1] = job + end + if #jobs > MAX_JOBS then return nil, "many" end + return jobs +end + +-- Eintrag der Mailingliste -> Briefe. reserved: Items fuer Brett-Auftraege (werden nicht gesendet) +function Mail:BuildList(entry, reserved) + local jobs = {} + if not entry or IsSelf(entry.name) then return jobs end + reserved = reserved or Mail.Reserved() + local stacks = Mail.BagStacks() + local picks = {} + for _, id in ipairs(entry.ids or {}) do + if not reserved[id] then + for _, s in ipairs(stacks) do + if s.id == id and not s.locked then picks[#picks + 1] = { id = id, count = s.count } end + end + end + end + local surplus = 0 + if entry.gold then + local have = (GetMoney and GetMoney()) or 0 + surplus = have - (entry.keep or 0) - (#picks + 1) * POSTAGE + if surplus < 100 then surplus = 0 end + end + for _, p in ipairs(picks) do jobs[#jobs + 1] = { to = entry.name, item = p } end + if surplus > 0 then jobs[#jobs + 1] = { to = entry.name, money = surplus } end + local n, stamp = #jobs, Stamp() + for i, job in ipairs(jobs) do + job.subject = n > 1 and string.format(FW.L.MAIL_SUBJECT_N, stamp, i, n) or string.format(FW.L.MAIL_SUBJECT_STAMP, stamp) + job.body = "" + end + return jobs +end + +function Mail:ListFor(name) + for _, e in ipairs(self.db.lists) do + if e.name:lower() == (name or ""):lower() then return e end + end +end + +function Mail:AddList(name) + name = Mail.CleanName(name) + if name == "" or name:find("[^%w\128-\255%-]") then return false, "invalid" end + if #self.db.lists >= MAX_LISTS then return false, "full" end + if self:ListFor(name) then return false, "exists" end + self.db.lists[#self.db.lists + 1] = { name = name, ids = {}, auto = false, gold = false, keep = 0 } + FW:Fire("MAIL_LISTS_CHANGED") + return true +end + +function Mail:RemoveList(entry) + for i, e in ipairs(self.db.lists) do + if e == entry then table.remove(self.db.lists, i) break end + end + FW:Fire("MAIL_LISTS_CHANGED") +end + +function Mail:ToggleListItem(entry, id) + for i, v in ipairs(entry.ids) do + if v == id then table.remove(entry.ids, i) FW:Fire("MAIL_LISTS_CHANGED") return false end + end + if #entry.ids >= MAX_ITEMS_PER_LIST then return nil end + entry.ids[#entry.ids + 1] = id + FW:Fire("MAIL_LISTS_CHANGED") + return true +end + +function Mail:HasListItem(entry, id) + for _, v in ipairs(entry.ids) do + if v == id then return true end + end + return false +end + +-- Eine Liste (entry) oder alle (entry = nil) jetzt senden. auto: nur Listen mit Haken +function Mail:SendLists(entry, autoOnly) + local reserved = Mail.Reserved() + local jobs = {} + for _, e in ipairs(self.db.lists) do + if (entry == nil or entry == e) and (not autoOnly or e.auto) then + local part = self:BuildList(e, reserved) + if #part == 0 and entry == e then Say(string.format(FW.L.MAIL_SEND_NOTHING_LIST, e.name)) end + for _, j in ipairs(part) do jobs[#jobs + 1] = j end + end + end + if #jobs == 0 then return false, "empty" end + return self:StartSend(jobs) +end + +-- Automatik: kurz nach dem Oeffnen, aber erst wenn Brett-Auftraege und andere Aktionen fertig sind +function Mail:ScheduleAuto() + local any + for _, e in ipairs(self.db.lists) do if e.auto then any = true end end + if not any then return end + local gen, tries, told = self.gen, 0, false + local function Try() + if not Mail.open or Mail.gen ~= gen then return end + if Mail.busy or Mail.BoardBusy() then + tries = tries + 1 + if Mail.BoardBusy() and not told then + told = true + Say(FW.L.MAIL_AUTO_WAIT_BOARD) + end + if tries < 60 then After(1.5, Try) end + return + end + Mail:SendLists(nil, true) + end + After(AUTO_DELAY, Try) +end + +------------------------------------------------------------------------ +-- Normales Fenster ausblenden (es bleibt technisch offen, damit Brett-Auftraege weiter funktionieren) +------------------------------------------------------------------------ + +function Mail.Replacing() + return Mail.db and Mail.db.settings.replace and true or false +end + +function Mail.Anchor() + local w = Mail.window + if Mail.Replacing() and w and w:IsShown() then return w end + return nil +end + +function Mail:ApplyReplace() + local f = _G.MailFrame + if not f then return end + if self.Replacing() and not self.uiFailed then + if not self.hidden then + self.hidden = { alpha = f:GetAlpha(), scale = f:GetScale() } + end + f:SetAlpha(0) + f:SetScale(0.01) + elseif self.hidden then + f:SetAlpha(self.hidden.alpha or 1) + f:SetScale(self.hidden.scale or 1) + self.hidden = nil + end +end + +function Mail:SetReplace(on) + self.db.settings.replace = on and true or false + self.uiFailed = nil + self:ApplyReplace() + FW:Fire("MAIL_REPLACE_CHANGED") + if self.open then + FW:Fire(on and "MAIL_WINDOW_SHOW" or "MAIL_WINDOW_HIDE") + end +end + +------------------------------------------------------------------------ +-- Ereignisse +------------------------------------------------------------------------ + +function Mail:OnMailShow() + self.open = true + self.gen = self.gen + 1 + self.selected = {} + self:ApplyReplace() + Trace("Briefkasten geoeffnet") + if self.Replacing() then FW:Fire("MAIL_WINDOW_SHOW") end + FW:Fire("MAIL_INBOX_CHANGED") + self:ScheduleAuto() +end + +function Mail:OnMailClosed() + if not self.open then return end + self.open = false + if self.busy == "inbox" then FinishOpen(self, "closed") end + if self.busy == "send" then self:FinishSend("closed") end + self.busy = nil + FW:Fire("MAIL_WINDOW_HIDE") +end + +function Mail:OnInboxUpdate() + if self.run then self.run.updated = true end + FW:Fire("MAIL_INBOX_CHANGED") +end + +function Mail:OnSendSuccess() + local s = self.send + if self.lastTo then + self:AddRecipient(self.lastTo) + self.lastTo = nil + end + if s and s.stage == "wait" then + s.ok = true + s.sent = s.sent + 1 + FW:Fire("MAIL_STATE") + end +end + +function Mail:OnSendFailed() + local s = self.send + if s and s.stage == "wait" then s.err = true end +end + +function Mail:OnEnable() + self.driver = self.driver or CreateFrame("Frame") + local acc = 0 + self.driver:SetScript("OnUpdate", function(_, elapsed) + acc = acc + (elapsed or 0) + if acc < STEP then return end + acc = 0 + local ok, err = pcall(function() + if Mail.busy == "inbox" and Mail.run then Mail:StepOpen() + elseif Mail.busy == "send" and Mail.send then Mail:StepSend() + else Mail.driver:Hide() end + end) + if not ok then + Trace("Fehler im Takt: " .. tostring(err)) + Mail.run, Mail.send, Mail.busy = nil, nil, nil + Mail.driver:Hide() + FW:Fire("MAIL_STATE") + end + end) + self.driver:Hide() + FW:RegisterEvent("MAIL_SHOW", function() pcall(Mail.OnMailShow, Mail) end, self) + FW:RegisterEvent("MAIL_CLOSED", function() pcall(Mail.OnMailClosed, Mail) end, self) + FW:RegisterEvent("MAIL_INBOX_UPDATE", function() pcall(Mail.OnInboxUpdate, Mail) end, self) + FW:RegisterEvent("MAIL_SEND_SUCCESS", function() pcall(Mail.OnSendSuccess, Mail) end, self) + FW:RegisterEvent("MAIL_FAILED", function() pcall(Mail.OnSendFailed, Mail) end, self) + if not self.hooked then + self.hooked = true + -- Empfaenger merken, egal wer sendet (auch das normale Fenster und Brett-Auftraege) + if hooksecurefunc and _G.SendMail then + pcall(hooksecurefunc, "SendMail", function(to) + if Mail.db then Mail.lastTo = to end + end) + end + if _G.MailFrame and MailFrame.HookScript then + pcall(MailFrame.HookScript, MailFrame, "OnShow", function() pcall(Mail.ApplyReplace, Mail) end) + -- das Ereignis MAIL_CLOSED kommt nicht immer; das Verschwinden des Fensters ist sicher + pcall(MailFrame.HookScript, MailFrame, "OnHide", function() pcall(Mail.OnMailClosed, Mail) end) + end + end + -- Briefkasten schon offen (z. B. nach /reload)? + if _G.MailFrame and MailFrame.IsShown and MailFrame:IsShown() then + After(0.5, function() pcall(Mail.OnMailShow, Mail) end) + end +end + +function Mail:OnDisable() + for _, ev in ipairs({ "MAIL_SHOW", "MAIL_CLOSED", "MAIL_INBOX_UPDATE", "MAIL_SEND_SUCCESS", "MAIL_FAILED" }) do + FW:UnregisterEvent(ev, self) + end + if self.driver then self.driver:Hide() end + self.run, self.send, self.busy = nil, nil, nil + if self.hidden then + local f = _G.MailFrame + if f then f:SetAlpha(self.hidden.alpha or 1) f:SetScale(self.hidden.scale or 1) end + self.hidden = nil + end +end diff --git a/MailUI.lua b/MailUI.lua new file mode 100644 index 0000000..f9d756c --- /dev/null +++ b/MailUI.lua @@ -0,0 +1,845 @@ +-- Freewood-OctoWoW-Mail: eigenes Briefkasten-Fenster (Freewood-Design, verschiebbar). +-- Es geht nur auf, wenn im Spiel ein Briefkasten geoeffnet wird, und nur, wenn es in den Einstellungen so gewaehlt ist. +-- Reiter: Posteingang | Senden | Mailinglisten. + +local ADDON, NS = ... + +local FW = _G.Freewood +local Mail = NS.Mail +if not FW or not Mail then + return +end + +local UI = FW.UI +local Util = FW.Util + +local W, H = 660, 560 +local ROW_H, ROW_GAP = 40, 4 +local OWNER = {} + +local window, content, footer, tabs +local current = "inbox" +local panes = {} +local picks = {} -- Senden-Reiter: gewaehlte Staepel { id, count, key } +local picker + +local function T(key) return FW.L[key] or key end + +------------------------------------------------------------------------ +-- Bausteine +------------------------------------------------------------------------ + +local function ColoredName(name, quality, id) + return "|cff" .. UI.RarityHex(quality) .. (name or ("#" .. tostring(id))) .. "|r" +end + +local function MakeEdit(parent, width, hintText, maxLetters, numeric) + local box = UI.CreatePanel(parent, "bgDeep", "bronze") + box:SetSize(width, 26) + local edit = CreateFrame("EditBox", nil, box) + edit:SetAutoFocus(false) + edit:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -2) + edit:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -8, 2) + edit:SetMaxLetters(maxLetters or 100) + if numeric and edit.SetNumeric then pcall(edit.SetNumeric, edit, true) end + if not edit:SetFont(UI.GetFontPath("body"), 12, "") then + edit:SetFont("Fonts\\FRIZQT__.TTF", 12, "") + end + edit:SetTextColor(UI.Color("text")) + edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) + edit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end) + if hintText then + box.hint = UI.CreateLabel(box, hintText, { size = 12, color = "textDim" }) + box.hint:SetPoint("LEFT", box, "LEFT", 10, 0) + end + edit:SetScript("OnTextChanged", function(self) + if box.hint then + if self:GetText() == "" then box.hint:Show() else box.hint:Hide() end + end + if box.onChange and not box.loading then box.onChange(self:GetText()) end + end) + box.edit = edit + function box:Set(text) + self.loading = true + edit:SetText(text or "") + self.loading = nil + if self.hint then + if (text or "") == "" then self.hint:Show() else self.hint:Hide() end + end + end + return box, edit +end + +local function Tooltip(frame, itemID, inboxIndex) + frame:EnableMouse(true) + frame:SetScript("OnEnter", function(self) + if not GameTooltip then return end + local idx = self.inboxIndex + local id = self.itemID + if idx and GameTooltip.SetInboxItem then + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + pcall(GameTooltip.SetInboxItem, GameTooltip, idx) + GameTooltip:Show() + elseif id then + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + pcall(GameTooltip.SetItemByID, GameTooltip, id) + GameTooltip:Show() + end + end) + frame:SetScript("OnLeave", function() + if GameTooltip then GameTooltip:Hide() end + end) +end + +-- Zeile mit Symbol und Namen +local function ItemRow(parent, h) + local row = UI.CreatePanel(parent, "card", "bronze") + row:SetHeight(h or 32) + row.icon = row:CreateTexture(nil, "ARTWORK") + row.icon:SetSize(24, 24) + row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + row.name = UI.CreateLabel(row, "", { size = 12 }) + row.name:SetWordWrap(false) + Tooltip(row) + return row +end + +local function FillItem(row, id, count) + row.itemID = id + local suffix = (count and count > 1) and (" x" .. count) or "" + row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + row.name:SetText(ColoredName(nil, 1, id) .. suffix) + Util.RequestItem(id, function(info) + if row.itemID ~= id or not info then return end + row.icon:SetTexture(info.icon) + row.name:SetText(ColoredName(info.name, info.quality, id) .. suffix) + end) +end + +local function Heading(parent, text, x, y) + local h = UI.CreateLabel(parent, text, { font = "heading", size = 13, color = "goldLight" }) + h:SetPoint("TOPLEFT", parent, "TOPLEFT", x or 0, y or 0) + return h +end + +------------------------------------------------------------------------ +-- Auswahlfenster fuer Items +------------------------------------------------------------------------ + +local function BuildPicker() + local p = UI.CreatePanel(window, "bgDeep", "gold") + p:SetSize(460, 440) + p:SetPoint("CENTER", window, "CENTER", 0, 0) + p:SetFrameStrata("DIALOG") + p:EnableMouse(true) + p.title = UI.CreateLabel(p, "", { font = "heading", size = 15, color = "goldLight" }) + p.title:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -14) + p.hint = UI.CreateLabel(p, "", { size = 11, color = "textDim", justifyV = "TOP" }) + p.hint:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -38) + p.hint:SetPoint("TOPRIGHT", p, "TOPRIGHT", -16, -38) + p.hint:SetHeight(28) + p.hint:SetWordWrap(true) + p.scroll = UI.CreateScrollFrame(p) + p.scroll:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -72) + p.scroll:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", -16, 52) + p.empty = UI.CreateLabel(p.scroll.child, T("MAIL_PICK_EMPTY"), { size = 12, color = "textDim" }) + p.empty:SetPoint("TOPLEFT", p.scroll.child, "TOPLEFT", 4, -6) + p.done = UI.CreateButton(p, T("MAIL_PICK_DONE"), 140, 28, function() p:Hide() end, "primary") + p.done:SetPoint("BOTTOM", p, "BOTTOM", 0, 14) + p.rows = {} + p:Hide() + + local function Row(i) + local row = p.rows[i] + if row then return row end + row = ItemRow(p.scroll.child, 32) + row.cb = UI.CreateCheckbox(row, "", function() return row.checked end, function(v) + if row.onToggle then row.onToggle(v) end + end) + row.cb:SetPoint("LEFT", row, "LEFT", 6, 0) + row.cb:SetWidth(22) + row.icon:SetPoint("LEFT", row, "LEFT", 34, 0) + row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0) + row.name:SetPoint("RIGHT", row, "RIGHT", -8, 0) + p.rows[i] = row + return row + end + + function p:Fill() + local data = {} + local stacks = Mail.BagStacks() + if self.mode == "list" then + local agg, order = {}, {} + for _, s in ipairs(stacks) do + local a = agg[s.id] + if not a then + a = { id = s.id, count = 0 } + agg[s.id] = a + order[#order + 1] = a + end + a.count = a.count + s.count + end + table.sort(order, function(a, b) return a.id < b.id end) + for _, a in ipairs(order) do data[#data + 1] = a end + local extraStart = #data + 1 + for _, id in ipairs(self.entry.ids) do + if not agg[id] then data[#data + 1] = { id = id, count = 0, extra = true } end + end + self.extraStart = extraStart + else + local seen = {} + for _, s in ipairs(stacks) do + if not s.locked then + local base = s.id .. ":" .. s.count + seen[base] = (seen[base] or 0) + 1 + data[#data + 1] = { id = s.id, count = s.count, key = base .. ":" .. seen[base] } + end + end + end + for i, d in ipairs(data) do local d = d + local row = Row(i) + row:ClearAllPoints() + row:SetPoint("TOPLEFT", p.scroll.child, "TOPLEFT", 0, -((i - 1) * 36)) + row:SetPoint("TOPRIGHT", p.scroll.child, "TOPRIGHT", 0, -((i - 1) * 36)) + FillItem(row, d.id, d.count) + if d.extra then row.name:SetText(row.name:GetText() .. " |cff8a8f9a(" .. T("MAIL_PICK_EXTRA") .. ")|r") end + if self.mode == "list" then + row.checked = Mail:HasListItem(self.entry, d.id) + row.onToggle = function() + local r = Mail:ToggleListItem(self.entry, d.id) + row.checked = (r == true) or (r == nil and false) + row.cb:Refresh() + if self.onChange then self.onChange() end + end + else + local function Index() + for k, pk in ipairs(picks) do if pk.key == d.key then return k end end + end + row.checked = Index() and true or false + row.onToggle = function() + local k = Index() + if k then + table.remove(picks, k) + row.checked = false + else + picks[#picks + 1] = { id = d.id, count = d.count, key = d.key } + row.checked = true + end + row.cb:Refresh() + if self.onChange then self.onChange() end + end + end + row.cb:Refresh() + row:Show() + end + for i = #data + 1, #p.rows do p.rows[i]:Hide() end + self.empty:SetShown(#data == 0) + self.scroll:SetContentHeight(#data * 36 + 4) + end + + return p +end + +-- mode "list": entry = Eintrag der Mailingliste; mode "send": Auswahl des Senden-Reiters +local function OpenPicker(mode, entry, onChange) + picker = picker or BuildPicker() + picker.mode, picker.entry, picker.onChange = mode, entry, onChange + if mode == "list" then + picker.title:SetText(string.format(T("MAIL_PICK_LIST_TITLE"), entry.name)) + picker.hint:SetText(T("MAIL_PICK_HINT_LIST")) + else + picker.title:SetText(T("MAIL_PICK_TITLE")) + picker.hint:SetText(T("MAIL_PICK_HINT_SEND")) + end + picker:Show() + picker:Raise() + picker:Fill() +end + +------------------------------------------------------------------------ +-- Reiter: Posteingang +------------------------------------------------------------------------ + +local function BuildInbox(parent) + local f = CreateFrame("Frame", nil, parent) + local rows = {} + + f.info = UI.CreateLabel(f, "", { size = 12, color = "textDim" }) + f.info:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -6) + + f.none = UI.CreateButton(f, T("MAIL_SELECT_NONE"), 130, 24, function() Mail:SelectAll(false) end, "default") + f.none:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) + f.all = UI.CreateButton(f, T("MAIL_SELECT_ALL"), 130, 24, function() Mail:SelectAll(true) end, "default") + f.all:SetPoint("RIGHT", f.none, "LEFT", -6, 0) + + f.open = UI.CreateButton(f, T("MAIL_OPEN_ALL"), 280, 30, function() + Mail:StartOpen(Mail:SelectedCount() > 0) + end, "primary") + f.open:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -32) + f.stop = UI.CreateButton(f, T("MAIL_STOP"), 110, 30, function() Mail:Stop() end, "danger") + f.stop:SetPoint("LEFT", f.open, "RIGHT", 8, 0) + + f.note = UI.CreateLabel(f, T("MAIL_NOTE_SAFE"), { size = 11, color = "textDim", justifyV = "TOP" }) + f.note:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0) + f.note:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + f.note:SetHeight(30) + f.note:SetWordWrap(true) + + f.scroll = UI.CreateScrollFrame(f) + f.scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -70) + f.scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 36) + f.empty = UI.CreateLabel(f.scroll.child, T("MAIL_INBOX_EMPTY"), { size = 12, color = "textDim" }) + f.empty:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 4, -6) + + local function Row(i) + local row = rows[i] + if row then return row end + row = UI.CreatePanel(f.scroll.child, "card", "bronze") + row:SetHeight(ROW_H) + row.cb = UI.CreateCheckbox(row, "", function() return row.mail and Mail.selected[row.mail.key] end, function(v) + if row.mail then Mail:ToggleSelect(row.mail.key, v) end + end) + row.cb:SetPoint("LEFT", row, "LEFT", 8, 0) + row.cb:SetWidth(22) + row.icon = row:CreateTexture(nil, "ARTWORK") + row.icon:SetSize(28, 28) + row.icon:SetPoint("LEFT", row, "LEFT", 38, 0) + row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + row.l1 = UI.CreateLabel(row, "", { size = 12 }) + row.l1:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -6) + row.l1:SetPoint("RIGHT", row, "RIGHT", -60, 0) + row.l1:SetWordWrap(false) + row.l2 = UI.CreateLabel(row, "", { size = 11, color = "textDim" }) + row.l2:SetPoint("TOPLEFT", row, "TOPLEFT", 76, -22) + row.l2:SetPoint("RIGHT", row, "RIGHT", -60, 0) + row.l2:SetWordWrap(false) + row.days = UI.CreateLabel(row, "", { size = 11, color = "textDim", justify = "RIGHT" }) + row.days:SetPoint("RIGHT", row, "RIGHT", -10, 0) + Tooltip(row) + rows[i] = row + return row + end + + function f.Refresh() + local list = Mail.ReadInbox() + local busy = Mail.busy + local takeable = 0 + for i, m in ipairs(list) do local m = m + local row = Row(i) + row.mail = m + row.inboxIndex = m.hasItem and m.i or nil + row:ClearAllPoints() + row:SetPoint("TOPLEFT", f.scroll.child, "TOPLEFT", 0, -((i - 1) * (ROW_H + ROW_GAP))) + row:SetPoint("TOPRIGHT", f.scroll.child, "TOPRIGHT", 0, -((i - 1) * (ROW_H + ROW_GAP))) + if m.itemTex then + row.icon:SetTexture(m.itemTex) + elseif m.money > 0 then + row.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01") + else + row.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01") + end + row.l1:SetText(m.subject ~= "" and m.subject or m.sender) + local parts = {} + if m.hasItem then parts[#parts + 1] = (m.itemName or "?") .. ((m.itemCount or 1) > 1 and (" x" .. m.itemCount) or "") end + if m.money > 0 then parts[#parts + 1] = string.format(T("MAIL_ROW_GOLD"), Mail.Money(m.money)) end + local text + if m.cod > 0 then + text = "|cffff7060" .. string.format(T("MAIL_ROW_COD"), Mail.Money(m.cod)) .. "|r" + elseif #parts == 0 then + text = T("MAIL_ROW_LETTER") + else + text = table.concat(parts, " + ") + end + row.l2:SetText(string.format(T("MAIL_ROW_FROM"), m.sender) .. " " .. text) + row.days:SetText(m.days and string.format(T("MAIL_ROW_DAYS"), math.floor(m.days)) or "") + row.cb:SetShown(m.take) + row:SetAlpha(m.take and 1 or 0.55) + row.cb:Refresh() + if m.take then takeable = takeable + 1 end + row:Show() + end + for i = #list + 1, #rows do rows[i].mail = nil rows[i]:Hide() end + f.empty:SetShown(#list == 0) + f.scroll:SetContentHeight(#list * (ROW_H + ROW_GAP) + 4) + f.info:SetText(string.format(T("MAIL_INBOX_COUNT"), #list)) + + local sel = Mail:SelectedCount() + f.open:SetLabel(sel > 0 and string.format(T("MAIL_OPEN_SELECTED"), sel) or T("MAIL_OPEN_ALL")) + f.open:SetButtonEnabled(Mail.open and not busy and (sel > 0 or takeable > 0)) + f.stop:SetShown(busy ~= nil) + f.all:SetButtonEnabled(not busy and takeable > 0) + f.none:SetButtonEnabled(not busy and sel > 0) + end + + return f +end + +------------------------------------------------------------------------ +-- Reiter: Senden +------------------------------------------------------------------------ + +local function BuildSend(parent) + local f = CreateFrame("Frame", nil, parent) + local rows = {} + local cod = false + + Heading(f, T("MAIL_TO"), 0, 0) + local toBox, toEdit = MakeEdit(f, 300, nil, 24) + toBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -20) + local dd + dd = UI.CreateDropdown(f, 170, 26, { + allText = T("MAIL_TO_RECENT"), + getOptions = function() + local o = {} + for _, n in ipairs(Mail:Recipients()) do o[#o + 1] = { value = n, text = n } end + return o + end, + onChange = function(v) + if v then toBox:Set(v) end + dd:SetValue(nil) + end, + }) + dd:SetPoint("LEFT", toBox, "RIGHT", 8, 0) + + Heading(f, T("MAIL_SUBJECT"), 0, -54) + local subjBox = MakeEdit(f, 628, nil, 64) + subjBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -74) + + Heading(f, T("MAIL_BODY"), 0, -108) + local body = UI.CreateTextArea(f, 628, 64) + body:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -128) + + -- Items + Heading(f, T("MAIL_ITEMS"), 0, -202) + local addBtn = UI.CreateButton(f, T("MAIL_ITEMS_ADD"), 170, 24, nil, "default") + addBtn:SetPoint("TOPLEFT", f, "TOPLEFT", 120, -198) + local itemScroll = UI.CreateScrollFrame(f) + itemScroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -228) + itemScroll:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -228) + itemScroll:SetHeight(104) + local itemsEmpty = UI.CreateLabel(itemScroll.child, T("MAIL_ITEMS_NONE"), { size = 12, color = "textDim" }) + itemsEmpty:SetPoint("TOPLEFT", itemScroll.child, "TOPLEFT", 4, -6) + + -- Gold und Nachnahme + local goldBox = MakeEdit(f, 90, T("MAIL_GOLD"), 7, true) + goldBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -344) + local silverBox = MakeEdit(f, 70, T("MAIL_SILVER"), 2, true) + silverBox:SetPoint("LEFT", goldBox, "RIGHT", 8, 0) + local copperBox = MakeEdit(f, 70, T("MAIL_COPPER"), 2, true) + copperBox:SetPoint("LEFT", silverBox, "RIGHT", 8, 0) + local codBox = UI.CreateCheckbox(f, T("MAIL_COD"), function() return cod end, function(v) cod = v and true or false end) + codBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -376) + codBox:SetWidth(620) + + local status = UI.CreateLabel(f, "", { size = 12, color = "textDim", justify = "LEFT" }) + status:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 6) + status:SetPoint("RIGHT", f, "RIGHT", -160, 0) + status:SetWordWrap(false) + local send = UI.CreateButton(f, T("MAIL_SEND"), 150, 30, nil, "primary") + send:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + + local function Copper() + return (tonumber(goldBox.edit:GetText()) or 0) * 10000 + (tonumber(silverBox.edit:GetText()) or 0) * 100 + (tonumber(copperBox.edit:GetText()) or 0) + end + + local function Form() + return { to = toEdit:GetText(), subject = subjBox.edit:GetText(), body = body.edit:GetText(), picks = picks, money = Copper(), cod = cod } + end + + local function SetStatus(text, colorKey) + status:SetText(text or "") + status:SetTextColor(UI.Color(colorKey or "textDim")) + end + + function f.Refresh() + for i, pk in ipairs(picks) do local pk, i = pk, i + local row = rows[i] + if not row then + row = ItemRow(itemScroll.child, 30) + row.icon:SetPoint("LEFT", row, "LEFT", 6, 0) + row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 0) + row.name:SetPoint("RIGHT", row, "RIGHT", -40, 0) + row.remove = UI.CreateButton(row, "X", 26, 22, nil, "danger") + row.remove:SetPoint("RIGHT", row, "RIGHT", -4, 0) + rows[i] = row + end + row:ClearAllPoints() + row:SetPoint("TOPLEFT", itemScroll.child, "TOPLEFT", 0, -((i - 1) * 34)) + row:SetPoint("TOPRIGHT", itemScroll.child, "TOPRIGHT", 0, -((i - 1) * 34)) + FillItem(row, pk.id, pk.count) + row.remove:SetScript("OnClick", function() + table.remove(picks, i) + f.Refresh() + end) + row:Show() + end + for i = #picks + 1, #rows do rows[i]:Hide() end + itemsEmpty:SetShown(#picks == 0) + itemScroll:SetContentHeight(#picks * 34 + 4) + codBox:Refresh() + + local busy = Mail.busy + local boardBusy = Mail.BoardBusy() + local jobs, err = Mail:BuildCompose(Form()) + if boardBusy then + SetStatus(T("MAIL_BOARD_BUSY"), "goldLight") + elseif busy then + SetStatus(T("MAIL_BUSY"), "textDim") + elseif jobs then + SetStatus(#jobs == 1 and T("MAIL_SEND_COUNT1") or string.format(T("MAIL_SEND_COUNT"), #jobs), "textDim") + elseif err == "to" then SetStatus(T("MAIL_SEND_NO_TO"), "textDim") + elseif err == "self" then SetStatus(T("MAIL_SEND_SELF"), "danger") + elseif err == "codItem" then SetStatus(T("MAIL_SEND_COD_NEEDS_ITEM"), "danger") + else SetStatus(T("MAIL_SEND_NOTHING"), "textDim") end + send:SetButtonEnabled(jobs ~= nil and Mail.open and not busy and not boardBusy) + end + + for _, b in ipairs({ goldBox, silverBox, copperBox, subjBox }) do b.onChange = function() if f:IsShown() then f.Refresh() end end end + toBox.onChange = function() if f:IsShown() then f.Refresh() end end + addBtn:SetScript("OnClick", function() OpenPicker("send", nil, f.Refresh) end) + send:SetScript("OnClick", function() + local jobs = Mail:BuildCompose(Form()) + if not jobs then return f.Refresh() end + local ok = Mail:StartSend(jobs) + if ok then f.pendingClear = #jobs end + f.Refresh() + end) + FW:On("MAIL_SENT_DONE", function(_, sent, total) + if f.pendingClear and sent >= f.pendingClear then + for i = #picks, 1, -1 do table.remove(picks, i) end + subjBox:Set("") + body.edit:SetText("") + goldBox:Set("") silverBox:Set("") copperBox:Set("") + cod = false + end + f.pendingClear = nil + if f:IsShown() then f.Refresh() end + end, OWNER) + FW:On("MAIL_RECIPIENTS_CHANGED", function() end, OWNER) + + -- Brett-Auftraege melden sich nicht bei uns; das Fenster fragt deshalb im Takt nach + local acc = 0 + f:SetScript("OnUpdate", function(_, elapsed) + acc = acc + (elapsed or 0) + if acc > 0.5 then + acc = 0 + f.Refresh() + end + end) + + return f +end + +------------------------------------------------------------------------ +-- Reiter: Mailinglisten +------------------------------------------------------------------------ + +local LIST_ROW_H = 96 + +local function BuildLists(parent) + local f = CreateFrame("Frame", nil, parent) + local rows = {} + + local nameBox, nameEdit = MakeEdit(f, 220, T("MAIL_LISTS_NAME_BOX"), 24) + nameBox:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + local status = UI.CreateLabel(f, "", { size = 11, color = "textDim" }) + local function Add() + local ok, why = Mail:AddList(nameEdit:GetText()) + if ok then + nameBox:Set("") + nameEdit:ClearFocus() + status:SetText("") + else + status:SetText(why == "exists" and "Name exists." or (why == "full" and "List is full." or "Invalid name.")) + end + end + local addBtn = UI.CreateButton(f, T("MAIL_LISTS_ADD"), 110, 26, Add, "primary") + addBtn:SetPoint("LEFT", nameBox, "RIGHT", 8, 0) + nameEdit:SetScript("OnEnterPressed", Add) + status:SetPoint("LEFT", addBtn, "RIGHT", 10, 0) + local allBtn = UI.CreateButton(f, T("MAIL_LISTS_ALL_NOW"), 190, 26, function() Mail:SendLists(nil, false) end, "default") + allBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0) + + local hint = UI.CreateLabel(f, T("MAIL_LISTS_HINT"), { size = 11, color = "textDim", justifyV = "TOP" }) + hint:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -34) + hint:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -34) + hint:SetHeight(28) + hint:SetWordWrap(true) + + local reserved = UI.CreateLabel(f, "", { size = 11, color = "goldLight" }) + reserved:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 2) + + local scroll = UI.CreateScrollFrame(f) + scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -66) + scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 24) + local empty = UI.CreateLabel(scroll.child, T("MAIL_LISTS_EMPTY"), { size = 12, color = "textDim" }) + empty:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 4, -6) + + local function Row(i) + local row = rows[i] + if row then return row end + row = UI.CreatePanel(scroll.child, "card", "bronze") + row:SetHeight(LIST_ROW_H) + row.name = UI.CreateLabel(row, "", { font = "heading", size = 14, color = "goldLight" }) + row.name:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -12) + row.items = UI.CreateButton(row, "", 130, 24, function() + if row.entry then OpenPicker("list", row.entry, function() f.Refresh() end) end + end, "default") + row.items:SetPoint("TOPLEFT", row, "TOPLEFT", 190, -8) + row.now = UI.CreateButton(row, T("MAIL_LISTS_NOW"), 120, 24, function() + if row.entry then Mail:SendLists(row.entry, false) end + end, "default") + row.now:SetPoint("LEFT", row.items, "RIGHT", 8, 0) + row.remove = UI.CreateButton(row, "X", 26, 24, function() + if row.entry then Mail:RemoveList(row.entry) end + end, "danger") + row.remove:SetPoint("TOPRIGHT", row, "TOPRIGHT", -8, -8) + UI.AttachTooltip(row.remove, function() return T("MAIL_LISTS_REMOVE") end, "") + row.selfNote = UI.CreateLabel(row, T("MAIL_LISTS_SELF"), { size = 11, color = "textDim" }) + row.selfNote:SetPoint("LEFT", row.items, "RIGHT", 8, 0) + + row.auto = UI.CreateCheckbox(row, T("MAIL_LISTS_AUTO"), function() return row.entry and row.entry.auto end, function(v) + if row.entry then row.entry.auto = v and true or false end + end) + row.auto:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -40) + row.auto:SetWidth(420) + row.gold = UI.CreateCheckbox(row, T("MAIL_LISTS_GOLD"), function() return row.entry and row.entry.gold end, function(v) + if row.entry then row.entry.gold = v and true or false end + end) + row.gold:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -66) + row.gold:SetWidth(170) + row.keepBox = MakeEdit(row, 80, nil, 7, true) + row.keepBox:SetPoint("TOPLEFT", row, "TOPLEFT", 186, -64) + row.keepBox.onChange = function(text) + if row.entry then row.entry.keep = math.max(0, math.floor(tonumber(text) or 0)) * 10000 end + end + row.keepLabel = UI.CreateLabel(row, T("MAIL_LISTS_GOLD_G"), { size = 12, color = "textDim" }) + row.keepLabel:SetPoint("LEFT", row.keepBox, "RIGHT", 8, 0) + rows[i] = row + return row + end + + function f.Refresh() + local lists = Mail.db.lists + for i, e in ipairs(lists) do + local row = Row(i) + row.entry = e + row:ClearAllPoints() + row:SetPoint("TOPLEFT", scroll.child, "TOPLEFT", 0, -((i - 1) * (LIST_ROW_H + ROW_GAP))) + row:SetPoint("TOPRIGHT", scroll.child, "TOPRIGHT", 0, -((i - 1) * (LIST_ROW_H + ROW_GAP))) + row.name:SetText(e.name) + row.items:SetLabel(string.format(T("MAIL_LISTS_ITEMS"), #e.ids)) + local me = Mail.IsSelf(e.name) + row.now:SetShown(not me) + row.selfNote:SetShown(me and true or false) + row.now:SetButtonEnabled(Mail.open and not Mail.busy and not Mail.BoardBusy()) + row.auto:Refresh() + row.gold:Refresh() + row.keepBox:Set(tostring(math.floor((e.keep or 0) / 10000))) + row:Show() + end + for i = #lists + 1, #rows do rows[i].entry = nil rows[i]:Hide() end + empty:SetShown(#lists == 0) + scroll:SetContentHeight(#lists * (LIST_ROW_H + ROW_GAP) + 4) + allBtn:SetButtonEnabled(#lists > 0 and Mail.open and not Mail.busy and not Mail.BoardBusy()) + local _, n = Mail.Reserved() + reserved:SetText(n > 0 and string.format(T("MAIL_LISTS_RESERVED"), n) or "") + end + + return f +end + +------------------------------------------------------------------------ +-- Fenster +------------------------------------------------------------------------ + +local TAB_DEFS = { + { id = "inbox", label = "MAIL_TAB_INBOX", build = BuildInbox }, + { id = "send", label = "MAIL_TAB_SEND", build = BuildSend }, + { id = "lists", label = "MAIL_TAB_LISTS", build = BuildLists }, +} + +local function SelectTab(id) + current = id + for _, def in ipairs(TAB_DEFS) do + local pane = panes[def.id] + if pane then + if def.id == id then pane:Show() pane.Refresh() else pane:Hide() end + end + if tabs[def.id] then tabs[def.id]:SetActive(def.id == id) end + end +end + +local function UpdateFooter() + if not footer then return end + local text = "" + if Mail.busy == "inbox" and Mail.run then + text = string.format(T("MAIL_RUN_PROGRESS"), Mail.run.items, Mail.run.money) + elseif Mail.busy == "send" then + local i, n, to = Mail:SendProgress() + if i then text = string.format(T("MAIL_SEND_PROGRESS"), math.max(i, 1), n, to) end + end + footer:SetText(text) +end + +local function SavePosition() + local left, top = window:GetLeft(), window:GetTop() + if left and top then Mail.db.settings.pos = { x = left, y = top } end +end + +local function BuildWindow() + local f = UI.CreatePanel(UIParent, "bgDeep", "gold") + f:SetSize(W, H) + f:SetFrameStrata("HIGH") + f:SetClampedToScreen(true) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function(self) self:StartMoving() end) + f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() SavePosition() end) + local pos = Mail.db.settings.pos + f:ClearAllPoints() + if pos and pos.x and pos.y then + f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", pos.x, pos.y) + else + f:SetPoint("CENTER", UIParent, "CENTER", -120, 40) + end + window = f + Mail.window = f + + local title = UI.CreateLabel(f, T("MAIL_WINDOW_TITLE"), { font = "heading", size = 16, color = "goldLight" }) + title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -12) + local close = UI.CreateButton(f, "X", 26, 24, function() + if CloseMail then pcall(CloseMail) end + if _G.MailFrame and HideUIPanel and MailFrame:IsShown() then pcall(HideUIPanel, MailFrame) end + f:Hide() + end, "danger") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10) + UI.AttachTooltip(close, function() return T("MAIL_CLOSE") end, "") + local div = UI.CreateDivider(f) + div:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -40) + div:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -40) + + tabs = {} + local prev + for _, def in ipairs(TAB_DEFS) do local def = def + local b = UI.CreateTabButton(f, T(def.label)) + if prev then b:SetPoint("LEFT", prev, "RIGHT", 4, 0) else b:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -44) end + b:SetScript("OnClick", function() SelectTab(def.id) end) + tabs[def.id] = b + prev = b + end + + content = CreateFrame("Frame", nil, f) + content:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -86) + content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 32) + for _, def in ipairs(TAB_DEFS) do + local pane = def.build(content) + pane:SetAllPoints(content) + pane:Hide() + panes[def.id] = pane + end + + footer = UI.CreateLabel(f, "", { size = 11, color = "textDim" }) + footer:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 10) + footer:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 10) + footer:SetWordWrap(false) + + f:Hide() + return f +end + +local function RefreshCurrent() + local pane = panes[current] + if window and window:IsShown() and pane and pane.Refresh then pane.Refresh() end + UpdateFooter() +end + +local function ShowWindow() + local ok, err = pcall(function() + if not window then BuildWindow() end + window:Show() + SelectTab(current) + UpdateFooter() + end) + if not ok then + -- Fehler: das normale Fenster zurueckgeben, damit der Briefkasten benutzbar bleibt + Mail.uiFailed = true + Mail:ApplyReplace() + Mail.Trace("Fenster-Fehler: " .. tostring(err)) + if FW.Print then FW:Print("Mail: " .. tostring(err)) end + end +end + +local function HideWindow() + if window then + if picker then picker:Hide() end + window:Hide() + end +end + +FW:On("MAIL_WINDOW_SHOW", function() ShowWindow() end, OWNER) +FW:On("MAIL_WINDOW_HIDE", function() HideWindow() end, OWNER) +FW:On("MAIL_STATE", function() RefreshCurrent() end, OWNER) +FW:On("MAIL_INBOX_CHANGED", function() RefreshCurrent() end, OWNER) +FW:On("MAIL_SELECTION_CHANGED", function() RefreshCurrent() end, OWNER) +FW:On("MAIL_LISTS_CHANGED", function() RefreshCurrent() if picker and picker:IsShown() then picker:Fill() end end, OWNER) + +------------------------------------------------------------------------ +-- Einstellungen +------------------------------------------------------------------------ + +if UI.RegisterSettingsSection then + UI:RegisterSettingsSection("mail", { + order = 118, + height = 190, + build = function(parent) + local f, stack = UI.CreateSection(parent, T("MAIL_SET_HEADER")) + local boxes = {} + local replace = UI.CreateCheckbox(f, T("MAIL_SET_REPLACE"), function() + return Mail.db.settings.replace + end, function(value) + Mail:SetReplace(value) + end) + stack:Add(replace, { fit = true, gap = 4 }) + boxes[#boxes + 1] = replace + local chat = UI.CreateCheckbox(f, T("MAIL_SET_CHAT"), function() + return Mail.db.settings.chat + end, function(value) + Mail.db.settings.chat = value and true or false + end) + stack:Add(chat, { fit = true, gap = 4 }) + boxes[#boxes + 1] = chat + local note = UI.CreateLabel(f, T("MAIL_SET_NOTE"), { size = 11, color = "textDim", justifyV = "TOP" }) + stack:Add(note, { fit = true, gap = 10 }) + function f.Refresh() + for _, cb in ipairs(boxes) do cb:Refresh() end + end + stack:Layout() + return f + end, + refresh = function(frame) + frame.Refresh() + end, + }) +end + +------------------------------------------------------------------------ +-- Befehl +------------------------------------------------------------------------ + +SLASH_FREEWOODMAIL1 = "/fwmail" +SlashCmdList["FREEWOODMAIL"] = function(msg) + msg = (msg or ""):lower():match("^%s*(.-)%s*$") + if msg == "on" then + Mail:SetReplace(true) + FW:Print(T("MAIL_CMD_ON")) + elseif msg == "off" then + Mail:SetReplace(false) + FW:Print(T("MAIL_CMD_OFF")) + elseif Mail.open then + if window and window:IsShown() then HideWindow() else ShowWindow() end + else + FW:Print(T("MAIL_CMD_CLOSED")) + end +end diff --git a/README.md b/README.md index 802073b..f7cbf33 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # Freewood-OctoWoW-Mail -Freewood-OctoWoW-Mail