pax_global_header00006660000000000000000000000064141663260760014525gustar00rootroot0000000000000052 comment=7b5d4d8f342b57a887f6220939897834b70a1021 cawbird-1.4.2/000077500000000000000000000000001416632607600131445ustar00rootroot00000000000000cawbird-1.4.2/._vimrc000066400000000000000000000001421416632607600144210ustar00rootroot00000000000000set wildignore+=*.c,*\~,CMakeFiles,Makefile,*.lo,test-driver,*.trs,*.stamp,Makefile.in,*.la,*.svg cawbird-1.4.2/.github/000077500000000000000000000000001416632607600145045ustar00rootroot00000000000000cawbird-1.4.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001416632607600166675ustar00rootroot00000000000000cawbird-1.4.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014301416632607600213570ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us fix problems and errors title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **System details:** - OS: [e.g. Ubuntu 18.04] - Version: [e.g. 1.0.4-61.2] - Package type: [e.g. Official RPM/Deb, Snap, Flatpak, distro package, or custom build] **Additional context** Add any other context about the problem here. Running `G_MESSAGES_DEBUG=cawbird cawbird` can give useful debug output. cawbird-1.4.2/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011361416632607600224150ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for a change or new feature title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. cawbird-1.4.2/.github/workflows/000077500000000000000000000000001416632607600165415ustar00rootroot00000000000000cawbird-1.4.2/.github/workflows/main.yml000066400000000000000000000005341416632607600202120ustar00rootroot00000000000000name: GitHub Polls Actions Bot on: issue_comment: types: [created, edited] # issue comment is created or edited issues: types: [opened, edited] # issue is opened or edited jobs: polls: runs-on: ubuntu-latest steps: - uses: imjohnbo/gh-polls-bot-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} cawbird-1.4.2/.tx/000077500000000000000000000000001416632607600136555ustar00rootroot00000000000000cawbird-1.4.2/.tx/config000066400000000000000000000004051416632607600150440ustar00rootroot00000000000000[main] host = https://transifex.com [corebird.corebirdpot] file_filter = po/.po host = https://transifex.com source_file = po/corebird.pot source_lang = en type = PO lang_map = id_ID: id, ro_RO: ro, pt_PT: pt, ko_KR: ko, hi_IN: hi, ja_JP: ja, tr_TR: tr cawbird-1.4.2/COPYING000066400000000000000000001045131416632607600142030ustar00rootroot00000000000000 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 . cawbird-1.4.2/README.md000066400000000000000000000343041416632607600144270ustar00rootroot00000000000000# Cawbird 1.4.2 [Cawbird](https://ibboard.co.uk/cawbird/) is a fork of the [Corebird](https://corebird.baedert.org/) Twitter client from Baedert, which became unsupported after Twitter disabled the streaming API. Cawbird works with the new APIs and includes a few fixes and modifications that have historically been patched in to IBBoard's custom Corebird build on his personal Open Build Service account[1](#footnote1). ## Packaging and installation Cawbird packages are built in the [Cawbird Open Build Service project](https://build.opensuse.org/project/show/home:IBBoard:cawbird). They can be installed from the [Cawbird download page](https://software.opensuse.org//download.html?project=home%3AIBBoard%3Acawbird&package=cawbird). **Install Cawbird** These packages are currently available for: * openSUSE * openSUSE Tumbleweed * openSUSE Leap 15.2 * openSUSE Leap 15.3 * Fedora * Fedora 32 * Fedora 33 * Fedora 34 * Fedora Rawhide * CentOS * CentOS 7 * CentOS 8 (including Streams) * Ubuntu * Ubuntu 18.04 (Bionic Beaver) * Ubuntu 20.04 (Focal Fossa) * Ubuntu 20.10 (Groovy Gorilla) * Ubuntu 21.04 (Hirstue Hippo) * Debian * 10 (Buster) * Testing * Unstable * Raspbian 10 i586, x86_64 (amd64), aarch64 (arm64) and armv7l are available on most platforms (where supported by the distro). ### Official distro repositories The following distros currently have their own official packages: * [Alpine Linux / postmarketOS (Edge)](https://pkgs.alpinelinux.org/packages?name=cawbird&branch=edge) * `sudo apk add cawbird` * [Arch Linux](https://www.archlinux.org/packages/community/x86_64/cawbird/) ("Community") * `pacman -Syu cawbird` * [Fedora (30+)](https://apps.fedoraproject.org/packages/cawbird) * `sudo dnf install cawbird` * [Guix](https://guix.gnu.org/en/packages/cawbird-1.4.1/) * `guix install cawbird` * [NixOS (19.09+)](https://github.com/NixOS/nixpkgs/blob/master/pkgs/applications/networking/cawbird/default.nix) * `nix-shell -p cawbird` for testing, `nix-env -iA cawbird` for permanent installation * [Solus](https://dev.getsol.us/source/cawbird/) * `sudo eopkg it cawbird` ## Community builds * [Cawbird Snap](https://snapcraft.io/cawbird) on Snapcraft.io * [Cawbird Flatpak](https://flathub.org/apps/details/uk.co.ibboard.cawbird) on Flathub.org * Arch Linux (AUR): * [Cawbird-git](https://aur.archlinux.org/packages/cawbird-git) ### Dependencies Twitter uses specific codecs for videos. These are provided by `libav` and are not included in the core repositories of many distros. The following additional repositories are known to include the required libraries: * openSUSE - [Packman](http://packman.links2linux.org/) * Fedora/CentOS - [RPMFusion](https://rpmfusion.org/) * Ubuntu - Universe ## Shortcuts | Key | Description | | :-----: | :----------- | | `Ctrl + t` | Compose Tweet | | `Back` | Go one page back (this can be triggered via the back button on the keyboard, the back thumb button on the mouse or `Alt + Left`) | | `Forward` | Go one page forward (this can be triggered via the forward button on the keyboard, the forward thumb button on the mouse or `Alt + Right`) | | `Alt + num` | Go to page `num` (between 1 and 7 at the moment) | | `Ctrl + Shift + s` | Show/Hide topbar | | `Ctrl + p` | Show account settings | | `Ctrl + k` | Show account list | | `Ctrl + Shift + p` | Show application settings | When a tweet is focused (via keynav): * `r` - reply * `tt` - retweet * `f` - favorite * `q` - quote * `dd` - delete * `Return` - Show tweet details * `k` - Print tweet details to stdout (debug builds) ## Limitations Due to [changes in the Twitter API](https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/migration/introduction), Cawbird has the following limitations: * Cawbird will update every two minutes * Cawbird does not get notified of the following actions when performed outside Cawbird, which will be refreshed on restart: * Unfavourite * Follow/Unfollow * Block/Unblock * Mute/Unmute * DM deletion * Some list changes All limitations are limitations imposed by Twitter and are not the fault of the Cawbird client. They have affected [all third-party client applications](http://apps-of-a-feather.com/). Cawbird has also been unable to implement the following features because Twitter did not provide a way for third-party applications to get the data: * Notification of Likes, RTs, quote tweets and any other interaction that appears in the "All" tab of twitter.com's Notifications but not in "Mentions" * No API is available for other notifications, only a [mentions API](https://developer.twitter.com/en/docs/tweets/timelines/api-reference/get-statuses-mentions_timeline) * DMs to Groups * Twitter's API only supports one-to-one DMs * Twitter explicitly [closed a request for this API](https://twitterdevfeedback.uservoice.com/forums/921790-twitter-developer-labs/suggestions/37689256-allow-access-to-dm-group-conversations-via-twitter) as "not a priority" * Polls * The free API does not include polls as tweet "entities" * Twitter doesn't even mark posts so that we can direct people to the web * Twitter's "[Bookmarks](https://blog.twitter.com/en_us/topics/product/2018/an-easier-way-to-save-and-share-tweets.html)" system * No API is available * Full threads in a single request * No API is available * Replies older than seven days * The free search is limited to returning results from the last seven days * All replies to a tweet * No API is available and the search results are not guaranteed to find all replies * Twitter [Cards](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards) * No API is available * Twitter doesn't even mark posts so that we can direct people to the web * This results in @TwitterDev [posting a message](https://mobile.twitter.com/TwitterDev/status/1222596295090757636) that many devs can't see! As of July 2020, Twitter has [announced v2 of the API](https://blog.twitter.com/developer/en_us/topics/tools/2020/introducing_new_twitter_api.html) and may support some of these features. We are [looking in to this](https://github.com/IBBoard/cawbird/issues/180) as that parts of the API are made available. ## Known issues There are no current known issues with running Cawbird. Previously, the following issues have occurred that were outside of our control: * Due to a [bug in GnuTLS](https://gitlab.com/gnutls/gnutls/issues/841#note_225110002), Cawbird has suffered from occasional TLS errors. This has been handled under [Cawbird bug 9](https://github.com/IBBoard/cawbird/issues/9) and GnuTLS have released a fix for the underlying problem * Due to a [bug in GStreamer 1.16.1](https://github.com/IBBoard/cawbird/pull/42#issuecomment-539437887), Cawbird suffered from glitchy audio and video when playing media. This has now been fixed in GStreamer * Some combination of GStreamer, GTK and the Video Acceleration API (VAAPI) on some Intel chips can [cause corruption of videos](https://github.com/IBBoard/cawbird/issues/279). This is an [old and on-going problem](https://github.com/baedert/corebird/issues/540). Removing `gstreamer-vaapi` or equivalent resolves the problem. * "Fragment downloading has failed consecutive times" messages from GStreamer appear to be due to changes in GStreamer's `adaptivedemux` code that are [fixed in v1.18.1](https://gstreamer.freedesktop.org/releases/1.18/#1.18.2) ## Translations Since February 2020, there has been a [Cawbird project on Transifex](https://www.transifex.com/cawbird/cawbird/dashboard/). Users can sign up on Transifex for free to help translate Cawbird. ## Testing Since August 2020, there has been a [Cawbird "unstable" package](https://software.opensuse.org//download.html?project=home%3AIBBoard%3Acawbird-unstable&package=cawbird-unstable) built on the Open Build Service. These packages are intended for users who can't build Cawbird from source but want to test new features. **Do not** use the unstable release unless you are testing new features and understand the risks. *They are not intended for everyday use*. They will be updated intermittently with new features from `git master`. They may have bugs. They may not get patched. They may be behind the main release. They may break things and eat your homework. It is recommended that you backup `~/.config/cawbird` before running Cawbird-Unstable. ## Contributing All contributions are welcome (artwork, design, code, just ideas, etc.) but if you're planning to actively change something bigger, talk to me first. ## Compiling Cawbird ### Preparation Twitter clients need keys and secrets so that Twitter can go through the OAuth process. Cawbird used to ship with a standard set of but has always supported custom keys through schema settings. However, that wasn't convenient for software builds. Cawbird now supports: a) per-user tokens and secrets (so each user uses a different "app") b) configuration of the default token and secret at build time What this means for developers is that you need to supply two build options with the key and the secret before the software will build. To stop them being trivially identifiable, we base64 encode them. If you wish to build your own "micro-fork" of the application then register at [developer.twitter.com](https://developer.twitter.com/) and create an application. To base64 encode the keys you can run `echo -n "" | base64`. Reasons you may wish to micro-fork Cawbird: * You want to package a modified version with your own patches (as IBBoard used to do with Corebird) * You want to appear retro and use the old Corebird keys to confuse people * You want to check whether you're getting hit by Twitter limiting *applications* (not just users - all users of the app in aggregate) to 100,000 calls to some endpoints ([docs](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-mentions_timeline)) Alternatively you can continue using the default keys by using the values `VmY5dG9yRFcyWk93MzJEZmhVdEk5Y3NMOA==` and `MThCRXIxbWRESDQ2Y0podzVtVU13SGUyVGlCRXhPb3BFRHhGYlB6ZkpybG5GdXZaSjI=` respectively. ### Compiling Cawbird uses the Meson build system rather than the more archaic autoconf/make combination. Building is as simple as: ```Bash meson build -Dconsumer_key_base64= -Dconsumer_secret_base64= ninja -C build ``` If you want to test translations locally then you will also need to: * pass `-Dlocaltextdomain=true` to meson * run `ninja -C build cawbird-gmo` to generate the binary `.mo` translations * run `for file in po/*.gmo; do mkdir -p "${file/.gmo}/LC_MESSAGES/"; cp $file "${file/.gmo}/LC_MESSAGES/cawbird.mo"; done` to put the `.mo` files in the expected places * run `pushd build; ./cawbird; popd` to run Cawbird from the build directory * to test a different language, run `cd build; LANGUAGE=aa_BB ./cawbird` with the appropriate language code Note that executing `build/cawbird` may result in one of the following errors: ```Bash Settings schema 'uk.co.ibboard.cawbird' is not installed Settings schema 'uk.co.ibboard.cawbird' does not contain a key named 'foo' ``` To fix this, use the schemas from the build directory: ```Bash GSETTINGS_SCHEMA_DIR=build/data/ GSETTINGS_BACKEND='memory' build/cawbird ``` Cawbird installs its application icon into `/usr/share/icons/hicolor/`, so an appropriate call to `gtk-update-icon-cache` might be needed. ### Build Dependencies * `gtk+-3.0 >= 3.22` * `glib-2.0 >= 2.44` * `json-glib-1.0` * `sqlite3` * `libsoup-2.4` * `librest-0.7` * `liboauth` * `gettext >= 0.19.7` * `vala >= 0.28` (makedep) * `meson` (makedep) * `gst-plugins-base-1.0` (for playbin, disable by passing `-Dvideo=false` to Meson) * `gst-plugins-bad-1.0 >= 1.6` or `gst-plugins-good-1.0` (disable by passing `-Dvideo=false` to Meson, default enabled) * Requires the `element-gtksink` feature, provided by `gstreamer1.0-gtk` on Ubuntu-based systems, `gstreamer1-plugins-bad-free-gtk` on older RPM-based systems and `gstreamer1-plugins-good-gtk` on newer RPM-based systems * `gst-libav-1.0` (disable by passing `-Dvideo=false` to Meson, default enabled) * `gspell-1 >= 1.2` (for spellchecking, disable by passing `-Dspellcheck=false` to Meson, default enabled) Note that the above packages are just rough estimations, the actual package names on your distribution may vary and may require additional repositories (e.g. RPMFusion in Fedora, or Packman in openSUSE) If you pass `-Dvideo=false` to the Meson script, you don't need any gstreamer dependency but won't be able to view any videos. ## Copyright Cawbird is released under the GPL v3 - see [COPYING](./COPYING) for more details. The [video fallback image](data/symbolic/apps/cawbird-video-placeholder.svg) is a Creative Commons "CC BY" licensed work [by Iris Li](https://thenounproject.com/term/film-reel/20395/). ## Footnotes 1: [home:IBBoard:desktop](https://build.opensuse.org/project/show/home:IBBoard:desktop) cawbird-1.4.2/cawbird.gresource.xml000066400000000000000000000074351416632607600173070ustar00rootroot00000000000000 ui/menus.ui ui/settings-dialog.ui ui/account-create-widget.ui ui/compose-window.ui ui/image-description-window.ui ui/tweet-list-entry.ui ui/tweet-info-page.ui ui/about-dialog.ui ui/dm-thread-entry.ui ui/profile-page.ui ui/dm-page.ui ui/search-page.ui ui/user-list-entry.ui ui/start-conversation-entry.ui ui/list-statuses-page.ui ui/list-list-entry.ui ui/new-list-entry.ui ui/user-lists-widget.ui ui/filter-page.ui ui/filter-list-entry.ui ui/modify-filter-dialog.ui ui/user-filter-entry.ui ui/account-dialog.ui ui/media-dialog.ui ui/modify-snippet-dialog.ui ui/shortcuts-window.ui ui/cb-emoji-chooser.ui data/symbolic/apps/cawbird-compose-symbolic.svg data/symbolic/apps/cawbird-conversation-symbolic.svg data/symbolic/apps/cawbird-dms-symbolic.svg data/symbolic/apps/cawbird-filter-symbolic.svg data/symbolic/apps/cawbird-mentions-symbolic.svg data/symbolic/apps/cawbird-new-window-symbolic.svg data/symbolic/apps/cawbird-reply-symbolic.svg data/symbolic/apps/cawbird-retweet-symbolic.svg data/symbolic/apps/cawbird-translate-symbolic.svg data/symbolic/apps/cawbird-profile-symbolic.svg data/symbolic/apps/cawbird-favorite-symbolic.svg data/symbolic/apps/cawbird-user-home-symbolic.svg data/symbolic/apps/cawbird-edit-find-symbolic.svg data/symbolic/apps/cawbird-video-placeholder.svg data/play.png data/play@2.png data/verified-small.png data/verified-small@2.png data/verified-large.png data/verified-large@2.png data/protected-account-small.png data/protected-account-small@2.png data/protected-account-large.png data/protected-account-large@2.png data/no_avatar.png data/no_banner.png ui/style.css sql/init/Create.1.sql sql/init/Create.2.sql sql/init/Create.3.sql sql/init/Create.4.sql sql/accounts/Create.1.sql sql/accounts/Create.2.sql sql/accounts/Create.3.sql sql/accounts/Create.4.sql sql/accounts/Create.5.sql sql/accounts/Create.6.sql sql/accounts/Create.7.sql cawbird-1.4.2/data/000077500000000000000000000000001416632607600140555ustar00rootroot00000000000000cawbird-1.4.2/data/cawbird.1000066400000000000000000000023441416632607600155550ustar00rootroot00000000000000.TH "cawbird" "1" "08. Feb. 2014" "man page by Malcolm J Lewis" "" .SH NAME .B cawbird - Native Gtk+ Twitter client for the Linux desktop. .SH SYNOPSIS .B cawbird [--tweet=@screen_name] .SH DESCRIPTION - Cawbird is a native GTK+ twitter client that provides vital features such as Direct Messages (DMs), tweet notifications, conversation views. - Additional features include the ability to change to the GTK+ dark theme, searching and media uploads. .SH KEYBOARD SHORTCUTS [command] .B [Ctrl + t] - Compose tweet .B [Back] - Go one page back (this can be triggered via the back button on the keyboard, the back thumb button on the mouse or [Alt + Left]) .B [Forward] - Go one page forward(this can be triggered via the forward button on the keyboard, the forward thumb button on the mouse or [Alt + Right]) .B [Alt + num] - Go to page [num] (between 1 and 5 at the moment) .B [Ctrl + Shift + s] - Show/Hide sidebar .B When a tweet is focused (via keynav) .B [r] - Reply .B [tt] - Retweet .B [l] - Like .B [dd] - Delete .B [Return] - Show tweet details .SH AUTHOR Updated by IBBoard. Originally written as Corebird by Timm Bäder. .SH HOMEPAGE https://ibboard.co.uk/cawbird/ .SH REPORTING BUGS https://github.com/ibboard/cawbird/issues cawbird-1.4.2/data/hicolor/000077500000000000000000000000001416632607600155145ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/16x16/000077500000000000000000000000001416632607600163015ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/16x16/apps/000077500000000000000000000000001416632607600172445ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/16x16/apps/uk.co.ibboard.cawbird.png000066400000000000000000000014471416632607600240120ustar00rootroot00000000000000‰PNG  IHDRóÿabKGDÿÿÿ ½§“ÜIDAT8}“Oh\UÆï¾7/sß̘igÒ¤Óµ•ÖfÚ¸q#¥¸°t׈bbA¡º)Yˆw‚+qQR¥h©"ju©ÐÅ€# ´£†I&Zl“V;üi¥3y3ï½ûÞ½®l‡6ø­Îßw8‡s>‹‡!>5™¨è°¶’Ʋ-ao88—Ý’u®V«©~±ÕOÆJOžŒ¢ð­®òÇ#,€6aÙdÓÙe×I´Øüíý‡ì+àûÿL—Ç:/ÇÆæÛ/.`€^™@kÍ÷ß|G½¾`¶¥·o¬Ö_ J㯗UG3ë·ÍX¹zݬ\½~¯µÖÌóÏ3¥ÁQS.z@„*<¥MâL˜¢PÜΕw˜>}‘’ÀÎ2}ú" ×ÖbòÄË PáÉr¹ìб‘§ËJ…eG¤Èå²\j4ù¹ÑbþÊ*——î×ÙGr8¡§º£Ö†3áh'ڟضesi®ÆÄäq¦Ž–Ù³kê_£uÂÌ©Iž98Š1†ÚÜ/Kª‘KíµGÒ»RQ¾a0Öµ?–Ʀ¸cˆZµBµúÍ›M2)C!_àËO¿âóOÎÆ!Xà¹Þ9 `oñ‰?ê”=7ƒçf)F9€°­hþÞ¢§ºô”ùH×k öéÊ…:ŒÕ¥ù¤¥‹'=<éáz.ÝÈ'P=€6 Øò³Ù¿g{`¡ùëL^n?«ÑI7òñÑRH)‘RNJͨƒm’$1ùLñÂRkþ{ghÜš³(w¼È.‡q@»}éI¤'iwÚô¢.žëÝÌg‹ï-µæ_4€ÝÿÊk›·ævî>“ÓùuדÞãûÛíº.7þükÖêÙÚZ¿ÖX­ÿØïqLÒââbÌÙwäŒÑz%±DJDÏÕ›µ`‹àÝ_áAT*•8ˆâªRA¥R©liÞr‚~lÜ]?k”Ñÿ§ùÍ!Xêʃ›VIEND®B`‚cawbird-1.4.2/data/hicolor/24x24/000077500000000000000000000000001416632607600162775ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/24x24/apps/000077500000000000000000000000001416632607600172425ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/24x24/apps/uk.co.ibboard.cawbird.png000066400000000000000000000025441416632607600240070ustar00rootroot00000000000000‰PNG  IHDRàw=øbKGDÿÿÿ ½§“IDATH‰­•]lWÇwî|ìnì];þØf³v$ÂNHR¥‘‘T *AU¨ôñ”„xˆ( èS¨(¢ _"-´ *T¥˜ê6ñ&NKÒ¤XIênìumÇö~Íîܹ÷ò°ñš& if¤;çüçÜ£™+xíe¯<—º[£w:Ž—•ÂÊØˆwpÅÙ>·ãÙ±Kc›å‹½ØÞçˆöô·šJmU:Ì5㦣ÑXkðW&HyÁ;¾›|#&~ôŸÅ“OÝ`¨o¨Ãñ¼'C~z¥±œ!q„ƒ­pk-Ö´5XkÈ$»â5~æ•ȨÞ˜=õÖ C¹¡Aûçùêüfk ;wîbddZkŽýåÓÓEØý‰»RòòËãœ*°@¶37+qÿdqbìºV¶ né^·í\_G֮ˬ·G?f«•ª]ÕÂÜ¢ýÞ·±?ôˆ/-´×—––íþÐæ2yÛÛ‘µÃëwÌlÍݱãz@nûÓÙtÎfÓ9ûõ¯~Ó*Ûw+¬…¶^«_·®"e÷åA».½ÞövôÛíù€Ãêm¸xk]…Ÿ4FãKŸ½_¸וL—VxèÈ1~{ô,‰T‚d*ÉÏæÀgº´€ë¹Ü÷ù{q¥‡#$KõÅC¹mû\Ç ¾V)/&¥ã’Nw1°!ßîìÇOøÇ™9^8QdóàZ~ñÂ9ê E¹®8¸ïîÖ\6 ’Ig¸²¼HµY]©ž½À\c쇵qE­õZؤ×4”¡3!éîL`‹ENùí¸°^'Ž4Ž T¬now`0!BT391Éí`ÿGÚØK¾7ÁÏr‹áÀ—`n%æ3ûPpºð:q³êcŒîÉ$[ƒDÿ·åÈ¡ÇyëÂtk]‡=»7ñü~Í…7ßäâù)žýý¯Ø³{®ëpaê~ü ‹ãˆf¾˜$Àº®õŸ[ —7ºŽD:.åå g'ÏÒ™NãùããÇû+Q3ÂCµZÁu\’A’ñcÇ9ðï2õ¯)”QÄF›˜t¢kúo•£‡ÀG²Û,7—(¸L¸¾ð’¬ÝØMª;h(„ ¼ÒdñÒ Õ Š›4ãÍ«Om4ù®üoN'îwZƒ0v¥Ö¾­uL¬cb£PZéˆP…H_àû~ûò<á Â($Š›(]­\¡MLÏšÞ²h˜ï·¿ƒ×æ^«IÄÃéDw¼jéf«2ÕÀ‘Î|ßÇq͸U}+6BiEàø2ù»“ ' mÀé™Â¡t2óË?¥[-¯¶5£ë*R4THãêÖDº‰ëHzR·];³fߪ¯¼öwQ*_~fCÏûl®4Wc Æj:3ärH)‘Râº.³ÅJ¥JG(‘Nd¢žŽ¾?©Lí³'æO¨÷´ 3ÏÝÖ“+t½›诪ª¬‡u†††ñ<Wº8ÒáÕW'¨U+¤ü´éëè?8Þw&/O쟟Ÿ××úÝðÀøèÀ÷8Æý’&Þ´ëž;·ôg{]€ùÅ+züùWÎûÂ?ß0úé3—? Ø÷òpo8ývaøxê®?ú¾¿À¼_/>u³ÜU9ïÒÒrµü’ïùøžO­VéVón07[üi¥VY(WËK/Îþìÿ­Õëõ“õ0œ{fáVón:ƒwkºT:ø¿Äü– –òeYJIEND®B`‚cawbird-1.4.2/data/hicolor/32x32/000077500000000000000000000000001416632607600162755ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/32x32/apps/000077500000000000000000000000001416632607600172405ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/32x32/apps/uk.co.ibboard.cawbird.png000066400000000000000000000036571416632607600240130ustar00rootroot00000000000000‰PNG  IHDR szzôbKGDÿÿÿ ½§“dIDATX…½Vkl\Gþfî½ûðz×ëø±»~ÆÆ&Ž’šÄŽ ihA!¡þBê„PTE *4¨ÊK4©ZÚ¦¡jˆ*I+Ò†„ç¥Ö©pœGã¦iÛqÖïÄö¾îî½wf?öo㤉+q¤£¹:÷Ìw¾9sÎÌ0,AÖ×­¯µ5 9޵B2réä¾äu±¨gÄ=†câN°Øí8µ´|Íí±&~ Ã¸Ÿoµ…U)IúH4MgšåÒÝ×4®]Î8Ö;\ºvêéOD`Ê þXÒü‹ßHXñú„•€#m0ÆÀÀÆÀ"‚Æ4ø½e(ó–_#%zl¨ŸDûï˜Àšðº¯3ƒ?3çšbÖ<888×Àc¼ÈŸ@P¤@¤ IA) ŸËjh6-3[ÆNýê¶ ´G:~MŒýp*>VJ h\C8\ƒ5k>3ibàÜûH›élrÁKJJжº >_ NõŸÂÌÌ4I)„Kk„¡{º[¢õîÆny«¬cu¤c˪H»Yá«¢j˜:WvÑë¯þƒÆ¯Lp9¶ ÁóCôÒ3¯ÐšÆu´¶qýù™—éâùA²m‡Ç¡ÑËQÚù×]Ô±²‹BþUøª¨µªM­­ÿÜÁ‡ðvÓàíuk¿}W¤Ã¬(­¢j„:Úº¨ïÝ~ZL”RÔýæaê~ó)¥õé}ûu®è¢P †*|U´"´Z­©íznÑàëëÖ{;ëî¾T]¦j˜jÊêéo, ¼ÄÍ‚çe×ß^£Ú`…üZ櫤µuŸ_îêÊÇ-TRJZ¿™ËÌ6+(h\GÓò&¬ÿâ=E$ÍŒƒ{ú±cÏI¤-‘íÆn°/”/|éóXÞ¸ס1 ñh™×¥oËÿ×󆿾?7~u#*CE`›Ÿ;Œ3C³HY ç†gðô<öl7Þ™ƒi£È¡H5î½ï^Œ¾< EliA*¹º³±3ròòÉ wE::M‘jm3Æá+õ±ëMB\GÊ’`ŒáƒÑyeí£q˜–€‚=/Œ1”\Î8’V<±èÑÂH&ïq¤íbŒ³lOOÎ@IµhkúeC9»nÇGìy‘Rbrbœq°Ó6¡éúꊷ¦”“ëkÆ€£bèâZÚš `¿}d#þþï³ ¾ÒÁc›‡lÚ„ö–Yßy ½hÛ.}8Œ·¿•ÅÍá[2 7×* $ghá4†D"CŽ¢ùÓËÁyvu^·Žï~³Éd›¶éT À¶§žÂ/Ÿø*+‹kF)…#Ž!‘Hx”åYG1ìÒ PŽQö\ß¾õüóµAˆâÊ~aû‹HÌňÇÇ‘˜aûŽ—Š|„#°w÷~lßúâõ»€àâ8×@€š@ç'­×òÅÂ9@ÀÛGŽ#9Ÿ8–ƒ±ÑqìÛ·©TªhŸ“‰êà 0“&Þ?óv½ò:þôÇça;¤’JF¿;ƒñýSÉÉṵ̀u ‡®]h6¸CsåÔ(=.ôCm[-¬L¦hÅãƒSpæ2VŽr ¤ G:°¥ '÷íHõÁ¦$gÚÚþhï`þ "IÖ±€7I2§RIH% ”€2V¥()aF‘*¥à x‘±ÒÊNvžÊâ(•Åå\ƒK3.ôG{ 5¤‰ŸT—TG ™›$ϯDÓ9t]¿A5MƒîÒr«ur$DK@Pv1uÓVÖÏóq Nœž7)ýdMY“*”“K›“K¡ Iê†ÕçDp¤] áäæ % ¥@°d8ø¾ÓѾý7€sWN=¯3ã/!“Oc>°Ó´™¡‹ªe;°…]˜ãH»°>·åžÊwý†çá…1o¸›'ãc{ëË[|žÒ•ñtLS¤r ”}í0—†¦–F0ÆÀ5^PÝÐ1<8ŒùÙ¹ûŸ—•TÒ2_e¤õåÞ+½é[€ÉÄØžê`íÕj_d]Z¤J-‘)<¹2© >ÕÚ ·Û k•R¡ÿ?§sE˜M¹Æ9ê‚MI·Ëµ+ãO|ëÌЙÌGcÝôu2Ÿè úªv—¹ƒ­A_y…"Q’¶Óp„ƒšúZ”/ e`zfçß;¡܆amªÜWÕ'ë{§Çú¶ÎÌÌ,úÓ3æe`üä(€W-ï ÜåOTøBw éÔÛ¦Uá2\ מeZ¨)kH¸u÷˜b@*ñdßåžþ[á,‘‘ÿNø>´‡Ú}¶ÐumSáMËRª^—ö£Ãǧn3/üã]ŠåìÔÙT,uuKü „°¬œ®LIEND®B`‚cawbird-1.4.2/data/hicolor/48x48/000077500000000000000000000000001416632607600163135ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/48x48/apps/000077500000000000000000000000001416632607600172565ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/48x48/apps/uk.co.ibboard.cawbird.png000066400000000000000000000062741416632607600240270ustar00rootroot00000000000000‰PNG  IHDR00Wù‡bKGDÿÿÿ ½§“ qIDAThÕYypœG•ÿu÷wÌ!4:,Ùº,[F‰ ÁÆNâ ÁTŠØ,G•YŽ@*©RÔn±ŠJ…9jÉn8…«b¶’…Ä1¤€Ù˜`bÇ–­È¶(ÛkDZdi4£kîo¾«»ùã›ãÓŒ$"±µoª§¿™yÝïý^¿î÷^ðÿœÈRL²µck§-ñnAœ j·ÊX”‚†)‘ $\ irî¦Á'Aå€B¿:>zèñàÚÕ×¶›ÿ’BÕ[4¦w8ÜŽ¸Â&¦cÂv-8 e`„AgAèZÀëÕt%Ÿ,8…W)Sþ}xôè¡¿ €-«¶õ;°¾§«-Žpš2…2f ®pA!¥iK½ôÞ¥¯'@½A4ÔŒ6-×¼ %èøØà“åK `[ÿ¶úlÖÚ£«–k4Lf'`¹6(¡ „x=PQžR– @J Yꥄ€A[];¢¡Û² g]á~b8öʉ%°±sË;S,ºcéb»(a ”‚¢¨|ÈܘKs”‡„”Â×@€®†^µpÚpó?:uið.\f5. à-›ï0ýîL!Õ0™K€ J(¥ÅgZ¶~„7µ‡”€„¤„(*/¥ôà^/‚z=Ñ5®é\»°ãTâTþopM׿ohð³±ÌXа² ÅÍH)ÃUýW៼=kz ‡!¸‹L:‡ãƒ'°÷¿÷"—ÍÍ™«>R]Ý…M›7¢®! JŒ|#¯â‰ÇžÄ¹ÿ= .„ð€B±¶ùMp„3LT禡 Cé¿ À[;6ß«)Á{ÆRƒ–k–èÜ÷À×ðŽwÝˆÞ¾Õ tî¶åàô©3xü?ŸÀó¿z°ãý;ðñOÞŠõ¯†®ksø…8wö5ØÿG<ôàC°, BrpÁ! °®¹Žp†2zÓ¶óçk]€·t^·3@ÙS±Ì¥ú‚c€VT~÷£»±}ÇM aÄÇøÉž'A ÅÇ>u+Vv¶-Êoä øŸç^À]ÿò˜V\AX·âjiÛ…_œü`õ8V£|÷?D5È_§Ì䊌™‚B0êxðëÿ†÷îÚ‰@hqå .R‡žž.lظ=+/˯j*zVw#iÄÁ?ô¾$ÞÆORdEýÊÕ-áæ±xvbØ?ŽVODDá¢k:Ÿ£¬¼iׯ߀·o»"åKÔ¹¦]k:®˜?bûÍ7¡ÿª~PʼÂP¸ÜÁt>ÔÕÀý×´]^À¦ÞztØ1‘'¬tÂPÏ÷?ýÙO£·¯gAáÉL‡‡GÌÞÏÚ7õâŽ;ïðö\Y>ÅT.ÚEí?¿âÿàZ…¯ ¢–[ðYŸBe zVw×lØ}ï©£xæÀd @´Ža×;ÖáŸ?¼µ†gï‹§‘s€hH™—(¥èéî¢(®€²¨Çd.A[ëV|póæÍw 9ÀÜ ººqÖ˜®DÖâÀ¦æfD#ó*xxO8‡`(Œpˆ!k<õ‡ó8rjtÏÏ^<‹¦–(tFæåñSC4‚hc´cŠ}Ò˜¡¬ƒO*ï)+]zØÒ}ÝõD’9+ ŠÊ B("‘‚ œ:/Å fÓLÛ š&÷¾÷ó8’a<ž‚Kؼ<~ ×…ilðô)¾{«oÙEp玦°?`sSPI ŠƒœBÁœWXc]ÀKΨ—ZÞÉ­Ô𦔩æñSÁ0adóžU-c¥`Á¾T°kî”CiA2™D&™WØíÿ¸ÝMZ9a‘º›5ܶsã_Åã§L:ƒT:]уæ¬<Umþ>Äß&&”68–íe“¾$†\b|4†-׿­&Q Tüðž÷â»?ÀTÊ@kcÿzëu˜žŠãGÿÜå¸ý¶ÍË ¨5ÊK)11‡ä²¢xÑàp”}¤c¤ã˜)`„hBVŠ#Rõô³ÇŸÂÛÞ¾ Ýk:k„67„ðàï*>2pßyäûh6‚{¿òn»í£xðÎóZÜO—^Ç3Oì«Ò¡¤ Dˆ˜)»”¦¤r(Uç°ÃÇOaðÈÇ]TXl»}-‘FLOMajr á0ö=½§ÏœYt¬mÛ8väœ:9¯”PH ×±dðí!¥¡1ÕW|ø‡Iß÷0þþe¸‹€ø‡¿‰°ÄôÌt¹hI&g¡**¾ýGÊUY5¹Ž‹C/à÷}kŽüRý€â8…iRº´ÃHUà§UðËJÅT.> ‘ÏçñåÏ}¿ùùóˆ%j”~õU˜ÉälÍo©TT¼°ÿ÷5¿ÅÆâxnßó¸÷s_…‘ÏÏ•-+F”k!8Ò™-²òP¨öœÆÔÏH)‰ªW©ü“ù\_ûâý¸á¦°ã· ½£ ¡P®ëb÷÷€Ûö‚V¶M ?ß÷,Ú£+Á#o ‹ãwÏîÇÀÁÃp… ¯Ü©j>@‘@–c]*ÍYÞ'}};õn\}­ÝrM0ª3ÑÚžQVÉR‰‚p8 !8Z×·D,€FTÄÿ4 Æòù|1mvÁ%‡+\páÂ-}'\¸ÅVzî_±Aضý©±£ÿ5DžΟÿ­eXÆÉÖúvŸÅ«ëÖRéç à’#ŸÏÀ…p]ÈKC(·B ‡5˜0+ÊËbBx2d©¼”¥Ï^_§×ƒš°àÓeÏñ[HUÙ—5¸‘%,¤‘Bâ3—÷ ‹¢ -¨AU0VSbÌ!•) AÛ° „OyYlEÔ %} Úë;‘·rþ<5T®Wç¤ÓC#Ç ¼0Øí)[›—¬.Š­,¤"œ Áº„àP˜²h“B"Ò}®âŸÇ/Ozêõ¨Š6©kô.¿ÎsV4*ïPXðX$ÐØ’µÒ ‚ƒƒ”{³@%¤eëSBÁ˜w+q¹ „€©®p‹.ãEwô|¿äZ€ 3ÚãæÜžáKƒqÿ|5ÒÆÓã©h¸Ån ·mOf.] *}¨ä4¥!¡×éX¹ª RÊ÷¥º¦ab<ŽÜl\z›·´§JÅ<—¾Õ–k[úÁ¹óʉKÇ>^­oMI §Æ†É›ÙgÖ¶ö»”Ð9¾ÊeåDð¬åÀÒ™ (õöÀb* ¹\®pÊ'Le.×'Ç뻣½  l¾ó\r-¸ÞñlìÙֺ׷EV­N³”K×gýÊg©qÇEGoTEYt88û§sp§h}¿åEùYHŽî¦µhÁK\ˆ›O^<<6Ÿž‹9¬ŒgÆÚl½º½¡³Ït Åt 5®Sz ÎÑÞÝŽºpxQ™l¯Ÿ¾P¶¶ç>nˆä „¡¯õ*©€½Æ¼shüå‹ )¹øŽd<Û×j5ZBm׆´P0m%½c­æ%к² Í-M táX0=;‹Ñ #¿÷ÇÉ 5£·©Ï2]ó€iêÛ‡/M-¦àåÙñ¦pë³ U¶­lèl&ËÛ¹ ) ¤Õ(úÖ¬æU^S5œ={S‰ÊéSŒ/A­½Í}²N‹Ä ËøÒÉñcŸŸ2.Ú—ÓíŠAÌLdÆö´ÛÎ…õЛÛêWÕëJP1nCBÀ4L¬ë_MÓæ÷)pôð1 / BÑjAWtµh 5% Nþ±@î¼80p¥zýÍÿÐlZuíÍ’Ò{Âjx½ ¼Ù²M%ke°õÝ[ÑÕ9ÿeV<1‰—žû#ÂZ=t%(LÏX®9f ë1áØ»»…^ˆjÙ•Ò‰Øà~ûûúvêõÆÔ-\ŠjF7؆½AaŠ^]U€]pÝ–PÛ9‹.ºÜúI¬§G禿€oŒYlX±©íÛ”‘ÏW‡BƒçŸ9:rðÖ7*ÓOó²7B“Ó“f³ù|udLg*9óØRË[r¯¼üòÙL6;VSétleSÓ K-oÉÀtjz r”BʦNíÝ»—/µ¬e0šˆw&*_Ag³y;6™Ø½²–ÀñC‡†Ò™ìx©´œNÎÄ—Ã}€eS3³'„”B ™š=¹î,#€ÄÌÄWg’³Ù©ÙYãÂèØýË%ç Ç…èÈgš"ÃR14pðørÉY6»4õ×% ÿŸ´ôû(vU±—"IEND®B`‚cawbird-1.4.2/data/hicolor/64x64/000077500000000000000000000000001416632607600163075ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/64x64/apps/000077500000000000000000000000001416632607600172525ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/64x64/apps/uk.co.ibboard.cawbird.png000066400000000000000000000111641416632607600240150ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞbKGDÿÿÿ ½§“)IDATxœå[{t\ÅyÿÍ̽wŸ’V»Z½V²ü’mlc;86Øœ&¡éIrzmChÚô”þÕ$'M{n hzÈi“ô´çà@JBŽ!P%¦²-ü’Ä6–¥]¯VZíjß{÷Îé{÷½zÛrÏégîîÕÜ™ï÷›of¾ï›+àÿ¹¥èd'v*™eÆZ€¬‚¯ç‚‚À#'±AÊ$ 3 $¨rŒå 3 '÷‡öÏ\iÝ®tËÀ¶­y^ø,¥ôVFU±QBŒ((À!ÀdQI!0LSHð¼2ošFÆæ»”({5)ž»„\V¶.Û±>oê_Q(ûF••1·)Mdõ,Ò…tžƒnèà’×t.­+…¦h°)v¸4ܶ6ØU;¸àÜ4„n!Jè?¶2ûÓ¯½ž¿:_n\¶õæ”(T]¦RÕ“ç9Äs1$sq)B@ÊÝ&=KY¤BB¢ø¿øÏ¥¹ÐîôÃëð ^(˜…¸Á {‰æºÿðØë³—¢û%pSÿ¶›¸ä?`L]©1ÕÍL!–ž†)Ep8)Á¯ÐPÃB|ñ§”üºk«Íƒž¶4f:ÏE ÓøaÊî»ÿÌ™—õ÷ƒá}ð¡µjI§ù÷l û$£Z{4ÁLf”ÐðÍH¨î\Vµ-KX€a]…”RXd8T–y—C!ª™52¨$Ÿ=|ûõ+NÀ }[7ä窪õåô4 &Æ!e8%ÔÝHDS+˜|iäëIR ´ÙÛ1à[ÝÈ%s<÷ÄšàŠ/íÃ>óŠpCïÖÏQFÑT›÷|ì=d iBA­Bm¾ô¹‰TÃ/‘Pœ uæ/„5ú%"„,’B°Â7›ê(ä écœh>6þFü²°9°õ>ÂØ½„¶sÑÓ0¥Y^O(Ün7¶ß²׬_žînHHLŒcèÍaœ<~Ò:÷€k6¬ÃMÛ¶ Y@€H8‚“'Nbè!d²™2pQWü®.ô¶õÉT>yŽ0mûÈùß„/ ×÷où–BÔ/ i¸ÆfÎ  ´| À½»ïÅàšUè]@K«ŠÂFcòBÑÉ(^~ñxúÉ}f­•RFñw߉ì#èêò£»¯ ª¦‚0 ŽD"‰‰sAœ=süýw07%¢ÅÞ†•¾A$óÉqÆøÖc&/‰€z·~Ž*ì{¦0Üc±÷ÊóœV&àO>÷Y|úî»°vãjh6m椔ˆNÎàÝÑÓxp÷×uë ôâ«ßÚ5×®AgwH³=Ò’|.c‡Oâ§?yO=ùt¼0ËŸšƒþuHæg‰flyo$1W{l>ð×ܼQú#­c3gÀ3BqßWwãÓwÿ!ÖnSæm„¸ZœèíïÁ7݈ƒCïÀÛÞŽoÿa\ÿÁëÐêi™<(ª‚@–/G‡¯Ão½]EBPÏ‚©Cçt¶ôxòzáC“ÉÐMÀÿN·Êøkšbë:= R6{ <¥¸ëîÏàî?½ ËæUº^(£ðú½¸îºkqÛGwaý®ª)ÕF‡ß‡'2é,N?e‘Pù}ÎÈ€·½­«ÓÙ©‡S¡·šµ3'½íþ§ŠcËùøYÂMŒ°2ŒP¬X¹ _}p7V_³º©W·Ih‡¿»ã¢Á—Äëó Ã߉¡ß¼d2iݵb ©|-] nxVì %Î7x´YÃ[Vl¿NcÚmI=A³F¶f•§„‚PŠ/ßûWXµnÕû_Õ¦Bµ©ïûyB6lº_øëÏWv$Z­+Áé©Spjn_çoÖFS ^xLSlžp2ŠRƒ¤Üp__Ö¬] ›}î¯Z„,:1‹•‹©ïpÚ1¸vݽ=eÐÕ»D3Ø4Çæ-Ûn®¾Áönìß¶‘P²*š‰@JaÍù xB(vÞ¶þÀ‚Ê Æðàž×žÉz}NÜÏ.¬ìó^–ú%Y¶¼ÛoÝgŸÞgy¥RÒâ¡D×õnn‰çf°«úÙ àfá*S=Ñôˆ5úe %·î¼î6ׂàÿâ¡p2”B"Ç/Pœ¸Ç=¿€÷‚±9ëŸ &1›31»@ýjñ´·â–;j©Ú…ˆeg`gŽM[[ûæ$àNÜɘ¢Ýœ+daJÓÚöJ.m¥qO{û‚[Þƒ{^CÂPà´©èëí€ä @¢ àÁ=¯5­Ïtw{árXëÂ<õ«EQxÚÛjâZ‡DRA¨ŠÖÎ?ž“€sËÃ;!-ñÜLÓh®t]¼“± !HçMŒ§A•âl#„ ËÔÌñR}¦(…cHåŒòïšÕo&ª¦YSµ®Xñ‡nè ”“Ê?𓀂‘ÿ¤ÂTg"?[ÁÕ’ „˜W™šQm5÷æsv¨¢²Úa!ç„Y›ƒõNdga£¶ÞÍ›7—;¨!€Ü,H!ªÂÖúp– ŸÏ—³7MA‚¯«1Ê t{] U .¶~C)¡ë:ª3Oõƒ¤õ(¥*"Úº¦0Âzuž/»•„T9WÖgBNŸ:‹|nþÌý÷ìB«fÔ%V•ãþ{v]rýjÉfs8sêtÓ‘/c)¤Áˆâ&› €ªœj#¤jF­ßüêÕÿ*2sÉÊ>/öÜ÷ l8àÑ<šÀú€{þöXhÇùñq<úƒÇð£'ÿï;·`ýù$<1‰×~ùë²–ÕÊZÃ,€PÊ$PÞëýBS¸0ë­æ0zx‘ð4–¯¥s›æÊ>/žxàSå¬dÆ?Ù»{~ð8º}0!ñÃýÿ½áK_ø|Óúó‰S‘(N=Þ wýÓ¦0AAÁÁ»K÷ª-€H”&â|]†ç÷>ÐÄ‚ù†b'Öv OýtžùéÏpÃÆMð¶{Ðéõbó†M:€o}ûê/$ÁóðÜÓÏs¾`] B)Dyà« ÂdÖ ,›¬HÕÓóå^ÆÉc§Íæ¥(ÅÞ½û0`2<‰ééiLMM!t!„îŽÅþâ•E·—Íäprô]¼ò⫨]“«3Ì¡”AJ J¤|¯ 0ªT=XÉÊÖ"ÎM|s÷C8r`½° ²B|ã¡oc ·““¼²×›¦‰ÉHÞ<úØãH¥R ¶§ë>8Їw? aš¨œ"HÔ© +Å*à”’²òVC‡³«Žrå‡Qi¥:cÎàk_¾ÃoŒ 9;¿ÒÏ>÷<Ú\nÄâ1˜¢1i+¥Äôô4z}]ØóXÓÀ­,ÉÙ†s|ùÄbqHY‰**JXºãÐ\à"鉦HS¾£Vn¤üpUv¶Ü¼u/Žà¾¿¼Ï=õ8rpÙLã”RâgÏ=)ÐósŸhqÎarŽXûz­d2YÅÏ~òvþï059Õp~Püna·8)éíÒÜBêy])­˜µÑ ªÒ¹àw¹ln-[È@ ´u•¤ª“ª{éTß}øŸÑß߇Ûÿv\{ÃFØ6¨jq=9|ô(šñÄ™êD2Ö¶6ìyô ì¸y€â¢›Ïé862Š_<ÿ BA˜²˜” §G¨ÿŽâXz^äy6q|z8]ê¯f©ÝÔµÉe·{ÎåxÚ?# Œ2+T{e”³C¥,Qé;¡ ”Aa Ú<@JÄé,®ß¼ÑhtAÀçóáØáSðð6 ÌÎ&À9‡¢ÜJ€–>›Ò„)LˆòUÀ´î Q¼BpmÏÏÅþéhèà—J}ÕL£‘£ÝÌŸõ8|EÖ,EÃ(³_}HQ]¸ÉŘM¢ÍÛ‚|>ߨÌQrÙ,Ú;=H$’ˆÅâeðõiðÊሥgù*¬¤’ž~W'ta$íšýߪ17äapÓÈ´;;*ÇR55žÌ4(W•¢6)‡Ï煞׋ִˆbížVpb@ˆRû¦UêÉ®Ü|кZ{‘7²ÁýçÞ82/ïL ½ªs}:ÐÖ”ÏàdÕ±T…„EDÑ$‹Å2AaÂTL8]J@]T!”Àn³ÃTjÛB”M¼t4’Q²¯ÓÓ4uùÍz¼Ír‚R ñ7RðlGKל#/¤€Õ#a6-R%P˜•Q¾ˆÂ(²XTæzcf#%]Q Û—y7²‡&öï] 8~&kæð,#JÓ9.J£Ò ”Y}aT‚Ò÷A£ V?%ðU¥|‰üú©Ù×>RäÏ4$2æLÈSÉîÈèÙáÁ®õ¾“á#€¬„•U›‡(~•´â‰Ñ*_™E±ù³H " $0Ÿs¡m<«-¶Vx~1›‰½täÂ_7ëfN­Âɉx·;Põ6»jWùÙÚkŽ`¥þe‚¾H)½ RL»?=®Us¼É©°hNŠÊ4¬ë܈T.1f'ì£Ád°i´4ï°„SÁ¯³kÐãð¬—,]H5¾Ú@*'1Ø(¿ã#$л¼Œ±‹"€ ±ßŽA&Ì:ðÕa6X%ë»7!cd¦™`· ‡ÞŠ`YÐ.#©ÐÏ}.ÿ.ŸÛß/¥¤éBÉço´€ªWœŠWYL¯uôûávºªWç/Œ2¤3YLüv¢qÔÑüÝ€Ò4`„a}Ïõ(ù˜É iP´Js('ô¶ôí2I^ìj ܪ2M ÍŽ[ËgUl Q9 „HIL¤ÐÓÙ c1;P\/2™ LaTœœÒv,«¯µ$hІkº6"[ÈÏ&ÿÔ¡ÐðÛ õÕt¨—ãÇVOôÿnJO<ëqú²ëº6€€”Wúš•_š0%·¾s˜’#‹CQµE着!™L”ÛäÒ¬íËÚ ÌªÒîôa}÷&™)d'M9jºè5½˜Jp'äd2ôl§³+ÎÛÖçpäyYÊþÊïõ¿”¢0 Î9V®(Æ ‹šGG&™nØz‹‹aeÞ3ʰҷ>§¿*ÌS˜Üq`løÌbq]äÞ„SÁƒÝ-}/éf~—ÏÝÝÖn÷²”ž…ªÐ³GHHpƒ#0€Ëé\Ô˜ÍçpìÐ(xÁ(¯þð%ÿð»»°Ú¿BˆD†§¿·zbù§_}uálÊ¥’SáDð_}ÎÎaÊu=­}·­…d ¢`…£UÁ”ðvv »»@(™·LG£8}âTíg$üîN¬ö¯ƒCu¦Ózêøè¡‰ágNàÄâ /…Käd2øf·ãÑl>g×TÛ`Wk·Öáê¤RHäxÖ2Õ¢¨;W¯$æ5UQqöì{+#§æBo[?–ûVéBê].ägÞ™úZ89±¨WâšÉe{Yzƒ§ÛnËÿ™$ä‹vÕÑ¡QµÅ’Èő̧ &î¸ã8ì¶yÛ!„àç/½„éñi´Ú[Ðb÷ ÕÞ nšzA©|!û?šª~ãÀØ[‡/‡ÞWäuù-ík b|†Pz§i Sí”×Êm+èéž÷X-‘JáÈ‚0„03ºiä #@a¶ÇT)~9Z|zrÅÿ`bóæÍ*‰h0!?¸âƯoºv}›spæÜ˜~êSß%’ *ËyÙñRäý½t222bØ`¿w“ÿ&)q7¥ÍÝB(’Ùô;}ï•Ö«$‹r„.—Ì&b'Ó)}.(¯çd,>óÌRê´¤Œ¼ùæ¯gâ±(›#‹ÏÆsÉÙ/¥NKJ‰NŸ¹@Æ05 :thz)ZjKÄ¿ŸL%zï/Ÿ×E_Zj}–œ€l<þÊt,6S¿ME§‰LjÏRë³ä?~¼053}ZÈÊù ”@h*:ºÿ¹¥ÖgÉ €Ètä_’©t99ÎfåL4öäÕÐåª uý…ðÔd¼”G G&c3¹Ô£WC—«BÀÈÈHv":Pà Î1;tjÿ•ÿ3ÙfrÅ=Á¹d:6û•PøÂ >;»džßÿ)Ùº}Çk[wÜò««©ÃU³˜™ ß~5û€ÿ¶ß±òˆ&ÄIEND®B`‚cawbird-1.4.2/data/hicolor/96x96/000077500000000000000000000000001416632607600163215ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/96x96/apps/000077500000000000000000000000001416632607600172645ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/96x96/apps/uk.co.ibboard.cawbird.png000066400000000000000000000200111416632607600240160ustar00rootroot00000000000000‰PNG  IHDR``â˜w8bKGDÿÿÿ ½§“¾IDATxœí}wtdW™çïÞ—*©[¹Û-·ÛÝÐv»“SÏÀŽ >3 ìÌ®“ñ¬e˜Ý3{–]‚Á aŒ1<ŒcX0Á ãÆƒ:g©%µrIªøâ½wÿ¨ô*I%µºÛ}–¯Î«zïÖÓ½ßýý¾ûÝð¾[þ(”ÿŸ…\iÊɶ®mž¸c®#½†Ct &Vˆ>Ñ@=B% ª7„“ç c”ã ä  ‚žØ7´ã~¥ëU(¯6¶m¬g²ô&pq3‡Ø&Aê„0á ŠD$HD(”LppÁ!„ç˜`‚ jpÆ!Ta W$Bž‡»ü²¶}ûàvãJ×ûŠpSÇ-=Bˆ·2°¿¤„ÜH“€h²$SMöB“5ØŽÃ1`2†­Ãá8`‚ Î9$I%‘@©•zàQ<ð(^h²2¥0l†£ƒ1懃ÁÅo%Jd€þêðÐÎÙ+Áe'`Íš5ª'|+çxP¢d3@m‰ͧøA)EL"nÅ‘°¢ˆ›q0n§Ô$eRYäÞÓoªâAP " ЂzƒÐm +&I® àW”ˆG í}>›ÉeËFÀƶõ\–?ƹø ¥ðÄS¥‰}3ÉiD„ $§!nØ+WWd0¹s‘º€DeÔøjPçm@¿Œ9ˆ[QÎ8cL° „JŸFƒñ¯ýýýö²PF.97öÜX [ý8€ÿ*¡^IÓ¼š³ÉLÅÆ3" )eHö, >²)™Ëb:Dþ[:­„ô»HŸ €PŠÿ ´Ûá׈éÌ09g3”пïjÿþÓxš-3,¹ª\ªŒïŽÒ@ÇÈBˆÿBe¢i*Õ0ÅTb³AH>è9k'©w’»Bú¾…”ÎÂ/„Û¥AOQ DöÌE†€_  %BcU3¢úLfŒñ Bèûú‡w?·<ÈäË%!`Cç–õœ‹')%+Uªy4Ù‹ñØ(&bÀÏ‚]zêÜM ò®s —SÛ 9òH…×ÂMò®5Å‹ÎÚ.4š0— ‹ÙügŽJ?rèìîÉåÂi¾š,I¶a› ™Ÿ% ‚ôÔÒ©øF#C`‚Ÿºð²àWªnÖùäY~î:C@¦_ÈŸý^Õ‡• }ð©~Ìè36cÌ”@ï?0¼ëGËWªNË$ëC7·R°ŸÐuÕç‚a`æ ’f"h à‹ ¨°puÒù"²æ_Þýä÷ó šW4Vµ »þ$í +i1.¾cVG8zô¨u±¸- 7vÞ|‡àüÿ"AO<½€±èº8ÜÖ^Ôr*—?w&r$ˆìuÁyðtšLd¬j¼-€™ä¬É;EùÏûÏíºì.š€ ¡- ŠïP!)>ÕGfN#¢Ïš•ºÎsŸ¹4dGAnBr1ñBóõþ?¯³-¸.¸;gÓ¹ëžÔy{m:j»ŽO2‡;s”H·Úut©ø]7†¶~T|Y¥ªBœž>‹Ù%@OÓ „¢*P…›7b㦛зúZÔÕÕÃãÕ`Û6’‰$Î>‹W_~»vîÅäø8r°»T.‡fNSÍ­ÍØ´e®_=º{ºáõz!+2LÃÄìÌ N<…ƒûâÀþƒHÄã%ˆá.Rx¶eT{k°ºifõ0w˜­SJÞxàüžÝKÁpÉÜØ±å¿O©²¦rÁqzò˜` Y—CAKX{GgÞsÿ}¸mÛí ” ~E4¯Y‘A)…àŒqX¦…¹pŽå`p`O>ñ=ìß½ßÕ½ª/Ò)›nÞ„wÝ÷.„BmP5µ µÐ‘Úmý„¦­žÂëñâ~?ÞzÏ[QU]…ú¦Z(ª²`Yœ Äçb˜›Æ¹3xø3_ÄðÐ…"倎®>ú÷EgwšÚV º6Jé‚e˜¦…‰ÑIÌÍDñ‹ŸýO|ó_`Z†ËêyA+ààÐd k[n€n'…aë:Ýtpx÷‘Å`¹hnèÜú ñï*Q&NM?mõ$÷I@êá‹_~-m­u·V|¡.039‹Ùð¾ôÙ¯àÅßgŒ¸ãwà#ÿ0jª±¢©„.Þ¶LÃÂÙÓÁ'?ú „Ãá<Às-"ý URñú¶ ˆa3;LùÆÅtÌÒb¼¡sójÂñ™Hš$I89u ¿jÕ*|ãñÇÐÑB[g+$yQEf…_À Ÿß‹u¯[˶qòØ)BðŽw¿ó¡÷ ½»5uÕ®¡ëâD–%44ÖC–dܶívØ{‘Håæ#Žp0› £«n%1CãŽsW_ÝÊoÎ :•”Y1+Wþ™¦ÙÆ ”ÐF¯â¥'&ƒ žr7Y·“OBsS¾ùÄãhílFCS=–ˆKžÈª •½½½ˆGbX½v5þêþw#ÔÛ×sÑùBP[W EV°å–­xáw/@OêÙï²÷¹Æb³5#è¬ï¡º“¬2˜Õ2½ðËJÊ«˜€.ÿŠ/S‚7½5ê©éã0Óv1øŠ¢â«þ3:zBhl]± VZ–àõ{Ñ×· ë7¬GkW34¶¬eT°mk®_ƒçþý9žîä‹&©sÓÖ4šdÓ±Ö¶C‡G£Ã'¬K%ÊÜÔ±åN|ɯ´‰Ø(f“á"à³çéN÷Áýl¹y3B=mKv ó‰¬HPTþ*¼ï²çÁê*8G P…ƒûæY}n’˜‚¨1‡:_=$"Iްß ´~óBì‚>_ îŽâ1J¨l2 c±Ñܰ’<Ÿr¶µ·ãî·ÝöžÖ%u†•Š?èƒ?è»dùSJÑÛ×…7¿õÍhimÍÍèÝõuú@€SSÇPå­%Ds(ùô‚e,tÃ`èÂû %¡ VM†gÏÈÌZó•ȹ ‚ûßûh>ª¦. WV<^|þê¾ÿèjõ.Ã+ ÃrL …Ï¢ÚS¯R"ý͆ÐÖëçË^6­Üä„V%Š6ŸDÂJf— r <Ëhikö;ï@ckãò"q¥½£·Ýq;š››sË)EuÏc± ààˆD¸`_/ïyû€Ö@Çò¦€'(Ÿ™:š¶‚œÿÏù}J(îyûÛpãÆÐÐT·¤ÊF~¹ãþí·‡±½3QíÕÐTùŠä²,c6<‡Ù™Y;róOŸ„0lm5ÔpôPsUë3cÑ ¥ï./ô†ÐæaERZu+¡ÙànðóF?ß~ò[¸iëk«]Ñoüx¾ó«—`qŒPH`P(Ã}w߀÷ݳñ²æã–ðÔ,öì܇Þû@z"ÆÁÓGþ¹ÈžßÚ„¤™`6³žzidß_—Ê·¬ ÚÚúJi}•§ ñ±T¯_öH5GǃŽÎ.x|‹~ê¿Ã·u&õÁa ದh>ØJæUü¯o>_q>:Wá0”Ò%åS(þ€ÐTO~ßWð€Ìs½Ñ¹¥Dóh „ µ½¥È 3u L%F¡PSlüE©|K°¥}‹—¼Á£Ètrªìla©Z¥Ê$hniãlÑãþ‡ŸÜ GJåàóiÐ4œçG:²_xrç‚ù¨ŠŸÏSÒÇ.”O)!„@pžëˆË¡ûšqpb²¤z¸à•àPéO(÷È*¢úlè,Øp¯Îç^>Ÿ/Û #cᨔÈ~ÿûý˜Ç zò—¨$ct*¶`>Šªb&¢ã÷ÏïÕŠ'i åSN¥ðú}Y+/mù®áÄ4üZèæ×wm«)Ò¥TAÎÝÔ¨ÃJ±[Ò Q ±Hü“†×b$E…¨†âõ—ºY/U˜êõA TCVK÷Gåò™OdJ]¤Ó ®Ý÷ÏÓP$„Sáö ó,MÇmª¤Ò¨>[&ã|Ö3̆ ÎÃäó(éG…•‰€W+îc–+Ÿyÿ† 膑‹c*0Èrn(¢G@‰¤qÁo*̳ˆ€mØ&SJºUYCÂJ–nj.qžšYxr]$-õp¶0qœ9h]Q~x»\ù”B035“·œ*üI3‰J ¶~_„V¢Ãî#Q%º(ü:›i!óÁèÈ(@lkqMûïþó­Ù¼kVÙÑñwÿéÖKžO)±¬”‹½{WÊ;  âvš¬A€¬)¼½ˆBø*b1Á`1³,Ø@ˆÇ㘞œB2¹¸°ûM×wà®­++‰R^D vo¾e6® ]ò|JI2žÄÔÄ’q½œ¥¤VÙ JHMaG\4/ç uˆ /vA!™‘@°g×>´w´¡ºfqMü¾ïN´®≟÷Ã2˜HÙ†D8T8¸ÿmðž·mÈÞ?‰à𑣜ãºëV£¡¾~IùT*ÓÓ³8°gæ h™—(3t˜ ™ÊP¯0—ù®ˆÁE5!”2Vú‰šÛâ /€Ï¿ˆ»ÞòÀ¶èÇï½ç&¼óëð»ýgðê©qÀúk[ð† ½¨ò§F3‰D_ýÚ£øÉÏžm§\!zçøøƒ‹æ¦¦ŠòYŒ8ƒžHbÇ »òA%ø(0U eÌÄæÄž¿€ˆjB„ìÌ3šÉÏ>ŸõÓ'Oãô©3¨k¬EK[Óü5+!U~ ÷ܱ÷ÜQä.1>1÷øŒŽ¡7Ô‰ºÚsÑvìÜ=ûöáѯ|ëÖ^?o>‹•Éñ)œ9ygOÉK'óŒ¼2­àœA€ DªvßU¢ 2ÄÒ÷³=ùÍ'15†ãTô\º"I$xÿ‡?#™Äm7£§³ ~Ÿ~Ÿ]í¸mÓfÔW×àƒü-Μ=»lå:¶ƒðä,žzâ)Ì¿v9¿0Á.òƾE(g-¿l+Jœ¹åı“8yô†F–ªož0ÆðÑO|fÒÄú5kaÛ††‡044„¡¡a ž?D<Žë®éCÓŠFÜÿÁaffy¶|Á±ÃÇpüH*ü&Oœw¤n „B&2!!æÜwÚ‰.K¥ %OŠ)ùÊ羊©‰0¦'gÒrAyòûOáÄÉX{íjÌE¢˜˜Ïú`ÌÁÔô&''±ª«Aoþñ3Ÿ¹èr§&Øž ã‘/‰¤ûB!$ÂiÄ}g± @žf¬(Ûå¤OE&è€ÀÜìþÇ/btdñhéùD%219‰Çÿ^ÝZ$u33ál¸y¡Äâ1L‡§Ñ×»ûûñü /,¹ÜX4ޱ‘q|ñÿæf#åì-›WÝ ¿£’‚Th$¡˜Q÷÷ÅPœ'„ȪâI‡õ‰p‹ ¥ËƉ£'ðèÃâìéAÌ„çŠþ¶ùÌC_@w[$B1==µàýÑhÉd×­\…Ï~þa° fÆ…23=‹s§‡ðõ‡ÃÉ#'³u®½f…GÙÖ ¿ìƒi[æÁy;lŠP=ô0BaŒÁ«øKB.ŠHq)"RaàRöüa/>÷ŸÇà™a\ÏÆ×T"gΞÅ˯¼‚P[¦¦§ÊZ~¡„g¨ VÃëñà׿ùMÅåq.pax ƒg‡ñÐ?<„=/î-QÏr¶ŽÜwÙú§îòk~XŽ.ØìÖ/¨‡‡­æ`ë$B¦c i%r‹Kežˆ•Jw§…'§qpO?ºz»a&<-ý€c~@>û…‡¡Rà±xåËÇB8Œ¡¾¦ϽðÞõï˜÷…@d.Š3'púø<ô?>3Yà _Eié°ud7vä¿Z«C‚qþìXtä™y €æ`èOÁJI’ÓAX¹µ’ * "‹cûoþ€ðd+š›073Æ9!é°ñ\cB`hh_~ä\Ûs &''+¶þŒØ¶†úLÏL£§§ííùbÌaH&tLOÎ`ðÌyŒ á{ßø>¾ûØwD³,D € Ó ©»¾¦cš ä_G#ÃûÝz–êP"=ÿ½Þ×àÀ)”jz®É`Ö7’ÖA\çœ1ìx~'ölß‹Õk¯Å¦[7a݆µÖó¦”âéŸþÍmH&`‹\âR$ÎÍÍ¢¹¡ ßÿÞôVCpžu„Df#8ÜûwîÇñÃ'À˜“ÿ/åjK¹Þrî8óò©~¨²\ŸS(£E[]K¶Ëõ¡›[%*Fê¼+ÈÑñ—‘°â¥ÃQæ½&%¿Ï„/ºÓEAmC|>,ÃB"–Ä´:…[6oÆäô{i:BÚÛBؾgÍFø>¨Éd³áYضÉP.º!›×5ÏÝ[ø·îëöšNÔÓ£ç_ÙÛU¨cÉðòð®Ñõ¡M'flu½f¬¬U—¿ÎšæÎ9¡à HUÒ¶mLOeÃ[ðÕúˆ%ƒ¤ZeXQ[‡ØùHzR~¬vF&=×^‹‹Ûýµ„üŽ9s^h„áè!äéR:–}z"é;·Ì¦ªL|çÂM²¬òi¥y‰J [Ÿ­Øµµ ©'A)½¨#‘ÐÑÒÔ[r²V\Ó“Õ“»t-$I”ñùùNǃ߀O €s.@ÅE€ ú8‚¡Á×T²*Õ)•R:÷“‚í?Y@4ºš:$“ú<±H•¦e *X¡e€/Þj”I/2’´ÞEm¡d½‹ñhv"jÌ ÆÃýç÷¾´(휅 ß[1»µ&TÂæK½x‘‚9ëÙm=¥¬Þ½ý²€ª(‚_t ER dá*§ÐW‹lZ®9Ež~…­¡„«‚€*©h¨j„ÍL“ú¹r8ÏûW&ò?9ŒMö Áß8Õ4ß’„䃜³z‘‡ÏïÍÆ-Ça;6|¾@¶u»·!dÜ/Q§¼ú¸¯ÊàÑY׋¸c,R5¤ýtIìÚqŒsñƒ¨>kuׯ¥…Ý­(¡tì<«ÅXh\p0ÂQåÀ±œ‹¶þÌ!‡Ï£ƒ•¹d]’»5d[lRÊå×h¬j‚á$ Pò±íØ^v$±`ƒ¢ðO8ŒqÖ`G{Éùð<éú.¯"¢ÐêE )+äP\,_ `œÃëñ‚Î IHë‚|RÜ:µð´žåÜoO}"ÉYá8öÉþó{Jv¾°o`ß„>4cf¨¶>Í_¤T‘uä(ò+Tär [x6¶tÙZTU'¬¨Ü¾ üüyA®Õ—nù-Á6øU?,n;å`§•ñ4šYÌ:9g„y_ãõ ùP¦3+¼”ËÉOcLÀqœe³þÌÁ9ƒà¥Á/í’xIð n£ó©~t7¬Â¬>crÁ¾þÒ஽ A[ýýý6}»åØAw}_þ°-Û!•Εv9¢LKàŽ=©CVÔek²$Á°-p–né–(²>¿4øÅÀspPô5]¹ä,w˜=\E=Ÿ¬ÛŠÃØ^Ùs† |(aF­&´Cyn&L?}/7M/ypޤn@–¥e³~IV 'up§¸¬òD¸HpY~a "µºÜ×´7m"‘·Tú›¤‹Š¿Òlk1¹µ®5Ø.Ž„²+¤HÇMîøéŒ”].ð’R@AOW'LËZ¼^†‘œNæüwÞ§{’˜nÏ-ÜÝ’Ý­ Þ†U¨ñÔ"bDlBð®þó{vTŠé¢9û‡÷>àØÖogôikeãjÔxëJøxQdYÂå~æ³~.8¬¤ÎyÞßE”@›à™{] pë*PX§|×ÔY׃Dô9 ‚}òàùÝ?Y ž‹¤¸ò˱ŽÌ&§­Õ-¯C½¿© Éf*T<ãÍå€`0†I–@(¹èòغUP¶ûàó¬nÔÁåR{ë¯AK0„H2l:ÂùÚÁá½_Z,˜Kú匑èˆS§­|ŠPc›Í¬–Pm§d113R™ËÉŠ(8ÙhƒÚÆ:Ôƒàé‡6K=dYFROâÜÉs`˹BWTÊ-e:YWçËÁ¬j\ºÀ D’3ãì[ýÃ{\ –KûéSÉA«¯îš$™±ÙtŒŽ¶`HResú D…BàŽ(ËŸ½â¾:/Z[Zá°‹’*šŠ±É Œ \g¬Ðå>y^Z¦/P¨‚5ͯƒ_ ¢ÏÙL°OõïùïKÅqÉÀàÜ Ó½æ‡É*«ÇtŒëª½µRcU fõ0œ‚ØÒRtˆì[aš€R¥aeWÛ¾(<ª†³ç13:õ¸×§A€UžZ¬m½L8"fE|¨h÷¼±’‹"1ÈÇ£#?k©n?o2ã.™*´³¦›XÌDÜŒ-è† £hD:…ª2zzzp±Kª¦âø‰ˆ†#ù.¥ ( A€‚îÚ^ô®èCʵu;9#(}ãKçwýübñ»h22>Ô^×ù Ã6þÜæ¶§%Ø&×ùê1æ`³ò6òbêrž(5­µÀïóK&€ ŽcGŽÃˆé%@çEà¦×ú걦y=¼ªQ=bšÌúƒGm†vŸ^Ü–¸074Ñ =f F ÇØ*S…tÖ÷…ȈY±ÜÃu×s»LÅ÷Wœq‚hil^òÒ´¬(˜ ‡qúè)8¶“nøÌd*=M[¾_ó£·q5BµÝˆ™;i& ñ`ÿðžŽDGÞ†S¡,+@j„4y¡­¶ý†cÜf;VuÀ”;ë{!Џ îg©ÉøàœÃ[@OW78[ªªâÌÀ9\87œnºA/ãÿ½ª+úÐS¿ ¶cЏslÇú)·•7õ_ØUñ«RYv22:7<~ktë7fƒ‘Ó16ZÌÔª´ ÜU¿~Õ‡9Ðm×V"‘£C¤{gªÊ¸æš• Ù=Ê‹É2Žž8Žðød ëÏC5ÞZtÕ÷¢§¡L0ÄŒ¨c8Ö>JÄ»û‡ö~i<>”¼8-=à}² Ûäx§ùלóOk²§V¦²Z¥Õ¤ÇDt 3úL;·ý3¥¯:€7ß}ꂵàb û(Á3¿ø%&‡F] ˆôy@ ¢Þ¿ÍéÀƒ¸e6·¹ÃìW©¤|âààÎíË D ¹,dä^Ü+ vŒÝŸý Dåm²¬0äQ|ZºÇtb }13 Î$YÆ–m·â†µë`;‹Ûy)Ic“xöWÏB&(’†*oµÞzÔûV@–d$Ì8,f6· çü„âŸË=@¿rY pËæ®Í]6'÷qÎß)Sy¥BS–¯OñC–d$­8bFj‡†;o»½h^±ÈŠ‚}ýý¸ph5€ V UöÀ´MèNB0Æ,›Ù b!ôIYsžÚwf_t᜗W®nYß½µ“2Ü͹óvJè™*B©MA5QÃÉmz TY†báÌÒB)ÅóÛw@š p¸Ã8çÌã0gÀ‹”Ð Åüuÿ¹þÈ‚™]ByMP 䯮-}àt“ßÂU~Ëú[×]×ÑÚNÓ$ /<÷✈ðƒ‘^¢Dìe÷öŸß1v u_´¼ È“®mÛ<ׂS[oܨt³¥Nœ‹>~èÎWÜ¿ð_\9YÊrôe•ÁíÛh,~zQ‹r”`lbŒ¯êîî¿Òú/$‹ÿ»+ †žüa,_WW]+UÒض‰x"±ïùç~}ÉþýÔrÉk¾€Å쟌\Õ+±~J)¦gfLÓ¶¿s¥õ®D®Šp¤¿ÿlMu]bàJ矼B002dÃÒŸ½Lê]”\-L]ÿqR_8ZÚ²LD#±¯¼òÊÒ¶e^f¹z0­MN&æ‹ÿ!” ‹Úº®?q¥õ­T®úz;wŽðù:a‚sCçM8Ò3eozÉUCÀÓO?ÍâÉäó‰dù¥xÓ¶0¾üò®Ñ˨ÚEÉUC$uãñ±É±²ËÂ33aG7Œ¯_N.V®*TáüvàüyÎxñ’„§Î³,ðyÃÁ_krUÐßßoÇ“ñŸ'’ÅÀ0 ÌFçÙ·¯ä¯”¿Våª"LÃþÖÈè…„ûb€ñ©IS7¯]9Í–&W«z:^<;8`;,÷L9ç8vê$º^v/ÖkU®:ž~úi–ГNÎÌ8‡y2i|ÿСCKÿa¢+$W 1û‘£'Ž9 ‡N7M[èJëµ¹* èïï‹Æb¿ŸšS“b.2·óHÿòýRße”×ü™ròú›nz=ˆt˜#6~ißk~í¿”\²¸ K-㣣ãÍ-SxöÐKûq¥õù£\¥òÿ'oÅH7|[IEND®B`‚cawbird-1.4.2/data/hicolor/scalable/000077500000000000000000000000001416632607600172625ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/scalable/apps/000077500000000000000000000000001416632607600202255ustar00rootroot00000000000000cawbird-1.4.2/data/hicolor/scalable/apps/uk.co.ibboard.cawbird.svg000066400000000000000000000316101416632607600250010ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/meson.build000066400000000000000000000023561416632607600162250ustar00rootroot00000000000000i18n = import('i18n') install_data( 'uk.co.ibboard.cawbird.gschema.xml', install_dir: join_paths(get_option('datadir'), 'glib-2.0', 'schemas') ) meson.add_install_script('meson_post_install.py') i18n.merge_file( input: 'uk.co.ibboard.cawbird.desktop.in', output: 'uk.co.ibboard.cawbird.desktop', po_dir: '../po/', type: 'desktop', install: true, install_dir: join_paths(get_option('datadir'), 'applications') ) if get_option('appdata') i18n.merge_file( input: 'uk.co.ibboard.cawbird.appdata.xml.in', output: 'uk.co.ibboard.cawbird.appdata.xml', po_dir: '../po/', type: 'xml', install: true, install_dir: join_paths(get_option('datadir'), 'metainfo') ) endif install_man('cawbird.1') conf = configuration_data() conf.set('bindir', join_paths(get_option('prefix'), 'bin')) configure_file( input: 'uk.co.ibboard.cawbird.service.in', output: 'uk.co.ibboard.cawbird.service', configuration: conf, install_dir: join_paths(get_option('datadir'), 'dbus-1/services') ) gnome.compile_schemas( build_by_default: true, depend_files: ['uk.co.ibboard.cawbird.gschema.xml'] ) # Install all application icons in one go install_subdir( 'hicolor', install_dir: join_paths(get_option('datadir'), 'icons') ) cawbird-1.4.2/data/meson_post_install.py000066400000000000000000000004321416632607600203420ustar00rootroot00000000000000#!/usr/bin/env python3 import os import subprocess schemadir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas') if not os.environ.get('DESTDIR'): print('Compiling gsettings schemas...') subprocess.call(['glib-compile-schemas', schemadir]) cawbird-1.4.2/data/no_avatar.png000066400000000000000000000044371416632607600165450ustar00rootroot00000000000000‰PNG  IHDR00Wù‡sBIT|dˆ pHYsÿÿ…@tEXtSoftwarewww.inkscape.org›î<œIDAThÕš]ŒUWÇûÜ™¹Ì'¡…B¡©ýR'Ò¦­5%Äh›hÄÄë‹1ñÁWM4> Æ߬Á5Æø ÕÔÆVbL›Ð©D‹ÐÒè0À -ÃtæãÎܽ|Ø_kŸsî€3ã&gÎÙçì{Îÿ¿Ö­½önÍþ¯>)ü·â `­­ë?ö&c0îø³¨“øÓÚ¿6Æ`<`w6ê‘© ‘ÈLj F@å¢(ë£Øñ¨“”fÈ º˜ÚEw‹Pø¡*Xƒ¥p ×’*_qb)‰„røzDL…ˆÎFAB 9muqX•aÊ–N÷Œ7„˜D$x Å€~Ί¼Š\ÿ:_kÑ aÆEMPêÝ+ë]I\Œó¡'Ôõ½ž¼Ð‰ˆ–\yÒ˽ ‚ Nóþ±óP®wñrq ÷¢ÚïJr `pâ¥ö)V2"¥ Eò‚Hžq\Ia“¦½Å“õ½M]ŸÔÇЀ§¤‘€›Œˆ©ï—ÓmôBATQÈ÷JóÎÒTt.AÆàWœhd’-’ô<QgwÈó9)Eê~ŒŸÜÅHJA#> :“b²´h²¤•µìA"TT“/-ÎóìÁ<ÿëï{Ó˜R©ŸWžÑ?Á#EÊP â[{y‰gàÐϾƒ•vÕ¶â¾ÐÔųLž9Áé¿¿ÀÜìTé}ÕJ¥#ê¦Ô Rߺpšñ×_aìÄ®]y·f„T®åzš}ñz~vš¸£±*ÌûâS|©ŽQ«-!Ô@Ä>óWßKßíí/U’ÛBÝ.*R0†7bŠSo§§ú7¾(‹{;bARáemÛQ» ± KÄß»t€žÞ~zzû•l5“‡¿…‚‚¦)]]MFnÝÀØÉ#)u5©r¼ˆ VbÅ(|VQj/ø¯Š0qö#›w7»âêMù^tZ†‚ìbì´côÆ_;ÊÂÜŒú°$™ÜZ¬_’–ˆ¥5•󧎰åÎ]Ù²3üÓ}-˜Bê\£ÊÝ;FÁ l{™ãÏ?•Y?n} @Û6VÚIBú(ýþä‹Ï°ÜZÀÃŽÑ+Y’K®ÔwÈXIbí­w`=;w? Àñ~Ëå §k¬ž¬ï¬Ý.-FÚî~åf§&8vèWlÿÀC o̤Y+¹Ð'd!¯«ˆI›"ž}Þ›°í6þùw¹6})«²ºÓ|»¤ÿvÍ3w,·æùË/¿Çrkîf÷â‹ãd1–e0Ê ²ŒÑÓÛÇCŸü2¦h0;5É3?þ&Ó“ã‹­Û¸ôk»Lî+í/-ÎóÜO¿Í¤Þ=‚¾Á›²,V–šÆZ㾇ÿPª<Ég\C7oahÃfνöWçf9uôOtu7Ù°õάtÖXú¨Máâ[¯òǃ¸|þMF÷îçƒùT-زŒÊ¿‘€+:«õy*ö ÛngxÓv.¼õ–ç9êo=„µË o¤»Ù›š:Úv‰ñ“G9üÔyå¹_К›cÝûvôs5ªÆò’´ZÇ]‰Ê¢E]ÏÏNóÒï~Äø©¿e/[Ëmܼõýô Ó좽´Èâü5®\<Ë;ão°ÜZˆcG6ñð§¿Â­wÜ—wJµ¥y¤3l‰X^ÝFyçÜ›œ<ü4c'^ÆÚº¬Ú†7ÝÎÝ>ÆÎ]ÒèêñÖ™L2°e"åfÊÿ•2ßi¨[ïV׿­…9&ÏgâÌ?™¹|‘¹™)ZóWit7éY×ÏàÈFnÙv7·ßÆ-ï‹i²¼7”.ï ¥Ü¯[ekÑ^…lý"ʯaÃZ6ç®æïn®cÛ½÷³íÞÔ²Ô¿/üõÁmý¦””u½’¤|-À¹ª ¶¤ÅNXhƒ1]â¢F/'Õ7¹¬"£våTëÔ:n¯WHèœ DÒJËHuà%/„@ÔòɈ&ÐüŠ"‰`õ #½w£Ö³a‰(ždœGt%ëú$Ÿ|†Õ±ÐY67L|L`›v#¿?ã­/’o¸]õýˆ#¬%$“PFFª©rM Ð+X_$XÝÔ.T—51 gÛëIfU‰ÖÁ­O܌қ  ½¡´Ϋþï(‘´ –YŸz$ýãÁV˂մ5ü¿^ÏÁš~#ʵr&ÒžX;hÝþØ8ù†ÁÕŠQIEND®B`‚cawbird-1.4.2/data/no_avatar.svg000066400000000000000000000120571416632607600165550ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/no_banner.png000066400000000000000000001361751416632607600165410ustar00rootroot00000000000000‰PNG  IHDR€@£¯Z]bKGDÿÿÿ ½§“ pHYs  šœtIMEÝ 51Ö¾‡žiTXtCommentCreated with GIMPd.e IDATxÚì}]zì:£p¾žõÍÊfËóTL ,ׯ+©É:ÿîÜÛç ™³5Ð(lå0ªü­f{5wMúÎ×Ñ([ƒ _^¹©¾Ï¢Ã$››{ûå䔓ÓÞpüž=½¯¹ý5ͳ¿"¦i¢|d;ÙÍI¡?‚9×÷1iÇŸóß6cßG`™‡Ë*0Ì€Î'íuÅ^~CùMüÿîÎ^ OWNßcú™1}òå÷Âv³æK™?훫ˆËµ43Gß5òûãósÍ>£ú¬é_ºL<ûìXýЋ–Ÿ‡ÀççöŸÿµïð9NÅ22^Ù-ëÑÊÊïqð¯´àÇN@ýCƾ‚1<ö r¸ /Æ ›„s‡ÏczœoÈýõ{¢ŽD'‘ŠÝã:®"fÃ_Ð'&™†ø­Zhvr@öá”\áÁ| ˜F·þ×ÒŸ=^4Ã1Ãñ㌡Áý”ËPPñtº.ø›$ŽMÇ|OœóŸ#|Ï ªu®  ™†’À^—AG&¥M%°¢ù¯}N¤`;Œf¼¶YbÕ÷šúU‹;6¬Z†©ú]ÝÅDÁÖ)ß`î‘áÑ0Vs~K( oÿ•ßp!ó:}§`ýó\@óëXvå­4Ûþ)n;þü*à‘“ƒ¿·'ñ”³ã׃#»;Åÿ¼›hÙê ;9úõ£­#lkÐD@£‰è—j®®2×ð.ã(ØÁHdXEléàÛ!pA°û©9ŒÞ f¥ æQq› j‡tYÁƒkS¡U–V‘´Añ£€!ÄwB—z^[Rà ÷HU›¬K”ö E%Å£6§Q±À4(â™Ö}ãÜbÌWœGF ®=ÝLñ‚Û×A½uã—HÙ·rÆš»ò«þºe)¾³ ᘢA-ß(Y@]5€Z"gÂÙ_Ãù¯^bƸ1kâ§š)i~³ó„_¨é.½ÕâżÎ.ZÉ­âXbwézçYÕéÈ?øµÐ£‹…±_ ý‹§=}ã¶ËŹæû¤GÀŸØìoãA0–ÆÏ£ïó3EÀs>—£˜†K†yËèv½~p´†m">Œ¶b:´:›Á!2‚A îõ˜\ɨ)GZÆ%ŒbÄ'»ØYúþŠÌ åQ#V ‡9ÀRäʱ [@T „—ø®Û‘Í´¡Og¤Ê7G‡2=‹)¿SÜy©êÑ öyüT¢lúlÁ`Ù™¦ÎªÇ¯Ô… n)×Ò¤Èz[æœ e/Í! ëV¨Ãº ©¬¼íÈî½Lç¶K‰p^þ0.“AT(žfLõ£>¿NfÚv²'Má6‡:å Cï’Hÿf ™³êµ¡¯c¸ÞߘJÓžåÎ.¡#]Ë0s~KÏî:¦œ¢gÝí‡ñô ÚîÞ/Ü àw³Ò%tÖÿ ࡽ3òà~ùv·§w:˃Ö÷¡Û÷ž<òÂL^¹ñp‚µçúçç°¼™qß ÓÑx䊧î$¶Òˆf 07£N0±…‘#W.›.aчBÓÓÎN­þRúŒó%ÚjÙwÅ4׿1 üØäúp°abcÚ׋ý®_—'ˆgdƒVcF‹®°'G˜ 6Rµ²°7ÓÛ ;J64ÙnþªÍô…sß\߀Ú#ö5Îî,‘Ô0(<שÏÕ ¡ˆ’CÄI”ØðÆÉi’.ôÿ×ôïFì®ÞŽ1ž¾‰’òøGÕöнà‹'r½ÀdFþ„ôLgŽ4WQï8ˆíæOÚY f/+9ÇšÇ`Þ>cöhüuñOsÏ~ðd à4ð÷ÝáûxVïé]džn¬|Ÿy{àà0Ý΋ì™ßq⃃߯ÄAë9úy´rËÎ_׬nqÛ ƒ˜|Kã8´e½a¹Ç’Óz§rDcW±Fô¿¶ÉÁÀæ<´Bü¯AÕ™1/®”yv†/•* ¥¨LA/ÙæVC{!F£îñˆÉ¥‡1€R˜ $Œ6`Y;†#›¦ˆ¡µJ×LZŒaögBi˜=6RùZç,Þ¥iŸFåÓË81÷£Gµ²æ™–„œ±%X‘:À‚§ã•¦çŒv§:1|èS@hÓ:÷Â3¼{Z®ëj-ð{v€ñ\Ë ¨Ë×ÒŒv©z½~+‡o}ð#"Í?by'>…<³Ò‰þ5‰«ŽpÇ29ž5áçÎâà~‡òeì«Àúù)ZÐGÕ•:cŒQ£ò €¼b3{Éš‡ˆÕÐ¥œ¶htŒê“°S$!î6AheU’¼l€…Q®gÀÍS´ÝœõðX'Íû|%Ò™‡©ËØzZ2Û‹)–«èj£Vâb(ü%ÎMõ°¤eBÄ€ ç:Ïc®š¤ØW®E»ŠÙ52wô‚ U¼™Ûg­øÂøìÆ9ù™Éb†Tò¦ š®Üø×ztž¼}¨î¼£YÓx’€È%°®Õß×ø&U¾«Ö Å- nÿk7p­MÒ¬”jœ×[4ï×ê¶9ÕŠ/õßßÏrZ¦ÎrJ*AX¾ƒÃú‰±©ž&¡€ì¿ÙÐRXÏ•5§ê·ÄΘ9W¹å^k@µv|{0ÆÿÕ[%T¬aÜ 8óçðʳZ™üë5×£ørþöY÷¡_Ñä\[ˆn²qŸŸ§IÒž èàLÀ,öî~¼É*èññøu_ŸäGÏο@xå½ö¦É€ÏÏ:æÂò®x1^Wî H8‹/âniKC{ ŠƒÍ÷]íHRMZÐM%8]züjÕ5«~¯xU…*2œÓQsmÝ7x§.[¯Æ y‡çº_NS&ÅŒñósÜÅëQÀŠ6ÖºK¢2å¦õtçmb“¡Î?mý¬œ´ þ6mõ”¥3¸)tòÃ*—û\cmUñ¥K5çÒ¹8së¤Öj¥R3æƒÅIZ“Nÿ&d±ºÜƒÌ2š0 ÜÞ@+1Û0™ÉÈ ØþÒ]zŽ9ßú^· ºuTàâ¿m ø5޽ìì6¨ÅôÐÖ³_Z”ƒg”ëž ìI…õâ †ö/`Lï§kðzî’ ä&˜‹­pàHgþÝr%'ãÐ8N¯GvrÞM:1U¾çÒ\Œô~ÿ÷Þ]ÿÌŠ[üü«lò¬\ÈûŽåñ·ü:Î>s8?ûÓZ$¼¨\ýIf¼þ¾ð¤Øt¼ß`ðTÏ(\}"àšk¿{¥öƒtÿ^VoîÐêÒ¢j6f'b¨£¾²8Rg­WîÉÂ#†ÓØl´¯…ÞeHØ¡}±f–¯U;ߊ1}ô\Õ ÛŸN2ÎggŸ¶õAyËì¢Ôà;"¾V»SjBñíÈuõ{§dqÎ ˜{$=dLð\æêWÌ/Xú••“Ì€äX§ü2‘l&ö;›ÅD8-CÃŽc‹~ØÝi¤ÏiŽ8“, ¬ÈˆV¿/Á ®}$e’T;¬Nwb}ë ó´âOÌg¤ÍúŒÄðƒ2®b¼êùܡգõו÷Ô2çëï¾ù¹¦ï/Lgàƒ€¯‹F_¤NßsH8éê¿j>? ØW-Ííj\¾Òõ®ÏÎ93^ǾG= ç.I<¸5b“.¸×O:­Ÿ)^‹”èF;_õ»˜¥º’£ÑÔX‘šΗÔ÷IÃa\ë e]’¯ÔÁýS@”ÙÚ(œR,¸ÝtW x¨@™g4|Ô€uŒ²¦S"š‡ÞZÄd§QWA¹Ä¨xKu©´¤Û{Ew7LToÖnÚŸ_0éu3f€‡Ñ~¯J’(ê|©ŠSVf®çuqF,iræP[:`ôíp°åm@Y4Ï@Õ ÐD ×˃y‰Œ?U„÷4­ýÖs´ÚzÖÌ}ˆsÚÌ™<ɱŒ•~c”®Âܧ3ó7ð$‘!–= ;ù[;8¦já8ëþÁ•-Ú©*Óo:¼tðÄ>øÏσð Ø·íÖ½â!³÷]ÃÁÛéš5·É%¨FK)5Hwv ]¼zÔI×t÷&”ƒ§¦Ž‹i0f^çRÜÔS1ëåuÿmÝŠÐiS£¡ÒÅò0© wä1ëÖõ¥hu!â-£G˲`7Üߟ½¤0œº{Q^Ê÷ Qxný•7”æúƒU¹:6\[¿ õÛU ,Žœ„e¯óEl€3—tãµ5–û—25hžÔ#lÄðõŽf}¨HÊXÞþzIm„ߨj·’šã¥Ëç‘ý‘ÆÈ5uÓ'Ý=á+óÂ3ü¨7]÷üÇ<¹üÆ9Øãƒ±|EšÓ]îTþkø$&'øÅó{Çk„²^#Ÿ„qÇÅ46Àw¸0¼ÛyÔ˜ï˜0À•Â;îô ú à§£†íœb„£]\+XY«\sè%GVó‚ÒrVn2òQÃòf©Î?(GÀŽJó(xúKÁ5#2_§äLh+³F‡ïZ65NáT;>탦E˜‰¨jÕöŸúMqØžfL[P³MÀz-?£7/NüIÕë¶sÅ…Hõ¹ä´3Êlt¾¿¹šx™a8+Û‘ ‡YÆOÒíLwûQ࣡ØvÍOžzU……ÀQýn)ù€0Ú)*)6ªRlw>‡µÆŽ©%4fÎt4Ë=˜h‘§íó¤ {Ã3É¥¨G­½ÔlVÒŒù5Bà{¨tåÑußÐ>’÷²­ówßrV¸æóôù1ÂøíÊ8ÇÅ­pÕ,ž*>žÛŒ|óúï>óˆ³é#µ0‚œˆV~k¼‚w¹}ð`ü2Ž]¤úYà%¼®ÙëèÔŒMˆb½ôç¡}–ÚÇ©ŠT16{p£}AE¹Õ_Sl=3+‹âòk¦Ãè4Á-¾`î*ßÊ`¯»(MFâ–æ$&Á1«9¤[‘zB´&#ˆZ­¬ØtƒÚøU‹=i´´¥x"é–Mê`m9!{FuqÎU1ŽR㞢mðîv.Rs‚æÿ…lî¾zŸfE-ÅñÉþ·y,•@šú4r:€±w?°h±ÀÙ3*š!ÍÞµÉ`Ñöíå6W$÷Ý ¾<8]6±y›¬{/»í]çrò§ð[˜ ¾Åý¾ˆƒzÑO¸‚ç·Œ?º‡unNõ8æóÜ ¸ßLâƒkÿ6^ÑOî°æJp²Ò›…åoŒ/LU˜Ðwˆ&¸D5­€IÕLn4•U!æ­àØ™8 fÄz]WAp‰-Öº:Í)iÈŒº`ì`!ëZ¤ÈŠDCmUAtçï¬XÙQ-w « W¯·JtÈ1:½]—yÃW«î»Ü©uI­—u¯J‡qª}²æB˜0bg9~Ôm“‚~L6O’'ˆP‘rÞBMº Ôƒˆ‘µÞü>®>%³]ò¤KÎ)(mÅkç|5¥jž4¥Ó=Åý èAW¢uŸxãÔÇå³pù]6¾¿nУÖoýŒÛ¢Ñ ‰÷ü©¦š·]âèƒÀÍÔ¡G‘Í3ó|<½ x½LO?úœ>þ[¸ßFýü¼M,³þn_Á¸ë)üõŠïTçS!db›ªdÕ¢êû›}=©™%(»88 ÑÑÊå¡ß¸ Q ž©–¬ÎHŠéS“Us±?ç" 9¾jbŠv˜*a8™ÆÈí{´çU*ÇÑJ¦#èoS+Ê “¢¨­e¾AÚøA C;‡8¡ý–¨ULýñ8Ã(|¬ ¤Öø:˜{Ý®€Ô):`3õѱGm8IS`$»YýoT¨ÚΜաŒø–§ôªñÐkŒ3E×¹ ÌæyܽëðË•¬Î Cû´óâHÛ#PÎu`åîøõ–ìA§»æ“7ô ß¥ËàèIïÆÍþâwFÀ‹a;Ž/V¶Ç<ÊX‰8šlaoVågæ*¶’œÖ¥v‘McáÛ·¿ÖOF?%‡]6Ly‰ëÅ÷ê0ßTÿ`=~¢rƬåÁÔÙѦRr¨üqÃ!ÙWÇ+‚…ß#þ5×¹!窪^zÃpÔéP¯ °®OZ·da Öûp_ºNÓ–#åßæöªeP‹9ª/ëHœƒo‚âêì§°êj™R¬Ðš…CÎ\”Œ#%¯Êxç°@Ÿ’h˜áåNRT‡iå»ZovŽd‚éM×o³ÀQßm Y0ùMzV„Æ:÷ú³«yPL3©”Ût wI¨qóÛê¨@êôóCýÑp%âÄ9¯è‰PæHÑWصâͧôqÜàg¢Rœr{ãøý5wâ:¿;n2×ó’ÄztÓ~kQ¦Q̹Lý’¢´Œ+l‘#Ú;¹ªÝ:É®úùÎQg®£õkB=Wžek†Ž­,n9Bªc #š3°Ý‚‘lNCæ@ðhf‡65rÕ8 #L äˆX‘k®éb˜nEÓ˜Õµ.tFšôK–LÆìþê(Ûlà1f7bë+,\·¥÷”hÐFuIÒŽç^¬I‡@¤Öó†Z©ëâJ^#ƒ_¦¹ÚÖ—ªóÏô†üO9æÌ.2].Š1§¢¾´G*»#|m›s¦=@IVD6Æ…ìÌ*^Poû”XÈb®hfMƒ¥¥7Q÷îÐôÖuÙÜ£>Ä+¿ù“ZГO<õ“‹×uUNrëWü±èçb Ü{_ŠÀ£îå•‚—ÎŽk-üÜ·¬üùy^J¦ouó¥FôJN¿‹bºŠáKB´•E<æÌg)²„Ö]˜óš¡Ñ¢g&,% …ÚÝÛ –op*Jõ} ýÍSÛi»+òØ]û[`Ÿ”Æ>¬k¿ìä—/-ŠÕ½Šü×NÊî Zô榹I[% x×Ôê²áî ºÍ· ±V…©ïºo„Š‚·LbÁK@á¿ï‘ÐM3¸š.êhZôí­›.š!ÅÑò™Ól›`àäï’b…mO2Z gH`8 ]žHM©|4‚Xàj }´/fåís•Ž´ùÌܯK®äqÛw“?ï?)~~c·×Î^—êÀ±ïF·Œí øæ9šÆ¸ê›x‡Ûöqª[ŸŸÓ"à±5`%BYÁ¾+¨w ï.U »z Œ˜ÒP×^T4™>#Õ¯Xƒé¼ZM¤‰…7T#†Šüºwô“RÉvJôoˆøÛ»U¬ùR{þpÌV嘡Õ²‚Ư«ÁøŠo‡æ¡×›hè¡ÚÊQ³F·2{‡QISÈT§zܲ¹ûŒ¦qú X"'VpàåJ­×ÜGÅŽ‰™Ÿü£êê쑊•cÞ 2áEAð .ƹ$g&hjÍ…ta’žhfþ«·XÙËC– ºèðÈ•Ð|‘dq(\ý­¿¹tTW]°ÌTŸ¨R¨m$ᱟ%ä`Eã¡Ûu+(¶ÓjÎO çZÇúÓQýÌç‡g ‹N|® ÙµsÎÝ}¾âN¡88?hùŃŸö1úi1~^q“¸•>.ú#g§^qù¥ª({ âÀ|ì}„Gr#@‹.0Ó¯AP¹sÛÕÍA/Ñ„2z=¾Ó—š!jböbŠœdQoÆŒ—;T»*áÅaQZÛ=, µõ™¢M^¥ú›OuRê`pÌCÄMÎ_+}W‘ºÑ¸ÞðPÊ—˜ –Ǭ0˜IºŠåV¢fœö€È>AJT'+Ó#;K$¥ñÓ'èxˆ.kEóo™aÍHýx˜éz÷eï©pÛûÚn™ù´kʈ¨ùW%­§»6>cw\›æ™Ñæ ß*^Ms«ìj´säz§ÓB°­Šf󷨝GÀx®ÄÕAræMa%îWJ¼û¤=¿–†{üé9œä àу}²Ç½ï¼bNW7áëªÐxñùÿ(ž¿É—½&®|¬ðÐæñ”6 'd£]›‚ųÓ`ÇØôè­jÐ:óèy!1±†ñ §=¬¾Uj}”*µq¡Ãô˜ÀÈêEÔÇ0hú˜az‡E·êx£ÙŽ“ñáÑÝ‹ª6;Í9§úbÚ™ŒÃÚø·Rœ7™ʪÅuÏUöš§ v¨³ªV%'œ®Rv—ƒßѨv”£ìR*—»2wWWæ¶Ö¨$çQÐv®‹Gͬà ÔÀç9]¼¢M­Fk[NÞµúûÒeã\gsóÜfmX†ö€§r%ϫ _ë™Ú…7ÎrL‚ƒïÄ?΂ÆÁêà“cêGÄÄ8^ÄZ¿dœ~ï;Z<òÂñسã wß}GsŸ³”~NúÅCÀ Å<ÒÁ•®|w®®µÎaS¤¤ê,¤Òí7ˆu¬¯‰£tE IDAT( ꦙ"]ûkSØ•+½öú ã0S=… ?ֹ帀3û3øüMx|œèÌ#¨dwLõ¤»äïv×·­xÍG,LÊMÇëuM£ã}.Gó1:âíŠSFRšC ¼ñúÏù¾S—$Zį‰ÊN§ÊÝ¥ìÐp3ÍRÓ'G2 œ½åXžÒ¦äFó܈3œ‘G:BÚr™5¢é©«"ÿYü†׸œ‰‰›1t?Â]gÞÿ†5Žó§ÙAdY…ö'/¤Žÿ¼®±¸Ò7¼Îy^æKEÕ¿ì†ô‰¯¾Ø=¬ë ò]ižfý„Ónæ#| ‘÷+œãW%u6+ûŽ R¥Z?ê#èL+?<ç{©éoµØ ­} ²T½0½QöêngD¯ãûf<ñ=õð#ŒSž†^àp¤¹úÜ4™G}^?IÇà7Í$au$ß}*>?/DÀ]d±’Ÿï£}ÃsÅé¶f'Љ£`‚)ÜiÀÚr&v˜ÆˆQ®FÛ½qC¤HV±ixÇÜ6 „r¡¿Ñ§¸a6N¶ ÀÙÎ’ÕhPÀ¡ÝÔ{ŠamO-÷cLdÇù·äKÚ»#^£¯°BD’º*l–;ª³gö°jQQzOÑ䊌¶ð–²a»üMÝ4ís ˆšÒÒ–v®§pWŸÞF‚0*ˆ÷ͧTÙ· ÁJ1梌ÎÉã;ÚN `{j¿)íÞ^q·U„•Ëüê$l½Œ}+FpËÎß>l†y»ªãŠð¨7Äûó³«ò±1,®ÑãÁ“Î÷¢^ÕçÃ+Nú@§¼ŠVå) fœr>Ðù× àuW¢.úXñ…XáË) Q5¨w6¡¯sd˜CO»Fç¹ûüSÙ›Šù"£XS®Ðê¯à×€´à+Ùt€VÝŸÒ¬ g˜Šœ’’€ã ;@$n¡Š3m¯TµÑÙZ·VT=<¶ó'Fz6ÝáÑõ¹îùa+…¡ÎØ¥2‡1±I UXÕ'Â0R¿²HqMëÐÉú{ê+‰6åD-hæ1”Ò4 uV¶â±((Ûqé)Dia›ŠixÜ$¢tÖ‘DE° «9ÝÞ¤W£K+HQàJc¨ù•x¥©Š\ζªÞ}Ì4qúüÊ@³Ì3ü—x‡y, ILnô>ÏJ}ÒNíŠSÓëµËÛðo"່šžGò÷ië¾ ÉÓFñCüó Äc®Oœíç°ÍïEJ8áÆ;ƒ¢À^?¯¨\®çʧˆë뎕àÃ(1³gPW›ÉAÊ&†ñ¢Ñbi£#½!]dh¤Ý¢‚³õ0€G‡]uœ¾^. ÎX·3|lø´Cjž„ÆtW¤X“!£€ùÊ*¼åpN>rínƒDýjrdgâõ]†&½ƒÖCi[c€É‰¶èa¥6NeÞB}‚GÉÁxÙèr½Y*|N—.¡áèRt»Ì—ŸÔÕho±¡ÀvöK•ïJ2ñ6¤–¼Ô BõKȪÁf2Ø=ãm•·Y®£*£‘~휃|㥮ây—Í «¹Õ\]|©†=ÅîÚåü© OR ÃûOìɨ°¸eªq¿å÷4ïØ“ìOÜf‚ôœ!ýÊŸÒC0V£ƒ™ñÊì:žFûÈ«‘øwxÑ`‘õµ&2ªF´cK¢J·Æ¼>è”›º‚ é¥Ì¹ÎaXéÖ¨5åø]š*­l‰æ€÷%ÂPm¦ZÍY ,<\›9OÔfT$Âf²aŒÊ¹Š7‘·¥14hãþkÂæ k¶@ŠK½SM×­PSê…/Þ4¿²ù ½§Þ Úùbmˆ ;ÆþÚ˜·ˆŸŒG¦Í¬$M.Is©·nîQæôÙä«ïJÝFW{nº·Skˆò«ç'?cJb„aß'¸ˆ…‹nF»Ž;§5T\miÑcbãkÐ`ÌÎGýú:šL|xü:ðú82áÓô•tù¸«úñ™‘ž~-O¾ÜõºvÁåiˆñÄLÀ]Âw–zþÔ†€yÕœs!¢™ãé1ý7Wo†ˆ<0{j S0n±àE¶õ#z›,ð‡Û‰ô-¦Få*yî6ÂÀÆ5hŒ:NuiW6Àd©ÅÆ:wn}ŒiXH" 85QÀ6`QòÂ>ÒM*ÖFÛ¼Á^ÚýLÁ߿ܱZ‰aÜU nj„ÓÛ+›ôT\îîͽXß% ë7.[åv¦yÛ\bÂGÞ/è3cŒÿB¥ÖÄ*©¸ùþˆõ2Ú"²éºìhd£Ã:ªË§P[µÑg)òéӭ0ŠŒ%§¬Áiò1’2âáYf‰õ¡Œ˜XzHUcÆý‰õNŸ,»îX¸©¢éUœpa¤‡‰o“ÚZ)«ƒÐô,Tyk˪™€Ñ-d1ˆfÌÐ\øç c°õ`cEÕ±âæË¤¬ìîÙÎf Q-+n²lzÆ£O.dU5Ë5w%˜K¼¯3¥›·çÇøh¤‰»J(ßë»OÀ¾¸Çéñt¡±'½ƒ1ƒÔØ»Õî¸R˜6(ãÖâ.¾ÅUú7‘Šq¿’óCŸ!ëÇÿp¡„€nŸ%ìÛÅ/ ¡XWn,;S•^…ª„Â,ØÂh5{Önü7Î dµ2F3W{ §:!¿2T7X”¡bÑo^é”6HÔóþüž‚¿åE…1Æ?-„šîênºžc£ì]ƒöyy(Owí7•þëó²Ob¥YÕŽÆ¿´<#Ý­àð³­ÚIB`ÇÕ4VJ·é½c<ʼ£mVÒÅn´ˆ)jÉq´¡Æ|Átÿdô£‚Ç* …*æ—Ö®ñžB̽EnÑÏŠ|‚Á1Æ­r–¸Ã›RÓyÊHÔìàþ”ÐaØAíg¹°•%¾^7]éú]ÉÎ?ƒIºm˜‡·¾æõ`4ïÄiÜò—0~cL÷BÁ#Üæöü Äã?ðè«»]˜l¯ã¬’È/¿Nx-||RáK惈y}®ˆ§1–±ûüý¸Póþ(a·ª<=‹R¯ï>º.~ǦR<Æã4Íê¸}š&ž.‡WðçÆ–ØÕËøü¬#`E™+Ê ŸmÅ%±uWóøŠÖ“O}¦&'Ž¿0æVUŠ‹‘€.ç>¤ÀÉ6~"CNu,®WMµØl9Š€DãP/U4 ²˜-,ôh”É@Óœæ9\…¢Æ®^‡Éÿ•\‚¦;æÕÖ¤$®Ïó¦ôï­º(W”ƒœ”¨KÔ`—¿—$ÚF©FÝ+“3ÅŽà­éˆ´Æª^ ÌÑМ€Âvìv vGZ$ %βlNiÕ³d´Z!ýw&©–åi°uì-Þîq+>ÆúøÚ÷\ÕÙtE#Ü™ä Jïø’Öáu¼âuÔ»~ü•ìïü¼ Èø× à»GdÏѬyÇêÚ[ œü]x„Ù©ðÌ Ç«ówîT}¿q’¹W“;É“dL_§ŸŸC¸Ã»Ý¬++¬ÙWNoÈï(tŠÔxcµ­_¢†ÓV…µÅ5VÝLµõ§’ät•1Ö\{I-6žw‘^Ê=þÄû;ôžÇ PT©â—yQM±Ù¢c›k=XÙÅÆ«XÖ—Öõ¢i\ì¸â|ŒÚ){’£0kf;Vø43áϵ*ü§ ªÑ—r¢¡ÆncH¥Fq¥Uªþ(·Fk,;/U˜¹í|šÙNî@ /'­ËFŒ("aßZÇ©(,Eé˜j™W1£b;¼;FéåLìî8K!+Ù¹?4Îê4§ý“ÒI<´zd%,>¶ŒHºÓÕI,l‡þpºït©LîmîJ°î§Ô¡Õ•ïbá»ÓÌñ§üÂØÏÌí5­òÃc¦ ·Mù =.1G§z]£ï œª³‡À”’ß0,„å7úÍäbÆúhä¯" ¨.áþ’ãx³Ï›f¾³¢àÓ)jM9N5œ;ÇÜ\1•Nßäà¤>N_8 :©~VË¥·ÚÁLˆ7°bÍN}LøØñ¼¡Bœ TÙnÏûßc¾Æg)íÀ„ÌFÍ1¨´tÜFí«,²ÃÐê‰{y;7倨šŽÝ—õ䆢TŸ9b¾µÄ¿JSkFNÍC¥KI·.ƒît±õ)` ò£ˆÄXMWì;¡Ã2Ú7 7ÕâÇŠýÜtšJª½ç’\(Õ÷ïÚð÷ÿkžÏ¢GMóÐÒZ|ªƒê+ÝÀëzÑMb°ë2ýÝÓì¨GÓÜ7iú®üÔ€ÿxÐýkf왌Ùó¤w-s]3ܼ.,»ò]<¾¿ãÉ9ÂÏOAÀÍÞôævAÅ<¢é´1ÜLõÊÉÅhX¡©wi2êÀ¾çŒë¬H7WFÕ1FqƄƇþæ•h7ÆþFëØcǾW»u±‹1¼¶Ôh~Ÿßk_ÈÝM‡‚L[¿`¾D•ÆÏ¡¼ßê¿›®®U— õW & áz³Ÿ)€‘“Ù׿ÿ­Ð€‚D!øÏ9L+ª‹ÎÜ×Bp6kJ(¬;rVUBC´‘Sµ[QÕ☢Š@*_½óöÆ—^ýmç‡StÄü³U÷!ª¸VÞ“&spšÊ¥u†åJH7jf“©h㨴£še•Í ÌÓËmwoGc—ºøNwòQ,>UóÞ|_ü;B°—ë ýM ýÌŠãIü›¯C¢;ƒ_ž£·èA?9°»Â!íêâÆ}^o¯ Û±³4#h5„.ªš ñÓu‘ÈOXn=‚"®r:Á^Ž£ºERoQÐÐÛø5ɌŠbôzŠ,£9!ݰptÄ&‘´ÕiйÚ!U.÷h3 ¾ÅsÓi92ÓãÇiޱyp)µÃÜí-õrxgªŒ‡¼¢Öôæ1òØHF(£®º‰ïŽS6æ·ed­'4Œ¼Bº¹hn¸¡ž¾®Ø…S\•Œ\Û†áË­ŸóŠÞht[ºEÑXŽÐUr'ÁÙ÷²ƒ…“d“à¢Ï%›¦“ÙvµmJN%>¢Q‡õKF ‰…ÎWÁ¦EÏ]÷Ê<#Ëæž]w+XW¹š«_ÉÞãàqWìù'aÑ <ʼö$rµÕ}u‘n‚žw#¸Acײ—O’».íñÁ‚ŸäÜCðÊ.{+Þä¡Ô:- %gNØ^[`Z#LL養,|Ôª|”¹\&ãðêE™Ìî+—ÀyÆÊòlˆ0w¶*`€ŸÕDV|‰NGì+Iд"³Q´ÎÅ:W£Ä×¹_´Ã¬ ù*ã]{‹á=¤Ñ·€-ªCCeÈ_3~mnUÔJËЕÂÆ>WÔ0Þ{èüœUT :ÃR8Î(ÓÔƒU{+Öb·RgFcqV9œ!Œ'æÄƵv>Ä¡r²B91×PWT½ë»â>ÄJ8bßÙÓ ÝC3ñW8 Û9ÎCìZÜœ9·Q…°ÜÒŽšçÈY™šh5$Æôaòy£ òï9'i, Þ-ëõߌwEÀ·Ç¹ï>Ú–ß%æ=õnÔ=¾Î“äjLÿkÀ ŽŸ€o‚PßHàý#é|<ç}­»3ÞÐÅÕeíó#*õò*ÊfD¨ç‚£çE+ΫŸèÕsÒ¯„Ž‚¢´îˆY5Ñ´ºqÁ—©íîÔ‘htžsqÚ‹¼åØäV©ÔB?W®/œšØð¾¿£€_L+µ©î>]µØs½’´gÝý{ؽ™¥4¤ÿ˜ù`Éä+ lùÄCVîq8yã““›Ef>ÂlSû2EW œ-NCò9ö˜õ[cÀÔ>óŠdO$î§øTAÚÐÆS•·"ÚÎ`iêœù‘Uµ£>h–³æ RUå)ZŽÔµØx-/ð—>¯´O¯÷㮪§ÒWæÕè˜>û;}À·Gʼá`8ÍUœä\7êWÿ‚ÝxNt…Gú ÝRa½N÷hY~ë½ôœ‹úTÊï‚€—ñèº{ÒΑçA¸%í¤„"¾Ã,bR]­†“¬zC )¡l^ñ¦È+ŠKñ7Bû3´ðˆy‡1˜jÒSè®7bÐÛIEPYÙÚÍìܱP‚w1êl¤ß×xŸF‡«™±¨ceÚ¹7§ÖæZðSSßJ|×M™™Ú­ N5¡¸ùü¨&7Õ™g§Æ/vDISúR¬dÍs„OÅj­ pEô^IÈäeú›€šìˆ\ôP9þEqæÉ‚#ÓgR¿uC¶oÖÆ°"µ•ó”Ci-š¹¨}ùYXÌM«7DÁò;… ø’ ¡È´Ëæ°æãØÏûbE R)¿Ëø… áägÿ}ëvfÑ«AÞ×ê÷>Â8ù}x¦þüü&Œ…/äÓMùrÚgŒ©NäDs°X³ ,b¦ÎÝŠ¥ :ÃÓZýÕv;œöK<ÞÕ§Óµ$[™¢Êþ™Læy(8Ì$ÆèÕVMÂ!¸Qi*èNRå•êµ,Ó„=³ù<-0F0PE¬Ð0î(‰pÿ…1GLˆ‘â3É‹DÔKƒH¿£°°;Õ¶«„ùì@ =ŠúþWÇpæ +„ŽÞ¨úd²MCYÙሺ¦Úkˆ»ÎÊ;VƉFñXeÝ)79uzâVЧú¨eë¸ÓâZh#´ÈÛ‚&ûf”=¤x—©h£ª-nÍ+êʘ>NVðë¼~|TßbçR,ú«ý€?QäÝç/[¼pVð°³,^!Î6A÷xö|î÷ëÞ 1Àãðy„>¯gÆç Äæ#>ȘaâÑ9·Œ-†Í'¿Õæ*´}Á…ôbAá'R.¸YG b­Ÿöþi ‹Ã Åеw9êZÑ ]_—*K¦ãÖôgWO$s5Ê‚5«Üï+±¶ßéÚcÕÖùÁcvÌ0}ËKWdzšÆ~½üËHH%²h’EêVY¾`E?‘‹ÞáWF×£Æu˜Ò‘Luk^JoôX!fäu—Õ,X-ž¼BµM7@KF×^jçkÂåU—©2U™]Ì’“€¬ãŬ92º«ónB·!kãÍXÐô)!c",•~ê¥bWZVnöwVÇü²|Ì«Þ})©Çý#|ÜÊDrù“‹‹{Üz·È÷¹Á-®5‹=[F/=ûÙ p7=}ó¢•Á9逯ßQ¿É¼ëW"àÝËëÖŽ ž^é²j°xîÝq~hzѳÙd ÝŠ¥Í`‘’‡Œ: ¡äªAí¦°¿Qé’âi+EÌèjŒá(­2ªNVÂXõ»-Ïy,k8Ìð€}ù*ÊNÓOF–µñnš7&â9'/Μ‡@1Ôië[‘á,S«Yø Ûë-ZùHGoúC7;ë*Po­Tìw­iš®P×?oFnÎ 2â4º]r“Xþ6œ÷ˆø~”ç«nWQŒ‚\nq¯Ê ö!æÀ¸fS-N÷ EkÔ¬€{è6â`UEJAS u‹SÖË9ˆ+§§ãÏ5Y”½Ð›cT´ÖùÕÝ»F½ Vô¶~5 úF—¡£uü.|À»ÏÆ0Ú½üGŸ#M|]ßêu§¯ÞŠJÄ˳e÷¢¯ß²”ü-Ê¿¯¿çW˜lcù“s¥h­–qþ i"ŒÚL'< QW±ž¹Å"í*f‚ë0 §wx­+Ó ›zCÃxšÜ€ò®ãÓNR*ÿŒŠ8ÝltZQ¨êÖ‚6Ðвå÷HW:·Ñî½0’l ÓÍX ‰©®JF©»øþº^£%¼…4òÄҜ܈T.ñ¨Ø(+û‡æÔŒ“}¹™×už=¾4.ÂéDËAÃVŒèõ«³o±íŽE¬'JµJŒ7ñâeTsÙ®W­Ã/$=A“á˜>°c‘3V¯Q³óŒ¦¹ã欠æÉ¹ b%óŠƒo´^ws®O øìòˆCÍK8<ŸvëUÔø àylÈ#ëyc0ûPÞÏÏ“ÑÿÑãd­½wí“/ð™§{~^ê)à>!S;“¯óд©1xbœþk° t@F\;JôípU#š°‡ 6Z­q¥ej,Íû“ÕÒm ¨p7G¦ÿd²›ŽOnæ+þðÀ‹Ó<‚L’Ç@¦Ê:†)eNšÌѼPDCJ– ¡C!PœŽì ¯®â(©ä°¤=å;©¥ ‚4Š7m©6ŽÀÆÀvl]§©S¶iüÄìÖÏ@×rw-°Åˆ-»e~¤#Ö\3®D¸jú\]Ú?,ìk–ÁÕ‰÷ñÓ‚ó.§Z,«Õ¤µYöá…ç¬|é¤V6Fe­s7:gY/©Cë&Åt—C‘ôzŸñüºVFõ,hžf÷ÉiœIøô3¾¦¹ûÂáÁÅò—ÌÀ3E¹ïW€çyvÅç烀§Î…;n QLúLg•ÿ«‹¤O9†2“M-å–3Âà ڡ,ø©uI7RÇHb05F5Iñ•^A)+UE¥eèᶸ5­`²Ôž5f 0󞂎­£¨rô\¡,ŒJ³ï­Æp¦xŒéâ'Cûƒ+¼ªã‘Uèj·zfoZXЛ™ØW¡¸Õºé(˜ʦŽññŒÿEašsES:U5rŒžc…ÿ¬yš <Åaä¯ê|n¢O©ÐÇ©È>¥ è«¿Pnù–}a&#K—³6ØÆ¯qŒþZ’ŽwÝ*$*ˆVÐU1{¬ÓLj÷~9ªT¥ƒšk2Îßh×áÝiöŽ5à³1oq9=Và´M„؃•ÏŽ[ęׇ÷ARx̽×~Åq\À ÿŒi¬±®½Â…^WÅJ¡»UÐ¥€¬D.tÀmm-6ÉàäÀ‰¥Ò`?Zª3’VR•s+µÞ ÿÅ\®ÝÌ€ÌPã Ó7\?ØÒ¤*lŠ*1K¡*`¾;×¢,™Î|¬\øFŽ®¸#»^غsâÇ1·ñâ,tG Í'_ Ñ`Jü m'—¶^½-’_¬éŠŽêЫy7¡o’,FõJ¢—Çe5ˆ6\ë\—- æœån-—b×¢i£îêè(œ¬† áw[õÖoåë#+rGg™ù¬nFŽF±¹úûâ¼.XzìÚ…éè•–ŽŽÝm²2+ªsS®[Ü,c^ÙÚcý»¿??ÛÿÎÍ z7í‹­Jó:žÆ óÝ냤P~´ü[ˆä1LE{ì·NÃkµ°?"Âì¢xš¡†Ýô§ z$¯qÀ(¨½|»5Ðdà ˜¾ôdIêMV›2±¢RÅâÚžÎoª•ÏÞ†Ÿ|ІßC¡Z­ñWøi¯%É CÀVÝlå0Kw;Ó|gªôÈ2)O™®h «nN‰;m²P“>T¨O—rø;Ü`^h—*vâJ·àá\‰¾°2'E]GGé«UÞÌ/óSÌ ñSÃFhöƒ”Ûˆ™Œµzs‰Ê4L'1íðw÷vèÊÓcšBìþÈÔ;£aù7áßo‚€á¤ ß>þâñðäü˜ãײ¨5v“Õ=æo¸Fg€wg³ºnÎÏÿtšeŒ>@õE¸y`bÎ [ß±ë &»@Ù˘b/U~Æè7—¡ˆlfµXÈe°ó«1^È„«„)Wq“¿ÐÀÖøê;nã:ª?§˜­ {÷1® - ®w*ó&báôõ‚°œºä×lPÐg#8= ÂC?f˜=£Lì0Ó "éÎ)(öà6ó# )¹)TGq‚ìç¤%É£š´"nŠÛAƒšœª@‡Ó$ë¬ïOÔQ´·X¯ˆ£É‘Bðè딌™ƒXqGá_:e“Kq¬ø+@²P®2Úô=KÏôeÆI0ßI  ©îšÅqªÔ~¥†¡4GÆr žÓz/«bij ôYØ1ÞƒÍΙW»?5àÀÞ'ª=‚­ûsì® ®Möjîúu€æUˆðEèïrÃ]‚q’!Þû< {N¼ÌìZâ¤qïùÙÇ ¢'5Œ>sŠ\¶Ïü3.IrqZc6îCtý+±’©@7jVÖØ—Žs»þ¸»ÐØ&UOWÙ-äicð±®iãòÛ좮j®8[™8÷¤?špšº5‘»WÂÊ5ø©t³¡±Q“‘0'4‡KLLÚùq7"æÀ˜Mý¬Ña,ÎUôEÐ)Wøfí¹ÆS‘æÊ;*ùÿ¨ž3R=xT£¥AŸÃhm¤(x·ë`„N%õÖG^–ÁöÚËJ{Gp{RË¥xGD=ëÜO즨Z`©/}Ê­¼7ÿÆãÃñ“ ¾ÅÕw©ïòÞÇǵcÆi·åùÆ<_¼¨’ÍG\̳&í%÷õ¡ì{ÆÉGÊ굸cÁ£Ã%¯ÄyЮ_Õ¢æ¡E ?KZ7£q î“ù¬—ò¢ç„„ú—?hï$±€-²F:²\Ú”V˜PŒy ‚J)*Tiöã¦wÔ'(åKD `êZ.XÍæ d1´Å·Ž™Ÿ<˸âõ¢ÔÉÝ6¢[4`ª[ƒ†ýÛâNÁÍ£ú#„ýhêЪ ß@ÎÒèRÔñcæñL}Óšê½0Ɔé›ýÇ9 ýKÊS£æ w2ÉÐPI „ÇNs¸cÉjÁ­Ó²n܆×FãPôU—ý/ýv;enô¥Gf -¹mMRg`d‡Ÿì¾ÌP[e“Ð Täçˆêw†¬CóȘ£ötã%nGxÖÅ»R™©˜ŽJugæü £ðtج´—*õ¦lÞ†¤¢üG}è×»xÖ½’VÞƒoÅ‚~œöòÙÂÀGŒÿµs‚—Ž‹Âs'›ÏW°OwÛà-ïâ‡fMN’.:ÊùÀâ >𠛣^¬N¸&XÖl¬šL0º"yR¡/8…)0¥o—i¨]}üg§ ñ¤ï¸¦´¿¢ÜZ|qBŽ÷Íâ¤zm¡—"„ì±Ú ­9}ÝoœVÖ)Üc³<¬NGÀ¿:ßâöšÍœ<íºróJ¡¦EZG¦z´Ü­kCvñJDXuYnvYƒ>E€˜0wbD–ñŒ„Y9Ö´ðÈÙ‡òá¿´´8Ç}þê2¶ Æ4ªÈ55þ€ƒÙåáº{±ŸWuÌd°p¤‘qp°£Û)LSæß¯tÀ¶Û¿Ö‹Ò@¢^­aæQ˯ÎÐðÒqŽZh. ØÖïh+¿“Ö/ü¹£eÉ3ÑÈ!NïG öÞ…Õ­…GžºóùÛ@ð»%°–:ìÿbªæNø:¿#ÜktüjáÓÿÚ¡7£f2aÉ—ŠwÐê8»V1y —7¥¡KERÑsãóSøºeÌ©¡7y3âíªÝú/=ëæŠÍÍ^iU²•‘^ðÿèKß‘… ÿBôºÑ°š]æ@fRú€ÓNûWŽˆhaä…2‹ 8¦ûSµx0I@ N ´ƒ“œaSƒë°EâˆIuö3­ƒï÷öüÙŸÔ¾(.a´z¿¬ÞlŠÓNûÉ3±«E²: €u™‰:Pw)ÓûP±lòBÞv(‹™°‚âa5§ «¼÷D¢KµtnÚü›¾êŸ24í]·Ï’Æò˜¡ÿÙ=u¥»’’Ö]„å øñ¾1¦»ö¼÷j¯Ö°]œ‡œOÖ”•pëI¯<盛ý› Þ»(Ì?å¼w8ÉçAqs^`ÝoîZò| ¨\a%Šâݵ±'±^xRì ÷wxÄÚ õ,j«áâŒqTèý ”E“uã—¿PêwdµŠÊ¾ ¹ÛÃ:åФ|m§´œƒÀ¹û/,X%ö·F{ÆXû7fø»™Õ8ZÎ ÁC9‡ÇŽ2cˆµFŽÝFåxœÐY›«©W~c¤¯ …9ÜanvÀ„œ%ºåzÉiFF1› :Œ¨4ޏ:öÄ;÷¿&+îG³²ζ¹C5nriæ#]öO>¯•êŠ7 »ÆÞt‡þ'‰Œš?K.Oâpœ¸ýóšn7ë‘¡¿;K÷ù1ÅÊ+ïÇ1ÅëÂCzw|B%|fóñaù=º*ùGÖWÍð_ÿáÝfìywý^þ}Ëoì•~)§ý+qÄÎþ'¶ñ¯M¯E5¢¢ä•J…JÆþ®FS¬Àœ57ÆBÖðr^zùÁ7÷iïÍáBôz![W Ôk8ö{‘!vë{šú·óûqjl‘¼ö=×ù‡ÆF»s’Ž;›žåvÌf¿åË Gf‚Fæ9çö¸˜ÚDÁ1+±UÅ«yÞ+¡z4F½7“¥oã LÁ¾z8rK Îô@È×8‡/›½Çî fPr˜á¡˜>*yg_M7`Á´Žê}=žÓ Ù;qÇjž ÿ`'•1ÆñÊî ÚU;ggóΚ×z—sºñ÷gCÀï‰ãE’/½äg®ì3MŠ^%Œü÷roÿƒüü\ƒ€%¸/LÈf#uJ%+ÍÒ&ÆÞ'àªt1*L²¶nTHά¢Äþ¿ì SQšc¢¢Ÿ¥Â®õ?t³*lR×¾°g •¼–ÓÄ„Ž^í]`¶«?®äjºø×RgFñ«¨qW%ÝÒãK™íÈû^hÐj×%,>:hÕùné½¹¨*ÇIaªª3õÂÒ“ûÓ·bp(QÕŒËüR^ò•uœêˆQqšÊOþ™=RÝpýŸ¢¸ó8& 5MEŠÙ„‰õ'mð-šÏ-Á:oú›6ùEUš÷+à›¢œ, E±ýÿ×ÌDÊFY—Û÷#ï—…,#eäc¥C÷`ïï•€¨±Žûß{›8öñý#Üëðé}±xÍÜï:Ú_–MY©a¼vl¼óGÇ+Ž÷Î?{Œù߯É|æýÁX¤^çO­Îb©ƒjz+AB/öúFf¥ø‹Ç.á®ÚZq»eArˆoY*rWßÙº±»·³$Õè _ÆH»´<mß—…eLñ|ªAvì•B`æÂ¤ÄŠ)[áçÞ ÉÇÈ@ê×kæœô’„+|R¬ø¼º½j¦2Ö¿´v·…dU®6ýô̦» º$Åê`òF9 cûŠwÑÐoi˜aÕÆÌMã5k3ÚÓ\¿TmÅÆàôËºëº Ël¾öÕyÌa‰ºZ¶6޼5W¡„é8ù± #ê…u¨”Qzèá¿všZwŠW¨6ÐëlaæFun!µªb\o÷›¿1§ïD|”°ÞöË‘)?¾ " ¬|K»ð«šzOº;†|y¾îyóƒ‡\ø»tN¿^ߢ*^´î¶hᙉ:jf¢ú$!JŒÇ­ÏµíÄüõÙ£¯^â5‘_ #œßæT2o£:Aºu©’°*GdÙ.9Æø·!¶ogߨŸ•‘©hW§NS‘¥Ä ©¦›ò=¼¼5›ª‘OWÛ®Ù ãvÕœó÷œx ëU¨2”V…Ó®@ƒ¿#" Ëov”x*³QîMLþÊ(î4Òf­¨\û,‡ {è¡áíen ¡ðNxJztˆ_¼z¾§eº‡#}êöð(6C—r3 Gž§Q¤Á÷Í „l–öL÷šYR/gbÄõÝn3ú<ÎN°¾×_»ŽA9=›w“á—ú.5׾ݖ½’~†{ Ü€ÞÞåçVF+®ùÞ³¼ \û}ÄÚ¿ƒ„øRÄùùy#¼â˜¸œàÀ!­þáT±ѹâ é{ë y_ÍÈ8ŒLšVhšŒzTwÕ†HÂÐ°óÆ©…žg™‰ 4ᙩ=›.äκC˜¬>åðÄçÑ™ž¿z°ËC¤úeÄþº`pãÚéNnuáBw¦>mÖæì­Xª¹˜zÒ$Ì–c i7MôzuQ¼ªP ›{\hÆ wñÊFý¦A£B¦s¦úú¯ÿf–aiY5&ÎuÙˆwë4 Ù‚HÁfÚ’ÿ¹„`ÝÃjéG´ÚtÇ) -µ³(oKLñkJ¹©¾‡š"vè•úÝæ6ƒôøšÞît£Ìr/¦ IJpp'b…r§ä„s1RÉBTÖwÔ]Ò¬’aMwé—Ž^L¥cx„Gê4Ç!z€TUµ§’ù£·W¦Ç¦µN»þÔí-R Z@©gŸfÑÌj ýkcþÖ8Š˜Wðô¼¦»Á=ÎÉ[ïw `8±ŠGD‹ ™¹Ã|;cœËÇŒ ï°Ùý1Û±8ÿ|â4”ÞÏaêÓûð ‡y¹ Å^ QO*ªDô#ÚI5¨úÒw¼Lëoe;gÝ«ŽÝºAžNwº×Ó"g¸tJpO '¨0_Ð>@Π „Îy BËÖ*uÕSÂÍÔ™­¹“W`ù¤htÂ[Ýlxë¢ü¥ýzÿcEõ¨Ÿ½‘ý’¶5úÖ16}ØM+bæ-GLlѰ»A+¸ê#+nÆŒÈÁ̸ UÔHW êÍÆŸ§fzòC„%ã%NL =˜¶è9àÚÖBñèÅ'б_`ÈÌŒSâh2õíµÁª±ÏÝÈ·“Rî¦ë鈦{ÈRÇ¡«»]/ûW?Ï•ÌèÏßPëhx,R3mëÇlü»ôófˆuôkÏ©Áñ¤x¯ñL¿ÃW\ïíWð*5é±xÞH«ÆÍc¸}nœ>mØÛÏßÏÎH½×üüq|”ñ5¦qÇz<’þ¨5ÈF ËxIà5…H—XLZ ¿ñk‘‡†AZ*!-úYy˜¦ñ7ð¨~±cƒµÅT¹@_,Sx‰á FdP2¨îÅ» ‹Îç?»:4Œ¯}­¯·Ü—ŒÝ˜4;eºt§1›kkv`5;e±¸RQmëÃæ7˜É(ÿ_®ì¦§‚—Þ1wCq±—wï^®ŠÇÎ)6ÃF8T½}†ùnÌsUÜfü‹âȵº,JÈÖA’Šbú}©—†Þ¢>ÖøAÖKů¥ž:,â$›z¶¹{XTÛžù±)â–þØq.â¨*r^îìR¹=ÇÒâuŽ¿œ"ãq0w»ŽŒ;~SŽßQË8¥ÒkñÍ}£x¾áÌÜ>{g«öñÁêÖ»Ÿ97¹Ãèø€{çüôœy#Ü›£ðìÄÌßCÀG3òú×}Ê]½èÀcüûznv<@Ìx¼æ3l"MUHÕÜKŠ…ý†Y´UYÄÓ³­•6Ë@I~*W|0¥?2"<ç¤TÇ nB@}í]*Ðÿd ±<ÊXo±L ¨RR³qö‚ÛQ1hRW¯­Øà8êBàîÌ|iüv𛂕w¢êì` G¦u3È+›Z`Z÷Ô›Zî>ˆ¤–éÂo”¨[»¥fûê¢Ñ´R3D‡®œmˆÌ¬7Vx†i1¦ËmdÁæVÖ«‰iø³Ô–]1é³znr¦².¿^°«Åí³¡©å7Œå[}Åý—«¿1šÏëo½Yå@¥ù£ýè û™ÁïÉX¾bŠð!”žf¿¾oç÷«Îøhœ{l~î¾pçAÑÝ—XŽAÖÅy„¨ ´éד6åë¹xñò2TM0Äaàî„á± -³jóêüîÁ ¼µ oIù÷ØH;…KÊwyY^äSѲLØ•=ðÝ:˜¨U$¦z€ÿ!,øk"yG§ç8°°ÿRd—+,æV¨\;û­ÓñNxu{Æ)Bbíp]³Ë‚¢rÍžJ‘ð·=›þ[‘8j,G¨ åÔõÚšŠ©Và¢Uó0È­£0Às+ÀæF¼ãhÛµî†*¬kÍe]."УO3íl\æ¼aMÇý–®¢Ö¤³v·[Á˜ I§¯ö”kSs‡5ÇB&u=`XQf–» }NQìŽòã\{+ÿMðq–¸ì^ÒÄ÷Š4o¸ç5WN÷Þ÷à g½º—ãíwÑq{\ºá7ñ–?ðwæ?gp­teU010-cG¼+˜ÏüžU4)V7Ñ](†oîÝ ºÊ"ƒ\=­¼Ä„;óD›w3ù-*6ÑMvÆNªOÔ—eôsE¹Þôz„ÑyUÕ) ¦7šÍU‘ŠQP@™£è1Ò:E†³±©{N9ènÆ«P&y”úÞJ|Êñ9„;YviÆ:e2á'ÇÅ¥?Kw§+x88lÍ8b‘z-1b£6DD*ëœÛ¾^&Åf¯ºç¿?@/Ž…F:%ãä’Ú¾jõGÒǪ*UÉa¨ÞÏú­é3Y­–ÕÓ7]‘àø¦Ê~oå†óXÈ×Îñw³M–Æ0å~ÿ…0ïŠöx?¬y­fŸ<{‡Î÷Zl÷hô€{_Ñ+°Î'é¡uÊûz ½ {'Œ¹8Ñçg¯ Ò.6YQQ¼¹Kx‡{&1²9•7ÚaR}‡Qñ¥Ö#s7e µö‰‚Œaj´á¥ÃMGIüèB7Q°Y–8æÄ·“HÕi‚0¥c]3§?~βs[Bê²h쟌ónRVÂX¤CF>ó÷qÀ¹Tm§|~ÀÏp.)ÇB^ œ½‚(Î9Æ©·dbR›'~00²Ókî'òTɼhðÅû­Îñ8Š«hÜ£¡.3˜Ì•rF¥-SØ´OTõŒ²`’ŠbÓ×Ë™÷mY;aƒsúà 2„0ÒÓÀasf?T)“™Ä?\ù[c¹Ì†j/&RÃÔ­¹úl߉ÇÖ½ûÖƒêõàW}>œåMðQLöˆÈ÷Dý²¼ …¼]öíÃt!žÖ ‰·Z‹»°΃ìŸ=<2!ôúD<}‡s »tÙüVÞ]—ß‹[QS]@DnµdcvZ}ÔA¥o™–?ìFIHÞG‚¤§ ¶Åðüf2JÈ‚E0¢ ˜1´‰mÓ¤ÙF§]káô}ØF u5 O¸lùŠ`‰Ðó®ðt³Oã:s›qaÜë ž¦é¤ û„WAÉrãEíâF»*ÌÄ“:D«ðP/[Æ5HåÖ2ª„=¼+𧇴D)ó£ˆ9™eç?2›4ÝÌM")ŸÃùÅûš£ñ‚…©Ý†È-ÙÍ‘%ÿž¾$»]Æ ‰ë"—ô lÒdIÓ MVoÎIjf¦Y8ÊmZÏæ6ïÙ¿ÓÌÓvÔäösaÞÿ,ï‚wñæÃãí{ãÇ8- }=Œã{‚oÂr9žòÁ€e¤;Ï௜¥Z²:Gš&šÐß+îiÔ.ì€Úåæ±qíÑü ,‘Ís® -l}¨]î!!µ Ú7=òÙëU0È;wÚßÉ v”kÞµ¢«ïßÞA”6瀘ç›%Pñ.ê!¿g©k ¯õx$¨®7íRŒÁzðJ ¯e&Ý(ÕmN qƒÉ\X|Ÿ&¢a&gÌÝ2r•ÌZï&©•2µXk-0tšF4«×lž< w&·%“#=Q=ïçôëŸ=F.©ýì„dÝ´Ck~гÙ_HÜ„F௰—ïŠÆ¾k¦ÇÉi­N0‡/¡£)×9%,ì†à23LŒ.;»ÂvZ¯-èK~”°wÁWžý\›kio W‚,þæmÿ¤yhŸ.ùùsáÚåo}Ò÷EÀÝ[}½Ÿi…=EÏLÐ"ò0uh°À0yPöM¬`ÄHÙÀ–æâ1™¥Æ _š |sqšŒý"F",׊)‡ñYšEˆ wm*ÄhUÀ£õm–þ…IgeªÓÕPC=;Dß¹›¹.0›µ†J‘ïÓ%¹ô0Úܦ‰Éüü°¶–m0ÏúJÒ±ÍæÆÒÌiA•kׂ]êVwX*Ýg§5éTãÄ(¬øæ}œ›)~ÕM ê qü% ‰ªr…t•í3Mô¹¿#Ŧñy¥k%4»'ƒwˆâ«#–¼PQ¬˜2[ᛡ‚ýÉ+?Àäî备Y~´ êV8ÕG»y ÿ)|N÷’GŸ/™–·›ëÇÄögFE÷ôŸ~uÞåóó´½&¾+îâŽg¤£ØwÊ{ççÂ9,ÍÅ‹ Z .¿£óŘ€è¯r.¥ã³/¥’bÁ"µl‹-5$Å“/7¡€¡ØÝ,Zºêí.*jSè9U£×,CXÁq!שåØßEQÁ*cÜ‹k«í‘æ<)‚é»;ù×]­BhQß*þV”¿H–mˈl"gÕ“MÙqtå–£ü>Rã¹Ðœ½…º?ßùÖ[tn+ÖifJkžK£3%ö#B¥”ëX¦=pN]¡#·ëkê¦*Ê JüáDšŸ=†;âiŒŠ”‹¢{Ë·ïñœ IDATäì¡N›Ñ W2”Ü|ûÎñîŠ Îú°fköh,oâ†Dg¡v…ÓÍÒ•»$žã@÷WõM>7'ÃI÷|+x› ~Ùl/ìݘ‰úsx]sŒ&²mP;8;-5µœKjÃÒêeZ1ƒë0ŒòT¥h·Ìꀤù/"¹À®EÅÚ[Ü0³ó¨¨k¡óIÓ@G=fò¦•)îhæ õ¥º,µº‰ÊocƒFÐna4ù@Ÿuði‡Dõ`ó>Â.îŽ+n›ÚÞ´7„T¦ÇKܳ3Ûfà[FIBw6lÖ`‚E6iÚ$ àq^¦ˆ9µ²ó7IódD‰iH»©mÑBrA5ÅhZ'HºC“iÏ`/Þ}[‚5g ìúÚ+]q|êšÔ-íû%ìµW;§o¨y–w4)»…Ï`Å“XFòaA?Áq¯úÓÙpÿª†OCQ¯Ê”œ Cÿ@_Ú©ÿù9 î0åz41š·}£5…ÊŒ½PÑLˆÎ ú½_Ç^û—?µÜÐm´5"SêVà XÖ%ïŸõžfn?DéöC']•+»ÝK\q#¦•ò•uL£×UÙ‚1›Îs·±“t/‰/pAfZå¼ãu¥Œ…Wʨ:/çä( _ÏætìÄe+ÌáõŒìJ¾vìg¹vŽ0wžÿ&ÿ7!`îÁAÞ†º0®>þ{B <ç*:<Áç®;_ôÝÓ-óççóóù<lº~‹i ²®0b@NnŽx±ñ‚(˜mȈIØ”¼q‚j«?Ï=1ö;*ßêX¤îh×3Méä´™ër®Ÿ•ñ¯]Aìhw«²¢z›Ý!ת;î'T?ÝT÷e(¾Wݱ6›Ò#jo=h9‰v©(íû´]Ûo­ùe=ÞØ ªØ×;Ge?®Ò× Ó]júYþÌøy¥HwâXÇíhËŒ¹Š ´þƒ£âÔ¢»˜1;e8ÉhÂ,ñým2á\Lˆs.^5UtBƒ­õkNq,Þ/Þ Ê«ç<àƒàwøõxw¦Þ÷±8æå°Ú¨ï>}$Ú¾›¦o1¬àìéϯó®‡â=>y Þâ½®Šç_‹ûVÈøž›ín+>r'9uŽMkºÜ@xóRÞa˜ÏR|Pun‚Í]L?·’çpÓ9K3&–xÿÖ7LvzËÃãfJææ;‹`Jßž¨­ó€ÆËÙ3sJ&n„¶Ö;§'tY=ÝubŽîø£Wߌô3cä›Ï¿üÞ †=3®çÓçÿÆÒ,Ÿxº?‚°ÿfžàƒ³?x´ùî6ú8èAÑE"ÆØfTÏ™ o á©ÚpKô»·Yãž”" /¼ø¯R3ƒºéœœùh_ØÑ •™»ùŸ@í‚D¦ÉµL›yÛÄíø¶yöŸ¿öYyÄÚÕ„zÔÞ_x3)N˜œŠ¢äåÊ%ñc9¬ O<ýGSSdu–5p;xã$4ÖÛÐ%œ'$WŠ^q» ¡rÜ33FA·Ø}m©CÖn±µc,ǼšÉ0U?%‹ïŒT¿–×…]öB!·*NG-Ϩù9õ\ê<;ÄßÖ+Oì¤ Í^­Í¾kºöŽë4‡|–0}gáª÷Úº6õ{!àóvÈ­Å¥×u»^ÁÖÆ+®õ:s•òŸ|úŽz­WÕ3Ï…óÝ4qžöþÑÞï~àú1<–£Ár]'ßÎ#ºõ»Ýº[Ë›¨~Õhc¥êc…ó fÈ Ó¬@T‡4:¯~×W§äè—vJ1•YÊÕ¦´k_ P®…I‹8d¤£úbÅÕÕZiéÉ6€tøl5ÃbQ$N„ïíhŒù…Ô½Mf¼»×ˆé¹ 3´Šè![ü8c^‡ú]®ÏE½îdÊZñ:›Ú-*ΣO¾+¥2ÀŸÉ™ Ãcâ$ sÕ1Î^²ýÛ|'u%HP,}猔²v“w Ž0e½¾lZåú«\Ð+’’M òZµßš½~øÂ»æ:Œ{‹G_÷ÌŸËÙ{ÌÞ˜ÿžÒƒÂ3ámrÜ ÇËKpäuÀ|Ê*¿"8ç݇Á;²Ýßæ¼$öÌ«ûe€ñÓ¾ˆ€y0 èfzESz€œBEL&hÆðñ†HÚŨ4_šn¹­š‚ú, žŠ>)gA¶öTµ/SGKœÞ­:uò‚w)îfV?Ù²Ên–rŠô+ÊÌÞD“-A3Uc޼‰¦[WW˜{=7³f´»4O±x@«Ðuo5+ÄSÎmR¼J¨Zɯòï´¤œaqI°{951GôÂ\ÙXÔš²mnP$62Í:{óŒE£–€Ÿ1NjžNa¶Hv¼qR¹.GXfÊŸåßS}G©|ƒˆŠZõ˜¾\ܬ©NÆÃ¸*ùœÄ÷ÅJVµù.MeÍu,æ˜{9rû° ?˜þåc~4ìãSfò&ÿ´šn¿`?ß8~|ðäŸDÀ+ú óøbLCC\³„ç !O› _ælW$¨»ÑVkAË1CÏÌg­ï†Ê ?9×¥Ã,3)ð£5÷Éù "öÈUm;×ëµ"b¬¶OœÍÆJ„X9Ý$ä!gÀÆê[C{LPþh0Œæpȯ4xå¿1a×g™gé¦Ü0L€“»£"ªÑ4·º h þIâ4^Q%>swƾÏuV+MØ.Ã^¹h–”mG!7lám-˜:‰õ±9|jW É®Í_®å¿iÆ¢æ™X”œ1´w+¿§¬3³µ{8âã®óÅ`t¿†ñ¥W…÷+>»ëÎ :ŒÀõ‘OÛ<¦¹aìð½ðízΓ >ªu›5~ÉѲUtÆcÇŠÝÞ‹@ðãùº#çöü{~a„Ü AøÀ›ìØpüy…ÛžrŸÌÜÓ0WÐCwG³6\ˆeºPF!.fÞ>Ùs׳O3‹¸#áÕ:¥jT]Z=‘ãëü­ 0¢å“Ïö'R& ñ9NkÑôf¼þ5?ÿ nˆ39Æ('9öº©Œ±8òæ\ņo‚?1š;b÷±¢\ÒlÏ.¹„Ì®çv.4U¹:v¥çêr£;ÍŸ \Æ)s¥¤ÈRäq9c ´6ÚC”&YãÔlÇÐ÷IuXBª¹s!V½’—æ FÝëÏŸ´FN@™À™!¤7k3jäÊõr_Œ·’pŒÏ™[˵÷ÖãÔ}ŽÔ)}–„7àÊÜ[9SÂaÖ|Ä¡c­xëuOZÁ©+ŸìÆ3Çåá“¿ ü±®ýÎ1*Üc™ø€ñã~ûOܨ¼çxøÐÂÇܤ6›ÅG‚Úè“?ãAøñ*¾;æÁçë Ë«‰GHE¢þKá?¸¡áBçÌs®m“¶@$žAL®¢ t„Í -±&&p==á™ ÍŠ×GÄpÿì Ih6ºùÆjÓö!çÑT 6•jÅƾ ØœèØÀ R]ð{fP¯.¸)³‹Äͬøµ.Z—H˜ã‡N¹à û7Ý.‡u½˜ZR£r¤‰Æiû¯ cê€T­ºGäÇ'íñˆw²ȾJÚvÉ+¼â³›h¾MQƒÍ£ ´YЉ5nù ÕÑ£¤P&Þ)r«ºùáÔ‰ï;¿&šTBÔ ¸pàÁ÷Ô ®½sv׸³¢_ý»YÐ'^Ãm3Ã)¦8þo é:ãa>oxt¢bü™{OßZ|Ñ•~6ɽð‚VóRO0÷ÿ Ìb‡äcZVÕù5ŸñROÄðÜã½ÐL¯ŠA™Š Ø#W½•¾+ Þ¯7b_§Ôã—'u$×E˜`À@Î0ßG葤qù7«jÇ>ž›c>Îò[ØD-*azÃ(mÕçIó8÷cç¯\.l]¤„ Á¸Äd·äSi–BG,]&iÞ¦qr¹Á©Z<Êmßö´¾KèÓr ºlëQƵþ{®]ÑjZì£ðߊ€^áæû¸XwRŠž(<ßnïÊ»^ïa_¾¦ð'Oû†h…§9ÈóŸTçÏÛñS ¾ûoéÃêšË½¿J fÂÃcAx€”\„Sõ´~[á Ò™ôBíÊTjëÑSMºÖ_iŒ‰Y-‘]¥9¶oÂc“`+µÕxQëç•KOIÑ£quàìæ?”Q4ß.Êïi|ˆ¾ïz±ÿ­÷EÀ÷Šù"TÇŠ:ÍîQ<›~ ‡íÌðÆiäMKò&«2ɾàÚ#ðèÆägo~fævÌå]¹®9vCÀM¨;¬F¬1¶’xÉNˆA…9qSÝW’‘…*¸³H`¡ŒçR¡ür ª5jÇŠ)Lw/F­ùÕú"Xu¤`¾ä æ¦oÉ%IµÃFüLIõ‚).oŸiõ¥evç.ÑCjfÅÇFažŠ•“óÏ(¹°ugGØŒà›„]­éæãÂO6ú˜”H;³zÐB¥¹40 ë0ÀªpÄŠLÕ[¡û,:# Vf‹fÖúC-Ì*¥÷7¹&7uMÔ75ó( ï—M·t2.ÎÀ¿hŒeHž=_{ÃÉ3ÇgEœù°ÉTʬ~ë?.¸$¡¶Çªk`!ƒ;ý÷Žïïòñ±ìzðõówjÀT®ïçÞÝ(”³Ëz}Îà^˜øê9ç=†w—+zUq÷¯á¤ÒzrŠŸEy«øuI-k#ÍýàÆÕƒ='6a)i4ÎLÚ­¿6¢FË–K,S뾫*ò挋1 CñüqVLžDNJ«²„Ú‰ÀÍúSà)AQÂQw݈™ RÒ/ =ÜÔ>à k3>ƒõFî.#œFb‹n6s % òøÉaDnv\4ÙŸ 6lg°AN£à'm‰U7ßÁ‚†ƒÑ @K;Ø01¥è”&àØµŸi̓|–K¼â3•Læ¶'éŽ3\¾Ž*ý³ñ™Ý$ÖŒúɤ¤½FT¨O¾Ô®–Ó¼{)YÈS’óH¶FäØ%¥¤RudÚÉ¡®s•»ßc!ç:–s´s}®æŒ-þí~À7â¼{•f_äD{Š3>9¿m?Ÿ¯þ.ÈéF¨{M Ît÷Â÷‹½/¼—yÛÍøgï£ðÂþ7Š3˜F. J˜-Ânþô‰¦³4æ%d½:ì‡e{¶ó(q4f•€‹Þrâ]ÇÖÉB[kU ¦ÑÝR©¯”Ò»¥ï™º.æªkµ£2Áµ¢ÌÄ›á7*¦ %]4…àôÏô]Žê‰Ô(|¡KÁ‚Yõœ17ÐÌÕ{p5n}§Á¤Vq²òŠºuâåšÛ·ñÏé8Ì4y}²Kþƒtë'Æ\"ó’w&èöú•ëßø½?U¯f:©ªmÔªwDß°c¯=Çq±[f8“kø„…+)ï¿ÓçÜ …OªƒÓfûï¦ÞŒsáñkð“£ÚGç÷ ä;U¡ï¤ß’¥àÍÓú;ðÁàò»ŽWuê?-[ö.›aqY?êÐwDÀ üäÖïhÎmãjÁè>«DðÉç ‰wE¨ox'Öwð9÷o¾XÜc}owwþ³ø`oÓMl±±ò]^‚ÅÜú[ à´7+`fÔ4¾DsÞg4P\Âý‰šqý7L]°±Ž·¡™z lŠŽ±'uŒYÅq“OÞÐ 2ÖgÖKB/Ñf‰I°{’4ÐT+"ÏTPD̺áàïECÍ‚dýíñó¥*H«?±ðN)X¦ÔTm…RcâÓ‡dU¡—˜=¸CW=õ¨îÄÚuºÔú Ý®Œ÷¦B¹ª}Ûw©{¥Ec}6n/=nqo\…»(ÍqíÍóùyîrâ Ì´ÖÓtLßüóϧˆ8Š!¡‰t”‡ì[z¿±4úÛ¢KJñ¦¥W€°Ô8±áøøq&;¤ŠPßjÍaf ÁÀ‘+j™‰uëÄWX£zd€W!©V‰¸òàà@*ÒÆe€8:‹¶³5‰„ÎbŒ„À˜á\\‹ €WÐÿV i*ŒGhðS\léy5î³ „9O$5Äó*Ìkˆ` çúM¾-”‚´dçÂhÎÔ«ewû$ÙT=¬¦ •;9_“õ¨ÌmÖ¼šTBeð‚‚¤µK›[9S/ý>¡¯v÷f¿qÐñnÒ]}½â¨ÉÊÉÄü^™.°/‡5d|4S;ÞVXÆñÍïãÞøk}ÀCZ·£Fþ™%8ÿÿü4‰ògöÝždæÏÆ]ÿK÷÷oCÀ+Q@Æ/¹'bûnA`Ðs.Ú¼M0¬˜Uë‘çÌªŠ¥>'—PÃhç~d^bÀp€b屋в›Êì»Èƒø9ož¹³ÑpIhÕÔPa±x®~Ek©Î0Žyþâ½d)¤.+ð8äÒr€ÇdÈ 4™†‘:é·”`‘4±Æ+ñü/Ìcô ¦Í¯H½œÎÏÕW:˜•Îé´ŽÍçA‹¿³²âNß—¬|ãÔýÍ„¨%ßÌ"®ŸO½æØê—R‡î^Ôª6 ‚‰ÚË[§o⓳"ÂÑ OÙIƒ©áF˜Îd“pžNCïæDõɆlœš»H7öEg{’¬9 «lnß®'˜MFj4¹ØÖô:ö]ÆÓx+-èׯ׷Çzë#¼¢Bö¾¢ã Ýg}WHã8Ç^Yii=­lõ™™xÊ%<Èœçëk8‰Zõ/CÀGÝW²ð+1Å4_OÁ4¹Û²ºê¢ŠÃAL™§¼®¿6†õÕ¹n´|Ö<°¼Ï˜¹V³äc6<÷Ær½ÄCŸá‹î&Üõ*>?׌ÏÏ¢½ÅU1@ah[§yeØf8Z«‰¿DÛi0Ä\»Å×wÎá?1]׃Ž÷k´¦í7Ïž“ëüÄpîùk(ÖÓ¤«ðÊÍ‚«>s‹/£zñ6ø&®‰}¨s ?7½!F+"ˆ¼,åÈ£ˆ³ŸO†½ß ó­êÊ© .DçŽòq!h¸[<ÑÔmï.TäŽ^4ë¼AJ¬ZÒ¥ˆ_‘ÖÕTdéÈôÜ*ˆ©¶ºÇ¨¯(ÕbmnŽ(§•°†P_j½…‹1k®¦½HÌ{b$L…êåhšz€IÌsö椲©bK{{ÚMÛúÑfwÐx0ç}Ö¹$Åü™¸QNÎJzg糫ÐÇœ“(cwn£-†Ž=Ê ßC• ;ºf}æ:S;ã”™sÓ£]Ô]W!ÂÌtCúü¼|B®k'å]Çü´ ñCçê¡ãçµsþ› ä¯¡Ž~ÎŒ€v¯$$º>­e¦´‰Œ³SõaâH>FRmT£)¸lHÓ…xú ?Ð9>…wÛ} 6 É⚌Çݯ:K&%Ó'ñ{:Xp¡ó'a=Éÿo\Ur]ù¬ùÿOnÍÃî¶%&@Bºé‡{Ïd'PT`Ù²”ž…­ÆO_cö° Üoïàdž“ÌXœñ™•¦¡p7ò‡Ì¿5«¹`–ÈÞ=€­~.Ó7G«Óÿ#Š]9•Âõß±•ª*ÑOÌ ª›EMÞˆ"1V;×F´ê;‰'nˆž³koðZ±ÙÚÈU=û§~Å1¾×Ù½ç=Ù{»ZЖzŒÄÂÙÈ °)B%ÕØ†ªáY˜™ÓóL‡šâ~øÆ  mVU €óËã /Ðåv`¹Faˆ2ÆŽVÈpEÒ1µhOx!îÃüc™¦/ðÛi†|eS*]ªp`:€%{¦.&»&¿Ö@ȘÎRê2/8çP©^a@òéÿÊÛ-©˜Ü;™Û¤aVv› OÙМ8ë…Sçò ™]èÜí ^¯ppµJ øÙ˜Î^9’“¢N{Iˆ3WïYýIìeµö —›ÊO\/¼nò^r–$_÷Š*“ÑÅÇ+¨z} ¾œS»KjLÀµà .øïq`ö£ÎµËÔ㈱¥/fVŠWÔFv·Ȭfs¾@žc¬ëëð,èèD›¿âWiD0¯*mFÞæmk¾6‚:´²Éq•ÚŠÍ.x ~Ì ~Œ´Žs„]nsâo¹4†ˆ‘‡ÉÒI–;_R‚%¾Èå—yQž{'c’€3Î TÜuFó#OôÏê ä¾g›BJËiûB˜LašÚãùÿ§@0ËLÊFþFክU¾TZ\/ŸäÍÄ ƒ¯tL帬 ·¾S¦Èxƒû@xòLO[½6jcýjô“¸;ø·Ÿh sÁ4>ðÒ^†D&ŽIÏO/®»ð’Sãß—ø€'ÕŸEÀU.¾/X7)nn¹<šŠk^³3?0:ux^«ã6ûnÑR«Kó2¤ôÕ´æ”ÅXÞ{@éÉRŒ_dÅd"-º#K…eª‘{”NúV>p08«jWª¾>]\S¸]ŒTJÇÙ1„¿l’|À(í°°¹–<Á€þ&£«6¥Mç²2Â)ȇ”š?Aê`UX tA¤,]JB!'"„¤´r!"åÕÐÜï€ÙH¾ùØ~–PX —!†@À䩿nÿ„¯Ûô½IÀaíH ÝuñÝ qG³3@²\;}7ýOÐÈ×64­lnW‡×E¿ºû€Þ_ †^4ßüw ¢o×sÆËg{öPîÍ^»wî3Øxá•v7NŒiõÚn(ün<WwÞä3½¶Ñ1I¨2iNï˜;‰ÖÐ(™:DEЄe¨« m^³ô4ô2Ó6*!×%æµ>M¦6Üht8ÊNhž6rÊa¤ñ£u‹bh•+‡ª1¦ý²UK ÂH(»XµÚŸÕ˜ýNdÇŠ‹«…«ÈÍUጱʷÏ4PŽôBاñ_¡tŒ‡¦6÷[#ñ£°Î_—ŒÂ\ôªjõæPïî@ëêQ&|ŸrN©ËApüÝúoM10«ßgþ¹uã$‘E¨®«Ty†’¤ãÀ´N5<¿!%Æ:‰¸ÜַɼïvzUøØFûhÖÀ²ñÌÇöñJXxÍ9ðÚßÅF>VtÉöÙòì9ûy?¹ž±ï»xÉ­õJdç]F!ºy½œÜ¦ìÝk6Ïý÷@À[£Œùç4lO-%ż¶èÎÚŠo7ÛЀÑãÇføS>÷Œ<ƒÇ0rÅ×"½Q«O ŒŽäƒP†a“{RîF…ÙîŤ.®Y•v0÷ Uº”“€ÃRƽ}B£‡T—­ðqrØ=â¾Àü4º ,yªàØØ&È/àrÒúA‘Å)o9ä+’,Tèû>¯ª¿€NUØ·ñØ÷ëˆ+±µJŸÆVñâdî>b³v¦Oͤ9UäÊû˜-š¨4—Ö? _#c…µ|‘H3:“"÷Àìn í=•8ç8_Îß{Ô›EæI>ÒÖÞVÔÞÈÿŽ0¶÷P^aÌö‹Æ‰ƒbýX=r«ôõ­¡(¿ GžJÑÓ纮UW;“Ñ4¼Âçýdûˆ´ÜïCÀh/AÇ=©£{28xh¼ø¯ é[/#v¯%p½–¸¬6 ¬¸”9ÓÛ©1SáI!kiصU¯}½|Ç7Ø]‹Ï'ÓÌ+éa·=‡°Ø9{¥_êºb§aåãÄz°ðŽb Dh¤?³ ý»$mB¥¼È£,£ívD¯E8˜u©*V-Æšw…ç|õš•¥}6¬? íì v@š{R |¥¢l+*ÎdÌè0>»Ò´Ê·ˆBØ‘ÛQàK 'P¸ ’Ë•²{Ò÷¿‹Yò}Ž" ÈxQ;ù\ù¹óù˜ÝãÖÒ gÝü·ówŸï»éŸMA¨½ì_Z%Û}=O5µ¬¶ö_©3{¿ìõ]ûÆk¿ËxÇLJ0êME÷ß+p?{´µ÷·r˜á裭œi­÷,óy3ÝÖ²Rq¬<%º·á±à Ø¹¨h2¿–A”#ÂQ-hP»ïd%âp"IÅ4À«FG0STÙK¼e9(œÔP5ÍÚM¤ÇÿYÄÑZÙkKQ7k\›ð„ö˜ƒ÷†‡1Õ û ¨Ø¼ÔøLàÀƯ Ðtƒ[¨s/s¾fZ y`Ê*=NH¾I¤±lW9]áClŠl=tW:¹ä2›³µÄ—|ª°á²WLuÏr‡´¶fТx~Lctñu‡} A7ÍÄÔÄ]„øøœÔÝñü;bþFà®n<+üÓÉžz÷u2Áõ-—>ùM5àϪ^à4‘Û½:¬¾qþìÃ[OCtòó?».©°»Òï¿Í¸ñÚBå¶Øç0A×keÔÏÝŸÿ"Ý‘9t•Ѽy÷Ï1+Þç€6ŽºKæ4œµ"ÓZ°uÁ˜Q%Ø">öšÃE@/'wLŠð“ Õë…}áJ_)Á;&ÙŠ ×îm”'PY5c|‘••‘k ïCÞQq#¸^áÂSÖ<]è.ù¼ÈQ±«r*ÚR|ë`Û3•Ѥií_î‚Å’ €@*˯°R'ÿ=y0{Ó'Þ²|.A±‚QÍCvþ7_•ŠÐbeµ«ÕbåÅ'ï£èñüolþ5:£ÏÊäVôX×÷Y YÛŽ9žyEö9Õé—ÁZô¸i žSÇÁ¯ñfüÅ,èÕ›®Ò冽Æ #:Š­c5¼pƼ~ü}ñvºüòvÒ!¸ž:®}ÐVù㸨ï2ëÇ/˜çÙŸÓå4Ž—èÇbé-k?ÅàÛH¾7;»NšS2® eS ²„ØZ¬4Àxò\òãs+Òp0ËIµmc0ˆ©Ú6¥'xI„‡(  ¨-ØÉ ÌNwuÜ ÊÅ“áÑ$' ¸ŸXoY@ëm…‰¨JçAKß5‘^)ñºci¯,Ü_UA‚ mÛÏÉB^œ³ +ŽÙû‡½u „GÞŠÏ ¯xËÂŒjɤJöI2š²Èjín>¡מ .8³±se¦Ú8~X)–˜óyEØoùd£Ë›y¬éoo­‰–˜µïS@߯ùq:ÇŸcë¶3ñ¯DÀ¶ñË)¿¸*ü)–ÆçÍN˜Õæðºu^Ï+'íŸÛùuözd¹Ã—éþ;›ÒÆ4B™Æ,Ößú6‰èWÜ« fÀ^7±6ì|„8L"²:ô‚¨¶³^[?ãèR]ž‡Ÿ 1¢þjdšké1'8¨9r7VÕŽÂCW‚C±ÚЬCÁM­cORì"h¤Ó‚G…¯¡P…êësuâÕË£†‘Á&ÈSÖ+*e%r{³¼á0t[cBÃç2Ï£ä3g¬Éý¯Ä0ˆþ`áûµÏÞ®¤,&õÀˆã­¯Nü*IRåïxooïÅk4côúÑØi÷6|ySãzƸ•ÝÔ d‡Ø·ä?w0îhäGçØt´ßGYÛý.+JŽú}÷+ðÛOÿN ¹¯^ ¼{ŧëíS_×"qe<ïyš»0v>ïgó‘ñ3#ÍÙÕÏb”:8ÏÈåcDä’—Pϼj´†Ä,Æ`ã3fi"E+dN5¦à<õ˜Ž1‚Ï18Š7ÕˆH˜!Çÿ´x‘)[PÉúBøMY>£Íµ¾+šìý¼\´×ASÂcŒÏr4­xþºÝ{”µ^>€QЧl‡_‡´x:Þµï#ˆ&¹Hx]mDÈÙp5NT3‰¼gÀÞ½œ ˆH·ð¸­Ö‘qcz¤Ýþ-Þ\éX1 ÷׿ýW/ ¦E¸,×YÉÎxT*ud­õØþ_#È|¥d¶±Â…©Ê_îW¿ê¼ô+cëŽ?Rç˜[+»î·ûüÓ0~ÑYZ× K¡=eíðòF}â“ï#LsÇBÌkÎp¨ Û'@ù>Ÿü® Ÿ„€çÌ´§Ú:J­ÝMC¡;¾k`¬“5rÿ_k)B73U%WO% “A­‰¯Š”ÂERG©wªa^÷Ö6Úà T|T»%VvÐH²äìTwï"+i‡ê`ÐXÎx]ô#£C¥tæÐUƒŒÃla—ûÂ&H«Èí=å èõ¥D‡KźQ²eì2t¥9â9—`!d×gλ~sEs-d´Ý1ÛÈ|'ºRyìa2XšžKìL‘뇞Ä\K®Ì‹]¢êê:𣛌‡Öß4q¢ó¸ÙoˆŠßœ³—µã©ÌnÇ»#Ü,è#P|ZùðŒ!]ï›aõ0À —êÑVkÆ8nœØ}™Ø³AqÝ{öFŸ¿[ã _é¤ÛÏlã}NQsp`‡ ‡بƒy,›¢ÎXÓ P–•Š—XàDg8Y»Z2ø;èüO›èRðiÕ´Bi:°Êr\ܽDÇ?UvfJg&Dæpê¨NðO÷õ ¹ £^Þb•½Lé”%B‰±…1ñTX¾2å:1r¿¼µÚÐܼi„l–§3w-G¬„ ͈‡¢¬‘‘StË)`éw¾ÀÊ¡¸,kÀécÐà Ñ,7®ÆoF5¤ü P¡p¡Y¹ì±+ºS©åÇáÔ÷Éf»ŽgéË匎U1—>¯2 ÃçòOâÎùsq®QQ9•qÖ)1j1ùFÀ'…«h¯"Åëó{½A,VVJ|ç\ï¦ãã2KöJä½u¯¾]¥ù¯!Ô”…€«¾¥‹¬ƒhÛ|° Pøœ¡·˜ ‹Ù¡¾ã`všæùË`z‚ƒFwîxŽÞÀ¾o•¶ ¸ËFÜ@ð#×ü,3öPòzÊyΙø˜#&¼ _&<þÊy«œuÖJÏ"ö&bÐ$ À´ª³ iˆL€ÇôK÷³MÇàõ–YRÝeSDlTÖ¶‰7`6ëf&unTóðø*nqÌ?sL%iå|Z¡7Ç”FõFè° Ÿ{/ô«È[}–x ÓñžJ§o¨ÀÁ7~%PØS÷ž«Ô»bÛÈ8ï4†Âåçäâ§>é˜Gácü•½Aí¥pß—$ØŠZ ü§ÔRl‰—M%æ5S7xÅÉYÖáÓ¡m¨"“JWD„ ŠÁæ5žFRËŠæ³KµØ T9É#£1½``NêÄ6tŸ¢Y¦lÆrÅŒàñä5y Té*f+XU[ÍX…›vNí2øŒ®J¤Ý'kYÿüˆØ ¦yæ%Þ2ÂÁEï&ñ«' oBÔ*ÿ·Wv[0«!nÐÙ­í2ôsÕ‹Sú¾Ï.䫿_ÇWÐvGPUÕ¸{Óœ *-s(ð4ê-Õàçz¦S÷h;'£óìQÁ¦7Ï_NŸÿb³tpðhäqç±P‡“T©:¢ýüónH†£›*X8áQïýWËXWØY£÷—‚¤;SÇ‚¶=JFÖSO…}/òÏžž¯Õý÷Kpìš]¬×=O6Xõi«»0kÿ§ ¬Q¼1²“Ç»³¸‰g5"×àA€) êÌÈÛ8OÞ "ÞµPÞ®-Ó sCúÀý³p°"]à«ÎF.@FyÏÖ68Űà.œc_3Ž«£F«:w‰]xуÑagÅŒÊ28ÀZdD…Ð=œƒ9H\å±9÷t²Ãq8œ¹‘ß8 MÉLF}½lMŒ"ª’~ÆñÅìñHòçUM·¨ÍuWkGì «"ËšÍΈ¶ª:£J_”sèï©j5‹ôò“Ê ³+ÙÓ±þä3z¢V‡ï`â­=¾ó…~Îw’½þã»üšxüíå¥í9<ûØ®ŒSGÙøí±¢HÇ Pã…>»,z¶Ó3Þ}SãJ“sÿ},¶y¤0fQ …t+èy#»‘шkŶïzïÌÅ_(`Yƒ´.¦&j‹ú+]’w™Õ¼P£ 2±³…ÜpM"rBÎ&ŽüåûzI!KXgÌÄbN–ûž¿~JÜ–¦bVrU…¿Ø'¾ñ6댺„kÑk›aAîNæ¯ðÂT^7´{á‹ð~ï mÞWY€®0å|üm×\öø"©ÀÉÆæÂ¿¨è@ êÍ4’¯ ™¾ÔªÁ]¿`è<¡Þ×V¡±£@ÌN»šgsû*¦Z´ÓTÝ+ìj·Èù00VÏÒ<þ’åN™ FÃТS÷µ.BÝØÐÛûÙœ½Ü¸ÛªçEGøËx'ÇøðòÏœç«ÅãgÔª/‚ß0†F§ðD„ÞÞà öjj¡Æ%Í®ûÙˆ‚¿7R?¯dÉ+'Eÿ²·§–ÌŠ£M£¡TBbA6rUJwÙå±#ã;ïŸE‘-Y‘UŠ èºó1Cà% ­Ž÷Ø„Ô ˜ÞRç+ôõ‚–ÄXy›šŒUg‹ªªP™¦Å#¦´‘²·:oÅ `´j¡^>è_ (}œÑ¥‡+âCµ7‹Vå<Œ²ñòÏdÌøÛ\• È Ï®ª˜lü›kçÉñwNDFÞ\„\K©vmªT¥8ëB¤ø]z7•;¼Hs¡zçyG&Ó) ­f2<Âé2ÈõˆÞÌ=g§~NÁjî˜w”ZVCÇ¢¤íoÍß¹kÀOÀÜ+D[y×xß¾qqÆÔ¿vã1¹[Ƽ6ÿ8j‰?<sï’Ç~“ñ vß…€GûÝÞPæÑAG-¥¨7ØJŸ¶KübŽGj5j\LôkرïãøÝ¡iÜ+µ@®ý8QÃYd)0}=É #ào*bx--|ÿn¬çejJ¾HÍ88ô;°Ìò<¹mfEJˆÒ[›[Æñƒ¸’ÿáAéPh:~³èo®¼W½•Щæ¼HAr-°]Äyä× LoÔâ±Î¶>Ÿ9n_ƒÌW!è¨Ú0J;#ÅŸ›õ Geií=°/chfÚ‡ò:jP ã$˜ô\Š—.„­|å9甕±…3[õÄ.}ðúüg›n«iû ÓTŠWÄ|lÏùÏ1½ýEÜ—Ð|;¾>Î ¾àq´çËó;<’>F¬š2á…Ë3O×>8®|Sàˆ£Ü÷ªX¸:TX¶’ç êç\2ÚÉ*³Ü¿ËNºú„+Z‚)Úá²zˆŠ¯s´uVа,i6;˜C e`SûØÙâY~úÊ/cÌV/ ÚY7¶l ו¿“pi/;?%ÁðÄù‚^#øÝЭ䔍~ŒÅƒˆ‘Œ›¡·2Ü6³ŠíáPÜ­Ðè9|0¢0ÊBÑ_0(ñ2­vƒ}‹Yï…c»ùéTcø€ï÷JR(2@«Ì#¨¹·ë~–!H¸›ÁX]œÐªÈ@Tºß$¢ÅJò‚sVYÏ’Ë*ê”,v]…P7¾ŒòUC'éz¦Tólk_“kŠ•ïð¯qóÐãã@Ù>VlüoÞ8bƒ½7Étÿ½ [õæ.AÕñ©°6ªÞº£lO©Sa ³¼»°ßM6çì¹ÄÅìÙå,:¨•µj_­1J-È©>AZ(PÝÚk?‘‚ß-kô$2»1O5!‰Ð ätÀE®xî?ÚjE`o•"ÒÂøòIÒØ¤Û5ë Jhº¾-»#£ª%£Ú:aªf £Ô+F…ùÈ›¶Àˆ!/ÅŒhš«XžÎUÕ/,ÎE\gå_ÅüP1Bó&väA:ÕáÚ©ƒ#t™ûšfÝUwï( iͼӳó>éÊaµ=¹ù^¨Å+ý/˜bè¢E#›Û¦žSœ.Îx#à'"ñ¾ÁünÐ)îÑi×\¨G;ˆÙ7¾yÁòœ À[ ïI¨×?ÝïLÜòeÓ(£Ïvë¨ôçÃ{z €q×ø&ÉP£ ½¼&ÑvD{ñeÁ{àd—$„¡,Q¤M¹¸Få}ÑhÅÂfãT©T¥]QCß§å†Vf]VŠÖ ¡UÃìaÇu¯Èâ¶ÖºØÝ´7ÊŸj¥IDATòl°¿;Û„ô W¿lH¬ù¨“÷ìf 8 !ê«W(Y{ñÆTS–1æ1žÎV#ï##íX{&­l?o6èÇùºlp•””A1K…‡q \3«ÜoeÆ{¾ Qô”‹0 Rµ*V¾³~^X¸åÖÈ\ÎYÁ\!îèOXã­Ñéîp°;Ñ£¶;ÝÀî87>°…÷Ì|TÂAW†6sG ý DsoÅÆÔè…—\ڢߧÞ#÷ßFDÓ÷µ(bŸRGtÞõÕ5FX ý”íBû­?$M±…èÒ3 ³¿/B}Ô3Ÿ³žTõ8 £ñn²ÕÄ,¸Áâ%& gÑ=)uñ´G´d9/ Þ-¦„’óìÍ‘·@í¡YøK¦2¾µ…ƒr–‚i\XÀXLw,{Ïóʨ§i(õ±Hï¦MY1–ùsÂâ ûV(\W—ƒòsQðgìë+”¼‹D÷­ }“ʱǂF÷w" †01áåºèüJ @b&Ÿ«C¡¬Ž•Ш{`ÊͨžŒ;láΓ|ú¯›{|;Þò3¶£Ò¸1ÿÉ>àë„ê;}TNò`yòÈvb`þ’ùAKN£wŠ÷øãº÷ÂÕ’CIêà’'²_’.ù0lEˆpT4Q(•´:¥F; ÑV®AFac’röàŠ.>xÖ.Y<¦-€Ä²ž©´€Œx¬Ò:fxzÝB:o…éíˆÍ°ó±ÜN®t7žÂ¹H­³ ‡n"õlpÂÁqÂrE9ðK…sªI<ÊØËO|IÆJ¿dsJêÇ?Á±G±Ð¨7(àɸxÅ*‘ž¥Àß6­Þì—Ü£dÆ1È `JFŽ`ʲÏç“örÐ3ò‘{ðc^*ýuºüA^)Ó0)”þ+Î0ÍžØ ?e¸ìpŒê]BûGä <ßÕXá«ðY•6vùi …Ì„i,±.{ÉnK¡ë·bns!3@k6¯\§¨“¸ìZv«¶$%̽i(Ï®âÒ÷3ÿ¼Ê”0FRæ„ëàË çÙ¹vò¦OáÈ•^^k¼¹èaP½›l4°ïF½Š§I}=;ä…¯S.=ä\«¥»³I毄Âç‰ÞÓÞç¨ßÔJl¤;Eš',Ÿ´„Ki€ßïBÀ[9c£ñýJ糊,[ýã‹{mäRÿ‘-‡k׊á¦0;á騺¬?1Gk#¨°5R¥öR-ùÛW)âŽ8§’½°ÔM[j O}ˆÝêTÁÊ^N¨:V çäBsrfô£;¿üÏ8¦èº½•žC@@c¥˜k–cäQ^YÚ!AíÈÙ—`£\9ˉêŠ5Ñ®ãÞ°jV}ˆºrs•Ôù+, J°<{O•O*ßMîѼ®%MiJZEå/¯7ú3©t!v ®OAs»ž×\y>÷ÕÇôø£2ûÊ\cýM>žs¤ÛsÜ|#àtqÝñ_MÍ ï8$Zút Wüðø‹×ÎáNöõÓNÆO_‘]É·ûþ{=n#Îr{ØÆïôÝ0ÖDoe^ÆÑ.õÑ¥ð|uU·J£ 96¯µk•€J/‹Sï ˜Ô;ïþûü?øÞâ5r­(bb>c5ÛR_ÉXíÈAÿõ³³û®\*,ۀėV„µ˜[‰‡é eV¤HF?ÌZßqjÒÖ¸QéfÓ$úNî n2 °/á¥ÏL{^t÷æþc`€ĵÞ/”à1 †´[PH–sÂhT!‡‘ñ¢0ÈVeÒY5#3íÇ ôÉ?F‘× {Ó™Y®«ìÈ'Õk_“FöÃJësÞ·ýìfmw\’FJÛ©Þ‡â­dóB(1/«[ k{LñDûì!E¸“BàÃ…·^¬,ÚˆÃ>W"û-B]8ûì»×'ì·W®®2‘÷ß^Œ­¢•¶VËÁq>†"沟HQêÌa(©•§%]'_YtV4©Îf 8·üT:=þ3 %š2 ÆÝ½©ûÓ¨Lã<_÷˜Ï÷øúÎNÃR¡UŽÜ¹¦Ñÿœ¤TXë'j3²O«³¬ØÛªòú÷õåÝ`Õá¼V«6w½ˆp/Ý# \Xô»C ŠS‘Œ å¦ £ùÐKnH*7Â*Ü2Ѫ§q9¡á|èV¯|œfŠÍ‘S]5§œSÄÖE.Dt¯Ž*1¢Œ[Ïù/ºÓ)S©_YûMÔ8‚5PòègÞ×ÓÁÜîûÿ»cökÿŒSØ¡CÚ4flùæISѹ";s}OÝ=Ï­ïËëœô‚A¢¡êqòµãdøÄ•½Ø3êÆ¬ŸŽ€çýLcc¼ÐfGÏŸÀ¥îh?f©ÀˆW»uOcg„ŸŒü_M´¼eü׊±^®Y†®ÞÏØi'dA2QŒÂÃøÓŒ|ÁIׄ.g~—„Ferö-ºŸ©ý2Õ\äo„SM" ±ÀìŸã¿¬ýSTÍ¹Š˜3È×´‘ƒ4§¾<ÿ™&F‰ceà>é‰ÜR q0È]Á½³sÄϹY4òUžÃ/ü‚<ó?T ©YK`¨M:b"ŽQ"֜ԟà]x÷üö5aq_ÈÿGÉmQŒ VV÷zO£ó\ݘI-Þ;6{Ê~³9‚çO:ŽÂ 7÷œ·t×€çæ68àð­¸{Eøó±óÖ6Í—PŽ_ZûGï÷Ø‹!ÂWîüçÂêqþƒá·"à1VÐF®”ÎÎáìyC ´T¨ë×òCÕPÊÏ–ÑRPu?{0{¡Œ,I/÷üÁ«4ÂXcDO£ 4B—›¥ÖÁés€îF8¸fF<Á=I”œ\%˜0Mø¯Çø¹0½ÌHZÁu9[DúS“—ØIŒ ޝä±9ÞK Ô´¬xš¤/©ÑM×’€q!e[ T¸ $Èá ©¥wäy*ÄìÔ˘2ÔÒ2"ÕîÌ@V9€y<‘kÉ ûòçyjäû3}øs3»Ñ£Z@C2ά…æç™¨|»çØÖÊh‡lÒgDÛz–tÝöÏ;mö8,ñqèºnü7AÞËlapм]6è:ÐjUëâ1žÿw\õŽxÁÀ¬_¹‘èý—”°Ðˆ_¬‰G#²8k¬Ÿ×¬×Ä5 ªËæBû\‹܉ë)±Ù=& qŒÈ­¬Œ¾¼’È¥g 8ØUµYËÉWyƒÞÖ`î·Ô16^ 6(²‘.¼ªs¬4Ϻ{ƒ¢µ×ÅQgÊäDé&•.î¶ü ‹Q O`ò 5i*£R*÷äYï:pËM×V½ão©rå9ßG¨£ý–ÙŠMûŠZh¿­¶rÂíFÀÇ„¨Vhžøõѧ~ô øÇ )ãí{âB‡¹è™q½S\nâ£F{ÿ‰€çª"ä•(\«èi«ç<òêÄ>óãÂçÕ©Yäàå¶\#ܧ‹ÒÏ¥3>*ˆ¹ hÈgt˜Æ|O-Š¢ªq%ìOœ¾b±•úVM¸äZ<˜Ç½€eÅid×á4õ6dOpeeä0¨QWÄ³ŽªaÐzhGaN¯Ÿ ïY”]¤øI ûI=ÍÏ|pQ…\åŸãæ qž¨o´ÖƘ¾Ð2ãÑì;S€òsÈÕV¥,†¸¼’C ¤°†ê÷6NÔ3í¾Eí´ãSÕË¡/Ì]Î(¯3@e3‘nóþNsÍçéSºåkW=·Çn¼ØG¥ý ë¼ßg´sÃmMlNÝx{îÁ¯étÄ%Õp>s¼àT›öÕS~·Xû.½@oZIì ß÷ß~܈\L •F Ò‰>¶ê‹nõ_´y—Õ‘|—œO¿ 0²Él "~7ôr-uè˜Z\®ûþ¿^öîåê¦aÑá÷ƒ ´ÊúVìÂk…XØüw²F´ dù ó‡Ò¾¶Ê" ¨nšWZf­¨i´Ð7©ˆÜ‹Ì>ßü[k,÷à~Ï‹O7 ˆ(„ލú5Àð(’f5U1}L°ßNQ+õKR«ówPÀàŠeíw å™I½â‰¤Ý Ä%‚û‰C}]׌ÿ}þåO/&Ý;£¸ ¥P­UÛʇõVÅþ>‚ëùÈ H·Ï‘7Þ2B>±˜Ñ1ùÁ#à—ÅG³…Ÿ«—R²}Ýø0›GÔì{”CóÆÒþ¾6r|Œ=>ùùм*\~î¿pG¤“‘ÙÖH¡Œ[íXs ÁåÎêK?%-ËH— ŸÁ¬cæƒx[Ù|ÊàJ«w] 4ëĪE?îÂ8µèeܼ λár±—ªïÑ$VD£Bø§˜2_±6 ‡•ÖÏšg®jò ª¬“¹f±é7cµyj…j‡ñÖ)\wü–6¨Š\¹Ia$/½¡î #øì³‚%ø]U{é#öL‡FÚÉ3~;¸ëYèMþ'™À¶Óë7™†oªô 2(YßgûÖísÞ'ù<Ÿúܧo:ÛêFÜy{Þ5à£ãצÎHŸÍ„<wüÍ/˜öTúÏ»(¼f±RŸ~:sƒãLsŸøùË*»'10pÄtÝpùúx«¿ãXD¬4¢–[n¡ê†âhy Î*œ†³¨tÚ$ƒ˜ÂßÜ [å бß[ßÌP#23Aó©Þ܈çPÏáw5Wôëeáª<<8[A($‰)8—ªyU8~⊨ûR.! QÊ‘×Ór¨>ˆFˆJ@î…'‚Ý„øöÕÊÆŒ&{yäm]r¿U¨[234ŠQ N±ˆùÝ#’h™› ¥ÒLp»p,f·æŸì $Þ5RÀ6Êa Ø{£X_¯×&>_Ox ž›÷þεö¸·@?ض"Ë Œëj Ìyêð¨mzFúՀψ(_†/ße˜óÄ/‚?tC^özñÚ5³GO·9üâ‡Õý÷^¼Õq~ߊP»BÒs\ÞñnjtƒIÞTpD…ÅQ°• é*í§ì_ôƒWL¢"¡§ 'MH g¾«‘Œ2Å—Ð5Ú¸o’Õ’+_[ÏüôÕ\PoÕ^‡ªW’êÊU¤Ç[“ÒVÀšY wÙ† ߇´(®Ns->u ZSÑ›[("ëÖ)Åð(d´ƒžÂú(zyUu3{(©g“ûÓX}š(^È(¸Öá«g*V‡÷žJŠç§p\b}É$ت£ÀØ·ø~éwûœšañ±v³|³l ÛâÛœ™4ŽÆ{çVÂ: Q!ñ™Nu¹Áöán­¥ûqÝ廸îzóÕ] Z¨³v©ißIË¿áï§ àŽ&ïóyl²ŸÏ<ºGÞ|ãXŽyCM4h2S,oƒè· Â4oGõਅ\ƒSDØöÐÃ#U¦‚t–+ÿ¾dd¸Cf3ßhÏwB#pMI±Ë’^´ë¾ó¨acпú¡심Ÿµ »³›=œ€bðX‡0  $ªÔ_ÞU¹,ŠªÓÓA£ù²ç#{ÖÃZMa"%®*#:TZÓÄ“ÿ)•¢XÖâÚÊ‘—;bCvgÔg¼¦[Qáª[#ƒÛ <µþtû }×Ë+iÚ«Ãͧ‰”ÝPùªµàaúD5ÛxôŸÀ3}þóÖjñT²5“XÇÇ7¾ @»[記½é¢úÞy;¶¦x>dÁFuxÝŸww>sǽ,—ó„‚Þ m¶F,°qé{âë£êùöøTL£$öêWúu襱X*ÛÀæÈÚ˜Ûi®#ÙÒB¶†š7˱ǯþ{±úUå®ÃXS8³MRµD M1ÃP}½ ´]9ìšQÓªã{̽çËÍO‰\}§¸¨4g—ÑEhΚۢ7š˜·cdÀžw/øñΜÕ ÊžKY!œg˜àÅô°™#TwtÑ—\âiäkñ³jÁ9Š«æëÑL©ÛeZ ™³U•:j¹Ci¹%?%ϱdÁÜêéw?ç4G“óºõÖªsÿ­Q:§ÍŽ£|@ûÞ,è‹Äá¯Ñ‘> ãâTÈ5ŸŒ™¾U5ü=$ñ44¿U™Þ Ïx&ÜËte¼kVÑÓ<Ê°îÆ±ÎHÚ ÑåFÿíP¹>Qk…ƒ zAa´9Äì1tFœˆHËÅéÁ—TŽ}­:”©õÛˆVì™NV B岫µ K^:)QŒŒ¿W¼êõÊŸÃŒâŠIàĪ÷°mï‰ÛÈ2ÆUv´Ÿ«YŽ|ŒÌPêÄþçYÛ kH‘qp°úU1Å¥áoèÌ“X–¤ƒ6J­(ã/*ä¿rÏ4€UÆSˆä:3A }Ü]ÞæïŽVí–öæøx£nÄÊøûj_sÍ £ÅétEǹp?êütn!)÷ ïBÕØs{¾¹G¯ûšÀè™›ßwÉŸ„ññWpÿMðsZ{”PÞ+Ê,cúßþ¨Î>…ÌTÖ… ÊÏ¡õÕŠ›#«L+ÝiÆ @áÇ (té™D“³Ê’ŸtoóK53õ ØÅò¤ÎTüLŒS¶BxFô›SÍ%dìçSDî(Ú ¹#œ™¹IzƬØÄ_ðÞS@Aç¥Ç#CaÀ~ã§r Ä6(½£Ï¨ô­­{pQ´ä‹ÇŒÿ•a¨~ܪh¢dÓ›²#w¤*/±"¬& 1È8—"ƒØ×w•Ú»8£ÂÓÓLÉF D‘4~*vq¶ur¢[9I}þPÇEÞ¿jà]Ös¼µ wŸlÚÖsŒw`ú¯8Z€wÓy:6^òït¸FïìÁ‹&·x÷ßñ}Œ8w:"ýŒ96~g4"šö¹âE®%¬iÊ£f)#‡ÚXÑog³FJS0°Èa âN[±à+]›YýÛ„Þ5&(-ºl1¾~”¶ò|z"‚öJžjh”4ÉM™õ¹êÈ.… Þ=š àD Åc‘&¨\{+y¼¼M…Qa‹*[2Âäh’ðŠc‘·!U`o«Qù\—ÝÙŒ­ÙÊvFÓ ë<¥+EÂÆÓX(Æwkÿ-c³£¡rÒ›ç_g¶máG7Þ Ÿ 1±cx›PZlâ]ó³¹»³Žç×PÄßÞ˜{ôe'ì´÷ØÉ…}ò“äþûcØæ:–cú*ïô¿9jYãä,ëÛèŸê¸ïÓ¯Æ2}…©*:˜÷¢Y#~)j›ï×­*Óy/EΨì"}W“2þ­À#¡dÕZMbV}2"ÞÕEpªÎŽßá©“çÎãÁÔ’/ =Ã(ªF˜›lƺp™  «ñœ+|fÓtv êÕ»ÂCqâT×BUEÇ­¡[õ/˜OøãÒ¯âcî8Œ¤-#ìB ƒ¸îQ/ºxQ옼®!”¼ˆEÜP÷ÍøÛ‚¿Y(}Okíœ#Nf‰Yb´Ú‰Ífì©A<§Wµ‡ç¼§˜žÒeî¶Ïˆn;%ßøÆú› 8:Üîèî¾Ì(5y_H†w£ðë‡Â ý }ße©w–ÃñÚµ¸±ï‡#`¡HÒA·ý¤Ï©.A«Ï¬ÓÓ6[Ü6_ɣȟ)Å\MxÁjìkž+ª˜nh¾Ô·ì"_’ŸIðòF±Pu¶A¥XHä$f`d¤… ÊÄ:>I3ÃU{Ì!”{®ËÀ)${@î®Ì <-”“Ãfõ.æ+”nN0Â1„XÁŠJ„óÌSÉÜ© Å@þoä=ÉòcJ9Ë54F·E‚®Ö#ËUêD.F4wE#ïöîÕ휆–Yp©J 7ÙC—ñõüåÐi#ë?Õ±ï¶+¾q‡;½ÕÉ~~ü­Z¹ºðþô•®g÷Mâ%Ã@o$ωå>} ÍÀÑÌ Ð9žÊy`ËÊî3~ûÃäFº —Š$ý¨¤“ïXããN'VG-«[¡¨4#ôƒŽGÑs°WR0 Ö“ ‚^Ä .îÆà.°—×]bxKZÐ`”œÝŠèë§&ÖéÍ×Ü+–r¤#™Ñ˜#‡Fá#_uTï~«VÚSëÚ³qÕ3‚Sá [Å7ô݆ia¡¼MµU¹•Ÿ.dRujÕß=Íå#ˆ¡‡ Î5ZŠßÈH>ERZ^ºQ8ãïoŸì¼¦ …gQG74P&9.kZ‚_Hˆ[«Bö6Æ3Úàú‰m[1hvpa3 —8rþÖ(ÎRv÷vü†ûo´Æüßø†ð¿it8á°¸Æõ⣖ ߪø3vÿ]ýïÿMk[æêâ×IEND®B`‚cawbird-1.4.2/data/play.png000066400000000000000000000021051416632607600155260ustar00rootroot00000000000000‰PNG  IHDR szzôsBIT|dˆ pHYs»»:ìãâtEXtSoftwarewww.inkscape.org›î<ÂIDATX…Å×[lUðoÆY™[k©€ø ZLãí8¥uÓMã-ƤÓ¬Ô¤$С¦¦-µMkk«ki)¥”p h@cT$ø2ŽŠ!R-¤K„^Rç¶»sá–L—.lÛÙúOæaç|g¾_Μœì`¶mC,/²,ÓF—qPQÔíHÿƒwþ`Y¦«¼l}ö4Ÿ×û>E‘×^|)ÿíT°Ø p¼ðìœÙ³/ž:ÖËÆÿê냽µõòðÈðEYVÖ#Iìwà\’a˜¨spYVt[Ù2i>EQä{‹öp¼@¦ €c†=P€ã°fU ÞݤórW|DSÔÇ ¯§¸ðÉôt¨®ª¤wWUΟ—‘q¼ÀW|†ã…§]zfÀD›0™ûŠ §k?í-,ðS9ÀñÂ[SL¥?¤±,ì¨ØBjvg<•™Ù[è+>ÍñÂüÄòBNtw~ɬY]ò*I’ý¹+ 7r¼ðÁã7!Là!(ó—z‚Ÿ5±K?·e˜ß9^È~Çqw±<³h´65°›6”s Ãüzï$}ÜõWðç¥ËP¨“eE9«éz9’Ä›«w Ë ´´}¡;ÿ³¬jÚ{H¿If^<`J‚³çÎCCS³bšf§ªiÛ$*Éο m{R'Ñ`(µõê¿WCŠ¢¼ƒ$ñÂdæÇlHr"‘tõôÇNœ [–U‰D>E’hN¶y< ©=ð‹ôêÔp8òƒ®ë$Þ˜Jã€Ä…·‡† ¡©E»€Ð¨ªj~$‰ßO§qÀƒ˲àÄɯ¬öÃv½¦éÕHÃn4¨ccc³¢†âîí»ŸfŸ(Ã#£HÓ4?’ÄÜj|?¶mß¿ |Å?6·¶š÷÷Ù;*?ÖoÑÇ ëœ5n_˜sÕ9^ÈahúkÃ4Ó â±î™ø<¿wâà ¿ ‰JIEND®B`‚cawbird-1.4.2/data/play.svg000066400000000000000000000050631416632607600155470ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/play@2.png000066400000000000000000000040151416632607600157120ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞsBIT|dˆ pHYsvv}Õ‚ÌtEXtSoftwarewww.inkscape.org›î<ŠIDATxœÝ›{P×Ç? ˆ "V±*AœŽbŒ5;kÕ¦“fÌ«i'I[ǘ&éØ˜XM}ʤ±ÁG«‰UAàò¼¼A‰MlŒ1I;DÚnVÔuŒÆÉˆFÑØJÁ ·¬—A‡Ý —]îÅÏwæž=ç÷ûÝßýî9¿sfWp»Ý!Jr8°xTå@®¦*.Ã}Á(¢$GŸ;¹|ø¦*ûmŒ­W0K@1ðëq±1¼´ôEbbÆr ú …%;øúÌY7P¼¢©J]¯El1&@”äHà»~ýú …ÎL¢£Gµ_s¹\”•ï¦p{)ÍÍÍ߯¢ßÆ÷S€dÐ> &ÅO¼éÇ„„„0Þ\ s2™9C 8*Q’§Ø­ % `ØÐ¡†GŽÁÆõkذ6ßðC Z”ä­¢$²Œ èÞÆ Ï- 7ÓÁ”É“Æå¢$(Jrœ%‘Ú„©„.( #±11$oÝÄk/¯ rð`€‡/DIþ£(É¡=ŠÔ&Ì t/ž1=0‡â|'>ò‚ „Žˆ’|¿ï¡Úƒ¹£ËÞÁËË—âHÚÂø¸8€ ÀÇ¢$o%y¤Ï†-Æ‹zîàÎIñ8ÓSX²h!áaaó/EI^"J²ï¶óU  ¸ÉXP¿zâqŠóœüøG³A_fÀç¢$ßc‰_c3h:|ZFTÔ0Ö½ñ:›6¬'zÔH€é€"J²ãFõÙëôŠnE–î¡À™É3O=IHHH°8!Jò<[š`ù*ÐUBCCYðìÓägg0Uœ0(½Q;ÄÚæø,­|áŽ1£IÚü¯®\ADDèµÃQQ’WŠ’l·K*A+xøÁ9çe3ç'÷ ¶ª(ÉÓíôëwtdHd$«_[Åæ7ÿì™$§¢O’‰¢$´Ã§ßæ3¤éÓÈÏÎôl°‚À1Q’jµ//«€п(/<·€ì´dâ'L ¼/JòN++I/u€ÿàa|\i)‰,]¼ˆððp€¹èÛíçEIîq€3 š$<ñØÏ)ÊÍbÖÌC€, R”äøÙ6kïÊHo2<*Š kXŸ°š¨aCf‡EI^'Jr_lü-Ð÷ΞEan6ýìQAM”ä{»k+`'Ao 0€å¿_LÚ¶­Œ‹}»ýQ’sDIþ^Wíä2Ø&ÅOÄ™žÊ¿!4´Ÿü}’ìÒ¾ÂËHà'ô£ú§æÍ%?;ƒiS÷cÌÆöytdtt4‰›6ò‡U+;žI~*Jòp£1¶‰ù“çÜOQn6wM¾àÀn£¾¶‰ù‹ÈÈÁl\¿†!‘‘³EIÛY?/u@ßT€‡ˆAƒ˜v·èùz_g}úö/´£´´µµõb(ÖÓxù2kž¯ï¬QÜ&<õõ ¼ž°ŽKõõUšªœé¬_ˆÁø6·»o*`ï¾OHÏrRßÐðð¸Q_£ÜP@ß’À¹óçÙšìàà¡Ãž¦ àyMU.ñ¢€¾‘—ËÅÎòÝ–l§¥å:À€åšª¼ímlŸWÀ±ã_²9)™S§¿=î<`•¦*ßue¼©Ú8W®^Å™[Àž÷?ðüQ'Ðå^Ù;¦ Ôe r)iÔýï[€à-`£¦*Íݵe®€¶ÀJÀź:¶9Ò©úç¿=MûÑÿõã¾Ú4ŸŒ´¹Ý¼»ç=œù…455\B4ÏÙÓGóÌ 8YSÖ¤TŽŸ8ái*–iªrÁ ûæË ÐÜÜBAq e»Þ¡µµà °XS•¬ôãEþ©Õêƒ$¥88ÿÍ€V xCS•+VûòRYíΜKõõ¤ed³ïÓö}Ë!`¡¦*Õvù ˜I°bï>Ò³shll¸¬¶iªÒj§_sôÂ$x¶ö‰É©ÒŽxš*ÐïõÓ¶;Ç¥pKK Å;vRZöW\.t£~·/¥°=“ ¢ Ù‘î™äÚ€ `µ¦*õ¶84ÁK)l­³‹uu¤¦gQ¹¿ÊÓT ,ÒT倵žºN¯( µµ•òwöPPTBÓµkõÀj CS¿žºx™zîàèÇHLqPsê´§i°ÒªJ®§Øv$ÖÐÐ@¦3ŠöuÜ®.ÑTåŸÚ€å«€ÛíæÃ½‘•“GCC#À5`ðMUZ| Ô.,­NÖœ")ÅÁÑcí»Ó àEMUj|ŽÐf,©›ššÈ/*a×»ól\jÑ×ô]–Di#=>©ü¬ŠÔŒ,.ÖÕ¸Ð7.k4U¹lUvb”€ÿÔÖž3xþ› $;ÒQÔö%ü_èkúÃAˆQTàêW'O8[{Ž;ÆŒn¿pýúuJËÊ))}Ûsݧ_œ4{uv;0?:z¯¬XƸØX>¯®¦¨dgueܾ¯Îˆ’<øý…È[¹ý_ž¸ñ´Õ2à`P ¼äß.¯ÏÿQëÅ%„òÌäIEND®B`‚cawbird-1.4.2/data/protected-account-large.png000066400000000000000000000010521416632607600212740ustar00rootroot00000000000000‰PNG  IHDR D¤ŠÆsBITÛáOà pHYsÄÄ•+tEXtSoftwarewww.inkscape.org›î<tEXtAuthorJakub Steineræû÷/tEXtDescriptionmimetypes7±íd!tEXtSourcehttp://jimmac.musichall.czifã^‡PLTE¿¿ºªª§¬¬¦À¿¼À¿½§§¤¨¨¥ŸŸœ»¹¶»º¶À¿¼œ›˜œ™»º·À¿¼ÁÁ¾ÂÁ¾Â¿ÄÃÀÄÄÁËÊÇËÊÈÒÑÏÓÒÐÕÕÓØØÖÞÝÜÞÞÜàßÞààÞááßâáàíììîîíïïîðïïððïññðùøøùùùûûûýýýþþþÿÿÿéYÉ tRNS4]_ÒÓíí÷þþþV£IDAT8OÕ˹‚@ EÑA@ÑAF@qC\àýÿ÷Á¤Ñ"±P ½Ešw¢Ty¡6l:ð„üì ÿ.M@Þù¤ùì(ªÍy!‚ü×}.5N™IØH`‹}wK$Pb÷XÚ *k»{µ+<ð¬æ@ýð·€ëgÀDÞÇF‰´'C~OÙâh z¨ é;†-–×õIEND®B`‚cawbird-1.4.2/data/protected-account-large@2.png000066400000000000000000000023641416632607600214650ustar00rootroot00000000000000‰PNG  IHDR@@ªiqÞ„iCCPICC profile(‘}‘=HÃP…OSE‘J‡vqX,ˆŠ8jŠP!Ô ­:˜¼ôš4$).Ž‚kÁÁŸÅªƒ‹³®®‚ øâäè¤è"%Þ—ZÄxÃ#çÝsxï>@hT˜fušn›édBÌæVÅžW¡/Œa™YÆœ$¥à[_÷ÔMuçYþ}V¿š·‰g™aÚÄÄÓ›¶ÁyŸ8ÊJ²J|NN€ Í*u£EÊ^÷¹woçÜþíiÍïHxr–ÃMðbKGDÿÿÿ ½§“ pHYsÄÄ•+IDATxÚíšKOQÇÿwz)ÕÚå)ŠB‚€˜ ºÀÄ,d«n5ñ¨ñ ù &úpEFjtI òY)-Œi+Ò¹ÇE¡)i™Vfzþ«æöÌ´çwï=çÜ“ °X,VKü­á»÷4×8%® ]UÇðöÍôyÏŒ0\6¹?Lx2|ãæWÛvÿ Áå«|À „BËú©œ€0€q«/ ñP¬ù8ýÚ6ÃC‚¿‰*ì(‹tW6À8Jòü( CaJá]D¥êêP]U Y±?+evv°õc ñXBh%ƒ!K㼂®èìêA °´‹4ŸÃövæ?Ïasó;4Ms "F5zû :¿§ÊÊJ\êíG]]ýîVñ@ìêî±5›B\ì솔îÞD„³MøýþܘR kk«H§’f>€¦H3¤Ìþ%ŸÏ‡–– X^^*j<(:€ššÚ}c‹ óˆF¿Aˆü¨OPŠHl ÿò•œmm8ŒÅÅùW8¸ï‰øÛA@Ó‰ QL p J)wÇ€ã,ßR¤B.…(j,¿ÃÐ^ͦy8‚+¥`šæ‘Ï*ux\™fÎ^Ó„ÐN."B[{ôžíAQ6•嫯À²ÄÍO{ºžK&“ø²´àh‰,t¾µ­‘HsA»PH·õ^èÊ}ÖuRJÌÍÎ8vPÒœ :£}φB!G‹#Î €0À` €WÂÆ¥;ö4!07;S’F¦Ïç;ý!,NËIÈ`  dÕI¶ ”^r~5•¶¤Óé”W¤R©má÷ÿ² ‹M–âŽN)f?Oݽs›lU‚¦©¯¬¬ 766†ƒÁ`Inl9)¥’É$Ö××7‰èaŒµ^¾xÞšÉdÆ•R£DtÚM„iMÓ¦¤”îݰôOò51ñJºÈyŒÝÊ€Åb±X…õ(±ûŸºz¯øIEND®B`‚cawbird-1.4.2/data/protected-account-small.png000066400000000000000000000006621416632607600213200ustar00rootroot00000000000000‰PNG  IHDR(-SsBITÛáOà pHYsÄÄ•+tEXtSoftwarewww.inkscape.org›î<tEXtAuthorJakub Steineræû÷/tEXtDescriptionmimetypes7±íd!tEXtSourcehttp://jimmac.musichall.czifã^NPLTEÀ¿¼ÀÀ½¥¥¡££ À¾¼³±¯³²¯š™–¿¾¼À¿¼ÅÄÁÅÄÂÅÅÂÆÅÂÐÏÍÕÔÒÖÕÓÝÜÚÝÜÛóòòóóóôóóúúúýüüýýýÿÿÿÔ`ÎðtRNS£¥ÜÞýþþ^ ¯UIDATWÈË@0 EÑ è-ú¤äÿÔ ]4g’ÜMB“"PTîb­)_1Ÿ®œ¼Oùȼ6€ÀÛ'\Û+ðÓOÐiš$Œ4Ì¢þ; ô lpIEND®B`‚cawbird-1.4.2/data/protected-account-small@2.png000066400000000000000000000010521416632607600214740ustar00rootroot00000000000000‰PNG  IHDR D¤ŠÆsBITÛáOà pHYsÄÄ•+tEXtSoftwarewww.inkscape.org›î<tEXtAuthorJakub Steineræû÷/tEXtDescriptionmimetypes7±íd!tEXtSourcehttp://jimmac.musichall.czifã^‡PLTE¿¿ºªª§¬¬¦À¿¼À¿½§§¤¨¨¥ŸŸœ»¹¶»º¶À¿¼œ›˜œ™»º·À¿¼ÁÁ¾ÂÁ¾Â¿ÄÃÀÄÄÁËÊÇËÊÈÒÑÏÓÒÐÕÕÓØØÖÞÝÜÞÞÜàßÞààÞááßâáàíììîîíïïîðïïððïññðùøøùùùûûûýýýþþþÿÿÿéYÉ tRNS4]_ÒÓíí÷þþþV£IDAT8OÕ˹‚@ EÑA@ÑAF@qC\àýÿ÷Á¤Ñ"±P ½Ešw¢Ty¡6l:ð„üì ÿ.M@Þù¤ùì(ªÍy!‚ü×}.5N™IØH`‹}wK$Pb÷XÚ *k»{µ+<ð¬æ@ýð·€ëgÀDÞÇF‰´'C~OÙâh z¨ é;†-–×õIEND®B`‚cawbird-1.4.2/data/render-icons.sh000077500000000000000000000005071416632607600170060ustar00rootroot00000000000000#!/bin/bash sizes=(16 24 32 48 64 96) for size in ${sizes[@]} do rsvg-convert ./uk.co.ibboard.cawbird.svg --width="${size}" --height="${size}" \ --format=png -o "./hicolor/${size}x${size}/apps/uk.co.ibboard.cawbird.png" done cp ./uk.co.ibboard.cawbird.svg ./hicolor/scalable/apps/uk.co.ibboard.cawbird.svg cawbird-1.4.2/data/symbolic/000077500000000000000000000000001416632607600156765ustar00rootroot00000000000000cawbird-1.4.2/data/symbolic/apps/000077500000000000000000000000001416632607600166415ustar00rootroot00000000000000cawbird-1.4.2/data/symbolic/apps/cawbird-compose-symbolic.svg000066400000000000000000000054361416632607600242670ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-conversation-symbolic.svg000066400000000000000000000050601416632607600253250ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-dms-symbolic.svg000066400000000000000000000113231416632607600233750ustar00rootroot00000000000000 image/svg+xml Gnome Symbolic Icon Theme Gnome Symbolic Icon Theme cawbird-1.4.2/data/symbolic/apps/cawbird-edit-find-symbolic.svg000066400000000000000000000111161416632607600244550ustar00rootroot00000000000000 image/svg+xml Gnome Symbolic Icon Theme Gnome Symbolic Icon Theme cawbird-1.4.2/data/symbolic/apps/cawbird-favorite-symbolic.svg000066400000000000000000000004451416632607600244340ustar00rootroot00000000000000cawbird-1.4.2/data/symbolic/apps/cawbird-filter-symbolic.svg000066400000000000000000000047661416632607600241140ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-mentions-symbolic.svg000066400000000000000000000117121416632607600244500ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-new-window-symbolic.svg000066400000000000000000000064501416632607600247150ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-profile-symbolic.svg000066400000000000000000000043601416632607600242550ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-reply-symbolic.svg000066400000000000000000000073011416632607600237460ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-retweet-symbolic.svg000066400000000000000000000116631416632607600243000ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-translate-symbolic.svg000066400000000000000000000107111416632607600246070ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/symbolic/apps/cawbird-user-home-symbolic.svg000066400000000000000000000140231416632607600245160ustar00rootroot00000000000000 image/svg+xml Gnome Symbolic Icon Theme Gnome Symbolic Icon Theme go-home cawbird-1.4.2/data/symbolic/apps/cawbird-video-placeholder.svg000066400000000000000000000136461416632607600243730ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/uk.co.ibboard.cawbird-devel.svg000066400000000000000000000347701416632607600217400ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/uk.co.ibboard.cawbird.appdata.xml.in000066400000000000000000000521471416632607600226600ustar00rootroot00000000000000 uk.co.ibboard.cawbird.desktop Cawbird Twitter Client CC0-1.0 cawbird

Cawbird is a native GTK+ twitter client that provides vital features such as Direct Messages (DMs), tweet notifications, conversation views.

Additional features include local viewing of videos, multiple inline images, Lists, Filters, multiple accounts, etc.

https://ibboard.co.uk/cawbird/ uk.co.ibboard.cawbird.desktop https://ibboard.co.uk/cawbird/appdata/screenshot1.jpg Generic timeline view when using Cawbird https://ibboard.co.uk/cawbird/appdata/screenshot2.jpg Typical Twitter profile https://ibboard.co.uk/cawbird/appdata/screenshot3.jpg Account settings can be configured https://ibboard.co.uk/cawbird/appdata/screenshot4.jpg Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast and Adwaita Dark Green) AppMenu HiDpiIcon ModernToolkit cawbird@ibboard.co.uk GPL-3.0+ IBBoard https://github.com/ibboard/cawbird/issues https://www.transifex.com/cawbird/cawbird/

Cawbird 1.4.2 is a maintenance release with two crash fixes, an emoji fix and updated translations.

  • Fixes:
    • Fixed crash when opening DMs if one of the DMs had entities defined out of order in the JSON (#391)
    • Fixed crash when first creating an account due to null account object (#399)
    • Alter emoji data checking to support changes with recent GTK 3.24 versions (#392)
  • Improvements:
    • Updated French and English translations

Hotfix to correct "Send" button state issues for direct messages

It has been a while in the making, but Cawbird 1.4 adds lots of features and improves our internationalisation.

  • Fixes:
    • Fixed repeat notifications due to message overlap (which we do to avoid missing messages!) (#292)
    • Fixed handling of links with ampersands in them (#305)
    • Fixed handling of Twitter video URLs with ampersands in them (#338)
    • Fixed cropping of tweets in timelines, particularly in narrow windows (#296)
    • Fixed handling of translations so that it works in environments like Flatpak (#297)
  • Improvements:
    • Added Normal/Large/X-Large/XX-Large text sizes (#33)
    • Added "Translate Content" option to tweets with option of using Google, Bing, DeepL or custom providers (#161)
    • Better counting of ZWJ emoji - Cawbird now passes Twitter's own compliance tests (#114)
    • Added Scottish Gaelic translations (thanks to @gaidheileamail1)
    • All strings in the UI are now translatable (#351, #362)
    • Add a confirmation to delete draft tweet on Cancel (#340)
    • Made threaded tweets more obvious by including "Reply to" line for self-reply threads in timeline (#339)
    • Tweets are now added to your timeline when you follow a user (and hidden when you unfollow them) (#19)
    • Improved rendering lag when focussing and unfocussing Cawbird (#204)
    • Improved spacing and alignment of images and added a border to define edges (#290, #306)
    • Added the ability to temporarily show a blocked or muted account (#299)
    • Reduced the size of the info icon for the image description notification to avoid it obscuring too much of the image (#294)
    • Swapped to using the 😀 smiley for emoji input rather than 🧠to be consistent with other applications and platforms (#298)
  • Developer:
    • Make it easier to build Cawbird with custom Twitter secrets so that distro builds don't consume the new (June 2019) 100,000 requests per day per app limits (#310)
    • Added an option to use local translation files rather than the system ones, for debugging without installation
    • Added a `-Dmswindows=true` config flag to allow builds that work with Windows's broken "mimetypes are just file extensions" behaviour

Cawbird 1.3.2 is a maintenance release due to a privacy issue caused by third-party builds of Cawbird using "debug" builds.

Users of native distro packages and other packages that used release builds should not have been affected. We appologise for any inconvenience caused and continue to recommend that users use native packages where possible.

  • Fixes:
    • Make it clearer to third-party packagers (e.g. Flatpak and Snap) when they are building debug builds instead of release builds, which log the plain text content of all tweets and DMs to the console, which is then captured in the system log (#274)
    • Make it possible to build Cawbird without GStreamer video support again (#273)
  • Improvements:
    • Swapped out `g_print` statements for `g_debug` and generally tidied up some debug logging (relates to #274)
    • Make videos open the browser when GStreamer support is disabled (related to #273)
    • Updated Russian translation (thanks to Даниил Пронин)
  • Developer:
    • Fixed intermittent crash of `inlinedownloader` tests

Cawbird 1.3.1 fixes several gaps in the block/mute behaviour so that your experience is controlled from the start, as well as including some media upload and mobile fixes and improvements.

  • Fixes:
    • Fix unblock and unmute wiping local cache of blocked/muted users
    • Load block/mute lists before downloading timeline/mentions to ensure we hide blocked/muted tweets at startup
    • Unblocking/unmuting now says "unblock" or "unmute" on the button instead of always saying "Unblock"
    • Fix media size checks so that videos don't fail because they're bigger than the image limit
    • Make a narrower user account list for narrow windows to avoid overflow and cropped "profile" button (#259)
    • Combine notification and tweet settings to allow narrower settings dialog for mobile
  • Improvements:
    • Hide all tweets in profile view when blocking/muting a user (we shouldn't assume people won't look at the profile)
    • Backfill progressively larger batches of tweets for profiles in case Disable RTs is hiding too many tweets so that the view doesn't scroll
    • Provide a tooltip message for why a media upload failed (e.g. Twitter doesn't support video codec)
    • Updated Dutch, Danish and Italian translations

Cawbird version 1.3 is the "look after yourself" edition. We've got lots of features and fixes, but there are several key improvements to managing your interactions that should have been implemented/fixed sooner. Our priority is making Cawbird accessible and letting users control their experience on Twitter.

  • Improvements:
    • Apply filtering across all feeds (including search)
    • Apply changes to filtering, muting, blocking and hiding RTs across all feeds instantly
    • Allow the filtering of mentioned @-handles
    • Load media in DMs and linkify users and hashtags (#12)
    • Added ability to send media in DMs (#14)
    • Added ability to delete DMs (#7)
    • Load older DMs (#3)
    • Add ability to upload videos and larger animated GIFs (#248)
    • Use HTTPS avatars throughout
    • Translations: Dutch and Danish (complete), Italian and French (updated)
  • Design changes:
    • More accessible approach to loading "replied to" tweets (#164)
    • Improved user completion behaviour and interaction with user completion list
    • Added outline to media upload indicator to make it clearer on light images
    • Remove whitespace around tweets (e.g. when user has new lines and then a quoted URL or media that we're displaying)
    • Allow Compose view to shrink to narrower screens (e.g. for PinePhone) (#212)
  • Fixes:
    • Fix DM load order
    • Fix missing "reply" icon by providing our own (#208)
    • Fixed crashes when playing video (#123)
    • Avoid a crash when returned JSON is invalid (#200)
    • Fixed widget height calculations for responsive and non-responsive views (#202)
    • Fixed layout when starting in responsive width
    • Avoid an unnecessary request for tweets when loading timelines
    • Fixed completion query for known users
    • Fixed various run-time and compiler warnings
    • Fixed handling of command-line parameters (#246)
  • Developer:
    • Added "appdata" build config to allow building on platforms without AppStream (#35)
    • Changed Meson configuration and instructions to allow running dev version without installing

Changes in Cawbird 1.2.1:

  • Bug fixes:
    • Fix crashes when images do not load correctly (#195)
    • Fix crashes when high-res image loads after dialog closes (#196) thanks to @lucaswerkmeister
  • Improvements:
    • Improve rendering of @-handles in mixed right-to-left/left-to-right text (#194)

Changes in Cawbird 1.2.0:

  • Improvements:
    • Resolved major accessibility issue with timelines and improved other bits of accessibility by naming/describing widgets (#143)
    • Added ability to read descriptions on images (#11)
    • Added ability to add descriptions to images when posting (#10)
    • Reduced notifications when first logging in (#137)
    • Window can now be reduced to a narrower width and has a "responsive" layout for tweets (#57)
    • Reduce memory usage by loading smaller (600px) thumbnails and only loading images on demand (#142). Also reduces bandwidth use (especially with images disabled)
    • Let image sets loop back to start, and show progress through images (#172)
    • Made "Replying to" on Tweet info page list all users (including self-reply) so that it is more obvious when the tweet is part of a thread (#158)
    • Reworked @-mention completion so that it works with multilingual text
    • Adjust "media link removal" behaviour so that we still show the link when image loading is disabled so that users know there is media
    • Stopped shipping librest and started using system version
    • Made account removal less "scary" - now removes rather than deleting account
    • Improved consistency of progress status while searching
    • Updated Danish translation (thanks @mads5408) and Dutch translation (thanks @Vistaus) to 100% coverage
  • Bug fixes:
    • Update handling of top-level domains to resolve character counting mismatch (#64)
    • Handle repeated attachment of the same image with the same path (#34)
    • Fix scrolling back to older tweets in thread on some platforms (#164)
    • Fixed @-mention completion not handling underscores
    • Fixed a crash when removing the currently open account
    • Fixed a usability issue where removing an account with multiple other accounts available showed the "Add account" interface with no account selector instead of showing another account
    • Fixed show/hide bug for media in quoted tweets
    • Put other users's lists in the correct place (created or subscribed)
    • Fixed builds on platforms without an implicit `#include <string.h>`

Changes in Cawbird 1.1.0:

  • Improvements:
    • Moved tweet replies below tweet in detailed view, so it now matches the layour of almost all other clients (#16)
    • Improved tweet threading (#17):
      • Now tries to build entire self-reply thread above *and* below the tweet
      • Sorts all self-replies first, then mentioned replies, then other replies
      • Shows more than five replies
      • Added RT marker to detailed tweet view
      • Reduce number of requests to Twitter API when moving up and down a thread
    • Added overlap to timeline tweet fetching to significantly reduce the likelihood of missing tweets due to "eventual consistency" of Twitter servers (#147)
    • Updated Danish translation (thanks @mads5408) and Dutch translation (thanks @Vistaus) to 100% coverage and increased coverage of Arabic translation (thanks @Ammar_Khaled and @Raayib)
  • Bug fixes:
    • Links in direct messages remain clickable after closing and opening (#13)
    • Added support for updated GTK Emoji data (#148 - backported into official v1.0.5 builds)
    • Fixed an SQL error when trying to complete names in tweets
  • UI changes:
    • Hidden and deleted tweets in threads no longer trigger error dialogs (#138 and #153)
    • Timestamps on direct messages now update (#30)
    • Fix keyboard navigation in detailed tweet view (#145)
    • Debug builds can dump tweet JSON and details from Tweet Info page (#26)
    • List name is now shown as window title (#135)
    • List now uses actual title, not unique URL-name
    • List of lists now updates (with caching)
    • Tweets in searches now show like/favourite status
    • Added translation strings for new "limited reply" tweets
    • Handle "operation cancelled" 'error' so that users don't see a dialog when clicking back too quickly

Changes in Cawbird 1.0.5:

  • Improvements:
    • Made Cawbird translatable through Transifex! (#47)
    • Added new Danish translation (thanks @mads5408), updated Catalan (thanks @joensgi), Dutch (thanks @Vistaus), and Italian (thanks @albanobattistella)
    • Improved image scaling to handle portrait monitors (#59)
  • Bug fixes:
    • Fixed parsing/display of some old tweets with bad encoding (#69)
    • Fixed URL encoding issues in user profiles breaking URL display (#78)
    • Removed case-sensitive filtering of @-handle in mentions so all mentions now show (#81)
    • Fixed segfault on CentOS 8 due to a bug in how RHEL/CentOS patch a glib function for FIPS compliance (#82)
  • UI changes:
    • Added dark mode toggle in settings (#67)
    • Improved error messages so that they're not just "Forbidden" or "Not Found" (#8, #41)
    • Protected accounts are now marked with a padlock (#18)
    • Made tweet timestamps into links to twitter.com (#129)
    • Added @-handle next to display name for retweets (#20)
    • Un-dimmed some labels to improve accessibility (#80)
    • Added tooltips for usernames, @-handles and Twitter client names (#87, #120)
    • DM composition area can now be resized (#72)
    • Tidied up CSS to remove warnings (#5)

Changes in Cawbird 1.0.4:

  • Reduce bespoke themeing and make the main bar use native colours
  • Import favourited images from Corebird (if they exist)
  • Fixed crash caused by Twitter sending invalid entity positions in tweet
  • Fixed tweet length error with counting prime marks (quotes)
  • Update Italian translation (thanks to @albanobattistella)
  • Completed Flatpak build (thanks to @p1u3o)

Changes in Cawbird 1.0.3:

  • Handle tweet media separatelty from quoted tweet media - now displays quote with image reply!
  • Fix silently handled tweet parsing errors from RT syncing changes
  • Tidy up README

Changes in Cawbird 1.0.2:

  • Handle TLS errors that we're getting from Twitter and GnuTLS
  • Add initial Flatpak manifest (thanks to @p1u3o)
  • Keep retweet status in sync across pages
  • Fix building on newer versions of Vala (thanks to @lucaswerkmeister)
  • Make image click positioning behaviour consistent

Changes in Cawbird 1.0.1:

  • Fix search with non-alphanumeric characters
  • Redesign logo in a Tango style
  • Migrates DMs, filters and snippets from old Corebird accounts

Changes in Cawbird 1.0:

  • First release after forking from Corebird
  • Supports new non-streaming API
  • Incorporates UI tweaks and customisations
  • Fixes source links for New Twitter web interface changes
cawbird moderate mild moderate workstation mobile
cawbird-1.4.2/data/uk.co.ibboard.cawbird.desktop.in000066400000000000000000000011431416632607600221060ustar00rootroot00000000000000[Desktop Entry] Name=Cawbird GenericName=Twitter Client Comment=Use Twitter from within a normal desktop application # TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! Keywords=twitter; Exec=cawbird Type=Application # TRANSLATORS: Do NOT translate or transliterate this text (this is an icon file name)! Icon=uk.co.ibboard.cawbird Categories=Network;GTK; DBusActivatable=true StartupWMClass=cawbird # TRANSLATORS: Do NOT translate or transliterate this text (these are enum types)! X-Purism-FormFactor=Workstation;Mobile; cawbird-1.4.2/data/uk.co.ibboard.cawbird.gschema.xml000066400000000000000000000132761416632607600222500ustar00rootroot00000000000000 false Specifies if Cawbird should request to use the dark variant of the gtk theme "Never" If/when the user should be notified about new tweets true Whether the user should be notified when another user mentions them in a tweet true Whether the user should be notified when another user writes them a direct message [""] List of accounts which will be opened when Cawbird is started. false true Whether to show the topbar or not "<Control>T" The accelerator to press in order to bring the ComposeTweetWindow up. Must be parseable with gtk_accelerator_parse "<Control><Shift>S" Accelerator to press in order to show/hide the sidebar "<Control><Shift>P" Accelerator to show the settings window "<Control>P" Accelerator to show the account dialog "<Control>K" Accelerator to show the account list (popover) {} (0,0,500,300) Size and position of the settings dialog "" The application's oauth consumer key. "" The application's oauth consumer secret false false Whether or not to use double-click activation in tweet timelines 1 Bitfield for text transformations true Whether to hide images marked as inappropriate by default or not "Show" When to show media attached to tweets "Normal" Scale factor for tweets in timelines "Google" Translation website to use "" Custom translation service URL using {SOURCE_LANG} for the source language code, {TARGET_LANG} for the target language code and {CONTENT} for the URI encoded tweet content "Alt" Key to use with 1…7/Left/Right for page switching cawbird-1.4.2/data/uk.co.ibboard.cawbird.service.in000066400000000000000000000001301416632607600220700ustar00rootroot00000000000000[D-BUS Service] Name=uk.co.ibboard.cawbird Exec=@bindir@/cawbird --gapplication-service cawbird-1.4.2/data/uk.co.ibboard.cawbird.svg000066400000000000000000000316101416632607600206310ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/data/verified-large.png000066400000000000000000000023771416632607600174610ustar00rootroot00000000000000‰PNG  IHDRÄé…csBIT|dˆ pHYsÔÔš;>°tEXtSoftwarewww.inkscape.org›î<|IDATH‰–KLTWÇß¹Ì 2( ¯ª£¢h}`EMÓh¤¾¢ Qºî®m⤛.Ú&mĤ‰éÖtÓ…éÒ… ±µÚD¥vU‘ÒÚâ¤Ãc†§Ã0ܹ_Àòªý¯î=ç;ÿßùÎwï9GXF¡ªÚLÍtÂ25¢º(ü@?*= ¿+Rç"«¾õÆ'öR>²Xceåw®Â¡1z¥h&ГéÁív“L&±mÕéEõ\~_ÞŦ¦Ó+BBÇχ0ÔÆ] J°´ˆ‚‚<Œ1é8ÇqˆF wõò¢³[ÇûÂdMûõ/;—„¬¯þfŸ£ú“¨úƒ¥Åì¨('+kÕR«ÖX –S±këknÉáTe>ãJÒò22#‹—øÜáág·›¬²<ŽIÖc¼ïìß-.—ëµU[s8P¾6ebO*c’I{[—*ìõ—üÖLèèa”¢µ¡ ü—"ÏÕ‘í¹ìß¼&ý.@n–…×›ÅÚuAµx‚±CËÔ”‹^ pèÍÞÞ¸z^[SÇ(7›‡Lû‰8§2Ä¡R ø óæ pYÂDJyUݞ˾W m1n=ffDAaþô§+{ ¢LgÞð¹9s´„ÊõÙ ;Üœ°,ƒÛãF•’ Ïå™-ö–@§vçᲄã;}8ŽòggŽíÈeï†ù€{Ó€ÅäÉtcÛI0´“%3åÅ™¸,IÏüD…‰”òF¾‡ÊÐüÌîN/ÑR²Ç“Ñ Tzl;Yâ8Æ®Ýbõª B~F„šÊüw[cÜz¸4 •rHÚIí1 ¿«B428Õé(—ú‰¼L.iÐж< @ª^ÿÓ;›æ¤Ã¥ßú‰%R 3X¦sÕî›zPùÁ¸$û6Hï‹În‹'ÒA±ñ—îF±'œy€åj0£x|ŒÎŽnz¼^»Þl½‘ÊÝt8®ªÕ‰Ä8¥¥%é½9n;ôL²-¸Š†¶Ñ—hjæÐÔØL,6*¢úéÓ«_5ZÃÁ÷s]®“±—£ÅÆ2øý¾ô˜¡ø$­‘w­ ZZÚhoëB ©=»ì .«@ÇÇ·©êgÄ|Ðß7à5–ÁŸïKg4:î,c;›AKK<$b,92tåô0LŸ'ÃÏêG|åÇ~U´&Ú7à‰áËËÁí^yWŽÇÇhjlžÎ@"ÆÑêç׿x<Ó¿àŒ_ÿÞ×ëë ð–ˆÑu¡€‚Å-<ã#}ƒt‡{éìèVUGPùÃdðþó?1×sÑÛ Uµ뽞9‡jñLàìmÅÆOÎÝ«zDõl{"ù=wj'_µ[2­²<“´F¡RT~”~„0ЄH7kü—‡—k—ü{ÿ"ëˆn‰ªÖIEND®B`‚cawbird-1.4.2/data/verified-large@2.png000066400000000000000000000047731416632607600176450ustar00rootroot00000000000000‰PNG  IHDR22?ˆ±sBIT|dˆ pHYs-¨-¨ç >)tEXtSoftwarewww.inkscape.org›î< xIDATh½škpSÇÇ{¯$¿$c[–XÆ6ÌlâC’™„¼ ´MB;yõ5Mgú¥íL“4¤Ì”é ÚN§3é´ýÐIgÚ¤ÓÐt’&ÁhÚ$ŒMŒ3Øl?eü–lK–îöƒ,ùJ–dI–ûÿ¶g÷ìžÿݳgÏî^A†°vÏ‘5š¦Ý+[…”Õ *€b o®‰z¤” Ê%àÿðÚ©721¾XŠrÅ®ÃBð¬< T§ÙÍxüín:àL×–´ˆTî:T§)ÊóžÔt‚&¡IÕøùõ“û?MU9%"•{Ù5¿ò+Ï$Ò5™ŒX,ydega0yúý¼3^&'=ø|³‰†‘Hþ¢ /^;õ‚+YÛ’&Rñðá'AüdAtÁ``µ£»Ý†ÍVDvNV¾f¦½¸\£¸†oÒßï"à÷Çj6Žäû='ö¿‘Œ}‹Ù´ï É=•ý;!åsÑuK6Vâ([…ª¦ça~€¾ƒtvtãžôİPþ!/×÷£öô%ê'!‘M÷4»s³Þp¿^ž•m¢¶®š²òÕˆ¥Å‹0$gw?—Û:ñy£l–òý¼ißígºãéǵ¢zïQ‹×¯¶ë厲UÜv{ &“q‰¦Ç†Ï7KKs;ý}CÑUŸäMyˆG&¦?lÚwÐ4åUßpwH&l©¯¡¶®:m7Jªªâ([‰ÑdÄ5|S_å˜5¨ ¥õ;Ž|~6­§ÄêÌãÎzEïNªª°ýÎzÖU­É¼åqPµ¾œmwÔ£(:…xÈ3eúm¬ö >måÃG¾†àȼ.4nßB©cårØ›ùùfVX¢ÜL4®Xÿ@×]ÿnÓ· R¹÷]jÊû@vH¶¥¾†òŠÒåµX‡5˜ ‚Qw0$[,yF†‡æÝL ï-®ºïÏcW?˜ É"\Kó+¿Ö漏•ÿWwÚY³‚»Ö[x¼ÑÊfGnX^µ¾œ²²Uº–¢È/”£zÝðŒ”ï>\/¿c.’ee›øÒŽ­Ëº°Ãfn.àŽªü`Y6¬ÊabÊëV0 °•Xééî#ÐBj·®ÛùÖÄÕ3àŸE¼Œ.×ÖU/[ˆ&ñ@mÛÖY"äŠì©/¢®,˜<›LF6×Eä¥B*ÊËáö0—ÅJùXHh¶äQV¾zÍŸ³x°¶€mk-1ë!x´¾-k‚dÊ+˜Í¹ú&O¬Ýsd Ì‚gÑÍNõÆÊŒíØñ €][ iŒC"EÖXƒ¹›°aãZ}µðó Ì?wž‚  #baex¨®† ó¢mÛû§8Þ:.;ÊV¡ ó} ù4€275aç[í(YÖ.€]u…l­L‚DßÿjC“2,3TJKKôÍjªvu(š¦Ý«—Úí¶ ™¼!wjH‚D«ÓÃÛG#H„`_YQ w¤FƒÐ-›­h©öÆ„ðèmEá…›­NÇ/ƒÅQ6J!!ؘLÆEE¶üÔ²"{ë“#q©71 €œœìè­aƒ„ÀŒx Eì®+ä¹{ìÔ”æ&lÖ™ ¡µeÉ‘x¯51‰Ì–ùñ¥ Ê†YÙñg#Ǥðxc1ÅÁ6_¾½@Ò94Äžú"jË' SÖ¼­B²B^yñ¢U‘ÙÀ·vØÃ$TEðx£•*{vLTH´öºS"`2ôÅü˜ç=*mÙ|çn;V³aAªžØVL…-r&!Ø{{ò$޷ާD"á£c °ààÅÖJ3ÙÆø| ŠàëÛmáÝWU5Ed¯ñÐâôp¼%=¾Ùˆ›—[ Þ6½3Þ o720žð£*xòN•¶l¾Ú`eãª$fÂé¦éÒéN„Ïaë„\•&c\ÇÌ$oœ»ÉÄTÌ»§0Œªà©»ll\³¨Í=Kw'÷䔾xM‘’+¡’Ï7ËtŒYñxüýã›ÌÌj êôH&Ílqz8ùÙxÚ30==}[Ù¡…f½ddx4¦òM÷,ÇÎÝÄ¿p%§‡¦£S,Œ¸Æ"ÊÙ¬(Šò¡^uÞQ/ï´Œ¦eH˜Dêª 0<4QžU´3Êõw_ê…y÷êïwáOðÙ?ïŸâì•/R8“$~?ƒº»mA{ß{úƒ+Äëú†}7vößÎ[\èŽ{{‹=™#p£w0òCK^‡ÐÁJ“¯á•ÜÙѽ¨ûœj› c0~zA'–b£¡IIGG·^PT1O¤çäþo†jÝ“z}‹všhiîqg”@owwDØ=6·4ô÷Zò0ÌÛöYçb2q÷˜s×&9yii!6>ß,—Û:õ"© ‡ á,q¢ëÌPÁúû×[ ˜®x<Ó8Ê_•Î$×]^6—åbPç®MrúòD).œocbâVX$¯v7½ô§D¬•»>’Šö] `ò–£ÉH‘uÁ#U¦|ýã><^Ú3LèêìáêÕžydÔd0~e´óTØÏ"ˆŒ_;í)Üpÿu`_Hæ%…™üüÄçì/¦tÌdÈôy ôÓÒ|9B&߸~ü'ù‚ÈDןl¸Ï¢1$paÉ_œL¦Ñß7ħç>CêB¨”ò牗Ý6f~n²þ)ß•5Mãü'-\íJû<5HèêèáüǗдˆüîD±ËúãX*qó¼M÷4{r³Nwèå¥;õ ›—õéíâ…Ë ôGW}4í7<8|ê…/¦I<†zrLo"ÄCzyèB¹¼ÂÈÐͪ”ÐÓ}ƒö¶˜aÿÄ´ß°/ X䯅‘ž³¾ÒúÇü³+^3€Æà€‹¾ÞTƒŠÙ’ùD–~?½ÎÎÒJ¯s@ÿl×D±Ëúí®ÿü a$Iú{V>rxŸ”ò Üà©¥¥%”Ø‹±•‘“ûB"„ééF\c 08/I•cñ=gÓþ&c_JŽQµû6¿¿¾™H×d2b¶ä’•ý ‡·{ñ_8âU£jx©óÝçãŸ)¢–‡¯{äÐf¿T^ž°ðz%=hšPµŸ9ßýéÅT•—´T×î9²& Ég…ä) &­NíRS ¼JÓë&C¨Ú}Ô@î”B6 e5BT"±"¾äH&Œ"e7B\ÈæYE;Ó÷ÞþLŒÿ?7e¤j5òIEND®B`‚cawbird-1.4.2/data/verified-small.png000066400000000000000000000007601416632607600174710ustar00rootroot00000000000000‰PNG  IHDR Vu\çsBIT|dˆ pHYs a aüÌJ%tEXtSoftwarewww.inkscape.org›î<mIDAT(‘•Ò½JÃPàsonb“˜Ö‹¦ˆ´ ‚(¢‹›? НáÐQ°O¢øÅÍIüüK+±¥4JEQš“ö^•.=óÇYÎ!B@f³ÈdE.PB¶»œ› Qêr! ÜsJ» Bd6‹–¢ÈÇc㦙ÍftMW¾ß†}ïxõšëA¸î”v+$½±ÏE¾_˜µFÍ$þÇP%8Õg\œ_U‚ œb²"ÆÆM³ÒòËI¼Î%Ðp›fõ±Q ”\6›Ñÿã¸.!¿’DLcpšŸ°¬´N ÉÑ.ç)MW‘N`É2¾1C~y1•áäægåhºŠ.ç)”¬ÍÄ1lÈŒH˜Ni0Téÿ•(m´Z>Ž._à}r,N=±ïµ!QÚ \ˆCÛv¼7¯ƒ£Ë&®èÙlÛŽÇ…8”¢«§^Ë߉ƌ„`\×}”ŸÛð“ÛÄÝÍC- ­þ‡ë÷_d¦Âƒ²ï_„IEND®B`‚cawbird-1.4.2/data/verified-small@2.png000066400000000000000000000015751416632607600176600ustar00rootroot00000000000000‰PNG  IHDRàw=øsBIT|dˆ pHYsÃÃpMBtEXtSoftwarewww.inkscape.org›î<úIDATH‰½–ÍNQÇs3 è§©bÓª`Y`Hl5ÄÔìÔ…+ÝôúòjxcP76øA(X˜`LŒgh§±e®‹Î@[)Tžä.î™{~ÿ¹÷ž9g)%õ,Ô;pˆ]öh³e€´=Æ´db¼CÙJ Ô;îWŸË¥JÀ«zN‹%¥"dè×’‰¥B½—A èx GZñ¼¸ÝÍUëLsCÏ1Ÿ]ÄÐsK@Ÿ–L<ª+ê¸ÜBÈöhD9r4„¢(lgRJæf5f¦³Ò²,¸«%·~°ßü¡ÛÓBìd'O˶àZËç ¤F§0ó€+ÎN„ ƒBù'ðXØÍÁý>b';BH`Ðf–(_h°=Q~~¦ÍÃÅ7º÷rè€öhDœ$A9téö åx9ÛßñÌ+íl»—î#žyáû:÷^|fxxĹø˜ œç„#­¿?WÉšü(I‘VÇW)@øÞ†á=Ç|œ:ì®ò={¿Ìø¼I «K].—*+ó|—Kp¼µ:ï;Ýþnàv7ãr©èR6ç ´¨\‹ 4«¸w«¼Ìä6/D}Ä#ÕðáwËL,˜Ôš?àU¾~ùÖ¦V:›\‚Ýûhn*'×é£Jë#L.vøˆ…ƒWš2†ž“kE‹Wsùª=Ç|\ïÞ»ÜØn33H‹%Å4W_0«Îààž¦ªùÓ· …ºpÓ\Á.†iA¹ä:y Àóée2ŸW· ~úÖ`R«¯a¥00Ÿ]Ä©KRÂãIOÆÚFl.¥d>»èLÇ„Ý,† =Çܬ¶±pÝ’˜™É8!IEND®B`‚cawbird-1.4.2/data/verified.svg000066400000000000000000000056331416632607600164020ustar00rootroot00000000000000 image/svg+xml cawbird-1.4.2/examples/000077500000000000000000000000001416632607600147625ustar00rootroot00000000000000cawbird-1.4.2/examples/accountdialog.vala000066400000000000000000000003711416632607600204440ustar00rootroot00000000000000 void main (string[] args) { Gtk.init (ref args); Utils.load_custom_css (); Settings.init (); var acc = new Account (1337, "Some Screen Name", "Some Name"); var dialog = new AccountDialog (acc); dialog.show_all (); Gtk.main (); } cawbird-1.4.2/examples/meson.build000066400000000000000000000003541416632607600171260ustar00rootroot00000000000000executable('tweetstates', 'tweetstates.vala', cb_resources, dependencies: cb_dep, vala_args: [ meson.source_root() + '/vapi/cawbird-internal.vapi', '--gresources=' + meson.source_root() + '/cawbird.gresource.xml', ] ) cawbird-1.4.2/examples/tweetstates.vala000066400000000000000000003414321416632607600202120ustar00rootroot00000000000000 Cb.Tweet parse_tweet (string input) { var parser = new Json.Parser (); try { parser.load_from_data (input); } catch (Error e) { error (e.message); } Cb.Tweet tweet = new Cb.Tweet (); tweet.load_from_json (parser.get_root (), 0, new GLib.DateTime.now_local ()); return tweet; } void main (string[] args) { Gtk.init (ref args); Settings.init (); Utils.load_custom_css (); Utils.load_custom_icons (); Utils.init_soup_session (); Twitter.get ().init (); var window = new Gtk.Window (); window.delete_event.connect (() => {Gtk.main_quit (); return true; }); var list = new Gtk.ListBox (); list.selection_mode = Gtk.SelectionMode.NONE; var list2 = new Gtk.ListBox (); list2.selection_mode = Gtk.SelectionMode.NONE; var list3 = new Gtk.ListBox (); list3.selection_mode = Gtk.SelectionMode.NONE; var scroller = new Gtk.ScrolledWindow (null, null); scroller.hscrollbar_policy = Gtk.PolicyType.NEVER; var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); box.homogeneous = true; // Ensure types new LazyMenuButton (); var fake_acount = new Account (1337, "baedert", "Foo Bar Baedert"); { // Normal tweet. var tweet = parse_tweet (NORMAL_TWEET); var row = new TweetListEntry (tweet, null, fake_acount); list.add (row); } { // Retweet var tweet = parse_tweet (RETWEET); var row = new TweetListEntry (tweet, null, fake_acount); list.add (row); } { // Normal, but with media attached var tweet = parse_tweet (NORMAL_WITH_MEDIA); var row = new TweetListEntry (tweet, null, fake_acount); list.add (row); } { // Normal quote var tweet = parse_tweet (NORMAL_QUOTE); var row = new TweetListEntry (tweet, null, fake_acount); list.add (row); } { // Quote with Media var tweet = parse_tweet (QUOTE_WITH_MEDIA); var row = new TweetListEntry (tweet, null, fake_acount); list2.add (row); } { // Retweet with media var tweet = parse_tweet (RETWEET_WITH_MEDIA); var row = new TweetListEntry (tweet, null, fake_acount); list2.add (row); } { // Empty (no text) tweet with media var tweet = parse_tweet (EMPTY_TWEET_WITH_MEDIA); var row = new TweetListEntry (tweet, null, fake_acount); list.add (row); } { // Retweet with media but no text var tweet = parse_tweet (EMPTY_RETWEET_WITH_MEDIA); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } { // Video + Thumbnail of that video, but should only show video var tweet = parse_tweet (VIDEO_AND_THUMBNAIL); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } { // Reply to ONE user. var tweet = parse_tweet (REPLY); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } { // Reply to TWO users. var tweet = parse_tweet (REPLY_TO_TWO); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } { // Reply to MORE users. var tweet = parse_tweet (REPLY2); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } { // Retweet of a tweet that replies to multiple users (2!) var tweet = parse_tweet (REPLY3); var row = new TweetListEntry (tweet, null, fake_acount); list3.add (row); } list.set_size_request (500, -1); list2.set_size_request (500, -1); list2.set_size_request (500, -1); box.add (list); box.add (list2); box.add (list3); scroller.add (box); window.add (scroller); window.show_all (); window.resize (1500, 900); Gtk.main (); } const string NORMAL_TWEET = """ { "created_at" : "Wed Aug 03 04:06:47 +0000 2016", "id" : 760688337610997764, "id_str" : "760688337610997764", "text" : "My dick was already out BEFORE Harambe died.", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 21369740, "id_str" : "21369740", "name" : "Rob DenBleyker", "screen_name" : "RobDenBleyker", "location" : "Dallas", "description" : "I'm not Rob Dyrdek. Don't follow me.", "url" : "https://t.co/5jh1OQTAOO", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/5jh1OQTAOO", "expanded_url" : "http://www.explosm.net", "display_url" : "explosm.net", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 126455, "friends_count" : 800, "listed_count" : 1284, "created_at" : "Fri Feb 20 03:26:24 +0000 2009", "favourites_count" : 1452, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 8088, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "49585E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_link_color" : "0C90F5", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 111, "favorite_count" : 335, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string RETWEET = """ { "created_at" : "Tue Aug 02 17:25:04 +0000 2016", "id" : 760526843699007488, "id_str" : "760526843699007488", "text" : "RT @wilw: OH: \"All fucking dentists are in the pocket of big floss!\"", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "wilw", "name" : "Wil Wheaton", "id" : 1183041, "id_str" : "1183041", "indices" : [ 3, 8 ] } ], "urls" : [ ] }, "source" : "Echofon", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 18948541, "id_str" : "18948541", "name" : "Seth MacFarlane", "screen_name" : "SethMacFarlane", "location" : "Los Angeles", "description" : "The Official Twitter Page of Seth MacFarlane - new album No One Ever Tells You available now on iTunes https://t.co/gLePVn5Mho", "url" : "https://t.co/o4miqWAHnW", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/o4miqWAHnW", "expanded_url" : "http://www.facebook.com/pages/Seth-MacFarlane/14105972607?ref=ts", "display_url" : "facebook.com/pages/Seth-Mac…", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ { "url" : "https://t.co/gLePVn5Mho", "expanded_url" : "http://itun.es/us/Vx9p-", "display_url" : "itun.es/us/Vx9p-", "indices" : [ 103, 126 ] } ] } }, "protected" : false, "followers_count" : 10520286, "friends_count" : 377, "listed_count" : 0, "created_at" : "Tue Jan 13 19:04:37 +0000 2009", "favourites_count" : 0, "utc_offset" : -25200, "time_zone" : "Pacific Time (US & Canada)", "geo_enabled" : false, "verified" : true, "statuses_count" : 5847, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/477598819715395585/g0lGqC_J_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/477598819715395585/g0lGqC_J_normal.jpeg", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Tue Aug 02 17:18:45 +0000 2016", "id" : 760525254489890818, "id_str" : "760525254489890818", "text" : "OH: \"All fucking dentists are in the pocket of big floss!\"", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 1183041, "id_str" : "1183041", "name" : "Wil Wheaton", "screen_name" : "wilw", "location" : "Los Angeles", "description" : "Barrelslayer. Time Lord. Fake geek girl. On a good day I am charming as fuck.", "url" : "http://t.co/UAYYOhbijM", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/UAYYOhbijM", "expanded_url" : "http://wilwheaton.net/2009/02/what-to-expect-if-you-follow-me-on-twitter-or-how-im-going-to-disappoint-you-in-6-quick-steps/", "display_url" : "wilwheaton.net/2009/02/what-t…", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 3050329, "friends_count" : 355, "listed_count" : 39190, "created_at" : "Wed Mar 14 21:25:33 +0000 2007", "favourites_count" : 557, "utc_offset" : -25200, "time_zone" : "Pacific Time (US & Canada)", "geo_enabled" : false, "verified" : true, "statuses_count" : 65696, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "022330", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/871683408/62c85b46792dfe6bfd16420b71646cdb.png", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/871683408/62c85b46792dfe6bfd16420b71646cdb.png", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/660891140418236416/7zeCwT9K_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/660891140418236416/7zeCwT9K_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/1183041/1368668860", "profile_link_color" : "F6101E", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "C0DFEC", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 249, "favorite_count" : 1418, "favorited" : false, "retweeted" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 249, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string NORMAL_WITH_MEDIA = """ { "created_at" : "Wed Aug 03 11:18:39 +0000 2016", "id" : 760797019908739072, "id_str" : "760797019908739072", "text" : "Someone went and ruined the last fun thing we had left https://t.co/a9Fc65NpIn", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 760796983409901568, "id_str" : "760796983409901568", "indices" : [ 55, 78 ], "media_url" : "http://pbs.twimg.com/media/Co7k01ZWAAAO7XA.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co7k01ZWAAAO7XA.jpg", "url" : "https://t.co/a9Fc65NpIn", "display_url" : "pic.twitter.com/a9Fc65NpIn", "expanded_url" : "http://twitter.com/internetofshit/status/760797019908739072/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 646, "resize" : "fit" }, "small" : { "w" : 680, "h" : 366, "resize" : "fit" }, "large" : { "w" : 1972, "h" : 1062, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 760796983409901568, "id_str" : "760796983409901568", "indices" : [ 55, 78 ], "media_url" : "http://pbs.twimg.com/media/Co7k01ZWAAAO7XA.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co7k01ZWAAAO7XA.jpg", "url" : "https://t.co/a9Fc65NpIn", "display_url" : "pic.twitter.com/a9Fc65NpIn", "expanded_url" : "http://twitter.com/internetofshit/status/760797019908739072/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 646, "resize" : "fit" }, "small" : { "w" : 680, "h" : 366, "resize" : "fit" }, "large" : { "w" : 1972, "h" : 1062, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "source" : "TweetDeck", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 3356531254, "id_str" : "3356531254", "name" : "Internet of Shit", "screen_name" : "internetofshit", "location" : "In your stuff", "description" : "Obviously the best thing to do is put a chip in it. Tips: internetofshit@gmail.com / Also on FB: https://t.co/VhThiGNgOo", "url" : null, "entities" : { "description" : { "urls" : [ { "url" : "https://t.co/VhThiGNgOo", "expanded_url" : "https://www.facebook.com/internetofshit", "display_url" : "facebook.com/internetofshit", "indices" : [ 97, 120 ] } ] } }, "protected" : false, "followers_count" : 126017, "friends_count" : 92, "listed_count" : 1630, "created_at" : "Fri Jul 03 09:04:06 +0000 2015", "favourites_count" : 2190, "utc_offset" : -25200, "time_zone" : "Pacific Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 2254, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/616895706150797312/ol4PeiHz_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/616895706150797312/ol4PeiHz_normal.png", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 103, "favorite_count" : 84, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" } """; const string NORMAL_QUOTE = """ { "created_at" : "Wed Aug 03 07:15:48 +0000 2016", "id" : 760735908287119360, "id_str" : "760735908287119360", "text" : "Gute Nachrichten zum Morgen: @EMMUREmusic haben die Aufnahmen zu ihrem neuen Album abgeschlossen! https://t.co/zGwh0FXMJi", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "EMMUREmusic", "name" : "EMMURE", "id" : 42820699, "id_str" : "42820699", "indices" : [ 29, 41 ] } ], "urls" : [ { "url" : "https://t.co/zGwh0FXMJi", "expanded_url" : "https://twitter.com/FrankiePalmeri/status/760334690737790976", "display_url" : "twitter.com/FrankiePalmeri…", "indices" : [ 98, 121 ] } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 51725030, "id_str" : "51725030", "name" : "impericon_de", "screen_name" : "impericon_de", "location" : "Leipzig/ Germany", "description" : "check out http://t.co/RbUklLeWkv for further information", "url" : "http://t.co/RbUklLeWkv", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/RbUklLeWkv", "expanded_url" : "http://www.impericon.com", "display_url" : "impericon.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ { "url" : "http://t.co/RbUklLeWkv", "expanded_url" : "http://www.impericon.com", "display_url" : "impericon.com", "indices" : [ 10, 32 ] } ] } }, "protected" : false, "followers_count" : 8077, "friends_count" : 1226, "listed_count" : 46, "created_at" : "Sun Jun 28 13:07:50 +0000 2009", "favourites_count" : 2657, "utc_offset" : 7200, "time_zone" : "Berlin", "geo_enabled" : true, "verified" : false, "statuses_count" : 17025, "lang" : "de", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "611222", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/868968985/73b34a4e0d724f8c8f61c06d76dcf925.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/868968985/73b34a4e0d724f8c8f61c06d76dcf925.jpeg", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/1374777540/ic_germany_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1374777540/ic_germany_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/51725030/1470217813", "profile_link_color" : "611222", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "DDFFCC", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 760334690737790976, "quoted_status_id_str" : "760334690737790976", "quoted_status" : { "created_at" : "Tue Aug 02 04:41:31 +0000 2016", "id" : 760334690737790976, "id_str" : "760334690737790976", "text" : "Finished tracking the record. 🖒", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twitter for Android", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 208089519, "id_str" : "208089519", "name" : "â›§", "screen_name" : "FrankiePalmeri", "location" : "World Warrior ", "description" : "シャドルー Best known as the singer of @EMMUREmusic\nFor booking/features-GodOfRue@Gmail.com\n#MVC2", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 6843, "friends_count" : 0, "listed_count" : 17, "created_at" : "Tue Oct 26 16:20:47 +0000 2010", "favourites_count" : 6444, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : true, "statuses_count" : 7346, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/744425940789276672/qOmnwRUv_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/744425940789276672/qOmnwRUv_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/208089519/1466846528", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : { "id" : "3b77caf94bfc81fe", "url" : "https://api.twitter.com/1.1/geo/id/3b77caf94bfc81fe.json", "place_type" : "city", "name" : "Los Angeles", "full_name" : "Los Angeles, CA", "country_code" : "US", "country" : "United States", "contained_within" : [ ], "bounding_box" : { "type" : "Polygon", "coordinates" : [ [ [ -118.668404, 33.704537999999999 ], [ -118.15540900000001, 33.704537999999999 ], [ -118.15540900000001, 34.337040999999999 ], [ -118.668404, 34.337040999999999 ] ] ] }, "attributes" : { } }, "contributors" : null, "is_quote_status" : false, "retweet_count" : 38, "favorite_count" : 136, "favorited" : false, "retweeted" : false, "lang" : "en" }, "retweet_count" : 1, "favorite_count" : 7, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "de" } """; const string QUOTE_WITH_MEDIA = """ { "created_at" : "Wed Aug 03 09:48:12 +0000 2016", "id" : 760774260315004928, "id_str" : "760774260315004928", "text" : "Wir sind am Wochenende übrigens wieder beim Wacken Open Air am Start - wer noch? https://t.co/xEEtAO37jF", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ { "url" : "https://t.co/xEEtAO37jF", "expanded_url" : "https://twitter.com/Wacken/status/760774020631556096", "display_url" : "twitter.com/Wacken/status/…", "indices" : [ 81, 104 ] } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 51725030, "id_str" : "51725030", "name" : "impericon_de", "screen_name" : "impericon_de", "location" : "Leipzig/ Germany", "description" : "check out http://t.co/RbUklLeWkv for further information", "url" : "http://t.co/RbUklLeWkv", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/RbUklLeWkv", "expanded_url" : "http://www.impericon.com", "display_url" : "impericon.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ { "url" : "http://t.co/RbUklLeWkv", "expanded_url" : "http://www.impericon.com", "display_url" : "impericon.com", "indices" : [ 10, 32 ] } ] } }, "protected" : false, "followers_count" : 8077, "friends_count" : 1226, "listed_count" : 46, "created_at" : "Sun Jun 28 13:07:50 +0000 2009", "favourites_count" : 2657, "utc_offset" : 7200, "time_zone" : "Berlin", "geo_enabled" : true, "verified" : false, "statuses_count" : 17025, "lang" : "de", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "611222", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/868968985/73b34a4e0d724f8c8f61c06d76dcf925.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/868968985/73b34a4e0d724f8c8f61c06d76dcf925.jpeg", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/1374777540/ic_germany_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1374777540/ic_germany_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/51725030/1470217813", "profile_link_color" : "611222", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "DDFFCC", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 760774020631556096, "quoted_status_id_str" : "760774020631556096", "quoted_status" : { "created_at" : "Wed Aug 03 09:47:15 +0000 2016", "id" : 760774020631556096, "id_str" : "760774020631556096", "text" : "Business as usual. Work on main stages continues. All is well. #Wacken https://t.co/6oNpNgSFmx", "truncated" : false, "entities" : { "hashtags" : [ { "text" : "Wacken", "indices" : [ 64, 71 ] } ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 760773998955421696, "id_str" : "760773998955421696", "indices" : [ 72, 95 ], "media_url" : "http://pbs.twimg.com/media/Co7P69oXYAAcTYR.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co7P69oXYAAcTYR.jpg", "url" : "https://t.co/6oNpNgSFmx", "display_url" : "pic.twitter.com/6oNpNgSFmx", "expanded_url" : "http://twitter.com/Wacken/status/760774020631556096/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "large" : { "w" : 2048, "h" : 1152, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 760773998955421696, "id_str" : "760773998955421696", "indices" : [ 72, 95 ], "media_url" : "http://pbs.twimg.com/media/Co7P69oXYAAcTYR.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co7P69oXYAAcTYR.jpg", "url" : "https://t.co/6oNpNgSFmx", "display_url" : "pic.twitter.com/6oNpNgSFmx", "expanded_url" : "http://twitter.com/Wacken/status/760774020631556096/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "large" : { "w" : 2048, "h" : 1152, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "source" : "Twitter for Android", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 15475088, "id_str" : "15475088", "name" : "Wacken Open Air", "screen_name" : "Wacken", "location" : "Wacken, Germany", "description" : "Welcome to the official W:O:A Twitter account! Impressum: https://t.co/0Nvmi1UOVI", "url" : "http://t.co/eklJ4O4IqD", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/eklJ4O4IqD", "expanded_url" : "http://www.wacken.com/", "display_url" : "wacken.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ { "url" : "https://t.co/0Nvmi1UOVI", "expanded_url" : "http://bit.ly/1xx5ODO", "display_url" : "bit.ly/1xx5ODO", "indices" : [ 60, 83 ] } ] } }, "protected" : false, "followers_count" : 61074, "friends_count" : 259, "listed_count" : 602, "created_at" : "Thu Jul 17 22:46:52 +0000 2008", "favourites_count" : 314, "utc_offset" : 7200, "time_zone" : "Ljubljana", "geo_enabled" : true, "verified" : false, "statuses_count" : 3660, "lang" : "de", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/436070844293345280/3EWufvsr.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/436070844293345280/3EWufvsr.jpeg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/627856569699598340/OsQ1UOLl_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/627856569699598340/OsQ1UOLl_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/15475088/1438527965", "profile_link_color" : "5BD006", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "EFEFEF", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : { "id" : "b442982971a0c2b5", "url" : "https://api.twitter.com/1.1/geo/id/b442982971a0c2b5.json", "place_type" : "city", "name" : "Wacken", "full_name" : "Wacken, Deutschland", "country_code" : "DE", "country" : "Germany", "contained_within" : [ ], "bounding_box" : { "type" : "Polygon", "coordinates" : [ [ [ 9.3435539999999992, 54.009207000000004 ], [ 9.4074019999999994, 54.009207000000004 ], [ 9.4074019999999994, 54.038155000000003 ], [ 9.3435539999999992, 54.038155000000003 ] ] ] }, "attributes" : { } }, "contributors" : null, "is_quote_status" : false, "retweet_count" : 7, "favorite_count" : 15, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" }, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "de" } """; const string RETWEET_WITH_MEDIA = """ { "created_at" : "Wed Aug 03 01:22:42 +0000 2016", "id" : 760647046835544064, "id_str" : "760647046835544064", "text" : "RT @shutupmikeginn: Hey @ScottAdamsSays, @eedrk & I are big fans so we recut Dilbert to deal with contemporary social issues! Please RT! ht…", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "shutupmikeginn", "name" : "shut up, mike", "id" : 246394886, "id_str" : "246394886", "indices" : [ 3, 18 ] }, { "screen_name" : "ScottAdamsSays", "name" : "Scott Adams", "id" : 2853461537, "id_str" : "2853461537", "indices" : [ 24, 39 ] }, { "screen_name" : "eedrk", "name" : "derek", "id" : 1350600582, "id_str" : "1350600582", "indices" : [ 41, 47 ] } ], "urls" : [ ], "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 143, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "photo", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "source_status_id" : 760628917837312000, "source_status_id_str" : "760628917837312000", "source_user_id" : 246394886, "source_user_id_str" : "246394886" } ] }, "extended_entities" : { "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 143, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "video", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "source_status_id" : 760628917837312000, "source_status_id_str" : "760628917837312000", "source_user_id" : 246394886, "source_user_id_str" : "246394886", "video_info" : { "aspect_ratio" : [ 35, 24 ], "duration_millis" : 50017, "variants" : [ { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.mpd" }, { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/262x180/bMwQb2LGoymiuNBF.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/524x360/akoT29AzRB-K7Tvf.mp4" } ] }, "additional_media_info" : { "monetizable" : false, "source_user" : { "id" : 246394886, "id_str" : "246394886", "name" : "shut up, mike", "screen_name" : "shutupmikeginn", "location" : "Los Angeles, CA", "description" : "writer (left handed) // shutupmikeginn @ gmail . com // @midnight", "url" : "https://t.co/JLpcO66Txj", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/JLpcO66Txj", "expanded_url" : "http://www.shutupmikeginn.com", "display_url" : "shutupmikeginn.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 155598, "friends_count" : 757, "listed_count" : 1675, "created_at" : "Wed Feb 02 18:21:51 +0000 2011", "favourites_count" : 58329, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 9001, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/246394886/1460523073", "profile_link_color" : "4A913C", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false } } } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 21369740, "id_str" : "21369740", "name" : "Rob DenBleyker", "screen_name" : "RobDenBleyker", "location" : "Dallas", "description" : "I'm not Rob Dyrdek. Don't follow me.", "url" : "https://t.co/5jh1OQTAOO", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/5jh1OQTAOO", "expanded_url" : "http://www.explosm.net", "display_url" : "explosm.net", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 126457, "friends_count" : 800, "listed_count" : 1284, "created_at" : "Fri Feb 20 03:26:24 +0000 2009", "favourites_count" : 1452, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 8088, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "49585E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_link_color" : "0C90F5", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Wed Aug 03 00:10:40 +0000 2016", "id" : 760628917837312000, "id_str" : "760628917837312000", "text" : "Hey @ScottAdamsSays, @eedrk & I are big fans so we recut Dilbert to deal with contemporary social issues! Please RT! https://t.co/Dth5YBHXeu", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "ScottAdamsSays", "name" : "Scott Adams", "id" : 2853461537, "id_str" : "2853461537", "indices" : [ 4, 19 ] }, { "screen_name" : "eedrk", "name" : "derek", "id" : 1350600582, "id_str" : "1350600582", "indices" : [ 21, 27 ] } ], "urls" : [ ], "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 121, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "photo", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } } } ] }, "extended_entities" : { "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 121, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "video", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "video_info" : { "aspect_ratio" : [ 35, 24 ], "duration_millis" : 50017, "variants" : [ { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.mpd" }, { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/262x180/bMwQb2LGoymiuNBF.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/524x360/akoT29AzRB-K7Tvf.mp4" } ] }, "additional_media_info" : { "monetizable" : false } } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 246394886, "id_str" : "246394886", "name" : "shut up, mike", "screen_name" : "shutupmikeginn", "location" : "Los Angeles, CA", "description" : "writer (left handed) // shutupmikeginn @ gmail . com // @midnight", "url" : "https://t.co/JLpcO66Txj", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/JLpcO66Txj", "expanded_url" : "http://www.shutupmikeginn.com", "display_url" : "shutupmikeginn.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 155598, "friends_count" : 757, "listed_count" : 1675, "created_at" : "Wed Feb 02 18:21:51 +0000 2011", "favourites_count" : 58329, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 9001, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/246394886/1460523073", "profile_link_color" : "4A913C", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 128, "favorite_count" : 523, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 128, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" } """; const string EMPTY_TWEET_WITH_MEDIA = """ { "created_at" : "Wed Aug 03 02:48:12 +0000 2016", "id" : 760668562323189761, "id_str" : "760668562323189761", "text" : "https://t.co/ffI4jeND7m", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 760668555239010304, "id_str" : "760668555239010304", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/Co5wBVLUsAASfCW.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co5wBVLUsAASfCW.jpg", "url" : "https://t.co/ffI4jeND7m", "display_url" : "pic.twitter.com/ffI4jeND7m", "expanded_url" : "http://twitter.com/RobDenBleyker/status/760668562323189761/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 820, "h" : 300, "resize" : "fit" }, "small" : { "w" : 680, "h" : 249, "resize" : "fit" }, "medium" : { "w" : 820, "h" : 300, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 760668555239010304, "id_str" : "760668555239010304", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/Co5wBVLUsAASfCW.jpg", "media_url_https" : "https://pbs.twimg.com/media/Co5wBVLUsAASfCW.jpg", "url" : "https://t.co/ffI4jeND7m", "display_url" : "pic.twitter.com/ffI4jeND7m", "expanded_url" : "http://twitter.com/RobDenBleyker/status/760668562323189761/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 820, "h" : 300, "resize" : "fit" }, "small" : { "w" : 680, "h" : 249, "resize" : "fit" }, "medium" : { "w" : 820, "h" : 300, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 21369740, "id_str" : "21369740", "name" : "Rob DenBleyker", "screen_name" : "RobDenBleyker", "location" : "Dallas", "description" : "I'm not Rob Dyrdek. Don't follow me.", "url" : "https://t.co/5jh1OQTAOO", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/5jh1OQTAOO", "expanded_url" : "http://www.explosm.net", "display_url" : "explosm.net", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 126457, "friends_count" : 800, "listed_count" : 1284, "created_at" : "Fri Feb 20 03:26:24 +0000 2009", "favourites_count" : 1452, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 8088, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "49585E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_link_color" : "0C90F5", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 90, "favorite_count" : 238, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "und" } """; const string EMPTY_RETWEET_WITH_MEDIA = """ { "created_at" : "Wed Aug 03 01:22:42 +0000 2016", "id" : 760647046835544064, "id_str" : "760647046835544064", "text" : "RT @shutupmikeginn: Hey @ScottAdamsSays, @eedrk & I are big fans so we recut Dilbert to deal with contemporary social issues! Please RT! ht…", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "shutupmikeginn", "name" : "shut up, mike", "id" : 246394886, "id_str" : "246394886", "indices" : [ 3, 18 ] }, { "screen_name" : "ScottAdamsSays", "name" : "Scott Adams", "id" : 2853461537, "id_str" : "2853461537", "indices" : [ 24, 39 ] }, { "screen_name" : "eedrk", "name" : "derek", "id" : 1350600582, "id_str" : "1350600582", "indices" : [ 41, 47 ] } ], "urls" : [ ], "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 143, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "photo", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "source_status_id" : 760628917837312000, "source_status_id_str" : "760628917837312000", "source_user_id" : 246394886, "source_user_id_str" : "246394886" } ] }, "extended_entities" : { "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 143, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "video", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "source_status_id" : 760628917837312000, "source_status_id_str" : "760628917837312000", "source_user_id" : 246394886, "source_user_id_str" : "246394886", "video_info" : { "aspect_ratio" : [ 35, 24 ], "duration_millis" : 50017, "variants" : [ { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.mpd" }, { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/262x180/bMwQb2LGoymiuNBF.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/524x360/akoT29AzRB-K7Tvf.mp4" } ] }, "additional_media_info" : { "monetizable" : false, "source_user" : { "id" : 246394886, "id_str" : "246394886", "name" : "shut up, mike", "screen_name" : "shutupmikeginn", "location" : "Los Angeles, CA", "description" : "writer (left handed) // shutupmikeginn @ gmail . com // @midnight", "url" : "https://t.co/JLpcO66Txj", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/JLpcO66Txj", "expanded_url" : "http://www.shutupmikeginn.com", "display_url" : "shutupmikeginn.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 155598, "friends_count" : 757, "listed_count" : 1675, "created_at" : "Wed Feb 02 18:21:51 +0000 2011", "favourites_count" : 58329, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 9001, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/246394886/1460523073", "profile_link_color" : "4A913C", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false } } } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 21369740, "id_str" : "21369740", "name" : "Rob DenBleyker", "screen_name" : "RobDenBleyker", "location" : "Dallas", "description" : "I'm not Rob Dyrdek. Don't follow me.", "url" : "https://t.co/5jh1OQTAOO", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/5jh1OQTAOO", "expanded_url" : "http://www.explosm.net", "display_url" : "explosm.net", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 126457, "friends_count" : 800, "listed_count" : 1284, "created_at" : "Fri Feb 20 03:26:24 +0000 2009", "favourites_count" : 1452, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 8088, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "49585E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/751256809873281024/-FuDkY2p_normal.jpg", "profile_link_color" : "0C90F5", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Wed Aug 03 00:10:40 +0000 2016", "id" : 760628917837312000, "id_str" : "760628917837312000", "text" : "", "truncated" : false, "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "ScottAdamsSays", "name" : "Scott Adams", "id" : 2853461537, "id_str" : "2853461537", "indices" : [ 4, 19 ] }, { "screen_name" : "eedrk", "name" : "derek", "id" : 1350600582, "id_str" : "1350600582", "indices" : [ 21, 27 ] } ], "urls" : [ ], "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 121, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "photo", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } } } ] }, "extended_entities" : { "media" : [ { "id" : 760627464456462336, "id_str" : "760627464456462336", "indices" : [ 121, 144 ], "media_url" : "http://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "media_url_https" : "https://pbs.twimg.com/ext_tw_video_thumb/760627464456462336/pu/img/ZmNeZzX7qcwzsuon.jpg", "url" : "https://t.co/Dth5YBHXeu", "display_url" : "pic.twitter.com/Dth5YBHXeu", "expanded_url" : "http://twitter.com/shutupmikeginn/status/760628917837312000/video/1", "type" : "video", "sizes" : { "medium" : { "w" : 600, "h" : 411, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 700, "h" : 480, "resize" : "fit" }, "small" : { "w" : 340, "h" : 233, "resize" : "fit" } }, "video_info" : { "aspect_ratio" : [ 35, 24 ], "duration_millis" : 50017, "variants" : [ { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.mpd" }, { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/262x180/bMwQb2LGoymiuNBF.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/pl/WgyGZ2bR67CZKbIe.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/ext_tw_video/760627464456462336/pu/vid/524x360/akoT29AzRB-K7Tvf.mp4" } ] }, "additional_media_info" : { "monetizable" : false } } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 246394886, "id_str" : "246394886", "name" : "shut up, mike", "screen_name" : "shutupmikeginn", "location" : "Los Angeles, CA", "description" : "writer (left handed) // shutupmikeginn @ gmail . com // @midnight", "url" : "https://t.co/JLpcO66Txj", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/JLpcO66Txj", "expanded_url" : "http://www.shutupmikeginn.com", "display_url" : "shutupmikeginn.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 155598, "friends_count" : 757, "listed_count" : 1675, "created_at" : "Wed Feb 02 18:21:51 +0000 2011", "favourites_count" : 58329, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 9001, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme15/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/523668808020422656/szD5CZyb_normal.jpeg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/246394886/1460523073", "profile_link_color" : "4A913C", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 128, "favorite_count" : 523, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 128, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" } """; const string VIDEO_AND_THUMBNAIL = """ { "created_at" : "Sat Nov 05 19:41:06 +0000 2016", "id" : 794987926157410308, "id_str" : "794987926157410308", "full_text" : "RT @mileysdrive: Truly iconic https://t.co/8gdSKk0Zx2", "truncated" : false, "display_text_range" : [ 0, 53 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "mileysdrive", "name" : "NSA agent jim", "id" : 2624180621, "id_str" : "2624180621", "indices" : [ 3, 15 ] } ], "urls" : [ ], "media" : [ { "id" : 793976804549754880, "id_str" : "793976804549754880", "indices" : [ 30, 53 ], "media_url" : "http://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "media_url_https" : "https://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "url" : "https://t.co/8gdSKk0Zx2", "display_url" : "pic.twitter.com/8gdSKk0Zx2", "expanded_url" : "https://twitter.com/LateNightSeth/status/793982502008397825/video/1", "type" : "photo", "sizes" : { "large" : { "w" : 1273, "h" : 715, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 382, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 674, "resize" : "fit" } }, "source_status_id" : 793982502008397825, "source_status_id_str" : "793982502008397825", "source_user_id" : 570290656, "source_user_id_str" : "570290656" } ] }, "extended_entities" : { "media" : [ { "id" : 793976804549754880, "id_str" : "793976804549754880", "indices" : [ 30, 53 ], "media_url" : "http://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "media_url_https" : "https://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "url" : "https://t.co/8gdSKk0Zx2", "display_url" : "pic.twitter.com/8gdSKk0Zx2", "expanded_url" : "https://twitter.com/LateNightSeth/status/793982502008397825/video/1", "type" : "video", "sizes" : { "large" : { "w" : 1273, "h" : 715, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 382, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 674, "resize" : "fit" } }, "source_status_id" : 793982502008397825, "source_status_id_str" : "793982502008397825", "source_user_id" : 570290656, "source_user_id_str" : "570290656", "video_info" : { "aspect_ratio" : [ 16, 9 ], "duration_millis" : 72440, "variants" : [ { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/320x180/wO9JSutxwKtnr-4M.mp4" }, { "bitrate" : 2176000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/1280x720/zMwtzVr0k_5SYkOG.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/amplify_video/793976804549754880/pl/Vj9EDa2m-8tTq332.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/640x360/1WAEEC98fuAilBVX.mp4" }, { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/amplify_video/793976804549754880/pl/Vj9EDa2m-8tTq332.mpd" } ] }, "additional_media_info" : { "title" : "", "description" : "", "call_to_actions" : { "visit_site" : { "url" : "https://www.youtube.com/user/LateNightSeth/featured" } }, "embeddable" : true, "monetizable" : false, "source_user" : { "id" : 570290656, "id_str" : "570290656", "name" : "Late Night", "screen_name" : "LateNightSeth", "location" : "Studio 8G - Rockefeller Center", "description" : "Official Twitter handle for Late Night with @SethMeyers, airing weeknights at 12:35/11:35c on @NBC. #LNSM", "url" : "http://t.co/af2p12D0GI", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/af2p12D0GI", "expanded_url" : "http://latenightseth.com", "display_url" : "latenightseth.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 291188, "friends_count" : 1743, "listed_count" : 1467, "created_at" : "Thu May 03 21:08:00 +0000 2012", "favourites_count" : 2440, "utc_offset" : -18000, "time_zone" : "Eastern Time (US & Canada)", "geo_enabled" : true, "verified" : true, "statuses_count" : 8635, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/438333165292097536/Z0HtuqUc.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/438333165292097536/Z0HtuqUc.jpeg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/781548560944865280/9dpThu5h_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/781548560944865280/9dpThu5h_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/570290656/1476483213", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" } } } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 21369740, "id_str" : "21369740", "name" : "Rob DenBleyker", "screen_name" : "RobDenBleyker", "location" : "Dallas", "description" : "I'm not Rob Dyrdek. Don't follow me.", "url" : "https://t.co/5jh1OQTAOO", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/5jh1OQTAOO", "expanded_url" : "http://www.explosm.net", "display_url" : "explosm.net", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 126727, "friends_count" : 815, "listed_count" : 1287, "created_at" : "Fri Feb 20 03:26:24 +0000 2009", "favourites_count" : 1543, "utc_offset" : -18000, "time_zone" : "Central Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 8276, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "49585E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/769014291756441601/b0axYzlg_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/769014291756441601/b0axYzlg_normal.jpg", "profile_link_color" : "0C90F5", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Thu Nov 03 19:59:57 +0000 2016", "id" : 794267893257170944, "id_str" : "794267893257170944", "full_text" : "Truly iconic https://t.co/8gdSKk0Zx2", "truncated" : false, "display_text_range" : [ 0, 36 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 793976804549754880, "id_str" : "793976804549754880", "indices" : [ 13, 36 ], "media_url" : "http://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "media_url_https" : "https://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "url" : "https://t.co/8gdSKk0Zx2", "display_url" : "pic.twitter.com/8gdSKk0Zx2", "expanded_url" : "https://twitter.com/LateNightSeth/status/793982502008397825/video/1", "type" : "photo", "sizes" : { "large" : { "w" : 1273, "h" : 715, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 382, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 674, "resize" : "fit" } }, "source_status_id" : 793982502008397825, "source_status_id_str" : "793982502008397825", "source_user_id" : 570290656, "source_user_id_str" : "570290656" } ] }, "extended_entities" : { "media" : [ { "id" : 793976804549754880, "id_str" : "793976804549754880", "indices" : [ 13, 36 ], "media_url" : "http://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "media_url_https" : "https://pbs.twimg.com/media/CwTHbn3XgAEcDEr.jpg", "url" : "https://t.co/8gdSKk0Zx2", "display_url" : "pic.twitter.com/8gdSKk0Zx2", "expanded_url" : "https://twitter.com/LateNightSeth/status/793982502008397825/video/1", "type" : "video", "sizes" : { "large" : { "w" : 1273, "h" : 715, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 382, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 674, "resize" : "fit" } }, "source_status_id" : 793982502008397825, "source_status_id_str" : "793982502008397825", "source_user_id" : 570290656, "source_user_id_str" : "570290656", "video_info" : { "aspect_ratio" : [ 16, 9 ], "duration_millis" : 72440, "variants" : [ { "bitrate" : 320000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/320x180/wO9JSutxwKtnr-4M.mp4" }, { "bitrate" : 2176000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/1280x720/zMwtzVr0k_5SYkOG.mp4" }, { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/amplify_video/793976804549754880/pl/Vj9EDa2m-8tTq332.m3u8" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/793976804549754880/vid/640x360/1WAEEC98fuAilBVX.mp4" }, { "content_type" : "application/dash+xml", "url" : "https://video.twimg.com/amplify_video/793976804549754880/pl/Vj9EDa2m-8tTq332.mpd" } ] }, "additional_media_info" : { "title" : "", "description" : "", "call_to_actions" : { "visit_site" : { "url" : "https://www.youtube.com/user/LateNightSeth/featured" } }, "embeddable" : true, "monetizable" : false, "source_user" : { "id" : 570290656, "id_str" : "570290656", "name" : "Late Night", "screen_name" : "LateNightSeth", "location" : "Studio 8G - Rockefeller Center", "description" : "Official Twitter handle for Late Night with @SethMeyers, airing weeknights at 12:35/11:35c on @NBC. #LNSM", "url" : "http://t.co/af2p12D0GI", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/af2p12D0GI", "expanded_url" : "http://latenightseth.com", "display_url" : "latenightseth.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 291188, "friends_count" : 1743, "listed_count" : 1467, "created_at" : "Thu May 03 21:08:00 +0000 2012", "favourites_count" : 2440, "utc_offset" : -18000, "time_zone" : "Eastern Time (US & Canada)", "geo_enabled" : true, "verified" : true, "statuses_count" : 8635, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/438333165292097536/Z0HtuqUc.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/438333165292097536/Z0HtuqUc.jpeg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/781548560944865280/9dpThu5h_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/781548560944865280/9dpThu5h_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/570290656/1476483213", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" } } } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 2624180621, "id_str" : "2624180621", "name" : "NSA agent jim", "screen_name" : "mileysdrive", "location" : "", "description" : "cristina yang", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 16939, "friends_count" : 124, "listed_count" : 48, "created_at" : "Thu Jun 19 23:54:07 +0000 2014", "favourites_count" : 7577, "utc_offset" : 7200, "time_zone" : "Bucharest", "geo_enabled" : true, "verified" : false, "statuses_count" : 5639, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/794467416331927552/LcPQANP8_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/794467416331927552/LcPQANP8_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/2624180621/1467532442", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 7523, "favorite_count" : 8028, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 7523, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" } """; const string REPLY = """ { "created_at" : "Sat Jan 28 09:23:50 +0000 2017", "id" : 825273167560208384, "id_str" : "825273167560208384", "full_text" : "@explodingwalrus \"definitely\" as in \"xdg-settings get default-url-scheme-handler https\" returns chrome's desktop file?", "truncated" : false, "display_text_range" : [ 17, 118 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "explodingwalrus", "name" : "Carl Draper", "id" : 44823886, "id_str" : "44823886", "indices" : [ 0, 16 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : 825269440556081153, "in_reply_to_status_id_str" : "825269440556081153", "in_reply_to_user_id" : 44823886, "in_reply_to_user_id_str" : "44823886", "in_reply_to_screen_name" : "explodingwalrus", "user" : { "id" : 2877682863, "id_str" : "2877682863", "name" : "Cawbird", "screen_name" : "cawbirdclient", "location" : "", "description" : "If there's no bug report for it, it doesn't exist.\n\nThis account is run by a highly-paid intern, not @baedert himself.\n\nStill not Cawbird. Or @Cawbird.", "url" : "https://t.co/yGvX7Nf6i3", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/yGvX7Nf6i3", "expanded_url" : "http://cawbird.baedert.org", "display_url" : "cawbird.baedert.org", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 430, "friends_count" : 3, "listed_count" : 24, "created_at" : "Sat Nov 15 08:08:32 +0000 2014", "favourites_count" : 22, "utc_offset" : 7200, "time_zone" : "Ljubljana", "geo_enabled" : false, "verified" : false, "statuses_count" : 376, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "FAB81E", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/533595923679432704/bIWqAMTk_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/533595923679432704/bIWqAMTk_normal.png", "profile_link_color" : "000000", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string REPLY_TO_TWO = """ { "created_at" : "Mon Apr 17 13:01:46 +0000 2017", "id" : 853956655020748801, "id_str" : "853956655020748801", "full_text" : "@kaidjohnson @gnome Not everyone feels as confident switching to Linux. UKUI gives them a comfort blanket of familiarity.", "truncated" : false, "display_text_range" : [ 20, 121 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "kaidjohnson", "name" : "Kai Johnson", "id" : 34798045, "id_str" : "34798045", "indices" : [ 0, 12 ] }, { "screen_name" : "gnome", "name" : "GNOME", "id" : 12579252, "id_str" : "12579252", "indices" : [ 13, 19 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : 853956134323048452, "in_reply_to_status_id_str" : "853956134323048452", "in_reply_to_user_id" : 34798045, "in_reply_to_user_id_str" : "34798045", "in_reply_to_screen_name" : "kaidjohnson", "user" : { "id" : 72915446, "id_str" : "72915446", "name" : "OMG! UBUNTU!", "screen_name" : "omgubuntu", "location" : "US/UK", "description" : "Your #1 source for the latest #Ubuntu Linux news, apps, tips, gaming & more. Got news? joey[at]ohso.io", "url" : "https://t.co/E0SDJKEG9w", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/E0SDJKEG9w", "expanded_url" : "http://www.omgubuntu.co.uk", "display_url" : "omgubuntu.co.uk", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 94994, "friends_count" : 54, "listed_count" : 3816, "created_at" : "Wed Sep 09 18:26:36 +0000 2009", "favourites_count" : 2689, "utc_offset" : 3600, "time_zone" : "London", "geo_enabled" : true, "verified" : false, "statuses_count" : 12699, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "4D1F41", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/458275648939622400/g2-00Dua.png", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/458275648939622400/g2-00Dua.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/627831364692389888/L3tujQ89_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/627831364692389888/L3tujQ89_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/72915446/1485460019", "profile_link_color" : "009999", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "FFFFFF", "profile_text_color" : "303030", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "regular" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 1, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string REPLY2 = """ { "created_at" : "Mon Apr 17 15:16:18 +0000 2017", "id" : 853990508326252550, "id_str" : "853990508326252550", "full_text" : "@jjdesmond @_UBRAS_ @franalsworth @4Apes @katy4apes @theAliceRoberts @JaneGoodallUK @Jane_Goodall @JaneGoodallInst And here's the link for tickets again ... https://t.co/a9lOVMouNK", "truncated" : false, "display_text_range" : [ 115, 180 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "jjdesmond", "name" : "Jimmy Jenny Desmond", "id" : 21278482, "id_str" : "21278482", "indices" : [ 0, 10 ] }, { "screen_name" : "_UBRAS_", "name" : "Roots and Shoots UOB", "id" : 803329927974096896, "id_str" : "803329927974096896", "indices" : [ 11, 19 ] }, { "screen_name" : "franalsworth", "name" : "Fran", "id" : 776983919287754752, "id_str" : "776983919287754752", "indices" : [ 20, 33 ] }, { "screen_name" : "4Apes", "name" : "Ian Redmond", "id" : 155889035, "id_str" : "155889035", "indices" : [ 34, 40 ] }, { "screen_name" : "katy4apes", "name" : "Katy Jedamzik", "id" : 159608654, "id_str" : "159608654", "indices" : [ 41, 51 ] }, { "screen_name" : "theAliceRoberts", "name" : "Prof Alice Roberts", "id" : 260211154, "id_str" : "260211154", "indices" : [ 52, 68 ] }, { "screen_name" : "JaneGoodallUK", "name" : "Roots & Shoots UK", "id" : 423423823, "id_str" : "423423823", "indices" : [ 69, 83 ] }, { "screen_name" : "Jane_Goodall", "name" : "Jane Goodall", "id" : 235157216, "id_str" : "235157216", "indices" : [ 84, 97 ] }, { "screen_name" : "JaneGoodallInst", "name" : "JaneGoodallInstitute", "id" : 39822897, "id_str" : "39822897", "indices" : [ 98, 114 ] } ], "urls" : [ { "url" : "https://t.co/a9lOVMouNK", "expanded_url" : "https://www.eventbrite.com/e/working-with-apes-tickets-33089771397", "display_url" : "eventbrite.com/e/working-with…", "indices" : [ 157, 180 ] } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : 853925036696141824, "in_reply_to_status_id_str" : "853925036696141824", "in_reply_to_user_id" : 21278482, "in_reply_to_user_id_str" : "21278482", "in_reply_to_screen_name" : "jjdesmond", "user" : { "id" : 415472140, "id_str" : "415472140", "name" : "Ben Garrod", "screen_name" : "Ben_garrod", "location" : "Bristol&Norfolk", "description" : "Monkey-chaser, TV-talker, bone geek and Teaching Fellow at @AngliaRuskin https://t.co/FXbftdxxTJ", "url" : "https://t.co/1B9SDHfWoF", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/1B9SDHfWoF", "expanded_url" : "http://www.josarsby.com/ben-garrod", "display_url" : "josarsby.com/ben-garrod", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ { "url" : "https://t.co/FXbftdxxTJ", "expanded_url" : "http://www.anglia.ac.uk/science-and-technology/about/life-sciences/our-staff/ben-garrod", "display_url" : "anglia.ac.uk/science-and-te…", "indices" : [ 73, 96 ] } ] } }, "protected" : false, "followers_count" : 6526, "friends_count" : 1016, "listed_count" : 128, "created_at" : "Fri Nov 18 11:30:48 +0000 2011", "favourites_count" : 25292, "utc_offset" : 3600, "time_zone" : "London", "geo_enabled" : true, "verified" : true, "statuses_count" : 17224, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/415472140/1477223840", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 6, "favorite_count" : 7, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; const string REPLY3 = """ { "created_at" : "Mon Apr 24 11:40:53 +0000 2017", "id" : 856473014560591872, "id_str" : "856473014560591872", "full_text" : "RT @cawbirdgtk: @baedert and @cawbirdclient so?", "truncated" : false, "display_text_range" : [ 0, 49 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "cawbirdgtk", "name" : "Z??!@*(&*²³¤²³¤", "id" : 993713617, "id_str" : "993713617", "indices" : [ 3, 15 ] }, { "screen_name" : "baedert", "name" : "Schupp & Wupp", "id" : 118055879, "id_str" : "118055879", "indices" : [ 17, 25 ] }, { "screen_name" : "cawbirdclient", "name" : "Cawbird", "id" : 2877682863, "id_str" : "2877682863", "indices" : [ 30, 45 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Z??!@*(&*²³¤²³¤", "screen_name" : "cawbirdgtk", "location" : "", "description" : "<asdf", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 3, "friends_count" : 4, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 9, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 909, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/993713617/1413106147", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Mon Apr 24 11:35:17 +0000 2017", "id" : 856471602883686400, "id_str" : "856471602883686400", "full_text" : "@baedert and @cawbirdclient so?", "truncated" : false, "display_text_range" : [ 9, 32 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "baedert", "name" : "Schupp & Wupp", "id" : 118055879, "id_str" : "118055879", "indices" : [ 0, 8 ] }, { "screen_name" : "cawbirdclient", "name" : "Cawbird", "id" : 2877682863, "id_str" : "2877682863", "indices" : [ 13, 28 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : 853198126106148864, "in_reply_to_status_id_str" : "853198126106148864", "in_reply_to_user_id" : 993713617, "in_reply_to_user_id_str" : "993713617", "in_reply_to_screen_name" : "cawbirdgtk", "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Z??!@*(&*²³¤²³¤", "screen_name" : "cawbirdgtk", "location" : "", "description" : "<asdf", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 3, "friends_count" : 4, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 9, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 909, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/993713617/1413106147", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" } """; cawbird-1.4.2/flatpak/000077500000000000000000000000001416632607600145665ustar00rootroot00000000000000cawbird-1.4.2/flatpak/uk.co.ibboard.cawbird.json000066400000000000000000000046341416632607600215220ustar00rootroot00000000000000{ "app-id":"uk.co.ibboard.cawbird", "runtime":"org.gnome.Platform", "runtime-version":"3.38", "sdk":"org.gnome.Sdk", "command":"cawbird", "desktop-file-name-prefix":"(Development) ", "finish-args":[ "--share=ipc", "--share=network", "--socket=fallback-x11", "--socket=wayland", "--socket=pulseaudio" ], "cleanup":[ "/include", "/share/gir-1.0", "/lib/pkgconfig", "/lib/girepository-1.0", "/share/vala-0.34", "/bin/vala*", "/bin/vapi*", "/share/devhelp", "/share/aclocal", "/lib/*.la", "/lib/libvala*", "/lib/vala-0.34", "/share/gtk-doc", "/share/man", "/share/vala" ], "modules":[ { "name":"enchant", "config-opts":[ "--disable-static", "--with-myspell-dir=/usr/share/hunspell" ], "cleanup":[ "/bin" ], "sources":[ { "type":"archive", "url":"https://github.com/AbiWord/enchant/releases/download/v2.2.13/enchant-2.2.13.tar.gz", "sha256":"eab9f90d79039133660029616e2a684644bd524be5dc43340d4cfc3fb3c68a20" } ] }, { "name":"rest", "sources":[ { "type":"archive", "url":"https://download.gnome.org/sources/rest/0.8/rest-0.8.1.tar.xz", "sha256":"0513aad38e5d3cedd4ae3c551634e3be1b9baaa79775e53b2dba9456f15b01c9" } ] }, { "name":"gspell", "config-opts":[ "--enable-vala=yes" ], "sources":[ { "type":"archive", "url":"https://download.gnome.org/sources/gspell/1.9/gspell-1.9.1.tar.xz", "sha256":"dcbb769dfdde8e3c0a8ed3102ce7e661abbf7ddf85df08b29915e92cd723abdd" } ] }, { "name":"oauth", "config-opts":[ "--enable-nss" ], "sources":[ { "type":"archive", "url":"http://downloads.sourceforge.net/liboauth/liboauth-1.0.3.tar.gz", "sha256":"0df60157b052f0e774ade8a8bac59d6e8d4b464058cc55f9208d72e41156811f" } ] }, { "name":"cawbird", "buildsystem":"meson", "config-opts": [ "--buildtype=debug", "-Dconsumer_key_base64=a1hVdm9YWGY0WnkxOUNKczlEVDBHRGVYcQ==" "-Dconsumer_secret_base64=Y2ZxS3F0cHNkanp6Q0hzeTEzY2pHSmpxMlNta3Fvd2VWdDZoVzY3cXFyNnVrV2xPc2k=" ], "sources":[ { "type":"dir", "path":".." } ] } ] } cawbird-1.4.2/meson.build000066400000000000000000000172541416632607600153170ustar00rootroot00000000000000project('Cawbird', ['vala', 'c'], version: '1.4.2', default_options: [ 'buildtype=debug' ]) prefix = get_option('prefix') localedir = join_paths(prefix, get_option('localedir')) gnome = import('gnome') srcdir = include_directories('src') # This is apparently bad practice, but it solves our "undefined reference to symbol 'floor@@GLIBC_2.2.5'" build problem cc = meson.get_compiler('c') libm = cc.find_library('m', required : false) oauth = cc.find_library('oauth') min_glib_version = '2.44' glib_dep = dependency('glib-2.0', version: '>=' + min_glib_version) gtk_dep = dependency('gtk+-3.0', version: '>=3.22') rest_dep = dependency('rest-0.7') json_dep = dependency('json-glib-1.0') sql_dep = dependency('sqlite3') soup_dep = dependency('libsoup-2.4') cb_deps = [ glib_dep, gtk_dep, rest_dep, json_dep, sql_dep, soup_dep, oauth, libm ] # Options enable_debug = get_option('buildtype') == 'debug' or get_option('buildtype') == 'debugoptimized' enable_video = get_option('video') enable_spellcheck = get_option('spellcheck') enable_examples = get_option('examples') enable_x11 = get_option('x11') use_windows_behaviour = get_option('mswindows') # Project arguments add_project_arguments('-DGETTEXT_PACKAGE="cawbird"', language: 'c') add_project_arguments('-DDATADIR="' + get_option('datadir') + '"', language: 'c') add_project_arguments('-DG_LOG_DOMAIN="cawbird"', language: 'c') add_project_arguments('--enable-deprecated', language: 'vala') if (enable_debug) add_project_arguments('-DDEBUG', language: 'c') add_project_arguments('-D', 'DEBUG', language: 'vala') endif if (enable_video) add_project_arguments('-DVIDEO', language: 'c') add_project_arguments('-D', 'VIDEO', language: 'vala') cb_deps += [dependency('gstreamer-video-1.0', version: '>= 1.6')] endif if (enable_spellcheck) add_project_arguments('-DSPELLCHECK', language: 'c') add_project_arguments('-D', 'SPELLCHECK', language: 'vala') cb_deps += [dependency('gspell-1', version: '>=1.0')] endif if (enable_x11) add_project_arguments('-DX11', language: 'c') add_project_arguments('-D', 'X11', language: 'vala') cb_deps += [dependency('x11')] endif if (enable_debug) warning('Debugging is enabled. Debug level logs will contain the full, plaintext content of ALL tweets and DMs.') endif if (use_windows_behaviour) add_project_arguments('-D', 'MSWINDOWS', language: 'vala') endif cawbird_lib_sources = files([ 'src/Account.vala', 'src/Cawbird.vala', 'src/ComposedTweet.vala', 'src/DMManager.vala', 'src/DMPage.vala', 'src/DMThreadsPage.vala', 'src/DefaultTimeline.vala', 'src/FavoritesTimeline.vala', 'src/FilterPage.vala', 'src/HomeTimeline.vala', 'src/IPage.vala', 'src/ListStatusesPage.vala', 'src/ListsPage.vala', 'src/MainWidget.vala', 'src/MainWindow.vala', 'src/MediaUpload.vala', 'src/MentionsTimeline.vala', 'src/NotificationManager.vala', 'src/OAuthProxyCallWithBody.vala', 'src/OAuthProxyCallWithQueryString.vala', 'src/ProfilePage.vala', 'src/SearchPage.vala', 'src/Settings.vala', 'src/TweetInfoPage.vala', 'src/Twitter.vala', 'src/UserEventReceiver.vala', 'src/async/Collect.vala', 'src/async/CollectById.vala', 'src/list/AddListEntry.vala', 'src/list/DMListEntry.vala', 'src/list/DMThreadEntry.vala', 'src/list/FilterListEntry.vala', 'src/list/ListListEntry.vala', 'src/list/NewListEntry.vala', 'src/list/SnippetListEntry.vala', 'src/list/StartConversationEntry.vala', 'src/list/TweetListEntry.vala', 'src/list/UserFilterEntry.vala', 'src/list/UserListEntry.vala', 'src/list/FavImageRow.vala', 'src/model/DMThreadsModel.vala', 'src/sql/Database.vala', 'src/sql/BaseStatement.vala', 'src/sql/DeleteStatement.vala', 'src/sql/InsertStatement.vala', 'src/sql/SelectStatement.vala', 'src/sql/UpdateStatement.vala', 'src/util/Benchmark.vala', 'src/util/Dirs.vala', 'src/util/ListUtils.vala', 'src/util/TweetUtils.vala', 'src/util/UserCompletion.vala', 'src/util/UserUtils.vala', 'src/util/Utils.vala', 'src/widgets/AccountCreateWidget.vala', 'src/widgets/AddImageButton.vala', 'src/widgets/AspectImage.vala', 'src/widgets/AvatarBannerWidget.vala', 'src/widgets/AvatarWidget.vala', 'src/widgets/BadgeRadioButton.vala', 'src/widgets/ChildSizedScroller.vala', 'src/widgets/CompletionTextView.vala', 'src/widgets/ComposeImageManager.vala', 'src/widgets/CropWidget.vala', 'src/widgets/DMPlaceholderBox.vala', 'src/widgets/DoubleTapButton.vala', 'src/widgets/FollowButton.vala', 'src/widgets/ImpostorWidget.vala', 'src/widgets/LazyMenuButton.vala', 'src/widgets/ListBox.vala', 'src/widgets/MediaButton.vala', 'src/widgets/MultiMediaWidget.vala', 'src/widgets/PixbufButton.vala', 'src/widgets/ResizableImage.vala', 'src/widgets/ReplyEntry.vala', 'src/widgets/ScrollWidget.vala', 'src/widgets/TextButton.vala', 'src/widgets/TweetListBox.vala', 'src/widgets/UserListsWidget.vala', 'src/widgets/FavImageView.vala', 'src/window/AboutDialog.vala', 'src/window/AccountDialog.vala', 'src/window/ComposeTweetWindow.vala', 'src/window/ImageDescriptionWindow.vala', 'src/window/MediaDialog.vala', 'src/window/ModifyFilterDialog.vala', 'src/window/ModifySnippetDialog.vala', 'src/window/SettingsDialog.vala', 'src/window/UserListDialog.vala', # C Sources 'src/CbTweet.c', 'src/CbTextTransform.c', 'src/CbMedia.c', 'src/CbMediaDownloader.c', 'src/CbTypes.c', 'src/CbUserCounter.c', 'src/CbFilter.c', 'src/CbAvatarCache.c', 'src/CbMediaImageWidget.c', 'src/CbTweetModel.c', 'src/CbTwitterItem.c', 'src/CbDeltaUpdater.c', 'src/CbUtils.c', 'src/CbBundle.c', 'src/CbBundleHistory.c', 'src/CbSnippetManager.c', 'src/CbSurfaceProgress.c', 'src/CbMediaVideoWidget.c', 'src/CbUserStream.c', 'src/CbMessageReceiver.c', 'src/CbUserCompletionModel.c', 'src/CbEmojiChooser.c', # libtweetlength sources (TODO: Should be a meson subproject) # https://github.com/baedert/libtweetlength 'src/libtl/libtweetlength.c', # Vapi files 'vapi/cawbird-internal.vapi', 'vapi/libtl.vapi' ]) # glib resources cb_resources = gnome.compile_resources( 'cawbird_resources', 'cawbird.gresource.xml', ) # config.h consumer_key = get_option('consumer_key_base64') consumer_secret = get_option('consumer_secret_base64') assert(consumer_key != '' and consumer_secret != '', 'Must specify `consumer_key_base64` and `consumer_secret_base64`') cdata = configuration_data() if get_option('localtextdomain') cdata.set('LOCALEDIR', 'NULL') else cdata.set_quoted ('LOCALEDIR', localedir) endif cdata.set_quoted ('GETTEXT_PACKAGE', 'cawbird') cdata.set_quoted ('CONSUMER_KEY', consumer_key) cdata.set_quoted ('CONSUMER_SECRET', consumer_secret) configure_file( #input : 'config.h.meson', output: 'config.h', configuration: cdata ) # library (for unit tests) cb_lib = static_library( 'cawbird', cawbird_lib_sources, dependencies: cb_deps, include_directories: [srcdir], vala_args: [ meson.source_root() + '/vapi/config.vapi', '--target-glib=' + min_glib_version, '--vapidir=' + meson.source_root() + '/vapi/', '--gresources=' + meson.source_root() + '/cawbird.gresource.xml', ], c_args: [ '-Werror=implicit-function-declaration' ] ) cb_dep = declare_dependency( link_with: cb_lib, dependencies: cb_deps, include_directories: [srcdir], ) # actual executable executable( 'cawbird', 'src/main.vala', cb_resources, dependencies: cb_dep, include_directories: srcdir, vala_args: [ meson.source_root() + '/vapi/config.vapi', meson.source_root() + '/vapi/cawbird-internal.vapi', meson.source_root() + '/vapi/libtl.vapi', ], install: true ) subdir('data') subdir('po') subdir('tests') if enable_examples subdir('examples') endif cawbird-1.4.2/meson_options.txt000066400000000000000000000012521416632607600166010ustar00rootroot00000000000000option('video', type: 'boolean', value: 'true') option('spellcheck', type: 'boolean', value: 'true') option('examples', type: 'boolean', value: 'false') option('appdata', type: 'boolean', value: 'true') option('x11', type: 'boolean', value: 'true') option('localtextdomain', type: 'boolean', value: 'false', description: 'Use local translation files rather than those installed in `prefix`' ) option('consumer_key_base64', type: 'string', description: 'base64 encoding of the Twitter consumer key') option('consumer_secret_base64', type: 'string', description: 'base64 encoding of the Twitter consumer secret') option('mswindows', type: 'boolean', value: 'false') cawbird-1.4.2/notes.md000066400000000000000000000124251416632607600146220ustar00rootroot00000000000000# Notes This file contains a variety of development notes that track how some bits of the code work. This could be included in the doc strings, but doing that would generally repeat the description a lot. Recording it here keeps it in one place. ## Translations Translations are linked to [Transifex](https://www.transifex.com/cawbird/cawbird/dashboard/). The base translation template is rebuilt using `ninja -C build cawbird-pot`. Translations can then be rebuilt using `ninja -C build cawbird-update-po`. It may also rebuild the `.pot` file. It doesn't say so in the docs, but the file updates when it's run. ## Fake Streaming Corebird was built for the Twitter streaming API. On 16th August 2018, Twitter closed down the API, which [broke lots of apps](http://apps-of-a-feather.com/). Twitter encouraged people to use the "Account Activity API", which was an Enterprise capability that cost thousands of dollars for even a small number of users. This was when Corebird was abandonned. However, the older polling APIs remained. Rather than completely re-architect Corebird (or be forced to use the Twitter website **\*shudders\***) IBBoard decided to revert to the polling APIs but then still pass the messages to Corebird as if they were still streamed. This is how Cawbird continues to work. There are four main streams: * Timeline * Mentions * Favourites * Direct Messages Each of these has its own "user stream" function (`CbUserStream.c`) that fetches its messages, dispatches them and queues another poll of the API. These are all initiated in `cb_user_stream_start ()` and then scheduled at regular intervals using `g_timeout_add_seconds_full`. The process of fake streaming by polling is: * Create a `RestProxyCall` that knows which endpoint to talk to and invoke it asynchronously * When the call ends, load the JSON object and find the array of tweets/DMs * For each item in the array, starting from the end (oldest) and decrementing the counter: * Get the JsonOjbect (so we can pull values from it) * Determine its message type (DMs only) * Track the last ID (timeline/favourites/mentions) so that our next request can reference it * Send the tweet to the `stream_tweet()` function with the appropriate type to mimic a streamed tweet * Wait for the next timeout This fetches tweets on a schedule through polling, but still feeds them to the rest of the application one-by-one as if they had been streamed. The `stream_tweet()` function iterates through all registered message receivers and dispatches the JSON node to each one in turn. These listeners implement `Cb.MessageReceiver` (in Vala) and are generally components like the `HomeTimeline`, `MentionsTimeline` and `DMThreadsPage`. Each receiver then implements the required method, which normally includes a series of `if (type == Cb.StreamMessageType.XXX) { … }` statements to ensure that only the required messages are handle (e.g. `MentionsTimeline` adds `Cb.StreamMessageType.MENTION` and deletes `Cb.StreamMessageType.DELETE`). This also means that we can pass all messages to all listeners and not worry about whether each listener is getting all the messages that it needs. ### Injecting tweets In addition to streaming the real tweets received from Twitter, we sometimes want to inject our own tweets. This is particularly useful when composing tweets, because Twitter returns the JSON of the tweet that we just created and so we can show it before it arrives in the next poll. This makes it easier to compose threads. Tweet injection is performed through the `cb_user_stream_inject_tweet()` function, which handles some special cases to make sure that the tweet is correct and fully formed. ## RT/Like/etc from user actions The control flow for the RT, Like and other actions in TweetUtils is normally: * User action (e.g. click) triggers a function call (`cb`) * `cb` disables the button (makes it insensitive) * Function calls `set_retweet_status.begin()` with an anoymous callback (`ac1`) * `set_retweet_status` begins an async HTTP call with an anonymous (`ac2`) * `set_retweet_status` `yields` and the user action function completes its outer block (which generally shouldn't do anything else because we've kicked off async methods and don't know what will happen) * The HTTP request ends and calls the anonymous callback `ac2` * `ac2` calls the HTTP call's `end()` function to retrieve the result * If this exceptions then an alert is shown and `set_retweet_status.callback()` is called to return execution to just after the `yield` * If the request was okay then the payload is retrieved, tweet flags are updated and `set_retweet_status.callback()` is called to return execution to just after the `yield` * `set_retweet_status` completes and returns success/failure, which causes `ac1` to be called * `ac1` calls `set_retweet_status.end (res)` to get the success/failure state, performs actions and completes * `ac2` continues from after the `set_retweet_status.callback()` call (which generally does nothing, but could result in the "success" sub-path being run if the exception path doesn't `return` in its `catch` block!) Note: if the call can fail/throw an exception then the callback should handle that and stop. Do not let the code continue while thinking everything was okay! This causes odd behaviour like the RT button status not matching the RT status of the tweet. cawbird-1.4.2/po/000077500000000000000000000000001416632607600135625ustar00rootroot00000000000000cawbird-1.4.2/po/LINGUAS000066400000000000000000000002431416632607600146060ustar00rootroot00000000000000ar ast ca ca@valencia da de de_DE en_GB eo es es_419 es_MX es_VE fa fi fr ga gd gl hi hu id it ja ko lt nb nl pl pt pt_BR ro ru sr sr_BA@latin tr uk_UA zh_CN zh_TWcawbird-1.4.2/po/POTFILES.in000066400000000000000000000033471416632607600153460ustar00rootroot00000000000000# List of source files containing translatable strings. # Please keep this file sorted alphabetically. data/uk.co.ibboard.cawbird.appdata.xml.in data/uk.co.ibboard.cawbird.desktop.in src/CbUtils.c src/Cawbird.vala src/DMPage.vala src/DMThreadsPage.vala src/DefaultTimeline.vala src/FavoritesTimeline.vala src/FilterPage.vala src/HomeTimeline.vala src/ListStatusesPage.vala src/ListsPage.vala src/MainWindow.vala src/MentionsTimeline.vala src/ProfilePage.vala src/SearchPage.vala src/TweetInfoPage.vala src/UserEventReceiver.vala src/list/DMThreadEntry.vala src/list/FavImageRow.vala src/list/TweetListEntry.vala src/util/TweetUtils.vala src/util/Utils.vala src/widgets/AccountCreateWidget.vala src/widgets/AvatarBannerWidget.vala src/widgets/CompletionTextView.vala src/widgets/FavImageView.vala src/widgets/FollowButton.vala src/widgets/ListBox.vala src/widgets/MediaButton.vala src/widgets/PixbufButton.vala src/widgets/TweetListBox.vala src/window/AccountDialog.vala src/window/ComposeTweetWindow.vala src/window/ImageDescriptionWindow.vala src/window/MediaDialog.vala src/window/ModifyFilterDialog.vala src/window/ModifySnippetDialog.vala src/window/SettingsDialog.vala src/window/UserListDialog.vala ui/about-dialog.ui ui/account-create-widget.ui ui/account-dialog.ui ui/cb-emoji-chooser.ui ui/compose-window.ui ui/dm-page.ui ui/filter-list-entry.ui ui/filter-page.ui ui/image-description-window.ui ui/list-list-entry.ui ui/list-statuses-page.ui ui/menus.ui ui/modify-filter-dialog.ui ui/modify-snippet-dialog.ui ui/new-list-entry.ui ui/profile-page.ui ui/search-page.ui ui/settings-dialog.ui ui/shortcuts-window.ui ui/start-conversation-entry.ui ui/tweet-info-page.ui ui/tweet-list-entry.ui ui/user-filter-entry.ui ui/user-list-entry.ui ui/user-lists-widget.ui cawbird-1.4.2/po/POTFILES.skip000066400000000000000000000015611416632607600157020ustar00rootroot00000000000000# List of source files that should *not* be translated. # Please keep this file sorted alphabetically. src/DefaultTimeline.c src/DMPage.c src/DMThreadsPage.c src/FavoritesTimeline.c src/FilterPage.c src/HomeTimeline.c src/list/DMThreadEntry.c src/list/FavImageRow.c src/ListsPage.c src/ListStatusesPage.c src/list/TweetListEntry.c src/MainWindow.c src/MentionsTimeline.c src/ProfilePage.c src/SearchPage.c src/TweetInfoPage.c src/UserEventReceiver.c src/util/Utils.c src/widgets/AccountCreateWidget.c src/widgets/AvatarBannerWidget.c src/widgets/CompletionTextView.c src/widgets/FavImageView.c src/widgets/FollowButton.c src/widgets/MediaButton.c src/widgets/PixbufButton.c src/widgets/TweetListBox.c src/window/AccountDialog.c src/window/ComposeTweetWindow.c src/window/ModifyFilterDialog.c src/window/ModifySnippetDialog.c src/window/SettingsDialog.c src/window/UserListDialog.c cawbird-1.4.2/po/ar.po000066400000000000000000000676551416632607600145470ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Med Touhami MAHDI , 2014 # mohammadA , 2016 # Abdulrahman Saud , 2020 # Ammar K , 2020 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Ammar K , 2020\n" "Language-Team: Arabic (http://www.transifex.com/cawbird/cawbird/language/" "ar/)\n" "Language: ar\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" msgid "Cawbird" msgstr "كوربرد" msgid "Twitter Client" msgstr "عميل تويتر" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "كوربرد عميل تويتر GTK+ أصيل يوÙّر ميزات حيوية مثل الرسائل المباشرة والإخطار " "بالتغريدات وعرض المحادثات." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "تشمل الميزات الإضاÙية على: عرض الÙيديوهات داخل التطبيق، عرض الصور المتعددة، " "القوائم، الÙلاتر، Ùˆ تسجيل الدخول بحسابات متعددة." msgid "Generic timeline view when using Cawbird" msgstr "عرض المخطط الزمني العام (Timeline) عند استخدام Cawbird" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "يمكن ضبط (تهيئه) إعدادات الحساب " msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "استحدم تويتر من داخل تطبيق ÙÙŠ سطح المكتب. " #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "تويتر;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "رسائل مباشرة" #, fuzzy msgid "Select Media" msgstr "اختيار صورة" msgid "Open" msgstr "Ø§ÙØªØ­" msgid "Cancel" msgstr "ألغ" #, fuzzy msgid "Selected file is not an image or video." msgstr "المل٠المحدد ليس صورة." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "حجم الصورة المحددة كبيرة جدا. الحجم الاقصى لكل صورة هو %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "حجم الصورة المحددة كبيرة جدا. الحجم الاقصى لكل صورة هو %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "حجم الصورة المحددة كبيرة جدا. الحجم الاقصى لكل صورة هو %'d MB" msgid "Insert Emoji" msgstr "Ø§Ø¶Ø§ÙØ© Emoji" msgid "Direct Conversation" msgstr "محادثة مباشرة" #, fuzzy msgid "Direct message threads" msgstr "رسائل مباشرة" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d لا توجد رسائل جديدة %s" msgstr[1] "%d رسالة جديدة من %s" msgstr[2] "%d رسالتان جديدة من %s" msgstr[3] "%d رسائل جديدة من %s" msgstr[4] "%d رسائل جديدة من %s" msgstr[5] "%d رسائل جديدة من %s" #, c-format msgid "New direct message from %s" msgstr "رسالة مباشرة جديدة من %s" msgid "Direct Messages" msgstr "رسائل مباشرة" #, fuzzy msgid "Liked tweets timeline" msgstr "ØªÙØ¶ÙŠÙ„ التغريدة" #, fuzzy msgid "Likes" msgstr "Ø§Ù„Ù…ÙØ¶Ù„Ø©" msgid "Add new Filter" msgstr "Ø§Ø¶Ø§ÙØ© Ùلتر جديد " msgid "Filters" msgstr "الÙلاتر " #, fuzzy msgid "Home timeline" msgstr "Ø¥Ø®ÙØ§Ø¡ ÙÙŠ الخط الزمني" #, c-format msgid "%s retweeted %s" msgstr "%s أعاد تغريد %s" #, c-format msgid "%s tweeted" msgstr "%s غرد" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d تغريدة جديدة! " msgstr[1] "%d تغريدة جديدة! " msgstr[2] "%d تغريدة جديدة! " msgstr[3] "%d تغريدة جديدة! " msgstr[4] "%d تغريدة جديدة! " msgstr[5] "%d تغريدة جديدة! " msgid "Home" msgstr "الرئيسية" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s غرد" msgid "Private" msgstr "خاص" msgid "Public" msgstr "عمومي" msgid "Lists" msgstr "قوائم" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "كتابة تغريدة" msgid "Add new Account" msgstr "أض٠حسابًا جديدًا" #, fuzzy msgid "Mentions timeline" msgstr "Ø¥Ø®ÙØ§Ø¡ ÙÙŠ الخط الزمني" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "الإشارات" msgid "Suspended Account" msgstr "حساب موقو٠" msgid "Protected profile" msgstr "مل٠محمي" #, c-format msgid "Tweet to @%s" msgstr "غرد Ù„Ù@%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s غرد" msgstr[1] "%s غرد" msgstr[2] "%s غرد" msgstr[3] "%s غرد" msgstr[4] "%s غرد" msgstr[5] "%s غرد" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "يتتبع" msgstr[1] "يتتبع" msgstr[2] "يتتبع" msgstr[3] "يتتبع" msgstr[4] "يتتبع" msgstr[5] "يتتبع" #, fuzzy, c-format msgid "Location: %s" msgstr "الإشعارات" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Ø¥Ø®ÙØ§Ø¡ ÙÙŠ الخط الزمني" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "المتتبعون" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "يتتبع" msgid "Protected Profile" msgstr "الحساب محمي " msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "المتتبعون" msgstr[1] "المتتبعون" msgstr[2] "المتتبعون" msgstr[3] "المتتبعون" msgstr[4] "المتتبعون" msgstr[5] "المتتبعون" #, fuzzy msgid "No tweets found" msgstr "لم يتم العثور على أي شيء" msgid "No users found" msgstr "لم يتم العثور على مستخدمين" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "غرد Ù„Ù@%s" msgid "Search" msgstr "البحث" msgid "Load More" msgstr "تحميل المزيد" msgid "Could not show tweet" msgstr "تعذر عرض التغريدة" msgid "This tweet is hidden by the author" msgstr "التغريدة مخÙية بواسطة الكاتب" msgid "This tweet is unavailable" msgstr "التغريدة غير متاحة" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "إعادات التغريدات" msgid "Open in Browser" msgstr "ÙØªØ­ ÙÙŠ Ø§Ù„Ù…ØªØµÙØ­." msgid "Source" msgstr "المصدر" msgid "Tweet Details" msgstr "ØªÙØ§ØµÙŠÙ„ التغريدة" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d غير مقروءة)" msgstr[1] "(%d غير مقروءة)" msgstr[2] "(%d غير مقروءة)" msgstr[3] "(%d غير مقروءة)" msgstr[4] "(%d غير مقروءة)" msgstr[5] "(%d غير مقروءة)" msgid "Delete" msgstr "حذÙ" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "إعادة التغريد" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "هل حقا تريد حذ٠هذا الحساب؟" #, c-format msgid "Block %s" msgstr "حظر %s" msgid "This tweet contains images marked as inappropriate" msgstr "تحتوي هذه التغريدة على صور تم وضع علامة عليها كغير لائقة" msgid "Show anyway" msgstr "عرض على أي حال" msgid "Could not authenticate you" msgstr "تعذر الوثوق بك" msgid "Sorry, that page does not exist" msgstr "عذرا، هذه Ø§Ù„ØµÙØ­Ø© غير موجودة" msgid "User not found." msgstr "لا يمكن العثور على المستخدم." msgid "User has been suspended." msgstr "تم تعليق المستخدم." msgid "Your account is suspended and is not permitted to access this feature" msgstr "تم ايقا٠حسابك. لا تملك الصلاحية للوصول إلى هذه الميزة" msgid "Rate limit exceeded" msgstr "تخطيت المعدل المسموح " msgid "Invalid or expired token" msgstr "الرمز غير صالح أو منتهي الصلاحية" msgid "The specified user is not a subscriber of this list." msgstr "المستخدم المحدد ليس مشتركًا ÙÙŠ هذه القائمة." msgid "The user you are trying to remove from the list is not a member." msgstr "المستخدم الذي تحاول إزالته من القائمة ليس عضوًا" msgid "Account update failed: value is too long." msgstr "ÙØ´Ù„ تحديث الحساب:القيمة المدخلة طويلة جدًا" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "لا يمكنك إرسال رسائل إلى المستخدمين الّذÙين لا يتابعونك " msgid "There was an error sending your message." msgstr "حدث خطأ أثناء إرسال رسالتك" msgid "You've already requested to follow this user." msgstr "لقد قمت Ø¨Ø§Ù„ÙØ¹Ù„ بطلب متابعة هذا المستخدم" msgid "You are unable to follow more people at this time" msgstr "لا يمكنك متابعة المزيد من الأشخاص ÙÙŠ الوقت الحالي" msgid "Sorry, you are not authorized to see this status" msgstr "عذرا، غير مصرح لك برؤية هذه الحالة " msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "يجب أن تكون التغريدة أقصر قليلاً." msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "يجب أن يسمح المستخدم باستقبال الرسائل الخاصة من أي شخص" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "لا يسمح بإستخدام الصور المتحركة GIFs عند تحميل صور متعددة" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "لقد قمت Ø¨Ø§Ù„ÙØ¹Ù„ بإعادة تغريد هذه التغريدة." msgid "You cannot send messages to this user." msgstr "لا يمكنك إرسال الرسائل لهذا المستخدم." msgid "The text of your direct message is over the max character limit." msgstr "النص المكتوب ÙÙŠ الرسالة تعدى الحد المسموح لعدد الأحرÙ." msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "الآن" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dدقيقة" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dساعة" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "الرد على %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "الرد على %s Ùˆ %d أخرى" msgstr[1] "الرد على %s Ùˆ %d أخرى" msgstr[2] "الرد على %s Ùˆ %d أخرى" msgstr[3] "الرد على %s Ùˆ %d أخرى" msgstr[4] "الرد على %s Ùˆ %d أخرى" msgstr[5] "الرد على %s Ùˆ %d أخرى" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "الرد على %s Ùˆ %s" msgid "Don't have a Twitter account yet?" msgstr "ليس لديك حساب تويتر حتى الآن؟" msgid "Create one" msgstr "أنشئ واحدًا" #, c-format msgid "Could not open %s" msgstr "تعذّر ÙØªØ­ %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "قن خاطئ" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "الحساب قيد الاستخدام" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "اختيار صورة" msgid "Unfollow" msgstr "إلغاء Ø§Ù„ØªØªØ¨ÙØ¹" msgid "Follow" msgstr "تَتبٌع" msgid "Loading…" msgstr "…يحمّل" #, fuzzy msgid "No entries found" msgstr "لم يتم العثور على مستخدمين" msgid "Retry" msgstr "إعادة المحاولة" msgid "Copy URL" msgstr "نسخ الرابط" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Ø­ÙØ¸ باسم..." msgid "Save Video" msgstr "Ø§Ø­ÙØ¸ الÙيديو" msgid "Save Image" msgstr "Ø­ÙØ¸ الصورة" msgid "Save" msgstr "Ø§Ø­ÙØ¸" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" msgid "Pick" msgstr "اختيار" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "عودة" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "إقتباس التغريدة" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "يسمح بإستخدام GIF واحد Ùقط للتغريدة." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgstr[5] "" msgid "Modify Filter" msgstr "تعديل الÙلتر" msgid "Matches" msgstr "يطابق" msgid "Doesn't match" msgstr "لا يتطابق" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "Ø§Ø¶Ø§ÙØ© إلى أو حذ٠المستخدم من القائمة" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "حول كوربرد" msgid "New Account" msgstr "حساب جديد" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "لمصادقة Cawbird ØŒ تحتاج إلى رقم تعري٠شخصي PIN من twitter.com بالحساب الذي " "ترغب ÙÙŠ Ø¥Ø¶Ø§ÙØªÙ‡" msgid "Request PIN" msgstr "طلب PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "تأكيد" msgid "Account Settings" msgstr "إعدادت الحساب" msgid "Name" msgstr "الإسم" msgid "Website" msgstr "الموقع الكتروني" msgid "Autostart" msgstr "بدء تلقائي" #, fuzzy msgid "Remove Account" msgstr "حذÙ" #, fuzzy msgid "Do you really want to remove this account?" msgstr "هل حقا تريد حذ٠هذا الحساب؟" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "الطبيعة Ùˆ الحيوانات" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "الاماكن Ùˆ Ø§Ù„Ø³ÙØ± " msgctxt "emoji category" msgid "Activities" msgstr "النشاطات" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "لم يتم العثور على نتيجة" msgid "Try a different search" msgstr "" msgid "Send" msgstr "إرسال" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "اظهر الصور Ø§Ù„Ù…ÙØ¶Ù„Ø©" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "الÙلاتر " #, fuzzy msgid "Filtered users" msgstr "الÙلاتر " msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "المستخدمين" #, fuzzy msgid "Set image description" msgstr "الوصÙ" msgid "Subscribe" msgstr "الإشتراك" msgid "Unsubscribe" msgstr "إلغاء الإشتراك" msgid "Subscribers:" msgstr "المشتركين" msgid "Members:" msgstr "الأعضاء" msgid "Creator:" msgstr "المنشئ" msgid "Created at:" msgstr "أنشئ ÙÙŠ" msgid "Edit" msgstr "تعديل" msgid "Mode:" msgstr "الوضع" msgid "Description" msgstr "الوصÙ" msgid "Settings" msgstr "الإعدادات" msgid "Shortcuts" msgstr "اختصارات" msgid "About" msgstr "حول" msgid "Quit" msgstr "الجروج" msgid "Add New Filter" msgstr "Ø§Ø¶Ø§ÙØ© Ùلتر جديد" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "الكلمة الدالة " msgid "Replacement" msgstr "" msgid "Create New List" msgstr "إنشاء قائمة جديدة" msgid "Name:" msgstr "الإسم" msgid "Create" msgstr "إنشاء" msgid "Write Direct Message" msgstr "كتابت رسالة مباشرة" msgid "Add to/Remove from List" msgstr "Ø§Ù„Ø¥Ø¶Ø§ÙØ© أو الحذ٠من القائمة" msgid "Blocked" msgstr "محضور" msgid "Muted" msgstr "مكتوم" msgid "Retweets disabled" msgstr "تعطيل إعادات التغريد" #, fuzzy msgid "More actions" msgstr "الإشارات" msgid "Follows you" msgstr "يتَتبعك" msgid "Tweets" msgstr "تغريدات" msgid "Followers" msgstr "المتتبعون" msgid "Following" msgstr "يتتبع" msgid "Use dark theme" msgstr "إستخدام النمط المظلم" #, fuzzy msgid "Shortcut key" msgstr "اختصارات" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Ø¥Ø®ÙØ§Ø¡ ÙÙŠ الخط الزمني" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "العرض دائما" msgid "Always hide" msgstr "Ø¥Ø®ÙØ§Ø¡ دائما" msgid "Hide in timeline" msgstr "Ø¥Ø®ÙØ§Ø¡ ÙÙŠ الخط الزمني" msgid "Auto scroll on new tweets" msgstr "التمرير التلقائي لالتغريدات الجديدة" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "الإشعارات" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "أبدا" msgid "Every" msgstr "كل" msgid "Stack 5" msgstr "تجميع 5" msgid "Stack 10" msgstr "تجميع 10" msgid "Stack 25" msgstr "تجميع 25" msgid "Stack 50" msgstr "تجميع 50" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "الواجهة" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "تغريدات" msgid "Round avatars" msgstr "صور رمزية مستديرة" msgid "Remove trailing hashtags" msgstr "إزالة الهاشتقات الزائدة" msgid "Remove media links" msgstr "إزالة روابط الوسائط" msgid "Hide inappropriate content" msgstr "اخÙÙŠ المحتوى الغير لائق" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "القصاصات" msgctxt "shortcuts window" msgid "General" msgstr "عام" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "عرض اعدادات الحساب" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "عرض إعدادات التطبيق" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "عد للخلÙ" msgctxt "shortcuts window" msgid "Go Forward" msgstr "إلى الأمام" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "تغريدات" msgctxt "shortcuts window" msgid "Retweet" msgstr "إعادة التغريد" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Ù…ÙØ¶Ù„Ø©" msgctxt "shortcuts window" msgid "Reply" msgstr "رد" msgctxt "shortcuts window" msgid "Quote" msgstr "إقتباس" msgctxt "shortcuts window" msgid "Show Details" msgstr "عرض Ø§Ù„ØªÙØ§ØµÙŠÙ„" msgctxt "shortcuts window" msgid "Delete" msgstr "حذÙ" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "عرض منتقي الرموز التعبيرية" msgid "Start new conversation" msgstr "بدأ محادثة جديدة" msgid "With:" msgstr "مع" msgid "Go" msgstr "التحدث" msgid "Quote" msgstr "إقتباس" msgid "Retweet tweet" msgstr "إعادت التغريد" #, fuzzy msgid "Like tweet" msgstr "ØªÙØ¶ÙŠÙ„ التغريدة" msgid "Reply to tweet" msgstr "الرد على التغريدات" msgid "More" msgstr "المزيد" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Ù…ÙØ¶Ù„Ø©" msgid "Reply" msgstr "رد" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "إعادة التغريد" msgid "Unblock" msgstr "ÙÙƒ الحظر" msgid "Show settings of this account" msgstr "عرض الإعدادات لهذا الحساب" msgid "Open in new window" msgstr "عرض ÙÙŠ Ù†Ø§ÙØ°Ù‡ جديدة" msgid "Go to profile" msgstr "عرض المل٠الشخصي" msgid "Created" msgstr "تم الإنشاء" msgid "Subscribed to" msgstr "مشترك ÙÙŠ" #~ msgid "Replying to" #~ msgstr "الرد على" #~ msgid "and" #~ msgstr "Ùˆ" #~ msgid "Actions" #~ msgstr "العمليات" #~ msgid "Add Image" #~ msgstr "Ø¥Ø¶Ø§ÙØ© صورة " #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "المتتبعون" #~ msgid "List" #~ msgstr "قائمة" cawbird-1.4.2/po/ast.po000066400000000000000000000614631416632607600147230ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # enolp , 2014-2017 # Xuacu Saturio , 2015 # Ḷḷumex03 , 2014 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2018-01-09 20:56+0000\n" "Last-Translator: Xuacu Saturio \n" "Language-Team: Asturian (http://www.transifex.com/cawbird/cawbird/language/" "ast/)\n" "Language: ast\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Veceru pa Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird ye un veceru nativu pa Twitter en GTK+ qu'apurre carauterístiques " "vitales como mensaxes direutos (DM), notificaciones de tuits, visión de " "conversaciones." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Carauterístiques adicionales qu'inclúin visión llocal de videos, imáxenes en " "llinia múltiples, llistes, filtros, cuentes múltiples, etc." msgid "Generic timeline view when using Cawbird" msgstr "Vista xenérica de llinia temporal al usar Cawbird" msgid "Typical Twitter profile" msgstr "Perfil típicu de Twitter" msgid "Account settings can be configured" msgstr "Puen configurase los axustes de cuentes" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Usa Twitter dende una aplicación d'escritoriu normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Amosar cuentes configuraes" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensaxes direutos" #, fuzzy msgid "Select Media" msgstr "Esbillar imaxe" msgid "Open" msgstr "Abrir" msgid "Cancel" msgstr "Encaboxar" #, fuzzy msgid "Selected file is not an image or video." msgstr "El ficheru seleicionáu nun ye una imaxe" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "La imaxe seleicionada ye demasiao grande. El tamañu máximu per imaxe ye de " "%'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "La imaxe seleicionada ye demasiao grande. El tamañu máximu per imaxe ye de " "%'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "La imaxe seleicionada ye demasiao grande. El tamañu máximu per imaxe ye de " "%'d MB" msgid "Insert Emoji" msgstr "Inxertar Emoji" msgid "Direct Conversation" msgstr "Conversación direuta" #, fuzzy msgid "Direct message threads" msgstr "Mensaxes direutos" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d mensaxe nuevu de %s" msgstr[1] "%d mensaxes nuevos de %s" #, c-format msgid "New direct message from %s" msgstr "Mensaxe direutu nuevu de %s" msgid "Direct Messages" msgstr "Mensaxes direutos" #, fuzzy msgid "Liked tweets timeline" msgstr "Conseñar tuit como favoritu" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "Amestar peñera nueva" msgid "Filters" msgstr "Peñeres" #, fuzzy msgid "Home timeline" msgstr "Anubrir na llinia temporal" #, c-format msgid "%s retweeted %s" msgstr "%s retuiteó %s" #, c-format msgid "%s tweeted" msgstr "%s tuiteó" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "¡%d tuit nuevu!" msgstr[1] "¡%d tuits nuevos!" msgid "Home" msgstr "Aniciu" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tuiteó" msgid "Private" msgstr "Priváu" msgid "Public" msgstr "Públicu" msgid "Lists" msgstr "Llistes" msgid "Show configured accounts" msgstr "Amosar cuentes configuraes" msgid "Compose Tweet" msgstr "Componer tuit" msgid "Add new Account" msgstr "Amestar cuenta nueva" #, fuzzy msgid "Mentions timeline" msgstr "Anubrir na llinia temporal" #, c-format msgid "%s mentioned %s" msgstr "%s mentó %s" msgid "Mentions" msgstr "Menciones" msgid "Suspended Account" msgstr "Cuenta suspendida" msgid "Protected profile" msgstr "Perfil protexíu" #, c-format msgid "Tweet to @%s" msgstr "Tuitear a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tuiteó" msgstr[1] "%s tuiteó" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Siguiendo" msgstr[1] "Siguiendo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificaciones" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Anubrir na llinia temporal" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Siguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Siguiendo" msgid "Protected Profile" msgstr "Perfil protexíu" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Siguidores" msgstr[1] "Siguidores" #, fuzzy msgid "No tweets found" msgstr "Nun s'alcontraron entraes" msgid "No users found" msgstr "Nun s'alcontraron usuarios" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tuitear a @%s" msgid "Search" msgstr "Guetar" msgid "Load More" msgstr "Cargar más" msgid "Could not show tweet" msgstr "Nun pudo amosase'l tuit" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retuits" msgid "Open in Browser" msgstr "Abrir nel restolador" msgid "Source" msgstr "Fonte" msgid "Tweet Details" msgstr "Detalles del tuit" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ensin lleer)" msgstr[1] "(%d ensin lleer)" msgid "Delete" msgstr "Desaniciar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retuit" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "¿De xuru que quies desaniciar esta cuenta?" #, c-format msgid "Block %s" msgstr "Bloquiar a %s" msgid "This tweet contains images marked as inappropriate" msgstr "Esti tuit contién imáxenes marcaes como inapropiaes" msgid "Show anyway" msgstr "Amosar de toes toes" #, fuzzy msgid "Could not authenticate you" msgstr "Nun pudo abrise %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Nun s'alcontraron usuarios" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfaz" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "El fragmentu yá existe" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Agora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Respondiendo a %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Respondiendo a %s y %d más" msgstr[1] "Respondiendo a %s y %d más" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Respondiendo a %s y %s" msgid "Don't have a Twitter account yet?" msgstr "¿Entá nun tienes una cuenta de Twitter?" msgid "Create one" msgstr "Crea una" #, c-format msgid "Could not open %s" msgstr "Nun pudo abrise %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN incorreutu" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "La cuenta yá ta n'usu" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Esbillar imaxe" msgid "Unfollow" msgstr "Dexar de siguir" msgid "Follow" msgstr "Siguir" msgid "Loading…" msgstr "Cargando..." #, fuzzy msgid "No entries found" msgstr "Nun s'alcontraron usuarios" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "Copiar URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Guardar como..." msgid "Save Video" msgstr "Guardar videu" msgid "Save Image" msgstr "Guardar imaxe" msgid "Save" msgstr "Guardar" msgid "Select Banner Image" msgstr "Seleiciona la imaxe del cartel" msgid "Image does not meet minimum size requirements:" msgstr "La imaxe nun tien el tamañu mínimu riquíu:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Anchor mínimu: %d pixel" msgstr[1] "Anchor mínimu: %d pixels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Altor mínimu: %d pixel" msgstr[1] "Altor mínimu: %d pixels" msgid "Pick" msgstr "Escoyer" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Atrás" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar tuit" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Sólo se permite un ficheru GIF por tweet" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar filtru" msgid "Matches" msgstr "Coincidencies" msgid "Doesn't match" msgstr "Nun casa" msgid "Modify Snippet" msgstr "Editar fragmentu" msgid "Snippet can't be empty" msgstr "El fragmentu nun puede tar vaciu" msgid "Replacement can't be empty" msgstr "El reemplazu nun puede tar vaciu" msgid "Snippet may not contain whitespace" msgstr "El fragmentu nun puede contener espacios en blancu" msgid "Snippet already exists" msgstr "El fragmentu yá existe" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Manín, comprueba esta versión nueva de #Cawbird \\ (•◡•) / #Perbién " "#LoNuevoSiempresYeMeyor" msgid "Add to or Remove User From List" msgstr "Amestar o desaniciar usuariu de la llista" msgid "You have no lists." msgstr "Nun tienes llistes." msgid "About Cawbird" msgstr "Tocante a Cawbird" msgid "New Account" msgstr "Cuenta nueva" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "P'autenticar Cawbird,necesites un PIN de twitter.com cola cuenta que quiés " "añadir" msgid "Request PIN" msgstr "Pidir PIN" msgid "Enter PIN from twitter.com below:" msgstr "Escribe abaxo'l PIN de twitter.com:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Axustes de la cuenta" msgid "Name" msgstr "Nome" msgid "Website" msgstr "Sitiu web" msgid "Autostart" msgstr "Aniciu automáticu" #, fuzzy msgid "Remove Account" msgstr "Desaniciar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "¿De xuru que quies desaniciar esta cuenta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Fustaxes y xente" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Cuerpu y vistíos" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animales y natura" msgctxt "emoji category" msgid "Food & Drink" msgstr "Comida y bébora" msgctxt "emoji category" msgid "Travel & Places" msgstr "Viaxes y llugares" msgctxt "emoji category" msgid "Activities" msgstr "Actividaes" msgctxt "emoji category" msgid "Objects" msgstr "Oxetos" msgctxt "emoji category" msgid "Symbols" msgstr "Símbolos" msgctxt "emoji category" msgid "Flags" msgstr "Banderes" msgid "No Results Found" msgstr "Nun s'alcontraron resultaos" msgid "Try a different search" msgstr "Tenta una gueta distinta" msgid "Send" msgstr "Unviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Ver imaxes favorites" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Peñeres" #, fuzzy msgid "Filtered users" msgstr "Peñeres" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuarios" #, fuzzy msgid "Set image description" msgstr "Descripción" msgid "Subscribe" msgstr "Soscribise" msgid "Unsubscribe" msgstr "Esborrase" msgid "Subscribers:" msgstr "Soscriptores:" msgid "Members:" msgstr "Miembros:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creáu a les:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Mou:" msgid "Description" msgstr "Descripción" msgid "Settings" msgstr "Axustes" msgid "Shortcuts" msgstr "Atayos" msgid "About" msgstr "Tocante a" msgid "Quit" msgstr "Colar" msgid "Add New Filter" msgstr "Amestar filtru nuevu" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Añadir fragmentu nuevu" msgid "Keyword" msgstr "Pallabra clave" msgid "Replacement" msgstr "Troquéu" msgid "Create New List" msgstr "Crear llista nueva" msgid "Name:" msgstr "Nome:" msgid "Create" msgstr "Crear" msgid "Write Direct Message" msgstr "Escribir mensaxe direutu nuevu" msgid "Add to/Remove from List" msgstr "Amestar a/Desaniciar de la llista" msgid "Blocked" msgstr "Bloquiáu" msgid "Muted" msgstr "Slienciáu" msgid "Retweets disabled" msgstr "Retuits deshabilitaos" #, fuzzy msgid "More actions" msgstr "Menciones" msgid "Follows you" msgstr "Síguete" msgid "Tweets" msgstr "Tuits" msgid "Followers" msgstr "Siguidores" msgid "Following" msgstr "Siguiendo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atayos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Anubrir na llinia temporal" msgid "Show inline media" msgstr "Ver multimedia en llinia" msgid "Always show" msgstr "Amosar siempres" msgid "Always hide" msgstr "Anubrir siempres" msgid "Hide in timeline" msgstr "Anubrir na llinia temporal" msgid "Auto scroll on new tweets" msgstr "Desplazamientu automáticu en tuits nuevos" msgid "Double-click activation" msgstr "Activación con clic doblu" msgid "Notifications" msgstr "Notificaciones" msgid "On New Tweets" msgstr "En tuits nuevos" msgid "Never" msgstr "Enxamás" msgid "Every" msgstr "Cada" msgid "Stack 5" msgstr "Pila de 5" msgid "Stack 10" msgstr "Pila de 10" msgid "Stack 25" msgstr "Pila de 25" msgid "Stack 50" msgstr "Pila de 50" msgid "On New Mentions" msgstr "En menciones nueves" msgid "On New Messages" msgstr "En mensaxes nuevos" msgid "Interface" msgstr "Interfaz" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tuits" msgid "Round avatars" msgstr "Avatares arredondiaos" msgid "Remove trailing hashtags" msgstr "Desaniciar les etiquetes del final" msgid "Remove media links" msgstr "Desaniciar los enllaces a multimedia" msgid "Hide inappropriate content" msgstr "Anubrir conteníu inapropiáu" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nun hai fragmentos configuraos." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Puedes activar los fragmentos escribiendo la clave y primiendo TAB." msgid "Snippets" msgstr "Fragmentos" msgctxt "shortcuts window" msgid "General" msgstr "Xeneral" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Componer tuit" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Ver les preferencies de la cuenta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Ver la ventana de cuentes" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Ver les preferencies de la aplicación" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Alternar barra cimera" msgctxt "shortcuts window" msgid "Go Back" msgstr "Dir atrás" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Dir alantre" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Dir a la nª páxina" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tuits" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retuit" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favoritu" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "Citar" msgctxt "shortcuts window" msgid "Show Details" msgstr "Amosar detalles" msgctxt "shortcuts window" msgid "Delete" msgstr "Desaniciar" msgctxt "shortcuts window" msgid "Compose" msgstr "Componer" msgid "Show Emoji Chooser" msgstr "Ver selector d'emoji" msgid "Start new conversation" msgstr "Entamar conversación nueva" msgid "With:" msgstr "Con:" msgid "Go" msgstr "Dir" msgid "Quote" msgstr "Citar" msgid "Retweet tweet" msgstr "Retuitear tuit" #, fuzzy msgid "Like tweet" msgstr "Conseñar tuit como favoritu" msgid "Reply to tweet" msgstr "Responder al tuit" msgid "More" msgstr "Más" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favoritu" msgid "Reply" msgstr "Contestar" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retuit" msgid "Unblock" msgstr "Desbloquiar" msgid "Show settings of this account" msgstr "Amosar los axustes d'esta cuenta" msgid "Open in new window" msgstr "Abrir nuna ventana nueva" msgid "Go to profile" msgstr "Dir al perfil" msgid "Created" msgstr "Creáu" msgid "Subscribed to" msgstr "Soscribise a" #~ msgid "Replying to" #~ msgstr "Respondiendo a" #~ msgid "and" #~ msgstr "y" #~ msgid "Actions" #~ msgstr "Aiciones" #~ msgid "Add Image" #~ msgstr "Amestar imaxe" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Siguidores" #~ msgid "List" #~ msgstr "Llista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Nun autorizáu. La mayor parte de les veces, esto significa que daqué " #~ "funciona mal nos sirvidores de Twitter y tendríes de tentar sero" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Nun pudieron cargase los tuits" cawbird-1.4.2/po/ca.po000066400000000000000000000617151416632607600145170ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # Estanislau Trepat , 2015 # Guillem Arias , 2016 # Pau Iranzo , 2014 # Sergi , 2015 # Joan Ensesa , 2020 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Joan Ensesa , 2020\n" "Language-Team: Catalan (https://www.transifex.com/cawbird/teams/107135/ca/)\n" "Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Client del Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "El Cawbird és un client del twitter natiu GTK+ que proveeix característiques " "bàsiques com missatges directes, notificacions de piulades i visualització " "de converses. " msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Característiques addicionals inclouen vista local de vídeos, múltiples " "imatges inline, Llistes, Filtres, múltiples comptes, etc." msgid "Generic timeline view when using Cawbird" msgstr "Timeline genèrica al utilitzar Cawbird" msgid "Typical Twitter profile" msgstr "Perfil de Twitter tipic" msgid "Account settings can be configured" msgstr "Els paràmetres del compte es poden configurar" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Utilitza Twitter en una aplicació d'escriptori normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostra els comptes configurats" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Missatges directes" #, fuzzy msgid "Select Media" msgstr "Selecciona la imatge" msgid "Open" msgstr "Obre" msgid "Cancel" msgstr "Cancel·la" #, fuzzy msgid "Selected file is not an image or video." msgstr "El fitxer seleccionat no és una imatge." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "La imatge seleccionada és massa gran. La mida máxima per imatge és de %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "La imatge seleccionada és massa gran. La mida máxima per imatge és de %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "La imatge seleccionada és massa gran. La mida máxima per imatge és de %'d MB" msgid "Insert Emoji" msgstr "Insereix Emoji" msgid "Direct Conversation" msgstr "Conversa directa" #, fuzzy msgid "Direct message threads" msgstr "Missatges directes" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nou missatge de %s" msgstr[1] "%d nous missatges de %s" #, c-format msgid "New direct message from %s" msgstr "Hi ha un missatge directe nou de %s" msgid "Direct Messages" msgstr "Missatges directes" #, fuzzy msgid "Liked tweets timeline" msgstr "Marca com a preferit" #, fuzzy msgid "Likes" msgstr "Preferits" msgid "Add new Filter" msgstr "Afegir un nou Filtre" msgid "Filters" msgstr "Filtres" #, fuzzy msgid "Home timeline" msgstr "Amaga-ho al timeline" #, c-format msgid "%s retweeted %s" msgstr "%s ha repiulat %s" #, c-format msgid "%s tweeted" msgstr "%s ha repiulat" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nou tweet!" msgstr[1] "%d noves piulades!" msgid "Home" msgstr "Inici" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s ha repiulat" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Públic" msgid "Lists" msgstr "Llistes" msgid "Show configured accounts" msgstr "Mostra els comptes configurats" msgid "Compose Tweet" msgstr "Redacteu una piulada" msgid "Add new Account" msgstr "Afegiu un compte nou" #, fuzzy msgid "Mentions timeline" msgstr "Amaga-ho al timeline" #, c-format msgid "%s mentioned %s" msgstr "%s ha mencionat a %s" msgid "Mentions" msgstr "Mencions" msgid "Suspended Account" msgstr "Compte suspès" msgid "Protected profile" msgstr "Perfil protegit" #, c-format msgid "Tweet to @%s" msgstr "Piula a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s ha repiulat" msgstr[1] "%s ha repiulat" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seguint" msgstr[1] "Seguint" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificacions" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Amaga-ho al timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidors" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seguint" msgid "Protected Profile" msgstr "Perfil protegit" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidors" msgstr[1] "Seguidors" #, fuzzy msgid "No tweets found" msgstr "No s'ha trobat cap resultat" msgid "No users found" msgstr "No s'han trobat usuaris" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Piula a @%s" msgid "Search" msgstr "Cerca" msgid "Load More" msgstr "Carrega'n més" msgid "Could not show tweet" msgstr "No s'ha pogut mostrar la piulada" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Repiulades" msgid "Open in Browser" msgstr "Obre al navegador" msgid "Source" msgstr "Origen" msgid "Tweet Details" msgstr "Detalls de la piulada" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d missatge no llegit)" msgstr[1] "(%d missatges no llegits)" msgid "Delete" msgstr "Suprimeix" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Repiular" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Vols eliminar aquest compte?" #, c-format msgid "Block %s" msgstr "Bloqueja a %s" msgid "This tweet contains images marked as inappropriate" msgstr "Aquest tweet conté imatges marcades com a inapropiades" msgid "Show anyway" msgstr "Mostra de totes maneres" #, fuzzy msgid "Could not authenticate you" msgstr "No s'ha pogut obrir %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "No s'han trobat usuaris" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfície" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "El Snippet ja existeix" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ara" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Respon a %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Respon a %s i a %d més" msgstr[1] "Respon a %s i a %d més" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Respon a %s i a %s" msgid "Don't have a Twitter account yet?" msgstr "Encara no tens un compte a Twitter?" msgid "Create one" msgstr "Creeu-ne un" #, c-format msgid "Could not open %s" msgstr "No s'ha pogut obrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN incorrecte" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Ja s'està utilitzant el compte" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Selecciona la imatge" msgid "Unfollow" msgstr "Deixa de seguir" msgid "Follow" msgstr "Segueix" msgid "Loading…" msgstr "S'està carregant..." #, fuzzy msgid "No entries found" msgstr "No s'han trobat usuaris" msgid "Retry" msgstr "Prova de nou" msgid "Copy URL" msgstr "Copia l'URL" #, fuzzy msgid "Reload image" msgstr "No s'ha pogut carregar la imatge" msgid "Save as…" msgstr "Anomena i desa..." msgid "Save Video" msgstr "Desa el video" msgid "Save Image" msgstr "Desa la imatge" msgid "Save" msgstr "Desa" msgid "Select Banner Image" msgstr "Selecciona la imatge del bàner" msgid "Image does not meet minimum size requirements:" msgstr "La imatge no té la mida mínima necessària:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Amplada mínima: %d píxel" msgstr[1] "Amplada mínima: %d píxels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Alçada mínima: %d píxel" msgstr[1] "Alçada mínima: %d píxels" msgid "Pick" msgstr "Tria" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "No s'ha pogut carregar la imatge" msgstr[1] "No s'han pogut carregar %u imatges" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Enrere" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Cita la piulada" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Només es permet un fitxer GIF per cada piulada." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modifica el filtre" msgid "Matches" msgstr "Coincideix" msgid "Doesn't match" msgstr "Sense coincidència" msgid "Modify Snippet" msgstr "Modificar Snippet" msgid "Snippet can't be empty" msgstr "El Snippet no pot estar buit" msgid "Replacement can't be empty" msgstr "El reemplaçament no pot estar buit." msgid "Snippet may not contain whitespace" msgstr "El Snippet no ha de contenir espai en blanc" msgid "Snippet already exists" msgstr "El Snippet ja existeix" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Ei, prova aquesta nova versió de #Cawbird! (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Afegeix o elimina un usuari de la llista" msgid "You have no lists." msgstr "No tens cap llista" msgid "About Cawbird" msgstr "Quant al Cawbird" msgid "New Account" msgstr "Compte nou" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Per autenticar el Cawbird, necessites un PIN de twitter.com pel compte que " "vols afegir" msgid "Request PIN" msgstr "Sol·licita un PIN" msgid "Enter PIN from twitter.com below:" msgstr "Introdueix a sota el PIN de twitter.com:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirma" msgid "Account Settings" msgstr "Paràmetres del compte" msgid "Name" msgstr "Nom" msgid "Website" msgstr "Lloc web" msgid "Autostart" msgstr "Inici automàtic" #, fuzzy msgid "Remove Account" msgstr "Suprimeix" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Vols eliminar aquest compte?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Emoticones & Gent" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Cos & Roba" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animals & Natura" msgctxt "emoji category" msgid "Food & Drink" msgstr "Menjar & Beure" msgctxt "emoji category" msgid "Travel & Places" msgstr "Viatges & Llocs" msgctxt "emoji category" msgid "Activities" msgstr "Activitats" msgctxt "emoji category" msgid "Objects" msgstr "Objectes" msgctxt "emoji category" msgid "Symbols" msgstr "Símbols" msgctxt "emoji category" msgid "Flags" msgstr "Banderes" msgid "No Results Found" msgstr "No s'han trobat resultats" msgid "Try a different search" msgstr "Prova una altra cerca" msgid "Send" msgstr "Envia" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Mostra les imatges preferides" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtres" #, fuzzy msgid "Filtered users" msgstr "Filtres" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuaris" #, fuzzy msgid "Set image description" msgstr "Descripció" msgid "Subscribe" msgstr "Subscriu-te" msgid "Unsubscribe" msgstr "Cancel·la la subscripció" msgid "Subscribers:" msgstr "Subscriptors:" msgid "Members:" msgstr "Membres:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creat el:" msgid "Edit" msgstr "Edita" msgid "Mode:" msgstr "Mode:" msgid "Description" msgstr "Descripció" msgid "Settings" msgstr "Paràmetres" msgid "Shortcuts" msgstr "Accessos directes" msgid "About" msgstr "Quant a" msgid "Quit" msgstr "Surt" msgid "Add New Filter" msgstr "Afegeix un filtre nou" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Afegeix un nou Snippet" msgid "Keyword" msgstr "Paraula clau" msgid "Replacement" msgstr "Reemplaçament" msgid "Create New List" msgstr "Crea una llista nova" msgid "Name:" msgstr "Nom:" msgid "Create" msgstr "Crea" msgid "Write Direct Message" msgstr "Escriu un missatge directe" msgid "Add to/Remove from List" msgstr "Afegeix/Elimina de la llista" msgid "Blocked" msgstr "Blocat" msgid "Muted" msgstr "Silenciat" msgid "Retweets disabled" msgstr "S'han desactivat les repiulades" #, fuzzy msgid "More actions" msgstr "Mencions" msgid "Follows you" msgstr "Us segueix" msgid "Tweets" msgstr "Piulades" msgid "Followers" msgstr "Seguidors" msgid "Following" msgstr "Seguint" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Accessos directes" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Amaga-ho al timeline" msgid "Show inline media" msgstr "Mostrar contingut inline" msgid "Always show" msgstr "Mostra sempre" msgid "Always hide" msgstr "Amaga sempre" msgid "Hide in timeline" msgstr "Amaga-ho al timeline" msgid "Auto scroll on new tweets" msgstr "Desplaça automàticament amb piulades noves" msgid "Double-click activation" msgstr "Activació de doble clic" msgid "Notifications" msgstr "Notificacions" msgid "On New Tweets" msgstr "Quan hi hagi piulades noves" msgid "Never" msgstr "Mai" msgid "Every" msgstr "Tots els" msgid "Stack 5" msgstr "Stack 5" msgid "Stack 10" msgstr "Stack 10" msgid "Stack 25" msgstr "Stack 25" msgid "Stack 50" msgstr "Stack 50" msgid "On New Mentions" msgstr "Quan hi hagi mencions noves" msgid "On New Messages" msgstr "Quan hi hagi missatges nous" msgid "Interface" msgstr "Interfície" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Piulades" msgid "Round avatars" msgstr "Avatars arrodonits" msgid "Remove trailing hashtags" msgstr "Elimina els hashtags posteriors" msgid "Remove media links" msgstr "Elimina els enllaços" msgid "Hide inappropriate content" msgstr "Amaga el contingut inapropiat" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "No s'han configurat els snippets" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Pots activar els snippets escrivint la paraula clau i pressionant TAB" msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "General" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Escriu una piulada" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostra els paràmetres del compte" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostra la finestra dels comptes" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostra els paràmetres de l'aplicació" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Canvia Topbar" msgctxt "shortcuts window" msgid "Go Back" msgstr "Enrere" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Endavant" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Vés a la pàgina #" msgctxt "shortcuts window" msgid "Tweets" msgstr "Piulades" msgctxt "shortcuts window" msgid "Retweet" msgstr "Repiular" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "M'agrada" msgctxt "shortcuts window" msgid "Reply" msgstr "Respon" msgctxt "shortcuts window" msgid "Quote" msgstr "Cita" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostra els detalls" msgctxt "shortcuts window" msgid "Delete" msgstr "Elimina" msgctxt "shortcuts window" msgid "Compose" msgstr "Redacta" msgid "Show Emoji Chooser" msgstr "Tria Emoji" msgid "Start new conversation" msgstr "Inicia una conversa nova" msgid "With:" msgstr "Amb:" msgid "Go" msgstr "Vés" msgid "Quote" msgstr "Cita" msgid "Retweet tweet" msgstr "Repiula la piulada" #, fuzzy msgid "Like tweet" msgstr "Marca com a preferit" msgid "Reply to tweet" msgstr "Respon a la piulada" msgid "More" msgstr "Més" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorit" msgid "Reply" msgstr "Contesta" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Repiular" msgid "Unblock" msgstr "Desbloqueja " msgid "Show settings of this account" msgstr "Mostra els paràmetres per a aquest compte" msgid "Open in new window" msgstr "Obre a una finestra nova" msgid "Go to profile" msgstr "Vés al perfil" msgid "Created" msgstr "Creat" msgid "Subscribed to" msgstr "Subscrit a" #~ msgid "Replying to" #~ msgstr "Respon a" #~ msgid "and" #~ msgstr "i" #~ msgid "Actions" #~ msgstr "Accions" #~ msgid "Add Image" #~ msgstr "Afegeix una imatge" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidors" #~ msgid "List" #~ msgstr "Llista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Sense autorització. La majoria de vegades això vol dir que els servidors " #~ "de Twitter tenen problemes i ho hauries de tornar a intentar més tard." cawbird-1.4.2/po/ca@valencia.po000066400000000000000000000523121416632607600163130ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Pau Iranzo , 2014 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Catalan (Valencian) (http://www.transifex.com/cawbird/cawbird/" "language/ca%40valencia/)\n" "Language: ca@valencia\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Client del Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "El Cawbird és un client del twitter natiu GTK+ que proveeix característiques " "bàsiques com missatges directes, notificacions de piulades i visualització " "de converses. " msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostra els comptes configurats" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Missatges directes" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Cancel·la" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversa directa" #, fuzzy msgid "Direct message threads" msgstr "Missatges directes" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" msgstr[1] "" #, c-format msgid "New direct message from %s" msgstr "Hi ha un missatge directe nou de %s" msgid "Direct Messages" msgstr "Missatges directes" #, fuzzy msgid "Liked tweets timeline" msgstr "Marca com a preferit" #, fuzzy msgid "Likes" msgstr "Preferits" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s ha repiulat %s" #, c-format msgid "%s tweeted" msgstr "%s ha repiulat" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgstr[1] "" msgid "Home" msgstr "Inici" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s ha repiulat" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Públic" msgid "Lists" msgstr "Llistes" msgid "Show configured accounts" msgstr "Mostra els comptes configurats" msgid "Compose Tweet" msgstr "Redacteu una piulada" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "Mencions" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "Mencions" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Perfil protegit" #, c-format msgid "Tweet to @%s" msgstr "Piula a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s ha repiulat" msgstr[1] "%s ha repiulat" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seguint" msgstr[1] "Seguint" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificacions" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Mencions" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidors" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seguint" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidors" msgstr[1] "Seguidors" #, fuzzy msgid "No tweets found" msgstr "No s'ha trobat cap resultat" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Piula a @%s" msgid "Search" msgstr "Cerca" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Repiulades" msgid "Open in Browser" msgstr "Obri al navegador" msgid "Source" msgstr "Origen" msgid "Tweet Details" msgstr "Detalls de la piulada" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgid "Delete" msgstr "Suprimeix" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Repiulades" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Voleu eliminar este compte?" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfície" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ara" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Respon a la piulada" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Creeu-ne un" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN incorrecte" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Ja s'està utilitzant el compte" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Deixa de seguir" msgid "Follow" msgstr "Segueix" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "No s'ha trobat cap resultat" msgid "Retry" msgstr "Prova de nou" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Guarda" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Arrere" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modifica el filtre" msgid "Matches" msgstr "Coincideix" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "Afig o elimina un usuari de la llista" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "Quant al Cawbird" msgid "New Account" msgstr "Compte nou" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Sol·licita un PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirma" msgid "Account Settings" msgstr "Paràmetres del compte" msgid "Name" msgstr "Nom" msgid "Website" msgstr "Lloc web" msgid "Autostart" msgstr "Inici automàtic" #, fuzzy msgid "Remove Account" msgstr "Suprimeix" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Voleu eliminar este compte?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Envia" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuaris" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Subscriviu-vos" msgid "Unsubscribe" msgstr "Cancel·la la subscripció" msgid "Subscribers:" msgstr "Subscriptors:" msgid "Members:" msgstr "Membres:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creat el:" msgid "Edit" msgstr "Edita" msgid "Mode:" msgstr "Mode:" msgid "Description" msgstr "" msgid "Settings" msgstr "Paràmetres" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Quant a" msgid "Quit" msgstr "Ix" msgid "Add New Filter" msgstr "Afig un filtre nou" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "Crea una llista nova" msgid "Name:" msgstr "Nom:" msgid "Create" msgstr "Crea" msgid "Write Direct Message" msgstr "Escriu un missatge directe" msgid "Add to/Remove from List" msgstr "Afig/Elimina de la llista" msgid "Blocked" msgstr "Blocat" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "S'han desactivat les repiulades" #, fuzzy msgid "More actions" msgstr "Mencions" msgid "Follows you" msgstr "Vos segueix" msgid "Tweets" msgstr "Piulades" msgid "Followers" msgstr "Seguidors" msgid "Following" msgstr "Seguint" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Mencions" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Desplaça automàticament amb piulades noves" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "Notificacions" msgid "On New Tweets" msgstr "Quan hi haja piulades noves" msgid "Never" msgstr "Mai" msgid "Every" msgstr "Tots els" msgid "Stack 5" msgstr "Stack 5" msgid "Stack 10" msgstr "Stack 10" msgid "Stack 25" msgstr "Stack 25" msgid "Stack 50" msgstr "Stack 50" msgid "On New Mentions" msgstr "Quan hi haja mencions noves" msgid "On New Messages" msgstr "Quan hi haja missatges nous" msgid "Interface" msgstr "Interfície" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Piulades" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Inicia una conversa nova" msgid "With:" msgstr "Amb:" msgid "Go" msgstr "Vés" msgid "Quote" msgstr "Cita" msgid "Retweet tweet" msgstr "Repiula la piulada" #, fuzzy msgid "Like tweet" msgstr "Marca com a preferit" msgid "Reply to tweet" msgstr "Respon a la piulada" msgid "More" msgstr "Més" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Repiulades" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "Mostra els paràmetres per a este compte" msgid "Open in new window" msgstr "Obri a una finestra nova" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Creat" msgid "Subscribed to" msgstr "Subscrit a" #~ msgid "Actions" #~ msgstr "Accions" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidors" #~ msgid "List" #~ msgstr "Llista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/cawbird.pot000066400000000000000000000456641416632607600157400ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, c-format msgid "Direct messages with %s" msgstr "" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" msgid "Direct message threads" msgstr "" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" msgstr[1] "" #, c-format msgid "New direct message from %s" msgstr "" msgid "Direct Messages" msgstr "" msgid "Liked tweets timeline" msgstr "" msgid "Likes" msgstr "" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "" #, c-format msgid "%s tweeted" msgstr "" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgstr[1] "" msgid "Home" msgstr "" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "" msgid "Private" msgstr "" msgid "Public" msgstr "" msgid "Lists" msgstr "" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "" msgid "Add new Account" msgstr "" msgid "Mentions timeline" msgstr "" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "" #, c-format msgid "Tweet to @%s" msgstr "" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "" msgstr[1] "" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "" msgstr[1] "" #, c-format msgid "Location: %s" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, c-format msgid "%s timeline" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, c-format msgid "%s followers" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, c-format msgid "%s following" msgstr "" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "" msgstr[1] "" msgid "No tweets found" msgstr "" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "" msgid "Open in Browser" msgstr "" msgid "Source" msgstr "" msgid "Tweet Details" msgstr "" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgid "Delete" msgstr "" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "" msgid "Are you sure you want to delete this tweet?" msgstr "" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, c-format msgid "Replying to %s" msgstr "" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "" msgid "Follow" msgstr "" msgid "Loading…" msgstr "" msgid "No entries found" msgstr "" msgid "Retry" msgstr "" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "" msgid "Matches" msgstr "" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "" msgid "New Account" msgstr "" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "" msgid "Account Settings" msgstr "" msgid "Name" msgstr "" msgid "Website" msgstr "" msgid "Autostart" msgstr "" msgid "Remove Account" msgstr "" msgid "Do you really want to remove this account?" msgstr "" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "" msgid "Unsubscribe" msgstr "" msgid "Subscribers:" msgstr "" msgid "Members:" msgstr "" msgid "Creator:" msgstr "" msgid "Created at:" msgstr "" msgid "Edit" msgstr "" msgid "Mode:" msgstr "" msgid "Description" msgstr "" msgid "Settings" msgstr "" msgid "Shortcuts" msgstr "" msgid "About" msgstr "" msgid "Quit" msgstr "" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "" msgid "Name:" msgstr "" msgid "Create" msgstr "" msgid "Write Direct Message" msgstr "" msgid "Add to/Remove from List" msgstr "" msgid "Blocked" msgstr "" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" msgid "More actions" msgstr "" msgid "Follows you" msgstr "" msgid "Tweets" msgstr "" msgid "Followers" msgstr "" msgid "Following" msgstr "" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" msgid "Timelines" msgstr "" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "" msgid "Every" msgstr "" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" msgid "Tweet scale" msgstr "" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "" msgid "Go" msgstr "" msgid "Quote" msgstr "" msgid "Retweet tweet" msgstr "" msgid "Like tweet" msgstr "" msgid "Reply to tweet" msgstr "" msgid "More" msgstr "" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "" msgid "Liked" msgstr "" msgid "Retweeted" msgstr "" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "" msgid "Open in new window" msgstr "" msgid "Go to profile" msgstr "" msgid "Created" msgstr "" msgid "Subscribed to" msgstr "" cawbird-1.4.2/po/da.po000066400000000000000000000651011416632607600145110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # Geert Wirken , 2014 # Nathan Follens, 2017 # Heimen Stoffels , 2014-2015, 2020 # Mads Damgaard Mortensen , 2020-2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Mads Damgaard Mortensen , " "2021\n" "Language-Team: Danish (https://www.transifex.com/cawbird/teams/107135/da/)\n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter klient" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as" " Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird er en GTK+ twitter klient med essentielle funktioner som Direkte " "Beskeder (DMs), tweet notifikationer og samtalevisning" msgid "" "Additional features include local viewing of videos, multiple inline images," " Lists, Filters, multiple accounts, etc." msgstr "" "Yderligere funktioner inkludere: Lokal videoafspilning, Flere indlejrede " "billeder, Lister, Filtre, Flere konti, osv." msgid "Generic timeline view when using Cawbird" msgstr "Generisk tidslinjevisning ved brug af Cawbird" msgid "Typical Twitter profile" msgstr "Typisk Twitter profil" msgid "Account settings can be configured" msgstr "Kontoindstillinger kan konfigureres" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Cawbird i forskellige temaer (Adwaita, Adwaita dark variant, High Contrast " "og Adwaita Dark Green)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Brug Twitter i en desktop applikation" #. TRANSLATORS: Search terms to find this application. Do NOT translate or #. localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "Viser kun 'Forfat tweet' vinduet for den angivne konto, intet andet." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` #. output msgid "account-name" msgstr "Konto-navn" #. TRANSLATORS: Description of the `--start-service` option for the command- #. line msgid "Start service" msgstr "Start service" #. TRANSLATORS: Description of the `--stop-service` option for the command- #. line msgid "Stop service, if it has been started as a service" msgstr "Stop service, hvis den er startet som en service" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the #. command-line msgid "Print configured startup accounts" msgstr "Print konfigureret opstarts konti" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Ã…ben vinduet for den angivne konto" #, c-format msgid "Direct messages with %s" msgstr "Direkte beskeder med %s" msgid "Select Media" msgstr "Vælg Medie" msgid "Open" msgstr "Ã…ben" msgid "Cancel" msgstr "Fortryd" msgid "Selected file is not an image or video." msgstr "Den valgte fil er ikke et billede eller en video." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Den valgte video er for stor. Den maksimale filstørrelse pr. video er %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Det valgte billede er for stort. Den maksimale filstørrelse pr. billede er " "%'d MB" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "Den valgte GIF er for stor, maksimal filstørrelse per GIF er %'d MB" msgid "Insert Emoji" msgstr "Indsæt Emoji" msgid "Direct Conversation" msgstr "Direkte samtale" msgid "Direct message threads" msgstr "Direkte beskedtrÃ¥de" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d ny besked fra %s" msgstr[1] "%d nye beskeder fra %s" #, c-format msgid "New direct message from %s" msgstr "Ny direkte besked fra %s" msgid "Direct Messages" msgstr "Direkte Beskeder" msgid "Liked tweets timeline" msgstr "Favorit tidslinje" msgid "Likes" msgstr "Favoritter" msgid "Add new Filter" msgstr "Nyt Filter" msgid "Filters" msgstr "Filtre" msgid "Home timeline" msgstr "Hjemme tidslinje" #, c-format msgid "%s retweeted %s" msgstr "%s retweetede %s" #, c-format msgid "%s tweeted" msgstr "%s retweetet" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nyt Tweet!" msgstr[1] "%d nye Tweets!" msgid "Home" msgstr "Hjem" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" #. when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "%s trÃ¥ds tweets" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Offentlig" msgid "Lists" msgstr "Lister" msgid "Show configured accounts" msgstr "Vis konti" msgid "Compose Tweet" msgstr "Forfat Tweet" msgid "Add new Account" msgstr "Ny Konto" msgid "Mentions timeline" msgstr "Omtale tidslinje" #, c-format msgid "%s mentioned %s" msgstr "%s omtalte %s" msgid "Mentions" msgstr "Omtaler" msgid "Suspended Account" msgstr "Suspenderet Konto" msgid "Protected profile" msgstr "LÃ¥st profil" #, c-format msgid "Tweet to @%s" msgstr "Tweet til @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d tweets" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Følger %d konto" msgstr[1] "Følger %d konti" #, c-format msgid "Location: %s" msgstr "Position: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile #. timeline view #, c-format msgid "%s timeline" msgstr "%s tidslinje" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users following the user #, c-format msgid "%s followers" msgstr "%s følgere" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users followed by the user #, c-format msgid "%s following" msgstr "%s følger" msgid "Protected Profile" msgstr "LÃ¥st Profil" msgid "User is blocked" msgstr "Bruger er blokeret" msgid "User is muted" msgstr "Bruger er muted" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d følger" msgstr[1] "%d følgere" msgid "No tweets found" msgstr "Ingen tweets fundet" msgid "No users found" msgstr "Ingen brugere fundet" #, c-format msgid "Users matching \"%s\"" msgstr "Brugere som matcher \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweets der matcher \"%s\"" msgid "Search" msgstr "Søg" msgid "Load More" msgstr "Indlæs mere" msgid "Could not show tweet" msgstr "Kunne ikke vise tweet" msgid "This tweet is hidden by the author" msgstr "Dette tweet er skjult af forfatteren" msgid "This tweet is unavailable" msgstr "Dette tweet er utilgængeligt" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. #. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "Oversæt til Dansk" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Ã…ben i Browser" msgid "Source" msgstr "Kilde" msgid "Tweet Details" msgstr "Tweet Detaljer" #. TRANSLATORS: Used for the "via " line in Tweet Info view when #. the client is blank msgid "an unknown client" msgstr "en ukendt klient" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ulæst)" msgstr[1] "(%d ulæste)" msgid "Delete" msgstr "Fjern" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweetet af %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "Er du sikker pÃ¥ at du ønsker at slette dette tweet?" #, c-format msgid "Block %s" msgstr "Bloker %s" msgid "This tweet contains images marked as inappropriate" msgstr "Dette tweet indeholder billeder markeret som upassende" msgid "Show anyway" msgstr "Vis alligevel" msgid "Could not authenticate you" msgstr "Kunne ikke autentificere dig" msgid "Sorry, that page does not exist" msgstr "Beklager, denne side eksistere ikke" msgid "User not found." msgstr "Bruger kunne ikke findes." msgid "User has been suspended." msgstr "Bruger er blevet suspenderet." msgid "Your account is suspended and is not permitted to access this feature" msgstr "Din konto er blevet suspenderet og kan ikke tilgÃ¥ denne feature" msgid "Rate limit exceeded" msgstr "Takstgrænse overskredet" msgid "Invalid or expired token" msgstr "Ugyldig eller udløbet token" msgid "The specified user is not a subscriber of this list." msgstr "Den angivende bruger er ikke abonnent af denne liste." msgid "The user you are trying to remove from the list is not a member." msgstr "Brugeren du prøver at fjerne fra listen er ikke medlem." msgid "Account update failed: value is too long." msgstr "Konto opdatering fejlede: værdi er for lang" msgid "Over capacity" msgstr "Over kapacitet" msgid "Internal error" msgstr "Intern fejl" msgid "You have already favorited this status." msgstr "Du har allerede favoriseret denne status." msgid "No status found with that ID." msgstr "Intet status fundet med dette ID." msgid "You cannot send messages to users who are not following you." msgstr "Du kan ikke sende beskeder til brugere som ikke følger dig." msgid "There was an error sending your message." msgstr "Der var en fejl under afsendelsen af din besked." msgid "You've already requested to follow this user." msgstr "Du har allerede anmodet om at følge denne bruger." msgid "You are unable to follow more people at this time" msgstr "Du kan ikke følge flere folk i øjeblikket" msgid "Sorry, you are not authorized to see this status" msgstr "Beklager, du er ikke autoriseret til at se denne status" msgid "User is over daily status update limit" msgstr "Bruger er over daglig statusopdaterings begrænsning" msgid "Tweet needs to be a bit shorter." msgstr "Tweetet er nød til at være lidt kortere" msgid "Status is a duplicate" msgstr "Status er en duplikat" msgid "Owner must allow dms from anyone." msgstr "Bruger er nød til at tillade dm's fra alle." msgid "Bad authentication data" msgstr "Ugyldige autentificerings data" msgid "Your credentials do not allow access to this resource." msgstr "Dine akkreditiver giver ikke adgang til denne resurse." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Denne anmodning virker automatiseret. For at beskytte vores brugere fra spam" " og andre ondsindet aktiviteter, kan vi ikke udføre denne handling lige nu." msgid "User must verify login" msgstr "Bruger skal validere login" msgid "Application cannot perform write actions." msgstr "Program kan ikke udføre skriv handlinger." msgid "You can’t mute yourself." msgstr "Du kan ikke mute dig selv." msgid "You are not muting the specified user." msgstr "Du muter ikke den angivne bruger." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "Animerede GIFs er ikke tilladt ved upload af flere billeder." msgid "The validation of media ids failed." msgstr "Valideringen af medie id fejlede." msgid "A media id was not found." msgstr "Et medie id kunne ikke findes." msgid "" "To protect our users from spam and other malicious activity, this account is" " temporarily locked." msgstr "" "For at beskytte brugere fra spam og andre ondsindede aktiviteter, er denne " "konto blevet lÃ¥st." msgid "You have already retweeted this Tweet." msgstr "Du har allerede retweetet dette Tweet." msgid "You cannot send messages to this user." msgstr "Du kan ikke sende beskeder til denne bruger." msgid "The text of your direct message is over the max character limit." msgstr "Teksten af denne direkte besked er over karaktergrænsen." msgid "Subscription already exists." msgstr "Abonnering eksistere allerede." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" "Du forsøgte at svare pÃ¥ et Tweet der er slettet eller utilgængeligt for dig." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "Dette Tweet overskrider det tilladte vedhæftnings typer." msgid "The given URL is invalid." msgstr "Det angivet URL er ugyldigt." msgid "Invalid / suspended application" msgstr "Ugyldig / suspenderet program" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" "Forfatteren af det originale tweet har begrænset hvem der kan svare pÃ¥ " "tweetet." msgid "Invalid media file" msgstr "Ugyldig medie fil" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "Ukendt fejlkode %lld under upload: %s" #, c-format msgid "Unknown error code %lld during upload" msgstr "Ukendt fejlkode %lld under upload" msgid "Now" msgstr "Nu" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d minut siden" msgstr[1] "%d minutter siden" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d time siden" msgstr[1] "%d timer siden" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dt" #. TRANSLATORS: Full-text date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, c-format msgid "Replying to %s" msgstr "Svare %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use #. the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Svare %s og %d yderligere" msgstr[1] "Svare %s og %d yderligere" #. TRANSLATORS: The first %s is a list of one or more comma-separated user #. names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "Svare %s og %s" msgid "Don't have a Twitter account yet?" msgstr "Har du ikke en Twitter konto endnu?" msgid "Create one" msgstr "Lav en" #, c-format msgid "Could not open %s" msgstr "Kunne ikke Ã¥bne %s" msgid "Failed to retrieve request token" msgstr "Modtog ikke anmodnings token" msgid "Wrong PIN" msgstr "Forkert PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" "Kontoen %s eksistere allerede med andre nøgler.\n" "\n" "Erstat den?" msgid "Account already in use" msgstr "Konto er allerede i brug" msgid "Failed to retrieve access token" msgstr "Modtog ikke adgangs token" msgid "Select Image" msgstr "Vælg billede" msgid "Unfollow" msgstr "Følg ikke" msgid "Follow" msgstr "Følg" msgid "Loading…" msgstr "Indlæser..." msgid "No entries found" msgstr "Ingen poster fundet" msgid "Retry" msgstr "Prøv igen" msgid "Copy URL" msgstr "Kopiere URL" msgid "Reload image" msgstr "Genindlæs billede" msgid "Save as…" msgstr "Gem som..." msgid "Save Video" msgstr "Gem Video" msgid "Save Image" msgstr "Gem Billede" msgid "Save" msgstr "Gem" msgid "Select Banner Image" msgstr "Vælg Bannerbillede" msgid "Image does not meet minimum size requirements:" msgstr "Billedet opfylder ikke minimumsstørrelseskrav:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimum bredte: %d pixel" msgstr[1] "Minimum bredte: %d pixels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimum højde: %d pixel" msgstr[1] "Minimum højde: %d pixels" msgid "Pick" msgstr "Vælg" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Kunne ikke indlæse billede" msgstr[1] "Kunne ikke indlæse %u billeder" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , #. , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Tilbage" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Fejl ved hentning af citeret tweet: %s\n" "\n" "Gem usendte tweet?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Fejl ved hentning af svar tweet: %s\n" "\n" "Gem usendte tweet?" msgid "Quote tweet" msgstr "Citerer tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the #. "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "Gem tweet som skabelon?" msgid "Only one animated GIF file per tweet is allowed." msgstr "Kun en animeret GIF fil per tweet er tilladt." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "%d tegn tilbage" msgstr[1] "%d tegn tilbage" #. TRANSLATORS: Values are current image index (1-based) and total image #. count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Billede %d af %d" msgstr[1] "Billeder %d af %d" msgid "Modify Filter" msgstr "Rediger filter" msgid "Matches" msgstr "Matcher" msgid "Doesn't match" msgstr "Matcher ikke" msgid "Modify Snippet" msgstr "Rediger Snippet" msgid "Snippet can't be empty" msgstr "Snippet kan ikke være tom" msgid "Replacement can't be empty" msgstr "Erstatning kan ikke være tom" msgid "Snippet may not contain whitespace" msgstr "Snippet kan ikke indeholde mellemrum" msgid "Snippet already exists" msgstr "Snippet eksistere allerede" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" "Oversættelses URL skal indeholde {SOURCE_LANG}, {TARGET_LANG} og {CONTENT} pladsholdere\n" "\n" "URL'en \"%s\" vil blive brugt i stedet." msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgstr "" "Hej, tjek lige denne nye #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Tilføj eller fjern bruger fra liste" msgid "You have no lists." msgstr "Du har ingen lister." msgid "About Cawbird" msgstr "Om Cawbird" msgid "New Account" msgstr "Ny konto" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "For at autentificere Cawbird skal du have en PIN fra twitter.com fra kontoen" " du ønsker at tilføje" msgid "Request PIN" msgstr "Anmod om PIN" msgid "Enter PIN from twitter.com below:" msgstr "Indtast PIN fra twitter.com herunder:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Bekræft" msgid "Account Settings" msgstr "Kontoindstillinger" msgid "Name" msgstr "Navn" msgid "Website" msgstr "Hjemmeside" msgid "Autostart" msgstr "Autostart" msgid "Remove Account" msgstr "Fjern konto" msgid "Do you really want to remove this account?" msgstr "Ønsker du virkeligt at fjerne denne konto?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys & Personer" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Krop & Tøj" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Dyr & Natur" msgctxt "emoji category" msgid "Food & Drink" msgstr "Mad & Drikke" msgctxt "emoji category" msgid "Travel & Places" msgstr "Rejser og Steder" msgctxt "emoji category" msgid "Activities" msgstr "Aktiviteter" msgctxt "emoji category" msgid "Objects" msgstr "Objekter" msgctxt "emoji category" msgid "Symbols" msgstr "Symboler" msgctxt "emoji category" msgid "Flags" msgstr "Flag" msgid "No Results Found" msgstr "Ingen resultater fundet" msgid "Try a different search" msgstr "Prøv en anden søgning" msgid "Send" msgstr "Send" msgid "Add Media" msgstr "Tilføj Medie" msgid "Show favorite images" msgstr "Vis favorit billeder" msgid "Click to edit" msgstr "Klik her for at redigere" msgid "Remove this Filter" msgstr "Fjern dette Filter" msgid "Filtered terms" msgstr "Filtrerede termer" msgid "Filtered users" msgstr "Filtrerede brugere" msgid "You can block users in their profile" msgstr "Du kan blokere brugere fra deres profil" msgid "Users" msgstr "Brugere" msgid "Set image description" msgstr "Angiv billede beskrivelse" msgid "Subscribe" msgstr "Tilmeld" msgid "Unsubscribe" msgstr "Afmeld" msgid "Subscribers:" msgstr "Tilmeldte:" msgid "Members:" msgstr "Medlemmer:" msgid "Creator:" msgstr "Skaber:" msgid "Created at:" msgstr "Skabt pÃ¥:" msgid "Edit" msgstr "Rediger" msgid "Mode:" msgstr "Tilstand:" msgid "Description" msgstr "Beskrivelse" msgid "Settings" msgstr "Indstillinger" msgid "Shortcuts" msgstr "Genveje" msgid "About" msgstr "Om" msgid "Quit" msgstr "Afslut" msgid "Add New Filter" msgstr "Nyt Filter" msgid "Regular Expression:" msgstr "Regulært udtryk:" msgid "Test:" msgstr "Test:" msgid "Add New Snippet" msgstr "Ny Snippet" msgid "Keyword" msgstr "Nøgleord" msgid "Replacement" msgstr "Erstatning" msgid "Create New List" msgstr "Ny Liste" msgid "Name:" msgstr "Navn:" msgid "Create" msgstr "Lav" msgid "Write Direct Message" msgstr "Skriv Direkte Besked" msgid "Add to/Remove from List" msgstr "Tilføj til/Fjern fra liste" msgid "Blocked" msgstr "Blokeret" msgid "Muted" msgstr "Muted" msgid "Retweets disabled" msgstr "Retweets deaktiveret" msgid "More actions" msgstr "Flere handlinger" msgid "Follows you" msgstr "Følger dig" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Følgere" msgid "Following" msgstr "Følger" msgid "Use dark theme" msgstr "Brug mørkt tema" msgid "Shortcut key" msgstr "Genvejstast" msgid "Alt" msgstr "Alt" msgid "Ctrl" msgstr "Ctrl" msgid "Shift" msgstr "Shift" msgid "Super" msgstr "Win" msgid "Primary" msgstr "Primær" msgid "Timelines" msgstr "Tidslinjer" msgid "Show inline media" msgstr "Vis medier" msgid "Always show" msgstr "Vis altid" msgid "Always hide" msgstr "Skjul altid" msgid "Hide in timeline" msgstr "Skjul i tidslinje" msgid "Auto scroll on new tweets" msgstr "Autoscroll ved nye tweets" msgid "Double-click activation" msgstr "Double-click aktivering" msgid "Notifications" msgstr "Notifikationer" msgid "On New Tweets" msgstr "Ved nye Tweets" msgid "Never" msgstr "Aldrig" msgid "Every" msgstr "Alle" msgid "Stack 5" msgstr "Stack 5" msgid "Stack 10" msgstr "Stack 10" msgid "Stack 25" msgstr "Stack 25" msgid "Stack 50" msgstr "Stack 50" msgid "On New Mentions" msgstr "Ved nye Omtaler" msgid "On New Messages" msgstr "Ved nye Beskeder" msgid "Interface" msgstr "Grænseflade" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "Normal" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "Large" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "X-Large" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "XX-Large" msgid "Tweet scale" msgstr "Tweet størrelse" msgid "Round avatars" msgstr "Runde avatars" msgid "Remove trailing hashtags" msgstr "Fjern efterfølgende hashtags" msgid "Remove media links" msgstr "Fjern medie links" msgid "Hide inappropriate content" msgstr "Fjern upassende indhold" msgid "Translation" msgstr "Oversættelse" msgid "Translation service" msgstr "Oversættelses service" msgid "Google" msgstr "Google" msgid "Bing" msgstr "Bing" msgid "DeepL" msgstr "DeepL" msgid "Custom" msgstr "Tilpasset" msgid "Custom translation URL" msgstr "Unikt oversættelses URL" msgid "No snippets configured." msgstr "Ingen snippets konfigureret." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Du kan aktivere snippets ved at skrive nøgleordet og trykke TAB." msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "Generalt" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Forfat Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Vis Kontoindstillinger" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Vis Kontipopover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Vis Programindstillinger" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "SlÃ¥ Topbar til/fra" msgctxt "shortcuts window" msgid "Go Back" msgstr "GÃ¥ Tilbage" msgctxt "shortcuts window" msgid "Go Forward" msgstr "GÃ¥ Fremad" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "GÃ¥ til side nth" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" msgctxt "shortcuts window" msgid "Like" msgstr "Favoriser " msgctxt "shortcuts window" msgid "Reply" msgstr "Svar" msgctxt "shortcuts window" msgid "Quote" msgstr "Citere" msgctxt "shortcuts window" msgid "Show Details" msgstr "Vis Detaljer" msgctxt "shortcuts window" msgid "Delete" msgstr "Fjern" msgctxt "shortcuts window" msgid "Compose" msgstr "Forfat" msgid "Show Emoji Chooser" msgstr "Vis Emoji vælger" msgid "Start new conversation" msgstr "Start ny samtale" msgid "With:" msgstr "Med:" msgid "Go" msgstr "Go" msgid "Quote" msgstr "Citere" msgid "Retweet tweet" msgstr "Retweet tweet" msgid "Like tweet" msgstr "Favoriser tweet" msgid "Reply to tweet" msgstr "Svar tweet" msgid "More" msgstr "Mere" msgid "Translate" msgstr "Oversæt" msgid "Like" msgstr "Favoriser" msgid "Reply" msgstr "Svar" msgid "Liked" msgstr "Liked" msgid "Retweeted" msgstr "Retweetet" msgid "Unblock" msgstr "Unblocker" msgid "Show settings of this account" msgstr "Vis indstillinger for denne konto" msgid "Open in new window" msgstr "Ã…ben i nyt vindue" msgid "Go to profile" msgstr "GÃ¥ til profil" msgid "Created" msgstr "Lavet" msgid "Subscribed to" msgstr "Tilmeldt" cawbird-1.4.2/po/de.po000066400000000000000000000623631416632607600145240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # baedert , 2014-2015 # brainscript , 2016 # encbladexp , 2014 # fgreinus , 2015 # Heiko Adams , 2016 # jupaja, 2014-2015 # Matthias Kellermann , 2016 # Michael , 2014 # Peter L., 2015-2017 # Peter Schwindt , 2015 # Philip Gillißen , 2015 # Richard Schwab , 2014 # Trolli Schmittlauch , 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:24+0000\n" "Last-Translator: baedert \n" "Language-Team: German (http://www.transifex.com/cawbird/cawbird/language/" "de/)\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter Client" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird ist ein nativer GTK+ Twitter-Client, der wesentliche Funktionen wie " "Direktnachrichten (DMs), Tweet-Benachrichtigungen und Konversationen bietet." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Weitere Funktionalitäten beinhalten die lokale Anzeige von Videos, mehrere " "eingebettete Bilder, Listen, Filter, mehrere Accounts, etc." msgid "Generic timeline view when using Cawbird" msgstr "Allgemeine Verlaufsansicht bei Nutzung von Cawbird" msgid "Typical Twitter profile" msgstr "Typisches Twitter-Profil" msgid "Account settings can be configured" msgstr "Account-Einstellungen können konfiguriert werden" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Nutzen Sie Twitter in einer normalen Desktopanwendung" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Zeige konfigurierte Accounts" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Direktnachrichten" #, fuzzy msgid "Select Media" msgstr "Bild auswählen" msgid "Open" msgstr "Öffnen" msgid "Cancel" msgstr "Abbrechen" #, fuzzy msgid "Selected file is not an image or video." msgstr "Ausgewählte Datei ist kein Bild" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" msgid "Insert Emoji" msgstr "Emoji einfügen" msgid "Direct Conversation" msgstr "Direktnachrichten" #, fuzzy msgid "Direct message threads" msgstr "Direktnachrichten" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d neue Nachricht von %s" msgstr[1] "%d neue Nachrichten von %s" #, c-format msgid "New direct message from %s" msgstr "Neue Direktnachricht von %s" msgid "Direct Messages" msgstr "Direktnachrichten" #, fuzzy msgid "Liked tweets timeline" msgstr "Favorisieren" #, fuzzy msgid "Likes" msgstr "Favoriten" msgid "Add new Filter" msgstr "Neuen Filter hinzufügen" msgid "Filters" msgstr "Filter" #, fuzzy msgid "Home timeline" msgstr "Aus Timeline ausblenden" #, c-format msgid "%s retweeted %s" msgstr "%s retweetete %s" #, c-format msgid "%s tweeted" msgstr "%s tweetete" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d neuer Tweet!" msgstr[1] "%d neue Tweets!" msgid "Home" msgstr "Timeline" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweetete" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Öffentlich" msgid "Lists" msgstr "Listen" msgid "Show configured accounts" msgstr "Zeige konfigurierte Accounts" msgid "Compose Tweet" msgstr "Tweet erstellen" msgid "Add new Account" msgstr "Neuen Account hinzufügen" #, fuzzy msgid "Mentions timeline" msgstr "Aus Timeline ausblenden" #, c-format msgid "%s mentioned %s" msgstr "%s erwähnte %s" msgid "Mentions" msgstr "Erwähnungen" msgid "Suspended Account" msgstr "Suspendierter Account" msgid "Protected profile" msgstr "Geschütztes Profil" #, c-format msgid "Tweet to @%s" msgstr "Tweet an @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweetete" msgstr[1] "%s tweetete" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Folgt" msgstr[1] "Folgt" #, fuzzy, c-format msgid "Location: %s" msgstr "Benachrichtigungen" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Aus Timeline ausblenden" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Followers" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Folgt" msgid "Protected Profile" msgstr "Geschütztes Profil" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Followers" msgstr[1] "Followers" #, fuzzy msgid "No tweets found" msgstr "Keine Einträge gefunden" msgid "No users found" msgstr "Keine Benutzer gefunden" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet an @%s" msgid "Search" msgstr "Suchen" msgid "Load More" msgstr "Mehr laden" msgid "Could not show tweet" msgstr "Konnte Tweet nicht anzeigen" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Im Browser öffnen" msgid "Source" msgstr "Quelle" msgid "Tweet Details" msgstr "Tweet Details" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ungelesen)" msgstr[1] "(%d ungelesene)" msgid "Delete" msgstr "Löschen" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweeten" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Willst Du diesen Account wirklich löschen?" #, c-format msgid "Block %s" msgstr "%s blockieren" msgid "This tweet contains images marked as inappropriate" msgstr "Das eingebettete Medium könnte sensibles Material beinhalten" msgid "Show anyway" msgstr "Trotzdem zeigen" #, fuzzy msgid "Could not authenticate you" msgstr "Konnte %s nicht öffnen" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Keine Benutzer gefunden" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Oberfläche" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Der Schnipsel existiert schon" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Jetzt" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dM" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dS" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Antwortend auf %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Antwortend auf %s und %d andere" msgstr[1] "Antwortend auf %s und %d andere" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Antwortend auf %s und %s" msgid "Don't have a Twitter account yet?" msgstr "Noch keinen Twitter Account?" msgid "Create one" msgstr "Erstelle einen" #, c-format msgid "Could not open %s" msgstr "Konnte %s nicht öffnen" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Falsche PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Account schon in Benutzung" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Bild auswählen" msgid "Unfollow" msgstr "Entfolgen" msgid "Follow" msgstr "Folgen" msgid "Loading…" msgstr "Lade…" #, fuzzy msgid "No entries found" msgstr "Keine Benutzer gefunden" msgid "Retry" msgstr "Erneut versuchen" msgid "Copy URL" msgstr "URL kopieren" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Speichern unter…" msgid "Save Video" msgstr "Speichere Video" msgid "Save Image" msgstr "Speichere Bild" msgid "Save" msgstr "Speichern" msgid "Select Banner Image" msgstr "Banner auswählen" msgid "Image does not meet minimum size requirements:" msgstr "Bild entspricht nicht der Minimalgröße:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Mindestbreite: %d Pixel" msgstr[1] "Mindestbreite: %d Pixel" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Mindesthöhe: %d Pixel" msgstr[1] "Mindesthöhe: %d Pixel" msgid "Pick" msgstr "Wähle" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Zurück" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Zitiere Tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Nur eine GIF Datei pro Tweet is erlaubt" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Filter ändern" msgid "Matches" msgstr "Passt" msgid "Doesn't match" msgstr "Keine Übereinstimmung" msgid "Modify Snippet" msgstr "Schnipsel bearbeiten" msgid "Snippet can't be empty" msgstr "Schnipsel kann nicht leer sein" msgid "Replacement can't be empty" msgstr "Ersetzung kann nicht leer sein" msgid "Snippet may not contain whitespace" msgstr "Der Schnipsel darf keinen whitespace (z. B. Leerzeichen) enthalten" msgid "Snippet already exists" msgstr "Der Schnipsel existiert schon" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hey, schau dir die neue #Cawbird version an! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Nutzer zu Liste hinzufügen oder entfernen" msgid "You have no lists." msgstr "Du hast keine Listen." msgid "About Cawbird" msgstr "Über Cawbird" msgid "New Account" msgstr "Neuer Account" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Um Cawbird zu authentifizieren wird ein PIN von twitter.com mit dem Account " "benötigt, der verwendet werden soll." msgid "Request PIN" msgstr "PIN anfordern" msgid "Enter PIN from twitter.com below:" msgstr "PIN von twitter.com eingeben:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Bestätigen" msgid "Account Settings" msgstr "Account Einstellungen" msgid "Name" msgstr "Name" msgid "Website" msgstr "Webseite" msgid "Autostart" msgstr "Autostart" #, fuzzy msgid "Remove Account" msgstr "Löschen" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Willst Du diesen Account wirklich löschen?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys & Menschen" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Körper & Bekleidung" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Tiere & Natur" msgctxt "emoji category" msgid "Food & Drink" msgstr "Essen & Trinken" msgctxt "emoji category" msgid "Travel & Places" msgstr "Reisen & Orte" msgctxt "emoji category" msgid "Activities" msgstr "Aktivitäten" msgctxt "emoji category" msgid "Objects" msgstr "Objekte" msgctxt "emoji category" msgid "Symbols" msgstr "Symbole" msgctxt "emoji category" msgid "Flags" msgstr "Flaggen" msgid "No Results Found" msgstr "Keine Resultate gefunden" msgid "Try a different search" msgstr "Versuche einen anderen Suchbegriff" msgid "Send" msgstr "Senden" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Zeige Lieblingsbilder" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filter" #, fuzzy msgid "Filtered users" msgstr "Filter" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Benutzer" #, fuzzy msgid "Set image description" msgstr "Beschreibung" msgid "Subscribe" msgstr "Abonnieren" msgid "Unsubscribe" msgstr "Abbestellen" msgid "Subscribers:" msgstr "Abonnenten" msgid "Members:" msgstr "Mitglieder:" msgid "Creator:" msgstr "Ersteller:" msgid "Created at:" msgstr "Erstellt um:" msgid "Edit" msgstr "Bearbeiten" msgid "Mode:" msgstr "Modus:" msgid "Description" msgstr "Beschreibung" msgid "Settings" msgstr "Einstellungen" msgid "Shortcuts" msgstr "Tastaturkürzel" msgid "About" msgstr "Über" msgid "Quit" msgstr "Beenden" msgid "Add New Filter" msgstr "Neuen Filter hinzufügen" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Neuen Schnipsel anlegen" msgid "Keyword" msgstr "Stichwort" msgid "Replacement" msgstr "Ersetzung" msgid "Create New List" msgstr "Neue Liste erstellen" msgid "Name:" msgstr "Name:" msgid "Create" msgstr "Erstellen" msgid "Write Direct Message" msgstr "Direktnachricht schreiben" msgid "Add to/Remove from List" msgstr "Hinzufügen / Entfernen aus der Liste" msgid "Blocked" msgstr "Blockiert" msgid "Muted" msgstr "Stummgeschaltet" msgid "Retweets disabled" msgstr "Retweets deaktiviert" #, fuzzy msgid "More actions" msgstr "Erwähnungen" msgid "Follows you" msgstr "Folgt dir" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Followers" msgid "Following" msgstr "Folgt" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Tastaturkürzel" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Aus Timeline ausblenden" msgid "Show inline media" msgstr "Eingebettete Medien anzeigen" msgid "Always show" msgstr "Immer anzeigen" msgid "Always hide" msgstr "Immer verstecken" msgid "Hide in timeline" msgstr "Aus Timeline ausblenden" msgid "Auto scroll on new tweets" msgstr "Automatisch scrollen bei neuen Tweets" msgid "Double-click activation" msgstr "Aktivieren per Doppelklick" msgid "Notifications" msgstr "Benachrichtigungen" msgid "On New Tweets" msgstr "Bei neuen Tweets" msgid "Never" msgstr "Niemals" msgid "Every" msgstr "Jeder" msgid "Stack 5" msgstr "Sammle 5" msgid "Stack 10" msgstr "Sammle 10" msgid "Stack 25" msgstr "Sammle 25" msgid "Stack 50" msgstr "Sammle 50" msgid "On New Mentions" msgstr "Bei neuen Erwähnungen:" msgid "On New Messages" msgstr "Bei neuen Nachrichten" msgid "Interface" msgstr "Oberfläche" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Runde Avatare" msgid "Remove trailing hashtags" msgstr "Entferne nachfolgende Hashtags" msgid "Remove media links" msgstr "Entferne Medienverweise" msgid "Hide inappropriate content" msgstr "Verstecke unangebrachte Inhalte" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Keine Schnipsel eingerichtet" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Du kannst Schnipsel auswählen, in dem du das Stichwort tippst und dann " "Tabulator drückst" msgid "Snippets" msgstr "Schnipsel" msgctxt "shortcuts window" msgid "General" msgstr "Allgemein" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Tweet erstellen" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Öffne Konto-Einstellungen" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Öffne Konto-Popover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Öffne Einstellungen" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Symbolleiste umschalten" msgctxt "shortcuts window" msgid "Go Back" msgstr "Zurück" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Weiter" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Gehe zu Seite n" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweeten" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favoriten" msgctxt "shortcuts window" msgid "Reply" msgstr "Antworten" msgctxt "shortcuts window" msgid "Quote" msgstr "Zitieren" msgctxt "shortcuts window" msgid "Show Details" msgstr "Zeige Details" msgctxt "shortcuts window" msgid "Delete" msgstr "Löschen" msgctxt "shortcuts window" msgid "Compose" msgstr "Verfassen" msgid "Show Emoji Chooser" msgstr "Zeige Emoji-Auswahl" msgid "Start new conversation" msgstr "Neue Unterhaltung beginnen" msgid "With:" msgstr "Mit:" msgid "Go" msgstr "Los" msgid "Quote" msgstr "Zitieren" msgid "Retweet tweet" msgstr "Tweet retweeten" #, fuzzy msgid "Like tweet" msgstr "Favorisieren" msgid "Reply to tweet" msgstr "Auf Tweet antworten" msgid "More" msgstr "Mehr" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favoriten" msgid "Reply" msgstr "Antworten" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweeten" msgid "Unblock" msgstr "Entsperren" msgid "Show settings of this account" msgstr "Zeige die Einstellungen dieses Accounts" msgid "Open in new window" msgstr "In neuem Fenster öffnen" msgid "Go to profile" msgstr "Zum Profil gehen" msgid "Created" msgstr "Erstellt" msgid "Subscribed to" msgstr "Anmelden zu" #~ msgid "Replying to" #~ msgstr "Antwortend auf" #~ msgid "and" #~ msgstr "und" #~ msgid "Actions" #~ msgstr "Aktionen" #~ msgid "Add Image" #~ msgstr "Bild anhängen" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Followers" #~ msgid "List" #~ msgstr "Liste" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Nicht authorisiert. Dies bedeutet meistens dass mit dem Twitter Server " #~ "etwas nicht stimmt und Sie es später erneut versuchen sollten." #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Konnte Tweets nicht laden" cawbird-1.4.2/po/de_DE.po000066400000000000000000000624001416632607600150640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # baedert , 2014-2015 # brainscript , 2016 # encbladexp , 2014 # fgreinus , 2015 # Heiko Adams , 2016 # jupaja, 2014-2015 # Matthias Kellermann , 2016 # Michael , 2014 # Peter L., 2015-2017 # Peter Schwindt , 2015 # Philip Gillißen , 2015 # Richard Schwab , 2014 # Trolli Schmittlauch , 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:24+0000\n" "Last-Translator: baedert \n" "Language-Team: German (Germany) (http://www.transifex.com/cawbird/cawbird/" "language/de_DE/)\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter Client" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird ist ein nativer GTK+ Twitter-Client, der wesentliche Funktionen wie " "Direktnachrichten (DMs), Tweet-Benachrichtigungen und Konversationen bietet." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Weitere Funktionalitäten beinhalten die lokale Anzeige von Videos, mehrere " "eingebettete Bilder, Listen, Filter, mehrere Accounts, etc." msgid "Generic timeline view when using Cawbird" msgstr "Allgemeine Verlaufsansicht bei Nutzung von Cawbird" msgid "Typical Twitter profile" msgstr "Typisches Twitter-Profil" msgid "Account settings can be configured" msgstr "Account-Einstellungen können konfiguriert werden" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Nutzen Sie Twitter in einer normalen Desktopanwendung" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Zeige konfigurierte Accounts" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Direktnachrichten" #, fuzzy msgid "Select Media" msgstr "Bild auswählen" msgid "Open" msgstr "Öffnen" msgid "Cancel" msgstr "Abbrechen" #, fuzzy msgid "Selected file is not an image or video." msgstr "Ausgewählte Datei ist kein Bild" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Das gewählte Bild ist zu groß. Die maximale Größe pro Bild beträgt %'d MB" msgid "Insert Emoji" msgstr "Emoji einfügen" msgid "Direct Conversation" msgstr "Direktnachrichten" #, fuzzy msgid "Direct message threads" msgstr "Direktnachrichten" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d neue Nachricht von %s" msgstr[1] "%d neue Nachrichten von %s" #, c-format msgid "New direct message from %s" msgstr "Neue Direktnachricht von %s" msgid "Direct Messages" msgstr "Direktnachrichten" #, fuzzy msgid "Liked tweets timeline" msgstr "Favorisieren" #, fuzzy msgid "Likes" msgstr "Favoriten" msgid "Add new Filter" msgstr "Neuen Filter hinzufügen" msgid "Filters" msgstr "Filter" #, fuzzy msgid "Home timeline" msgstr "Aus Timeline ausblenden" #, c-format msgid "%s retweeted %s" msgstr "%s retweetete %s" #, c-format msgid "%s tweeted" msgstr "%s tweetete" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d neuer Tweet!" msgstr[1] "%d neue Tweets!" msgid "Home" msgstr "Timeline" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweetete" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Öffentlich" msgid "Lists" msgstr "Listen" msgid "Show configured accounts" msgstr "Zeige konfigurierte Accounts" msgid "Compose Tweet" msgstr "Tweet erstellen" msgid "Add new Account" msgstr "Neuen Account hinzufügen" #, fuzzy msgid "Mentions timeline" msgstr "Aus Timeline ausblenden" #, c-format msgid "%s mentioned %s" msgstr "%s erwähnte %s" msgid "Mentions" msgstr "Erwähnungen" msgid "Suspended Account" msgstr "Suspendierter Account" msgid "Protected profile" msgstr "Geschütztes Profil" #, c-format msgid "Tweet to @%s" msgstr "Tweet an @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweetete" msgstr[1] "%s tweetete" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Folgt" msgstr[1] "Folgt" #, fuzzy, c-format msgid "Location: %s" msgstr "Benachrichtigungen" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Aus Timeline ausblenden" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Followers" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Folgt" msgid "Protected Profile" msgstr "Geschütztes Profil" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Followers" msgstr[1] "Followers" #, fuzzy msgid "No tweets found" msgstr "Keine Einträge gefunden" msgid "No users found" msgstr "Keine Benutzer gefunden" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet an @%s" msgid "Search" msgstr "Suchen" msgid "Load More" msgstr "Mehr laden" msgid "Could not show tweet" msgstr "Konnte Tweet nicht anzeigen" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Im Browser öffnen" msgid "Source" msgstr "Quelle" msgid "Tweet Details" msgstr "Tweet Details" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ungelesen)" msgstr[1] "(%d ungelesene)" msgid "Delete" msgstr "Löschen" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweeten" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Willst Du diesen Account wirklich löschen?" #, c-format msgid "Block %s" msgstr "%s blockieren" msgid "This tweet contains images marked as inappropriate" msgstr "Das eingebettete Medium könnte sensibles Material beinhalten" msgid "Show anyway" msgstr "Trotzdem zeigen" #, fuzzy msgid "Could not authenticate you" msgstr "Konnte %s nicht öffnen" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Keine Benutzer gefunden" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Oberfläche" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Der Schnipsel existiert schon" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Jetzt" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dM" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dS" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Antwortend auf %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Antwortend auf %s und %d andere" msgstr[1] "Antwortend auf %s und %d andere" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Antwortend auf %s und %s" msgid "Don't have a Twitter account yet?" msgstr "Noch keinen Twitter Account?" msgid "Create one" msgstr "Erstelle einen" #, c-format msgid "Could not open %s" msgstr "Konnte %s nicht öffnen" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Falsche PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Account schon in Benutzung" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Bild auswählen" msgid "Unfollow" msgstr "Entfolgen" msgid "Follow" msgstr "Folgen" msgid "Loading…" msgstr "Lade…" #, fuzzy msgid "No entries found" msgstr "Keine Benutzer gefunden" msgid "Retry" msgstr "Erneut versuchen" msgid "Copy URL" msgstr "URL kopieren" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Speichern unter…" msgid "Save Video" msgstr "Speichere Video" msgid "Save Image" msgstr "Speichere Bild" msgid "Save" msgstr "Speichern" msgid "Select Banner Image" msgstr "Banner auswählen" msgid "Image does not meet minimum size requirements:" msgstr "Bild entspricht nicht der Minimalgröße:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Mindestbreite: %d Pixel" msgstr[1] "Mindestbreite: %d Pixel" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Mindesthöhe: %d Pixel" msgstr[1] "Mindesthöhe: %d Pixel" msgid "Pick" msgstr "Wähle" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Zurück" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Zitiere Tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Nur eine GIF Datei pro Tweet is erlaubt" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Filter ändern" msgid "Matches" msgstr "Passt" msgid "Doesn't match" msgstr "Keine Übereinstimmung" msgid "Modify Snippet" msgstr "Schnipsel bearbeiten" msgid "Snippet can't be empty" msgstr "Schnipsel kann nicht leer sein" msgid "Replacement can't be empty" msgstr "Ersetzung kann nicht leer sein" msgid "Snippet may not contain whitespace" msgstr "Der Schnipsel darf keinen whitespace (z. B. Leerzeichen) enthalten" msgid "Snippet already exists" msgstr "Der Schnipsel existiert schon" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hey, schau dir die neue #Cawbird version an! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Nutzer zu Liste hinzufügen oder entfernen" msgid "You have no lists." msgstr "Du hast keine Listen." msgid "About Cawbird" msgstr "Über Cawbird" msgid "New Account" msgstr "Neuer Account" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Um Cawbird zu authentifizieren wird ein PIN von twitter.com mit dem Account " "benötigt, der verwendet werden soll." msgid "Request PIN" msgstr "PIN anfordern" msgid "Enter PIN from twitter.com below:" msgstr "PIN von twitter.com eingeben:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Bestätigen" msgid "Account Settings" msgstr "Account Einstellungen" msgid "Name" msgstr "Name" msgid "Website" msgstr "Webseite" msgid "Autostart" msgstr "Autostart" #, fuzzy msgid "Remove Account" msgstr "Löschen" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Willst Du diesen Account wirklich löschen?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys & Menschen" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Körper & Bekleidung" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Tiere & Natur" msgctxt "emoji category" msgid "Food & Drink" msgstr "Essen & Trinken" msgctxt "emoji category" msgid "Travel & Places" msgstr "Reisen & Orte" msgctxt "emoji category" msgid "Activities" msgstr "Aktivitäten" msgctxt "emoji category" msgid "Objects" msgstr "Objekte" msgctxt "emoji category" msgid "Symbols" msgstr "Symbole" msgctxt "emoji category" msgid "Flags" msgstr "Flaggen" msgid "No Results Found" msgstr "Keine Resultate gefunden" msgid "Try a different search" msgstr "Versuche einen anderen Suchbegriff" msgid "Send" msgstr "Senden" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Zeige Lieblingsbilder" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filter" #, fuzzy msgid "Filtered users" msgstr "Filter" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Benutzer" #, fuzzy msgid "Set image description" msgstr "Beschreibung" msgid "Subscribe" msgstr "Abonnieren" msgid "Unsubscribe" msgstr "Abbestellen" msgid "Subscribers:" msgstr "Abonnenten" msgid "Members:" msgstr "Mitglieder:" msgid "Creator:" msgstr "Ersteller:" msgid "Created at:" msgstr "Erstellt um:" msgid "Edit" msgstr "Bearbeiten" msgid "Mode:" msgstr "Modus:" msgid "Description" msgstr "Beschreibung" msgid "Settings" msgstr "Einstellungen" msgid "Shortcuts" msgstr "Tastaturkürzel" msgid "About" msgstr "Über" msgid "Quit" msgstr "Beenden" msgid "Add New Filter" msgstr "Neuen Filter hinzufügen" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Neuen Schnipsel anlegen" msgid "Keyword" msgstr "Stichwort" msgid "Replacement" msgstr "Ersetzung" msgid "Create New List" msgstr "Neue Liste erstellen" msgid "Name:" msgstr "Name:" msgid "Create" msgstr "Erstellen" msgid "Write Direct Message" msgstr "Direktnachricht schreiben" msgid "Add to/Remove from List" msgstr "Hinzufügen / Entfernen aus der Liste" msgid "Blocked" msgstr "Blockiert" msgid "Muted" msgstr "Stummgeschaltet" msgid "Retweets disabled" msgstr "Retweets deaktiviert" #, fuzzy msgid "More actions" msgstr "Erwähnungen" msgid "Follows you" msgstr "Folgt dir" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Followers" msgid "Following" msgstr "Folgt" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Tastaturkürzel" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Aus Timeline ausblenden" msgid "Show inline media" msgstr "Eingebettete Medien anzeigen" msgid "Always show" msgstr "Immer anzeigen" msgid "Always hide" msgstr "Immer verstecken" msgid "Hide in timeline" msgstr "Aus Timeline ausblenden" msgid "Auto scroll on new tweets" msgstr "Automatisch scrollen bei neuen Tweets" msgid "Double-click activation" msgstr "Aktivieren per Doppelklick" msgid "Notifications" msgstr "Benachrichtigungen" msgid "On New Tweets" msgstr "Bei neuen Tweets" msgid "Never" msgstr "Niemals" msgid "Every" msgstr "Jeder" msgid "Stack 5" msgstr "Sammle 5" msgid "Stack 10" msgstr "Sammle 10" msgid "Stack 25" msgstr "Sammle 25" msgid "Stack 50" msgstr "Sammle 50" msgid "On New Mentions" msgstr "Bei neuen Erwähnungen:" msgid "On New Messages" msgstr "Bei neuen Nachrichten" msgid "Interface" msgstr "Oberfläche" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Runde Avatare" msgid "Remove trailing hashtags" msgstr "Entferne nachfolgende Hashtags" msgid "Remove media links" msgstr "Entferne Medienverweise" msgid "Hide inappropriate content" msgstr "Verstecke unangebrachte Inhalte" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Keine Schnipsel eingerichtet" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Du kannst Schnipsel auswählen, in dem du das Stichwort tippst und dann " "Tabulator drückst" msgid "Snippets" msgstr "Schnipsel" msgctxt "shortcuts window" msgid "General" msgstr "Allgemein" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Tweet erstellen" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Öffne Konto-Einstellungen" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Öffne Konto-Popover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Öffne Einstellungen" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Symbolleiste umschalten" msgctxt "shortcuts window" msgid "Go Back" msgstr "Zurück" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Weiter" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Gehe zu Seite n" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweeten" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favoriten" msgctxt "shortcuts window" msgid "Reply" msgstr "Antworten" msgctxt "shortcuts window" msgid "Quote" msgstr "Zitieren" msgctxt "shortcuts window" msgid "Show Details" msgstr "Zeige Details" msgctxt "shortcuts window" msgid "Delete" msgstr "Löschen" msgctxt "shortcuts window" msgid "Compose" msgstr "Verfassen" msgid "Show Emoji Chooser" msgstr "Zeige Emoji-Auswahl" msgid "Start new conversation" msgstr "Neue Unterhaltung beginnen" msgid "With:" msgstr "Mit:" msgid "Go" msgstr "Los" msgid "Quote" msgstr "Zitieren" msgid "Retweet tweet" msgstr "Tweet retweeten" #, fuzzy msgid "Like tweet" msgstr "Favorisieren" msgid "Reply to tweet" msgstr "Auf Tweet antworten" msgid "More" msgstr "Mehr" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favoriten" msgid "Reply" msgstr "Antworten" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweeten" msgid "Unblock" msgstr "Entsperren" msgid "Show settings of this account" msgstr "Zeige die Einstellungen dieses Accounts" msgid "Open in new window" msgstr "In neuem Fenster öffnen" msgid "Go to profile" msgstr "Zum Profil gehen" msgid "Created" msgstr "Erstellt" msgid "Subscribed to" msgstr "Anmelden zu" #~ msgid "Replying to" #~ msgstr "Antwortend auf" #~ msgid "and" #~ msgstr "und" #~ msgid "Actions" #~ msgstr "Aktionen" #~ msgid "Add Image" #~ msgstr "Bild anhängen" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Followers" #~ msgid "List" #~ msgstr "Liste" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Nicht authorisiert. Dies bedeutet meistens dass mit dem Twitter Server " #~ "etwas nicht stimmt und Sie es später erneut versuchen sollten." #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Konnte Tweets nicht laden" cawbird-1.4.2/po/en_GB.po000066400000000000000000000643521416632607600151060ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # IBBoard . , 2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: IBBoard . , 2021\n" "Language-Team: English (United Kingdom) (https://www.transifex.com/cawbird/teams/107135/en_GB/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter Client" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as" " Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird is a native GTK+ twitter client that provides vital features such as" " Direct Messages (DMs), tweet notifications, conversation views." msgid "" "Additional features include local viewing of videos, multiple inline images," " Lists, Filters, multiple accounts, etc." msgstr "" "Additional features include local viewing of videos, multiple inline images," " Lists, Filters, multiple accounts, etc." msgid "Generic timeline view when using Cawbird" msgstr "Generic timeline view when using Cawbird" msgid "Typical Twitter profile" msgstr "Typical Twitter profile" msgid "Account settings can be configured" msgstr "Account settings can be configured" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Use Twitter from within a normal desktop application" #. TRANSLATORS: Search terms to find this application. Do NOT translate or #. localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" "Shows only the 'compose tweet' window for the given account, nothing else." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` #. output msgid "account-name" msgstr "account-name" #. TRANSLATORS: Description of the `--start-service` option for the command- #. line msgid "Start service" msgstr "Start service" #. TRANSLATORS: Description of the `--stop-service` option for the command- #. line msgid "Stop service, if it has been started as a service" msgstr "Stop service, if it has been started as a service" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the #. command-line msgid "Print configured startup accounts" msgstr "Print configured startup accounts" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Open the window for the given account" #, c-format msgid "Direct messages with %s" msgstr "Direct messages with %s" msgid "Select Media" msgstr "Select Media" msgid "Open" msgstr "Open" msgid "Cancel" msgstr "Cancel" msgid "Selected file is not an image or video." msgstr "Selected file is not an image or video." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "The selected video is too big. The maximum file size per video is %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "The selected image is too big. The maximum file size per image is %'d MB" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgid "Insert Emoji" msgstr "Insert Emoji" msgid "Direct Conversation" msgstr "Direct Conversation" msgid "Direct message threads" msgstr "Direct message threads" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d new Direct Message from %s" msgstr[1] "%d new Direct Messages from %s" #, c-format msgid "New direct message from %s" msgstr "New direct message from %s" msgid "Direct Messages" msgstr "Direct Messages" msgid "Liked tweets timeline" msgstr "Liked tweets timeline" msgid "Likes" msgstr "Likes" msgid "Add new Filter" msgstr "Add new Filter" msgid "Filters" msgstr "Filters" msgid "Home timeline" msgstr "Home timeline" #, c-format msgid "%s retweeted %s" msgstr "%s retweeted %s" #, c-format msgid "%s tweeted" msgstr "%s tweeted" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d new Tweet" msgstr[1] "%d new Tweets" msgid "Home" msgstr "Home" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" #. when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "%s list tweets" msgid "Private" msgstr "Private" msgid "Public" msgstr "Public" msgid "Lists" msgstr "Lists" msgid "Show configured accounts" msgstr "Show configured accounts" msgid "Compose Tweet" msgstr "Compose Tweet" msgid "Add new Account" msgstr "Add new Account" msgid "Mentions timeline" msgstr "Mentions timeline" #, c-format msgid "%s mentioned %s" msgstr "%s mentioned %s" msgid "Mentions" msgstr "Mentions" msgid "Suspended Account" msgstr "Suspended Account" msgid "Protected profile" msgstr "Protected profile" #, c-format msgid "Tweet to @%s" msgstr "Tweet to @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d tweets" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Following %d account" msgstr[1] "Following %d accounts" #, c-format msgid "Location: %s" msgstr "Location: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile #. timeline view #, c-format msgid "%s timeline" msgstr "%s timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users following the user #, c-format msgid "%s followers" msgstr "%s followers" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users followed by the user #, c-format msgid "%s following" msgstr "%s following" msgid "Protected Profile" msgstr "Protected Profile" msgid "User is blocked" msgstr "User is blocked" msgid "User is muted" msgstr "User is muted" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d follower" msgstr[1] "%d followers" msgid "No tweets found" msgstr "No tweets found" msgid "No users found" msgstr "No users found" #, c-format msgid "Users matching \"%s\"" msgstr "Users matching \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweets matching \"%s\"" msgid "Search" msgstr "Search" msgid "Load More" msgstr "Load More" msgid "Could not show tweet" msgstr "Could not show tweet" msgid "This tweet is hidden by the author" msgstr "This tweet is hidden by the author" msgid "This tweet is unavailable" msgstr "This tweet is unavailable" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. #. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "Translate to English" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Open in Browser" msgid "Source" msgstr "Source" msgid "Tweet Details" msgstr "Tweet Details" #. TRANSLATORS: Used for the "via " line in Tweet Info view when #. the client is blank msgid "an unknown client" msgstr "an unknown client" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d unread)" msgstr[1] "(%d unread)" msgid "Delete" msgstr "Delete" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweeted by %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "Are you sure you want to delete this tweet?" #, c-format msgid "Block %s" msgstr "Block %s" msgid "This tweet contains images marked as inappropriate" msgstr "This tweet contains images marked as inappropriate" msgid "Show anyway" msgstr "Show anyway" msgid "Could not authenticate you" msgstr "Could not authenticate you" msgid "Sorry, that page does not exist" msgstr "Sorry, that page does not exist" msgid "User not found." msgstr "User not found." msgid "User has been suspended." msgstr "User has been suspended." msgid "Your account is suspended and is not permitted to access this feature" msgstr "Your account is suspended and is not permitted to access this feature" msgid "Rate limit exceeded" msgstr "Rate limit exceeded" msgid "Invalid or expired token" msgstr "Invalid or expired token" msgid "The specified user is not a subscriber of this list." msgstr "The specified user is not a subscriber of this list." msgid "The user you are trying to remove from the list is not a member." msgstr "The user you are trying to remove from the list is not a member." msgid "Account update failed: value is too long." msgstr "Account update failed: value is too long." msgid "Over capacity" msgstr "Over capacity" msgid "Internal error" msgstr "Internal error" msgid "You have already favorited this status." msgstr "You have already favourited this status." msgid "No status found with that ID." msgstr "No status found with that ID." msgid "You cannot send messages to users who are not following you." msgstr "You cannot send messages to users who are not following you." msgid "There was an error sending your message." msgstr "There was an error sending your message." msgid "You've already requested to follow this user." msgstr "You've already requested to follow this user." msgid "You are unable to follow more people at this time" msgstr "You are unable to follow more people at this time" msgid "Sorry, you are not authorized to see this status" msgstr "Sorry, you are not authorized to see this status" msgid "User is over daily status update limit" msgstr "User is over daily status update limit" msgid "Tweet needs to be a bit shorter." msgstr "Tweet needs to be a bit shorter." msgid "Status is a duplicate" msgstr "Status is a duplicate" msgid "Owner must allow dms from anyone." msgstr "Owner must allow DMs from anyone." msgid "Bad authentication data" msgstr "Bad authentication data" msgid "Your credentials do not allow access to this resource." msgstr "Your credentials do not allow access to this resource." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgid "User must verify login" msgstr "User must verify login" msgid "Application cannot perform write actions." msgstr "Application cannot perform write actions." msgid "You can’t mute yourself." msgstr "You can’t mute yourself." msgid "You are not muting the specified user." msgstr "You are not muting the specified user." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "Animated GIFs are not allowed when uploading multiple images." msgid "The validation of media ids failed." msgstr "The validation of media IDs failed." msgid "A media id was not found." msgstr "A media ID was not found." msgid "" "To protect our users from spam and other malicious activity, this account is" " temporarily locked." msgstr "" "To protect our users from spam and other malicious activity, this account is" " temporarily locked." msgid "You have already retweeted this Tweet." msgstr "You have already retweeted this Tweet." msgid "You cannot send messages to this user." msgstr "You cannot send messages to this user." msgid "The text of your direct message is over the max character limit." msgstr "The text of your direct message is over the max character limit." msgid "Subscription already exists." msgstr "Subscription already exists." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "The Tweet exceeds the number of allowed attachment types." msgid "The given URL is invalid." msgstr "The given URL is invalid." msgid "Invalid / suspended application" msgstr "Invalid / suspended application" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "The original Tweet author restricted who can reply to this Tweet." msgid "Invalid media file" msgstr "Invalid media file" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "Unknown error code %lld during upload: %s" #, c-format msgid "Unknown error code %lld during upload" msgstr "Unknown error code %lld during upload" msgid "Now" msgstr "Now" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d minute ago" msgstr[1] "%d minutes ago" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d hour ago" msgstr[1] "%d hours ago" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, c-format msgid "Replying to %s" msgstr "Replying to %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use #. the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Replying to %s and %d other" msgstr[1] "Replying to %s and %d others" #. TRANSLATORS: The first %s is a list of one or more comma-separated user #. names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "Replying to %s and %s" msgid "Don't have a Twitter account yet?" msgstr "Don't have a Twitter account yet?" msgid "Create one" msgstr "Create one" #, c-format msgid "Could not open %s" msgstr "Could not open %s" msgid "Failed to retrieve request token" msgstr "Failed to retrieve request token" msgid "Wrong PIN" msgstr "Wrong PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgid "Account already in use" msgstr "Account already in use" msgid "Failed to retrieve access token" msgstr "Failed to retrieve access token" msgid "Select Image" msgstr "Select Image" msgid "Unfollow" msgstr "Unfollow" msgid "Follow" msgstr "Follow" msgid "Loading…" msgstr "Loading…" msgid "No entries found" msgstr "No entries found" msgid "Retry" msgstr "Retry" msgid "Copy URL" msgstr "Copy URL" msgid "Reload image" msgstr "Reload image" msgid "Save as…" msgstr "Save as…" msgid "Save Video" msgstr "Save Video" msgid "Save Image" msgstr "Save Image" msgid "Save" msgstr "Save" msgid "Select Banner Image" msgstr "Select Banner Image" msgid "Image does not meet minimum size requirements:" msgstr "Image does not meet minimum size requirements:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimum width: %d pixel" msgstr[1] "Minimum width: %d pixels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimum height: %d pixel" msgstr[1] "Minimum height: %d pixels" msgid "Pick" msgstr "Pick" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Failed to load images" msgstr[1] "Failed to load %u images" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , #. , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Back" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgid "Quote tweet" msgstr "Quote tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the #. "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "Keep tweet as draft?" msgid "Only one animated GIF file per tweet is allowed." msgstr "Only one animated GIF file per tweet is allowed." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "%d character remaining" msgstr[1] "%d characters remaining" #. TRANSLATORS: Values are current image index (1-based) and total image #. count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Image %d of %d" msgstr[1] "Image %d of %d" msgid "Modify Filter" msgstr "Modify Filter" msgid "Matches" msgstr "Matches" msgid "Doesn't match" msgstr "Doesn't match" msgid "Modify Snippet" msgstr "Modify Snippet" msgid "Snippet can't be empty" msgstr "Snippet can't be empty" msgid "Replacement can't be empty" msgstr "Replacement can't be empty" msgid "Snippet may not contain whitespace" msgstr "Snippet may not contain whitespace" msgid "Snippet already exists" msgstr "Snippet already exists" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders\n" "\n" "The URL \"%s\" will be used instead" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgstr "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Add to or Remove User From List" msgid "You have no lists." msgstr "You have no lists." msgid "About Cawbird" msgstr "About Cawbird" msgid "New Account" msgstr "New Account" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgid "Request PIN" msgstr "Request PIN" msgid "Enter PIN from twitter.com below:" msgstr "Enter PIN from twitter.com below:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirm" msgid "Account Settings" msgstr "Account Settings" msgid "Name" msgstr "Name" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Autostart" msgid "Remove Account" msgstr "Remove Account" msgid "Do you really want to remove this account?" msgstr "Do you really want to remove this account?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys & People" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Body & Clothing" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animals & Nature" msgctxt "emoji category" msgid "Food & Drink" msgstr "Food & Drink" msgctxt "emoji category" msgid "Travel & Places" msgstr "Travel & Places" msgctxt "emoji category" msgid "Activities" msgstr "Activities" msgctxt "emoji category" msgid "Objects" msgstr "Objects" msgctxt "emoji category" msgid "Symbols" msgstr "Symbols" msgctxt "emoji category" msgid "Flags" msgstr "Flags" msgid "No Results Found" msgstr "No Results Found" msgid "Try a different search" msgstr "Try a different search" msgid "Send" msgstr "Send" msgid "Add Media" msgstr "Add Media" msgid "Show favorite images" msgstr "Show favourite images" msgid "Click to edit" msgstr "Click to edit" msgid "Remove this Filter" msgstr "Remove this Filter" msgid "Filtered terms" msgstr "Filtered terms" msgid "Filtered users" msgstr "Filtered users" msgid "You can block users in their profile" msgstr "You can block users in their profile" msgid "Users" msgstr "Users" msgid "Set image description" msgstr "Set image description" msgid "Subscribe" msgstr "Subscribe" msgid "Unsubscribe" msgstr "Unsubscribe" msgid "Subscribers:" msgstr "Subscribers:" msgid "Members:" msgstr "Members:" msgid "Creator:" msgstr "Creator:" msgid "Created at:" msgstr "Created at:" msgid "Edit" msgstr "Edit" msgid "Mode:" msgstr "Mode:" msgid "Description" msgstr "Description" msgid "Settings" msgstr "Settings" msgid "Shortcuts" msgstr "Shortcuts" msgid "About" msgstr "About" msgid "Quit" msgstr "Quit" msgid "Add New Filter" msgstr "Add New Filter" msgid "Regular Expression:" msgstr "Regular Expression:" msgid "Test:" msgstr "Test:" msgid "Add New Snippet" msgstr "Add New Snippet" msgid "Keyword" msgstr "Keyword" msgid "Replacement" msgstr "Replacement" msgid "Create New List" msgstr "Create New List" msgid "Name:" msgstr "Name:" msgid "Create" msgstr "Create" msgid "Write Direct Message" msgstr "Write Direct Message" msgid "Add to/Remove from List" msgstr "Add to/Remove from List" msgid "Blocked" msgstr "Blocked" msgid "Muted" msgstr "Muted" msgid "Retweets disabled" msgstr "Retweets disabled" msgid "More actions" msgstr "More actions" msgid "Follows you" msgstr "Follows you" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Followers" msgid "Following" msgstr "Following" msgid "Use dark theme" msgstr "Use dark theme" msgid "Shortcut key" msgstr "Shortcut key" msgid "Alt" msgstr "Alt" msgid "Ctrl" msgstr "Ctrl" msgid "Shift" msgstr "Shift" msgid "Super" msgstr "Super" msgid "Primary" msgstr "Primary" msgid "Timelines" msgstr "Timelines" msgid "Show inline media" msgstr "Show inline media" msgid "Always show" msgstr "Always show" msgid "Always hide" msgstr "Always hide" msgid "Hide in timeline" msgstr "Hide in timeline" msgid "Auto scroll on new tweets" msgstr "Auto scroll on new tweets" msgid "Double-click activation" msgstr "Double-click activation" msgid "Notifications" msgstr "Notifications" msgid "On New Tweets" msgstr "On New Tweets" msgid "Never" msgstr "Never" msgid "Every" msgstr "Every" msgid "Stack 5" msgstr "Stack 5" msgid "Stack 10" msgstr "Stack 10" msgid "Stack 25" msgstr "Stack 25" msgid "Stack 50" msgstr "Stack 50" msgid "On New Mentions" msgstr "On New Mentions" msgid "On New Messages" msgstr "On New Messages" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "Normal" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "Large" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "X-Large" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "XX-Large" msgid "Tweet scale" msgstr "Tweet scale" msgid "Round avatars" msgstr "Round avatars" msgid "Remove trailing hashtags" msgstr "Remove trailing hashtags" msgid "Remove media links" msgstr "Remove media links" msgid "Hide inappropriate content" msgstr "Hide inappropriate content" msgid "Translation" msgstr "Translation" msgid "Translation service" msgstr "Translation service" msgid "Google" msgstr "Google" msgid "Bing" msgstr "Bing" msgid "DeepL" msgstr "DeepL" msgid "Custom" msgstr "Custom" msgid "Custom translation URL" msgstr "Custom translation URL" msgid "No snippets configured." msgstr "No snippets configured." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "You can activate snippets by writing the keyword and pressing TAB." msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "General" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Compose Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Show Account settings" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Show Accounts Popover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Show Application Settings" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Toggle Topbar" msgctxt "shortcuts window" msgid "Go Back" msgstr "Go Back" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Go Forward" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Go to nth page" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" msgctxt "shortcuts window" msgid "Like" msgstr "Like" msgctxt "shortcuts window" msgid "Reply" msgstr "Reply" msgctxt "shortcuts window" msgid "Quote" msgstr "Quote" msgctxt "shortcuts window" msgid "Show Details" msgstr "Show Details" msgctxt "shortcuts window" msgid "Delete" msgstr "Delete" msgctxt "shortcuts window" msgid "Compose" msgstr "Compose" msgid "Show Emoji Chooser" msgstr "Show Emoji Chooser" msgid "Start new conversation" msgstr "Start new conversation" msgid "With:" msgstr "With:" msgid "Go" msgstr "Go" msgid "Quote" msgstr "Quote" msgid "Retweet tweet" msgstr "Retweet tweet" msgid "Like tweet" msgstr "Like tweet" msgid "Reply to tweet" msgstr "Reply to tweet" msgid "More" msgstr "More" msgid "Translate" msgstr "Translate" msgid "Like" msgstr "Like" msgid "Reply" msgstr "Reply" msgid "Liked" msgstr "Liked" msgid "Retweeted" msgstr "Retweeted" msgid "Unblock" msgstr "Unblock" msgid "Show settings of this account" msgstr "Show settings of this account" msgid "Open in new window" msgstr "Open in new window" msgid "Go to profile" msgstr "Go to profile" msgid "Created" msgstr "Created" msgid "Subscribed to" msgstr "Subscribed to" cawbird-1.4.2/po/eo.po000066400000000000000000000604511416632607600145330ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Iris Ilexiris , 2017-2018 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2018-02-06 18:45+0000\n" "Last-Translator: Iris Ilexiris \n" "Language-Team: Esperanto (http://www.transifex.com/cawbird/cawbird/language/" "eo/)\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Tviterokliento" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird estas denaska GTK+ kliento por Tvitero. Äœi enhavas gravajn eblecojn—" "ekzemple tujmesaÄoj, tvitavizoj, kaj vidigo de interparoloj." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Aliaj eblecoj estas vidigi filmeton kaj plurajn bildojn loke. AnkaÅ­, listoj, " "filtriloj, pluraj kontoj, ktp." msgid "Generic timeline view when using Cawbird" msgstr "Malpreciza kronologovidigo kiam Cawbird uzata" msgid "Typical Twitter profile" msgstr "Malpreciza Tviteroprofilo" msgid "Account settings can be configured" msgstr "Konto agordoj povas agordanta" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Uzi Tviteron simila al normala komputerprogramo" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;tvitero;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Montri agordajn kontojn" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "TujmesaÄoj" #, fuzzy msgid "Select Media" msgstr "Elekti bildon" msgid "Open" msgstr "Malfermi" msgid "Cancel" msgstr "Nuligi" #, fuzzy msgid "Selected file is not an image or video." msgstr "La elektita dosiero ne estas bildo." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "La elektita bildon estas tro granda. La maksimuma dosierlargo por imagoj " "estas %'d MB." #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "La elektita bildon estas tro granda. La maksimuma dosierlargo por imagoj " "estas %'d MB." #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "La elektita bildon estas tro granda. La maksimuma dosierlargo por imagoj " "estas %'d MB." msgid "Insert Emoji" msgstr "Enmeti miensimbolo" msgid "Direct Conversation" msgstr "Tujinterparolo" #, fuzzy msgid "Direct message threads" msgstr "TujmesaÄoj" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nova mesaÄo el %s" msgstr[1] "%d novaj mesaÄoj el %s" #, c-format msgid "New direct message from %s" msgstr "Nova tujmesaÄo el %s" msgid "Direct Messages" msgstr "TujmesaÄoj" #, fuzzy msgid "Liked tweets timeline" msgstr "Favorati tviton" #, fuzzy msgid "Likes" msgstr "Favoratoj" msgid "Add new Filter" msgstr "Aldoni novan filtrilon" msgid "Filters" msgstr "Filtriloj" #, fuzzy msgid "Home timeline" msgstr "KaÅi en kronologio" #, c-format msgid "%s retweeted %s" msgstr "%s retvitis %s" #, c-format msgid "%s tweeted" msgstr "%s tvitis" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nova tvito!" msgstr[1] "%d novaj tvitoj!" msgid "Home" msgstr "Hejmo" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tvitis" msgid "Private" msgstr "Malpublika" msgid "Public" msgstr "Publika" msgid "Lists" msgstr "Listoj" msgid "Show configured accounts" msgstr "Montri agordajn kontojn" msgid "Compose Tweet" msgstr "Skribi tviton" msgid "Add new Account" msgstr "Aldoni novan konton" #, fuzzy msgid "Mentions timeline" msgstr "KaÅi en kronologio" #, c-format msgid "%s mentioned %s" msgstr "%s menciis %s" msgid "Mentions" msgstr "Mencioj" msgid "Suspended Account" msgstr "Ĉi tiu konto ĉesita" msgid "Protected profile" msgstr "Karakterizon malpublikita" #, c-format msgid "Tweet to @%s" msgstr "Tviti al @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tvitis" msgstr[1] "%s tvitis" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Postiras" msgstr[1] "Postiras" #, fuzzy, c-format msgid "Location: %s" msgstr "Avizoj" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "KaÅi en kronologio" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Postiroj" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Postiras" msgid "Protected Profile" msgstr "Karakterizon malpublikita" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Postiroj" msgstr[1] "Postiroj" #, fuzzy msgid "No tweets found" msgstr "Nenio elementoj trovitaj" msgid "No users found" msgstr "Uzantojn netrovitajn" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tviti al @%s" msgid "Search" msgstr "Serĉi" msgid "Load More" msgstr "Åœargi pli" msgid "Could not show tweet" msgstr "Ne povis montri tviton" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retvitoj" msgid "Open in Browser" msgstr "Malfermi en retumilo" msgid "Source" msgstr "Fonto" msgid "Tweet Details" msgstr "Tvitodetaloj" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d nelegita)" msgstr[1] "(%d nelegitaj)" msgid "Delete" msgstr "Forigi" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retviti" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Ĉu vi vere volas forigi ĉi tiun konton?" #, c-format msgid "Block %s" msgstr "Blokigi %s" msgid "This tweet contains images marked as inappropriate" msgstr "Tio ĉi tvito povus havi malkonvenajn bildojn." msgid "Show anyway" msgstr "Montri iel" #, fuzzy msgid "Could not authenticate you" msgstr "Ne povis malfermi %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Uzantojn netrovitajn" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Fasado" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Kodaĵo ekzistas jam" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Nun" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Respondas al %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Respondas al %s kaj %d aligentuloj" msgstr[1] "Respondas al %s kaj %d aligentuloj" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Respondas al %s kaj %s" msgid "Don't have a Twitter account yet?" msgstr "Ĉu vi ne havas Tviterokonton jam?" msgid "Create one" msgstr "Krei konton" #, c-format msgid "Could not open %s" msgstr "Ne povis malfermi %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "MalÄustan PIN-on" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Konto jam uzas" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Elekti bildon" msgid "Unfollow" msgstr "Malpostiri" msgid "Follow" msgstr "Postiri" msgid "Loading…" msgstr "Åœargas…" #, fuzzy msgid "No entries found" msgstr "Uzantojn netrovitajn" msgid "Retry" msgstr "Reprovi" msgid "Copy URL" msgstr "Kopii URL-on" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Konservi kiel…" msgid "Save Video" msgstr "Konservi filmeton" msgid "Save Image" msgstr "Konservi bildon" msgid "Save" msgstr "Konservi" msgid "Select Banner Image" msgstr "Elekti gazetkapon" msgid "Image does not meet minimum size requirements:" msgstr "Bildo ne konsidas minimumajn grandobezonojn" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimuma larÄo: %d bildero" msgstr[1] "Minimuma larÄo: %d bilderoj" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimuma alto: %d bildero" msgstr[1] "Minimuma alto: %d bilderoj" msgid "Pick" msgstr "Elekti" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Reiri" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citi tviton" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Nur unu GIF po tvito permesitas." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Redakti filtrilon" msgid "Matches" msgstr "Egalas" msgid "Doesn't match" msgstr "Ne egalas" msgid "Modify Snippet" msgstr "Redakti kodaĵon" msgid "Snippet can't be empty" msgstr "Kodaĵo ne povas esti malplena" msgid "Replacement can't be empty" msgstr "AnstataÅ­tigo ne povas esti malplena" msgid "Snippet may not contain whitespace" msgstr "Kodaĵo ne povas enhavi blankspacon" msgid "Snippet already exists" msgstr "Kodaĵo ekzistas jam" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Saluton! Vidu ĉi tiun novan version de #Cawbird! \\ (•◡•) / #mojose " "#novaĉiamplibonas" msgid "Add to or Remove User From List" msgstr "Aldoni/forigi uzanton el la listo" msgid "You have no lists." msgstr "Vi havas nenio listojn." msgid "About Cawbird" msgstr "Pri Cawbird" msgid "New Account" msgstr "Nova konto" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Por verigi Cawbird-on, vi bezonas PIN-on el twitter.com por la konto vi " "deziras aldoni." msgid "Request PIN" msgstr "Peti PIN-on" msgid "Enter PIN from twitter.com below:" msgstr "Entajpi PIN-on el twitter.com malsupre:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Konfirmi" msgid "Account Settings" msgstr "Kontoagordoj" msgid "Name" msgstr "Nomo" msgid "Website" msgstr "RetpaÄo" msgid "Autostart" msgstr "AÅ­tomata komenci" #, fuzzy msgid "Remove Account" msgstr "Forigi" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Ĉu vi vere volas forigi ĉi tiun konton?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Miensimboloj kaj homoj" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Korpo kaj vestoj" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Bestoj kaj naturo" msgctxt "emoji category" msgid "Food & Drink" msgstr "Nutraĵo" msgctxt "emoji category" msgid "Travel & Places" msgstr "VojaÄo kaj lokoj" msgctxt "emoji category" msgid "Activities" msgstr "Agadoj" msgctxt "emoji category" msgid "Objects" msgstr "Objektoj" msgctxt "emoji category" msgid "Symbols" msgstr "Simboloj" msgctxt "emoji category" msgid "Flags" msgstr "Flagoj" msgid "No Results Found" msgstr "Nenio trovitaj resultoj" msgid "Try a different search" msgstr "Provu alian serĉon" msgid "Send" msgstr "Sendi" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Montri favoratbildojn" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtriloj" #, fuzzy msgid "Filtered users" msgstr "Filtriloj" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Uzantoj" #, fuzzy msgid "Set image description" msgstr "Priskribo" msgid "Subscribe" msgstr "Subskribi" msgid "Unsubscribe" msgstr "Malsubskribi" msgid "Subscribers:" msgstr "Subskriboj:" msgid "Members:" msgstr "Membroj:" msgid "Creator:" msgstr "Verkanto:" msgid "Created at:" msgstr "Kreita je:" msgid "Edit" msgstr "Redakti" msgid "Mode:" msgstr "Moduso:" msgid "Description" msgstr "Priskribo" msgid "Settings" msgstr "Agordoj" msgid "Shortcuts" msgstr "Kurtvojoj" msgid "About" msgstr "Pri" msgid "Quit" msgstr "Ĉesi" msgid "Add New Filter" msgstr "Aldoni novan filtrilon" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Aldoni novan kodaĵon" msgid "Keyword" msgstr "Ĉefvorto" msgid "Replacement" msgstr "AnstataÅ­igo" msgid "Create New List" msgstr "Krei novan liston" msgid "Name:" msgstr "Nomo:" msgid "Create" msgstr "Krei" msgid "Write Direct Message" msgstr "Skribi tujmesaÄon" msgid "Add to/Remove from List" msgstr "Aldoni/forigi el listo" msgid "Blocked" msgstr "Blokigita" msgid "Muted" msgstr "Mutigita" msgid "Retweets disabled" msgstr "Revtitoj malebligita" #, fuzzy msgid "More actions" msgstr "Mencioj" msgid "Follows you" msgstr "Postiras vin" msgid "Tweets" msgstr "Tvitoj" msgid "Followers" msgstr "Postiroj" msgid "Following" msgstr "Postiras" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Kurtvojoj" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "KaÅi en kronologio" msgid "Show inline media" msgstr "Montri enpaÄajn mediojn" msgid "Always show" msgstr "Montri ĉiam" msgid "Always hide" msgstr "KaÅi ĉiam" msgid "Hide in timeline" msgstr "KaÅi en kronologio" msgid "Auto scroll on new tweets" msgstr "Rulumi automatan je novaj tvitoj" msgid "Double-click activation" msgstr "Aktivigi per duoblan klakon" msgid "Notifications" msgstr "Avizoj" msgid "On New Tweets" msgstr "Kiam okazas novan tviton" msgid "Never" msgstr "Neniam" msgid "Every" msgstr "Ĉia" msgid "Stack 5" msgstr "Stakos 5" msgid "Stack 10" msgstr "Stakos 10" msgid "Stack 25" msgstr "Stakos 25" msgid "Stack 50" msgstr "Stakos 50" msgid "On New Mentions" msgstr "Kiam okazas novan mencion" msgid "On New Messages" msgstr "Kiam okazas novan mesaÄon" msgid "Interface" msgstr "Fasado" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tvitoj" msgid "Round avatars" msgstr "Rondajn profilbildojn" msgid "Remove trailing hashtags" msgstr "Forigi malantaÅ­irajn haketojn" msgid "Remove media links" msgstr "Forigi retligojn por aÅ­dvidaĵoj" msgid "Hide inappropriate content" msgstr "KaÅi malkonvenan enhavon" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nenio kodaĵoj agordis." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Vi povas puÅu la taban klavon ekspansii kodaĵojn." msgid "Snippets" msgstr "Kodaĵoj" msgctxt "shortcuts window" msgid "General" msgstr "Äœenerala" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Skribi tviton" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Montri kontagordojn" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Montri Åvebajn kontojn konsilojn" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Montri programagordojn" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Baskuligi supronavigadon" msgctxt "shortcuts window" msgid "Go Back" msgstr "Reiri" msgctxt "shortcuts window" msgid "Go Forward" msgstr "AntaÅ­iri" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Iri al la n-a paÄo" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tvitoj" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retviti" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favorati" msgctxt "shortcuts window" msgid "Reply" msgstr "Respondi" msgctxt "shortcuts window" msgid "Quote" msgstr "Citi" msgctxt "shortcuts window" msgid "Show Details" msgstr "Montri detalojn" msgctxt "shortcuts window" msgid "Delete" msgstr "Forigi" msgctxt "shortcuts window" msgid "Compose" msgstr "Skribi" msgid "Show Emoji Chooser" msgstr "Montri miensimbolilon" msgid "Start new conversation" msgstr "Komenci novan interparolon" msgid "With:" msgstr "Kun:" msgid "Go" msgstr "Iri" msgid "Quote" msgstr "Citi" msgid "Retweet tweet" msgstr "Retviti tviton" #, fuzzy msgid "Like tweet" msgstr "Favorati tviton" msgid "Reply to tweet" msgstr "Respondi al tvito" msgid "More" msgstr "Pli" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorati" msgid "Reply" msgstr "Respondi" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retviti" msgid "Unblock" msgstr "Malblokigi" msgid "Show settings of this account" msgstr "Montri agordojn por ĉi tiu konto" msgid "Open in new window" msgstr "Malfermi en nova fenestro" msgid "Go to profile" msgstr "Eniri al karakterizo" msgid "Created" msgstr "Kreita" msgid "Subscribed to" msgstr "Subskribis al" #~ msgid "Replying to" #~ msgstr "Respondas al" #~ msgid "and" #~ msgstr "kaj" #~ msgid "Actions" #~ msgstr "Agoj" #~ msgid "Add Image" #~ msgstr "Aldoni bildon" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Postiroj" #~ msgid "List" #~ msgstr "Listo" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Vi ne estas rajtigitan. Plejparto, ĉi tiu signifas eraro kun la Tvitero-" #~ "serviloj. Provi denove malfruan." #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Nepovus elÅuti tvitojn" cawbird-1.4.2/po/es.po000066400000000000000000000573041416632607600145420ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Alejandro Moreno , 2016 # auroszx , 2014 # baedert , 2016 # Carlos C Soto , 2015 # Guillem Arias , 2016 # Jose G. Jimenez S. , 2014-2015 # Jose Manuel Calero Gonzalez , 2014-2015 # Sebastian Reyes , 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Spanish (http://www.transifex.com/cawbird/cawbird/language/" "es/)\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliente Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird es un cliente nativo GTK+ para Twitter que provee funciones útiles " "como Mensajes Directos (DMs), notificación de tweets, y ver conversaciones." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Las características adicionales incluyen la visualización local de los " "vídeos, múltiples imágenes en línea, listas, filtros, múltiples cuentas, etc." msgid "Generic timeline view when using Cawbird" msgstr "Vista de timeline genérica al usar Cawbird" msgid "Typical Twitter profile" msgstr "Perfil de Twitter típico" msgid "Account settings can be configured" msgstr "Los ajustes de la cuenta pueden ser configurados" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Usa Twitter desde una aplicación de escritorio normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar cuentas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensajes directos" #, fuzzy msgid "Select Media" msgstr "Seleccionar imagen" msgid "Open" msgstr "Abrir" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "La imagen seleccionada es demasiado grande. El tamaño máximo por imagen es " "%'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "La imagen seleccionada es demasiado grande. El tamaño máximo por imagen es " "%'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "La imagen seleccionada es demasiado grande. El tamaño máximo por imagen es " "%'d MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversación directa" #, fuzzy msgid "Direct message threads" msgstr "Mensajes directos" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nuevo mensaje de %s" msgstr[1] "%d nuevos mensajes de %s" #, c-format msgid "New direct message from %s" msgstr "Nuevo mensaje directo de %s" msgid "Direct Messages" msgstr "Mensajes directos" #, fuzzy msgid "Liked tweets timeline" msgstr "Añadir a favoritos" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "Añadir nuevo filtro" msgid "Filters" msgstr "Filtros" #, fuzzy msgid "Home timeline" msgstr "Esconder en la timeline" #, c-format msgid "%s retweeted %s" msgstr "%s retuiteado %s" #, c-format msgid "%s tweeted" msgstr "%s tuiteado" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nuevo Tweet!" msgstr[1] "%d nuevos Tweets!" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tuiteado" msgid "Private" msgstr "Privado" msgid "Public" msgstr "Público" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "Mostrar cuentas configuradas" msgid "Compose Tweet" msgstr "Escribir Tweet" msgid "Add new Account" msgstr "Añadir nueva cuenta" #, fuzzy msgid "Mentions timeline" msgstr "Esconder en la timeline" #, c-format msgid "%s mentioned %s" msgstr "%s mencionó a %s" msgid "Mentions" msgstr "Menciones" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "perfil protegido" #, c-format msgid "Tweet to @%s" msgstr "Tweet para @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tuiteado" msgstr[1] "%s tuiteado" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Siguiendo" msgstr[1] "Siguiendo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificaciones" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Esconder en la timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Siguiendo" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "No se han encontrado entradas" msgid "No users found" msgstr "No se han encontrado usuarios" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet para @%s" msgid "Search" msgstr "Buscar" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "No se puede mostrar este Tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Abrir en navegador" msgid "Source" msgstr "Fuente" msgid "Tweet Details" msgstr "Detalles del tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d sin leer)" msgstr[1] "(%d sin leer)" msgid "Delete" msgstr "Eliminar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retwittear" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "¿Seguro que quieres eliminar esta cuenta?" #, c-format msgid "Block %s" msgstr "Bloquear a %s" msgid "This tweet contains images marked as inappropriate" msgstr "" "Este tweet contiene imágenes que pueden herir la sensibilidad de algunas " "personas" msgid "Show anyway" msgstr "Mostrar de todas formas" #, fuzzy msgid "Could not authenticate you" msgstr "No se puede abrir %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "No se han encontrado usuarios" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfaz" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "El snippet ya existe" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ahora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Responder" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Crear una" #, c-format msgid "Could not open %s" msgstr "No se puede abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN incorrecto" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "La cuenta ya está en uso" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Seleccionar imagen" msgid "Unfollow" msgstr "Dejar de seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "No se han encontrado usuarios" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "Copiar URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Guardar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Atrás" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar Filtro" msgid "Matches" msgstr "Concuerda" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "Modificar snippet" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "El snippet puede no contener espacio en blanco" msgid "Snippet already exists" msgstr "El snippet ya existe" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "¡Ey, revisa esta nueva versión de #Codebird! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Agregar o Quitar usuario desde la lista" msgid "You have no lists." msgstr "No tienes listas." msgid "About Cawbird" msgstr "Sobre Cawbird" msgid "New Account" msgstr "Nueva cuenta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Solicitar PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Configuración de cuentas" msgid "Name" msgstr "Nombre" msgid "Website" msgstr "Página Web" msgid "Autostart" msgstr "Autoiniciar" #, fuzzy msgid "Remove Account" msgstr "Eliminar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "¿Seguro que quieres eliminar esta cuenta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtros" #, fuzzy msgid "Filtered users" msgstr "Filtros" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuarios" #, fuzzy msgid "Set image description" msgstr "Descripción" msgid "Subscribe" msgstr "Suscribirse" msgid "Unsubscribe" msgstr "Anular suscripción" msgid "Subscribers:" msgstr "Suscriptores:" msgid "Members:" msgstr "Miembros:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creada el:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "Descripción" msgid "Settings" msgstr "Preferencias" msgid "Shortcuts" msgstr "Atajos" msgid "About" msgstr "Acerca de" msgid "Quit" msgstr "Salir" msgid "Add New Filter" msgstr "Añadir nuevo filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Añadir nuevo fragmento" msgid "Keyword" msgstr "Clave" msgid "Replacement" msgstr "Reemplazo" msgid "Create New List" msgstr "Crear lista nueva" msgid "Name:" msgstr "Nombre:" msgid "Create" msgstr "Crear" msgid "Write Direct Message" msgstr "Enviar mensaje directo" msgid "Add to/Remove from List" msgstr "Añadir/Eliminar de la lista" msgid "Blocked" msgstr "Bloqueado" msgid "Muted" msgstr "Slienciado" msgid "Retweets disabled" msgstr "Retweets desactivados" #, fuzzy msgid "More actions" msgstr "Menciones" msgid "Follows you" msgstr "Te sigue" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Siguiendo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atajos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Esconder en la timeline" msgid "Show inline media" msgstr "Mostrar media inline" msgid "Always show" msgstr "Mostrar siempre" msgid "Always hide" msgstr "Ocultar siempre" msgid "Hide in timeline" msgstr "Esconder en la timeline" msgid "Auto scroll on new tweets" msgstr "Desplazar automaticamente al cargar nuevos tweets" msgid "Double-click activation" msgstr "Activación Doble-click" msgid "Notifications" msgstr "Notificaciones" msgid "On New Tweets" msgstr "Para tweets nuevos" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Todos" msgid "Stack 5" msgstr "Cada 5" msgid "Stack 10" msgstr "Cada 10" msgid "Stack 25" msgstr "Cada 25" msgid "Stack 50" msgstr "Cada 50" msgid "On New Mentions" msgstr "Para menciones nuevas" msgid "On New Messages" msgstr "Para mensajes nuevos" msgid "Interface" msgstr "Interfaz" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Avatares Redondeados" msgid "Remove trailing hashtags" msgstr "Eliminar hashtags posteriores" msgid "Remove media links" msgstr "Eliminar enlaces" msgid "Hide inappropriate content" msgstr "Ocultar contenido que pueda herir la sensibilidad de algunas personas" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "No hay fragmentos configurados" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Puedes activar los fragmentos escribiendo la clave y presionando TAB" msgid "Snippets" msgstr "Fragmentos" msgctxt "shortcuts window" msgid "General" msgstr "General" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Escribir Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostrar ajustes de la cuenta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostrar ventana de cuentas" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostrar ajustes de la aplicación" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Alternar TopBar" msgctxt "shortcuts window" msgid "Go Back" msgstr "Volver" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Continuar" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ir a la página #" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retwittear" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Me gusta" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "Citar" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostrar detalles" msgctxt "shortcuts window" msgid "Delete" msgstr "Eliminar" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Iniciar conversación nueva" msgid "With:" msgstr "Con:" msgid "Go" msgstr "Ir" msgid "Quote" msgstr "Citar" msgid "Retweet tweet" msgstr "Hacer retweet" #, fuzzy msgid "Like tweet" msgstr "Añadir a favoritos" msgid "Reply to tweet" msgstr "Responder" msgid "More" msgstr "Mas" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorito" msgid "Reply" msgstr "Responder" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retwittear" msgid "Unblock" msgstr "Desbloqueo" msgid "Show settings of this account" msgstr "Mostrar ajustes de la cuenta" msgid "Open in new window" msgstr "Abrir en una nueva ventana" msgid "Go to profile" msgstr "Ir al perfil" msgid "Created" msgstr "Creada" msgid "Subscribed to" msgstr "Suscrito a" #~ msgid "Actions" #~ msgstr "Acciones" #~ msgid "Add Image" #~ msgstr "Añadir imagen" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "No se pudieron cargar los tweets" cawbird-1.4.2/po/es_419.po000066400000000000000000000512421416632607600151320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Enrique GF , 2017 # esteban cap , 2016 # Guillermo Peralta , 2015 # Jose G. Jimenez S. , 2014 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Spanish (Latin America) (http://www.transifex.com/cawbird/" "cawbird/language/es_419/)\n" "Language: es_419\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliente de Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Corebid es un cliente nativo GTK+ de twitter que permite realizar " "actividades diarias como enviar mensajes directos (MD), notificaciones de " "tweets, vista de menciones, y más." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Las características adicionales incluyen visualización de videos e imágenes " "en la aplicación, así como uso de listas, filtros, multiples cuentas, etc." msgid "Generic timeline view when using Cawbird" msgstr "Vista general del timeline al usar Cawbird" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "Los ajustes de la cuenta pueden ser configurados." msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensajes Directos" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "Mensajes Directos" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nuevos Mensajes de %s" msgstr[1] "%d nuevos Mensajes de %s" #, c-format msgid "New direct message from %s" msgstr "Nuevo mensaje directo de %s" msgid "Direct Messages" msgstr "Mensajes Directos" #, fuzzy msgid "Liked tweets timeline" msgstr "Favoritos" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s retweeteados %s" #, c-format msgid "%s tweeted" msgstr "%s tweeteado" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nuevos Tweets!" msgstr[1] "%d nuevos Tweets!" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweeteado" msgid "Private" msgstr "" msgid "Public" msgstr "" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "Escribir Tweet" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "Menciones" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "Menciones" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "" #, c-format msgid "Tweet to @%s" msgstr "" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweeteado" msgstr[1] "%s tweeteado" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "" msgstr[1] "" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificaciones" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Menciones" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Dejar de seguir" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Dejar de seguir" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Dejar de seguir" msgstr[1] "Dejar de seguir" #, fuzzy msgid "No tweets found" msgstr "No se encontraron entradas" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "Buscar" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Abrir en el Navegador" msgid "Source" msgstr "Fuente" msgid "Tweet Details" msgstr "" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgid "Delete" msgstr "" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweets" msgid "Are you sure you want to delete this tweet?" msgstr "" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ahora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, c-format msgid "Replying to %s" msgstr "" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Crear una" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN Incorrecto" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "La cuenta ya está en uso" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Dejar de seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "No se encontraron entradas" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Guardar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar Filtro" msgid "Matches" msgstr "Coincidencias" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "Agregar o Quitar usuario desde la lista" msgid "You have no lists." msgstr "Tu no tienes listas." msgid "About Cawbird" msgstr "Acerca de Cawbird" msgid "New Account" msgstr "" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "" msgid "Account Settings" msgstr "" msgid "Name" msgstr "" msgid "Website" msgstr "" msgid "Autostart" msgstr "" msgid "Remove Account" msgstr "" msgid "Do you really want to remove this account?" msgstr "" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "" msgid "Unsubscribe" msgstr "" msgid "Subscribers:" msgstr "" msgid "Members:" msgstr "" msgid "Creator:" msgstr "" msgid "Created at:" msgstr "" msgid "Edit" msgstr "" msgid "Mode:" msgstr "" msgid "Description" msgstr "" msgid "Settings" msgstr "Configuración" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Acerca de" msgid "Quit" msgstr "Salir" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "" msgid "Name:" msgstr "" msgid "Create" msgstr "" msgid "Write Direct Message" msgstr "" msgid "Add to/Remove from List" msgstr "" msgid "Blocked" msgstr "" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" #, fuzzy msgid "More actions" msgstr "Menciones" msgid "Follows you" msgstr "" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "" msgid "Following" msgstr "" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Menciones" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "Notificaciones" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "" msgid "Go" msgstr "" msgid "Quote" msgstr "" msgid "Retweet tweet" msgstr "" msgid "Like tweet" msgstr "" msgid "Reply to tweet" msgstr "" msgid "More" msgstr "" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweets" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "" msgid "Open in new window" msgstr "" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Creado" msgid "Subscribed to" msgstr "Subscrito a" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "No fue posible cargar los tweets" cawbird-1.4.2/po/es_MX.po000066400000000000000000000542441416632607600151460ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Enrique GF , 2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Spanish (Mexico) (http://www.transifex.com/cawbird/cawbird/" "language/es_MX/)\n" "Language: es_MX\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Cliente de Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird es un cliente nativo de twitter el cual provee características " "vitales como Mensajes Directos (DM's), notificaciones de tuits, vistas de " "conversación." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Características adicionales incluye visualización de vídeos local, imágenes " "múltiples, Listas, Filtros, múltiples cuentas, etc." msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar cuentas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensajes Directos" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversación Directa" #, fuzzy msgid "Direct message threads" msgstr "Mensajes Directos" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nuevo mensaje de %s" msgstr[1] "%d nuevos mensajes de %s" #, c-format msgid "New direct message from %s" msgstr "Nuevo mensaje directo de %s" msgid "Direct Messages" msgstr "Mensajes Directos" #, fuzzy msgid "Liked tweets timeline" msgstr "Favorito" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s retuiteado %s" #, c-format msgid "%s tweeted" msgstr "%s tuiteado" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nuevo tuit" msgstr[1] "%d nuevos tuits" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tuiteado" msgid "Private" msgstr "Privado" msgid "Public" msgstr "Público" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "Mostrar cuentas configuradas" msgid "Compose Tweet" msgstr "Escribir Tuit" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "Menciones" #, c-format msgid "%s mentioned %s" msgstr "%s mencionó a %s" msgid "Mentions" msgstr "Menciones" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Perfil protegido" #, c-format msgid "Tweet to @%s" msgstr "Tuitear a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tuiteado" msgstr[1] "%s tuiteado" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Siguiendo" msgstr[1] "Siguiendo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificaciones" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Menciones" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Siguiendo" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "No se encontraron entradas" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tuitear a @%s" msgid "Search" msgstr "Buscar" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "No se puede mostrar el tuit" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retuits" msgid "Open in Browser" msgstr "Abrir en Navegador" msgid "Source" msgstr "Origen" msgid "Tweet Details" msgstr "Detalles de Tuit" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "%d sin leer" msgstr[1] "%d sin leer" msgid "Delete" msgstr "Borrar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retuiteado" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "¿Realmente quieres borrar esta cuenta?" #, c-format msgid "Block %s" msgstr "Bloquear a %s" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "No se puede abrir %s" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interface" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ahora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Responder" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Crear Una" #, c-format msgid "Could not open %s" msgstr "No se puede abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN Incorrecto" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Cuenta en uso" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Dejar de Seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "No se encontraron entradas" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Guardar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Regresar" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar Tuit" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar Filtro" msgid "Matches" msgstr "Coincidencias" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "¡Hey, revisa esta nueva versión de #Cawbird! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Agregar o Quitar Usuario de la Lista" msgid "You have no lists." msgstr "No tienes listas." msgid "About Cawbird" msgstr "Acerca de Cawbird" msgid "New Account" msgstr "Nueva Cuenta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Solicitar PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Parámetros de cuenta" msgid "Name" msgstr "Nombre" msgid "Website" msgstr "Sitio Web" msgid "Autostart" msgstr "Autoiniciar" #, fuzzy msgid "Remove Account" msgstr "Borrar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "¿Realmente quieres borrar esta cuenta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuarios" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Suscribir" msgid "Unsubscribe" msgstr "De suscribir " msgid "Subscribers:" msgstr "Suscriptores:" msgid "Members:" msgstr "Mienbros:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creado en:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "" msgid "Settings" msgstr "Parámetros" msgid "Shortcuts" msgstr "Atajos" msgid "About" msgstr "Acerca de" msgid "Quit" msgstr "Quitar" msgid "Add New Filter" msgstr "Agregar nuevo filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Agregar Nuevo Snippet" msgid "Keyword" msgstr "Keyword" msgid "Replacement" msgstr "Reemplazar" msgid "Create New List" msgstr "crear Nueva Lista" msgid "Name:" msgstr "Nombre:" msgid "Create" msgstr "Crear" msgid "Write Direct Message" msgstr "Escribir Mensaje Directo" msgid "Add to/Remove from List" msgstr "Agregar/quitar de la Lista" msgid "Blocked" msgstr "Bloqueado" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "Retuits desactivados" #, fuzzy msgid "More actions" msgstr "Menciones" msgid "Follows you" msgstr "Siguiéndote" msgid "Tweets" msgstr "Tuits" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Siguiendo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atajos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Menciones" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Auto avance en nuevos tuits" msgid "Double-click activation" msgstr "Activación doble click" msgid "Notifications" msgstr "Notificaciones" msgid "On New Tweets" msgstr "Nuevos Tuits" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Siempre" msgid "Stack 5" msgstr "Mostrar 5" msgid "Stack 10" msgstr "Mostrar 10" msgid "Stack 25" msgstr "Mostrar 25" msgid "Stack 50" msgstr "Mostrar 50" msgid "On New Mentions" msgstr "Nuevas Menciones" msgid "On New Messages" msgstr "Nuevos Mensajes" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tuits" msgid "Round avatars" msgstr "Avatares redondos" msgid "Remove trailing hashtags" msgstr "Remover hashtags de rastreo" msgid "Remove media links" msgstr "Remover links de contenido" msgid "Hide inappropriate content" msgstr "Ocultar contenido inapropidado" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "No hay snippets configurados" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Puedes activar snippets escribiendo una palabra y presionando la tecla TAB" msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "General" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Escribir Tuit" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostrar Parámetros de Cuenta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostrar Cuentas" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostrar Parámetros de la Aplicación" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Cambiar Barra Superior" msgctxt "shortcuts window" msgid "Go Back" msgstr "ir Atrás" msgctxt "shortcuts window" msgid "Go Forward" msgstr "ir Adelante" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "ir a Página" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tuits" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retuiteado" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favorito" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostrar Detalles" msgctxt "shortcuts window" msgid "Delete" msgstr "Borrar" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "iniciar nueva conversación" msgid "With:" msgstr "Con:" msgid "Go" msgstr "ir" msgid "Quote" msgstr "Citar" msgid "Retweet tweet" msgstr "Retuitear" #, fuzzy msgid "Like tweet" msgstr "Favorito" msgid "Reply to tweet" msgstr "Responder" msgid "More" msgstr "Más" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorito" msgid "Reply" msgstr "Respuesta" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retuiteado" msgid "Unblock" msgstr "Desbloquear" msgid "Show settings of this account" msgstr "Mostrar parámetros de esta cuenta" msgid "Open in new window" msgstr "Abrir en nueva ventana" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Creado" msgid "Subscribed to" msgstr "Suscribir a" #~ msgid "Actions" #~ msgstr "Acciones" #~ msgid "Add Image" #~ msgstr "Agregar imágen" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "No fue posible cargar los tweets" cawbird-1.4.2/po/es_VE.po000066400000000000000000000537201416632607600151320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Jerry Anselmi , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Spanish (Venezuela) (http://www.transifex.com/cawbird/cawbird/" "language/es_VE/)\n" "Language: es_VE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Cliente de Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird es un cliente nativo GTK+ de Twitter que proporciona funciones " "vitales tales como mensajes directos (DM), notificaciones, vistas de " "conversación." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Las características adicionales incluyen la visión local de los vídeos, " "múltiples imágenes en línea, listas, filtros, múltiples cuentas, etc." msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar cuentas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensajes directos" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversación directa" #, fuzzy msgid "Direct message threads" msgstr "Mensajes directos" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nuevo Mensaje de %s" msgstr[1] "%d nuevos Mensajes de %s" #, c-format msgid "New direct message from %s" msgstr "Nuevo mensaje directo de %s" msgid "Direct Messages" msgstr "Mensajes directos" #, fuzzy msgid "Liked tweets timeline" msgstr "Tweet favorito" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s retweeteado %s" #, c-format msgid "%s tweeted" msgstr "%s tweeteado" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nuevo Tweet!" msgstr[1] "%d nuevos Tweets!" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweeteado" msgid "Private" msgstr "Privado" msgid "Public" msgstr "Publico" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "Mostrar cuentas configuradas" msgid "Compose Tweet" msgstr "Hacer un Tweet" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "Menciones" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "Menciones" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Perfil protegido" #, c-format msgid "Tweet to @%s" msgstr "Tweet a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweeteado" msgstr[1] "%s tweeteado" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Siguiendo" msgstr[1] "Siguiendo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificaciones" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Menciones" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Siguiendo" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "No se encontraron entradas" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet a @%s" msgid "Search" msgstr "Buscar" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "No se pudo mostrar el tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Abrir en un navegador" msgid "Source" msgstr "Fuente" msgid "Tweet Details" msgstr "Detalles del tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d sin leer)" msgstr[1] "(%d sin leer)" msgid "Delete" msgstr "Borrar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweets" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "¿Estas seguro de que quieres eliminar esta cuenta?" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "No se pudo abrir %s" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfaz" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ahora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Responder tweet" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Crear una" #, c-format msgid "Could not open %s" msgstr "No se pudo abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN incorrecto" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "La cuenta ya esta siendo usada" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Dejar de seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "No se encontraron entradas" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Guardar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Volver" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Cita de tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar filtro" msgid "Matches" msgstr "Coincidencias" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hey, echa un vistazo a esta nueva versión #Cawbird! \\ (• â—¡ •) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Agregar o eliminar usuario de la lista" msgid "You have no lists." msgstr "No tienes listas." msgid "About Cawbird" msgstr "Acerca de Cawbird" msgid "New Account" msgstr "Nueva cuenta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Solicitud de PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Configuraciones de la cuenta" msgid "Name" msgstr "Nombre" msgid "Website" msgstr "Sitio Web" msgid "Autostart" msgstr "inicio automatico" #, fuzzy msgid "Remove Account" msgstr "Borrar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "¿Estas seguro de que quieres eliminar esta cuenta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "usuarios" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Suscribirse" msgid "Unsubscribe" msgstr "Darse de baja" msgid "Subscribers:" msgstr "Suscriptores:" msgid "Members:" msgstr "Miembros:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creado en:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "" msgid "Settings" msgstr "Ajustes" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Acerca de" msgid "Quit" msgstr "Salir" msgid "Add New Filter" msgstr "Agregar un nuevo filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Agregar un nuevo fragmento" msgid "Keyword" msgstr "Palabra clave" msgid "Replacement" msgstr "Reemplazo" msgid "Create New List" msgstr "Crear una nueva lista" msgid "Name:" msgstr "Nombre:" msgid "Create" msgstr "Crear" msgid "Write Direct Message" msgstr "Escribir un mensaje directo" msgid "Add to/Remove from List" msgstr "Agregar/Eliminar de la lista" msgid "Blocked" msgstr "Bloqueado" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "Retweets desabilitado" #, fuzzy msgid "More actions" msgstr "Menciones" msgid "Follows you" msgstr "Le sigue" msgid "Tweets" msgstr "trailing hashtags" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Siguiendo" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Menciones" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Desplazamiento automático con nuevos tweets" msgid "Double-click activation" msgstr "Activación con doble clic" msgid "Notifications" msgstr "Notificaciones" msgid "On New Tweets" msgstr "En nuevos Tweets" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Cada" msgid "Stack 5" msgstr "Grupo de 5" msgid "Stack 10" msgstr "Grupo de 10" msgid "Stack 25" msgstr "Grupo de 25" msgid "Stack 50" msgstr "Grupo de 50" msgid "On New Mentions" msgstr "En nuevas menciones" msgid "On New Messages" msgstr "En nuevos mensajes" msgid "Interface" msgstr "Interfaz" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "trailing hashtags" msgid "Round avatars" msgstr "Redondear aavaataares" msgid "Remove trailing hashtags" msgstr "Eliminar trailing hashtags" msgid "Remove media links" msgstr "Eliminar enlaces de imagenes, videos y audios" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "No hay fragmentos configurados." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Puede activar fragmentos escribiendo la palabra clave y presionar TAB." msgid "Snippets" msgstr "Fragmento" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Iniciar una nueva conversación" msgid "With:" msgstr "Con:" msgid "Go" msgstr "Ir" msgid "Quote" msgstr "Cita" msgid "Retweet tweet" msgstr "Retweet tweet" #, fuzzy msgid "Like tweet" msgstr "Tweet favorito" msgid "Reply to tweet" msgstr "Responder tweet" msgid "More" msgstr "Más" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorito" msgid "Reply" msgstr "Responder" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweets" msgid "Unblock" msgstr "Desbloquear" msgid "Show settings of this account" msgstr "Mostrar los ajustes de esta cuenta" msgid "Open in new window" msgstr "Abrir en una nueva ventana" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Creado" msgid "Subscribed to" msgstr "Suscrito a" #~ msgid "Actions" #~ msgstr "Acciones" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/fa.po000066400000000000000000000515641416632607600145230ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # amraei , 2014 # Morteza Parvini , 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Persian (http://www.transifex.com/cawbird/cawbird/language/" "fa/)\n" "Language: fa\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "پیام مستقیم" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "انصراÙ" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "پیام مستقیم" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" #, c-format msgid "New direct message from %s" msgstr "" msgid "Direct Messages" msgstr "پیام مستقیم" #, fuzzy msgid "Liked tweets timeline" msgstr "توییت های مورد علاقه" #, fuzzy msgid "Likes" msgstr "علاقه مندی ها" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "" #, c-format msgid "%s tweeted" msgstr "%s توییت شد" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgid "Home" msgstr "خانه" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s توییت شد" msgid "Private" msgstr "خصوصی" msgid "Public" msgstr "عمومی" msgid "Lists" msgstr "لیست ها" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "توییت کنید" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "اشاره ها" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "اشاره ها" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Ù¾Ø±ÙˆÙØ§ÛŒÙ„ Ù…Ø­Ø§ÙØ¸Øª شده" #, c-format msgid "Tweet to @%s" msgstr "" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s توییت شد" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "" #, fuzzy, c-format msgid "Location: %s" msgstr "اعلان ها" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "اشاره ها" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "دنبال نکنید" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "دنبال نکنید" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "دنبال نکنید" #, fuzzy msgid "No tweets found" msgstr "چیزی ÛŒØ§ÙØª نشد!" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "جستجو" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "توییت های مجدد" msgid "Open in Browser" msgstr "توی مرورگر باز بشه" msgid "Source" msgstr "منبع" msgid "Tweet Details" msgstr "" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgid "Delete" msgstr "حذÙ" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "بازتوییت" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "آیا از پاک‌کردن این حساب، اطمینان دارید؟" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "رابط کاربری" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "اکنون" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "پاسخ به توییت" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN اشتباهه" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "دنبال نکنید" msgid "Follow" msgstr "دنبال کردن" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "چیزی ÛŒØ§ÙØª نشد!" msgid "Retry" msgstr "" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "ذخیره سازی" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "بازگشت" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "" msgid "Matches" msgstr "مطابقت داره" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "اضاÙÙ‡ کردن به/ حذ٠از لیست" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "درباره Cawbird" msgid "New Account" msgstr "" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "تایید" msgid "Account Settings" msgstr "تنظیمات حساب" msgid "Name" msgstr "نام" msgid "Website" msgstr "وب‌سایت" msgid "Autostart" msgstr "" msgid "Remove Account" msgstr "" #, fuzzy msgid "Do you really want to remove this account?" msgstr "آیا از پاک‌کردن این حساب، اطمینان دارید؟" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "ارسال Ú©Ù†" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "کاربران" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "مشترک شو" msgid "Unsubscribe" msgstr "لغو اشتراک" msgid "Subscribers:" msgstr "مشترکین:" msgid "Members:" msgstr "اعضا:" msgid "Creator:" msgstr "ایجاد کننده" msgid "Created at:" msgstr "ایجاد شده در:" msgid "Edit" msgstr "ویرایش" msgid "Mode:" msgstr "حالت:" msgid "Description" msgstr "" msgid "Settings" msgstr "تنظیمات" msgid "Shortcuts" msgstr "" msgid "About" msgstr "درباره" msgid "Quit" msgstr "خروج" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "کلیدواژه" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "یه لیست جدید ایجاد کنید" msgid "Name:" msgstr "نام" msgid "Create" msgstr "ایجاد کنید" msgid "Write Direct Message" msgstr "یه پیام مستقیم بنویسید" msgid "Add to/Remove from List" msgstr "اضاÙÙ‡ کردن به/ حذ٠از لیست" msgid "Blocked" msgstr "بلاک شده" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" #, fuzzy msgid "More actions" msgstr "اشاره ها" msgid "Follows you" msgstr "شمال رو دنبال Ù…ÛŒ کنه" msgid "Tweets" msgstr "توییت ها" msgid "Followers" msgstr "" msgid "Following" msgstr "" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "اشاره ها" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "اعلان ها" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "هرگز" msgid "Every" msgstr "هرکسی" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "رابط کاربری" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "توییت ها" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "عمومی" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "توییت کنید" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "نمایش تنظیمات حساب" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "نمایش تنظیمات برنامه" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "بازتوییت" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "پاسخ دادن" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "جزییات" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "همراه با:" msgid "Go" msgstr "برو" msgid "Quote" msgstr "نقل قول" msgid "Retweet tweet" msgstr "بازنشر توییت" #, fuzzy msgid "Like tweet" msgstr "توییت های مورد علاقه" msgid "Reply to tweet" msgstr "پاسخ به توییت" msgid "More" msgstr "بیشتر" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "بازتوییت" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "" msgid "Open in new window" msgstr "" msgid "Go to profile" msgstr "" msgid "Created" msgstr "ایجاد شد" msgid "Subscribed to" msgstr "مشترک شد" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/fi.po000066400000000000000000000614341416632607600145300ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Jiri Grönroos , 2014-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 17:13+0000\n" "Last-Translator: Jiri Grönroos \n" "Language-Team: Finnish (http://www.transifex.com/cawbird/cawbird/language/" "fi/)\n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter-sovellus" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird on GTK+:lla toteutettu Twitter-sovellus, joka tarjoaa muun muassa " "yksityisviestit, ilmoitukset twiiteistä ja keskustelunäkymän." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Lisäominaisuuksiin kuuluu muun muassa videoiden katselu suoraan " "sovelluksesta, useat upotetut kuvat, listat, suodattimet, useat tilit jne." msgid "Generic timeline view when using Cawbird" msgstr "Yleinen aikajana Cawbirdissä" msgid "Typical Twitter profile" msgstr "Tyypillinen Twitter-profiili" msgid "Account settings can be configured" msgstr "Tilin asetuksia on mahdollista muuttaa" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Käytä Twitteriä tavallisella työpöytäsovelluksella" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Näytä määritetyt tilit" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Yksityisviestit" #, fuzzy msgid "Select Media" msgstr "Valitse kuva" msgid "Open" msgstr "Avaa" msgid "Cancel" msgstr "Peru" #, fuzzy msgid "Selected file is not an image or video." msgstr "Valittu tiedosto ei ole kuva." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Valittu kuva on liian suuri. Kuva voi olla kooltaan korkeintaan %'d Mt." #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Valittu kuva on liian suuri. Kuva voi olla kooltaan korkeintaan %'d Mt." #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Valittu kuva on liian suuri. Kuva voi olla kooltaan korkeintaan %'d Mt." msgid "Insert Emoji" msgstr "Lisää emoji" msgid "Direct Conversation" msgstr "Yksityiskeskustelu" #, fuzzy msgid "Direct message threads" msgstr "Yksityisviestit" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d uusi viesti käyttäjältä %s" msgstr[1] "%d uutta viestiä käyttäjältä %s" #, c-format msgid "New direct message from %s" msgstr "Uusi yksityinen viesti käyttäjältä %s" msgid "Direct Messages" msgstr "Yksityisviestit" #, fuzzy msgid "Liked tweets timeline" msgstr "Lisää twiitti suosikkeihin" #, fuzzy msgid "Likes" msgstr "Suosikit" msgid "Add new Filter" msgstr "Lisää uusi suodatin" msgid "Filters" msgstr "Suodattimet" #, fuzzy msgid "Home timeline" msgstr "Piilota aikajanalla" #, c-format msgid "%s retweeted %s" msgstr "%s twiittasi uudelleen %s" #, c-format msgid "%s tweeted" msgstr "%s twiittasi" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d uusi twiitti!" msgstr[1] "%d uutta twiittiä!" msgid "Home" msgstr "Koti" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s twiittasi" msgid "Private" msgstr "Yksityinen" msgid "Public" msgstr "Julkinen" msgid "Lists" msgstr "Listat" msgid "Show configured accounts" msgstr "Näytä määritetyt tilit" msgid "Compose Tweet" msgstr "Kirjoita twiitti" msgid "Add new Account" msgstr "Lisää uusi tili" #, fuzzy msgid "Mentions timeline" msgstr "Piilota aikajanalla" #, c-format msgid "%s mentioned %s" msgstr "%s mainitsi %s" msgid "Mentions" msgstr "Maininnat" msgid "Suspended Account" msgstr "Jäädytetty tili" msgid "Protected profile" msgstr "Suojattu profiili" #, c-format msgid "Tweet to @%s" msgstr "Twiittaa käyttäjälle @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s twiittasi" msgstr[1] "%s twiittasi" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seuraa" msgstr[1] "Seuraa" #, fuzzy, c-format msgid "Location: %s" msgstr "Ilmoitukset" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Piilota aikajanalla" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seuraajia" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seuraa" msgid "Protected Profile" msgstr "Suojattu profiili" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seuraajia" msgstr[1] "Seuraajia" #, fuzzy msgid "No tweets found" msgstr "Kohteita ei löytynyt" msgid "No users found" msgstr "Käyttäjiä ei löytynyt" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Twiittaa käyttäjälle @%s" msgid "Search" msgstr "Etsi" msgid "Load More" msgstr "Lataa lisää" msgid "Could not show tweet" msgstr "Twiitin näyttäminen epäonnistui" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Uudelleentwiittaukset" msgid "Open in Browser" msgstr "Avaa selaimessa" msgid "Source" msgstr "Lähde" msgid "Tweet Details" msgstr "Twiitin tiedot" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d lukematon)" msgstr[1] "(%d lukematonta)" msgid "Delete" msgstr "Poista" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Twiittaa uudelleen" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Haluatko varmasti poistaa tämän tilin?" #, c-format msgid "Block %s" msgstr "Estä %s" msgid "This tweet contains images marked as inappropriate" msgstr "Tämä twiitti sisältää asiattomaksi merkittyjä kuvia" msgid "Show anyway" msgstr "Näytä silti" #, fuzzy msgid "Could not authenticate you" msgstr "Kohteen %s avaaminen ei onnistunut" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Käyttäjiä ei löytynyt" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Käyttöliittymä" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Tekstileike on jo olemassa" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Nyt" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%d min" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%d t" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Vastataan taholle %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Vastataan taholle %s ja %d muulle" msgstr[1] "Vastataan taholle %s ja %d muulle" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Vastataan taholle %s ja %s" msgid "Don't have a Twitter account yet?" msgstr "Ei vielä Twitter-tiliä?" msgid "Create one" msgstr "Luo uusi tili" #, c-format msgid "Could not open %s" msgstr "Kohteen %s avaaminen ei onnistunut" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Väärä PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Tili on jo käytössä" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Valitse kuva" msgid "Unfollow" msgstr "Lopeta seuraaminen" msgid "Follow" msgstr "Seuraa" msgid "Loading…" msgstr "Ladataan…" #, fuzzy msgid "No entries found" msgstr "Käyttäjiä ei löytynyt" msgid "Retry" msgstr "Yritä uudelleen" msgid "Copy URL" msgstr "Kopioi osoite" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Tallenna nimellä…" msgid "Save Video" msgstr "Tallenna video" msgid "Save Image" msgstr "Tallenna kuva" msgid "Save" msgstr "Tallenna" msgid "Select Banner Image" msgstr "Valitse bannerikuva" msgid "Image does not meet minimum size requirements:" msgstr "Kuva ei täytä vähimmäisvaatimuksia koon suhteen:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Vähimmäisleveys: %d pikseli" msgstr[1] "Vähimmäisleveys: %d pikseliä" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Vähimmäiskorkeus: %d pikseli" msgstr[1] "Vähimmäiskorkeus: %d pikseliä" msgid "Pick" msgstr "Valitse" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Takaisin" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Lainaa twiittiä" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Vain yksi GIF-tiedosto per twiitti on sallittu." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Muokkaa suodatinta" msgid "Matches" msgstr "Vastaa" msgid "Doesn't match" msgstr "Ei vastaa" msgid "Modify Snippet" msgstr "Muokkaa tekstileikettä" msgid "Snippet can't be empty" msgstr "Tekstileike ei voi olla tyhjä" msgid "Replacement can't be empty" msgstr "Korvike ei voi olla tyhjä" msgid "Snippet may not contain whitespace" msgstr "Tekstileike ei voi sisältää tyhjätilaa" msgid "Snippet already exists" msgstr "Tekstileike on jo olemassa" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hei, #Cawbird'in uusi versio on nyt saatavilla! \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Lisää tai poista käyttäjä listalta" msgid "You have no lists." msgstr "Sinulla ei ole listoja." msgid "About Cawbird" msgstr "Tietoja - Cawbird" msgid "New Account" msgstr "Uusi tili" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Asettaaksesi Cawbirdin tunnistautumisen tulee sinun noutaa twitter.comista " "PIN sille tilille, jonka haluat lisätä" msgid "Request PIN" msgstr "Pyydä PIN" msgid "Enter PIN from twitter.com below:" msgstr "Kirjoita PIN twitter.comista alapuolelle:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Vahvista" msgid "Account Settings" msgstr "Tilin asetukset" msgid "Name" msgstr "Nimi" msgid "Website" msgstr "Verkkosivusto" msgid "Autostart" msgstr "Käynnistä automaattisesti" #, fuzzy msgid "Remove Account" msgstr "Poista" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Haluatko varmasti poistaa tämän tilin?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Hymiöt ja ihmiset" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Ruumis ja vaatetus" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Eläimet ja luonto" msgctxt "emoji category" msgid "Food & Drink" msgstr "Ruoka ja juoma" msgctxt "emoji category" msgid "Travel & Places" msgstr "Matkustaminen ja paikat" msgctxt "emoji category" msgid "Activities" msgstr "Toimet" msgctxt "emoji category" msgid "Objects" msgstr "Objektit" msgctxt "emoji category" msgid "Symbols" msgstr "Symbolit" msgctxt "emoji category" msgid "Flags" msgstr "Liput" msgid "No Results Found" msgstr "Tuloksia ei löytynyt" msgid "Try a different search" msgstr "Yritä eri hakuehtoja" msgid "Send" msgstr "Lähetä" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Näytä suosikkikuvat" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Suodattimet" #, fuzzy msgid "Filtered users" msgstr "Suodattimet" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Käyttäjät" #, fuzzy msgid "Set image description" msgstr "Kuvaus" msgid "Subscribe" msgstr "Tilaa" msgid "Unsubscribe" msgstr "Lopeta tilaus" msgid "Subscribers:" msgstr "Tilaajat:" msgid "Members:" msgstr "Jäsenet:" msgid "Creator:" msgstr "Tehnyt:" msgid "Created at:" msgstr "Tehty:" msgid "Edit" msgstr "Muokkaa" msgid "Mode:" msgstr "Tila:" msgid "Description" msgstr "Kuvaus" msgid "Settings" msgstr "Asetukset" msgid "Shortcuts" msgstr "Pikanäppäimet" msgid "About" msgstr "Tietoja" msgid "Quit" msgstr "Lopeta" msgid "Add New Filter" msgstr "Lisää uusi suodatin" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Lisää uusi tekstileike" msgid "Keyword" msgstr "Avainsana" msgid "Replacement" msgstr "Korvike" msgid "Create New List" msgstr "Luo uusi lista" msgid "Name:" msgstr "Nimi:" msgid "Create" msgstr "Luo" msgid "Write Direct Message" msgstr "Kirjoita yksityisviesti" msgid "Add to/Remove from List" msgstr "Lisää listalle tai poista listalta" msgid "Blocked" msgstr "Estetty" msgid "Muted" msgstr "Mykistetty" msgid "Retweets disabled" msgstr "Uudelleentwiittaukset poistettu käytöstä" #, fuzzy msgid "More actions" msgstr "Maininnat" msgid "Follows you" msgstr "Seuraa sinua" msgid "Tweets" msgstr "Twiitit" msgid "Followers" msgstr "Seuraajia" msgid "Following" msgstr "Seuraa" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Pikanäppäimet" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Piilota aikajanalla" msgid "Show inline media" msgstr "Näytä sisennetty media" msgid "Always show" msgstr "Näytä aina" msgid "Always hide" msgstr "Piilota aina" msgid "Hide in timeline" msgstr "Piilota aikajanalla" msgid "Auto scroll on new tweets" msgstr "Vieritä automaattisesti uusien twiittien myötä" msgid "Double-click activation" msgstr "Kaksoisnapsautuksen aktivointi" msgid "Notifications" msgstr "Ilmoitukset" msgid "On New Tweets" msgstr "Uusissa twiiteissä" msgid "Never" msgstr "Ei koskaan" msgid "Every" msgstr "Joka" msgid "Stack 5" msgstr "5 pino" msgid "Stack 10" msgstr "10 pino" msgid "Stack 25" msgstr "25 pino" msgid "Stack 50" msgstr "50 pino" msgid "On New Mentions" msgstr "Uusissa maininnoissa" msgid "On New Messages" msgstr "Uusissa viesteissä" msgid "Interface" msgstr "Käyttöliittymä" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Twiitit" msgid "Round avatars" msgstr "Pyöreät avatarit" msgid "Remove trailing hashtags" msgstr "Poista lopussa olevat hashtagit" msgid "Remove media links" msgstr "Poista medialinkit" msgid "Hide inappropriate content" msgstr "Piilota asiaton sisältö" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Tekstileikkeitä ei ole määritetty." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Voit aktivoida tekstileikkeet kirjoittamalla avainsanan ja painamalla TAB." msgid "Snippets" msgstr "Tekstileikkeet" msgctxt "shortcuts window" msgid "General" msgstr "Yleiset" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Twiittaa" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Näytä tilin asetukset" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Näytä tili-ikkuna" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Näytä sovelluksen asetukset" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Yläpalkki päälle/pois" msgctxt "shortcuts window" msgid "Go Back" msgstr "Edellinen" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Seuraava" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Siirry sivulle ..." msgctxt "shortcuts window" msgid "Tweets" msgstr "Twiitit" msgctxt "shortcuts window" msgid "Retweet" msgstr "Twiittaa uudelleen" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Lisää suosikkeihin" msgctxt "shortcuts window" msgid "Reply" msgstr "Vastaa" msgctxt "shortcuts window" msgid "Quote" msgstr "Lainaa" msgctxt "shortcuts window" msgid "Show Details" msgstr "Näytä tiedot" msgctxt "shortcuts window" msgid "Delete" msgstr "Poista" msgctxt "shortcuts window" msgid "Compose" msgstr "Kirjoita" msgid "Show Emoji Chooser" msgstr "Näytä emoji-valitsin" msgid "Start new conversation" msgstr "Aloita uusi keskustelu" msgid "With:" msgstr "Osallistujat:" msgid "Go" msgstr "Aloita" msgid "Quote" msgstr "Lainaa" msgid "Retweet tweet" msgstr "Twiittaa uudelleen" #, fuzzy msgid "Like tweet" msgstr "Lisää twiitti suosikkeihin" msgid "Reply to tweet" msgstr "Vastaa twiittiin" msgid "More" msgstr "Lisää" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Suosikki" msgid "Reply" msgstr "Vastaa" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Twiittaa uudelleen" msgid "Unblock" msgstr "Lopeta estäminen" msgid "Show settings of this account" msgstr "Näytä tämän tilin asetukset" msgid "Open in new window" msgstr "Avaa uudessa ikkunassa" msgid "Go to profile" msgstr "Siirry profiiliin" msgid "Created" msgstr "Luotu" msgid "Subscribed to" msgstr "Tilannut" #~ msgid "Replying to" #~ msgstr "Vastataan taholle" #~ msgid "and" #~ msgstr "ja" #~ msgid "Actions" #~ msgstr "Toiminnot" #~ msgid "Add Image" #~ msgstr "Lisää kuva" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seuraajia" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Ei valtuutusta. Yleensä tämä tarkoittaa, että Twitter-palvelimilla on " #~ "ongelmia, joten yritä pian uudelleen" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Twiittien lataaminen epäonnistui" cawbird-1.4.2/po/fr.po000066400000000000000000000673151416632607600145450ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # akern , 2016 # Baptiste Mille-Mathias , 2017 # Guillaume Hayot, 2015-2016 # newincpp , 2014 # rc spam , 2016 # RoPP , 2015 # Seboss666 , 2015 # Skyward , 2016 # Étienne Deparis , 2017 # Prince Ofsky , 2020 # b86ed1d35b1d961a2deda4048cbeb435_9cc1908 <292f9acf80cf4cd77a412d609587069a_937386>, 2020 # Mattherix , 2020 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Mattherix , 2020\n" "Language-Team: French (https://www.transifex.com/cawbird/teams/107135/fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Client Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird est un client Twitter natif en GTK+ proposant les fonctions vitales " "telles que les messages directs (dm), les notifications, les vues en " "conversations." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Les fonctions additionnelles incluent le visionnage en local des vidéos, les " "insertions multiples d'images, les listes, les filtres, les comptes " "multiples, etc." msgid "Generic timeline view when using Cawbird" msgstr "Vue générique de la timeline avec Cawbird" msgid "Typical Twitter profile" msgstr "Profil Twitter typique" msgid "Account settings can be configured" msgstr "Les réglages de comptes peuvent être configurés" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Utiliser Twitter depuis une application de bureau classique" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Afficher les comptes configurés" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, c-format msgid "Direct messages with %s" msgstr "Message direct avec %s" #, fuzzy msgid "Select Media" msgstr "Sélectionner une image" msgid "Open" msgstr "Ouvrir" msgid "Cancel" msgstr "Annuler" #, fuzzy msgid "Selected file is not an image or video." msgstr "Le fichier sélectionnée n'est pas une image." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "L'image sélectionnée est trop grosse. La taille maximale autorisée par image " "est de %'d Mo" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "L'image sélectionnée est trop grosse. La taille maximale autorisée par image " "est de %'d Mo" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Le sélectionné est trop gros. La taille maximale autorisée par GIF est de " "%'d Mo" msgid "Insert Emoji" msgstr "Insérer Emoji" msgid "Direct Conversation" msgstr "Conversation directe" msgid "Direct message threads" msgstr "Fils de messages directs" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nouveau message de %s" msgstr[1] "%d nouveaux messages de %s" #, c-format msgid "New direct message from %s" msgstr "Nouveau message privé de %s" msgid "Direct Messages" msgstr "Messages privés" #, fuzzy msgid "Liked tweets timeline" msgstr "Timeline favorites" #, fuzzy msgid "Likes" msgstr "Favoris" msgid "Add new Filter" msgstr "Ajouter un nouveau filtre" msgid "Filters" msgstr "Filtres" msgid "Home timeline" msgstr "Timeline principale" #, c-format msgid "%s retweeted %s" msgstr "%s a retweeté %s" #, c-format msgid "%s tweeted" msgstr "%s a tweeté" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nouveau tweet !" msgstr[1] "%d nouveaux tweets !" msgid "Home" msgstr "Accueil" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "%s tweets dans la liste" msgid "Private" msgstr "Privé" msgid "Public" msgstr "Public" msgid "Lists" msgstr "Listes" msgid "Show configured accounts" msgstr "Afficher les comptes configurés" msgid "Compose Tweet" msgstr "Écrire un nouveau Tweet" msgid "Add new Account" msgstr "Ajouter un nouveau compte" msgid "Mentions timeline" msgstr "Timeline des Mentions" #, c-format msgid "%s mentioned %s" msgstr "%s a mentionné %s" msgid "Mentions" msgstr "Mentions" msgid "Suspended Account" msgstr "Compte Suspendu" msgid "Protected profile" msgstr "Profil protégé" #, c-format msgid "Tweet to @%s" msgstr "Tweet à @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d tweets" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Suit %d compte" msgstr[1] "Suit %d comptes" #, c-format msgid "Location: %s" msgstr "Localisation: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, c-format msgid "%s timeline" msgstr "%s timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, c-format msgid "%s followers" msgstr "%s abonnés" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, c-format msgid "%s following" msgstr "%s abonnements" msgid "Protected Profile" msgstr "Profil Privé" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d abonné" msgstr[1] "%d abonnés" msgid "No tweets found" msgstr "Pas de tweet trouvé" msgid "No users found" msgstr "Aucun utilisateur trouvé" #, c-format msgid "Users matching \"%s\"" msgstr "Utilisateurs correspondant à \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweets correspondant à \"%s\"" msgid "Search" msgstr "Rechercher" msgid "Load More" msgstr "Charger plus" msgid "Could not show tweet" msgstr "Impossibe d'afficher le tweet" msgid "This tweet is hidden by the author" msgstr "Ce tweet est masqué par son auteur" msgid "This tweet is unavailable" msgstr "Ce tweet n'est pas disponible" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Ouvrir dans un navigateur" msgid "Source" msgstr "Source" msgid "Tweet Details" msgstr "Détails du tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d non lu)" msgstr[1] "(%d non lus)" msgid "Delete" msgstr "Supprimer" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweeté par %s (@%s)" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Voulez vraiment retirer ce compte ?" #, c-format msgid "Block %s" msgstr "Bloquer %s" msgid "This tweet contains images marked as inappropriate" msgstr "Ce tweet contient des images identifiées comme inappropriées" msgid "Show anyway" msgstr "Montrer quand même" msgid "Could not authenticate you" msgstr "Nous n'arrivons pas à vous authentifier" msgid "Sorry, that page does not exist" msgstr "Désolé, cette page n'existe pas" msgid "User not found." msgstr "Ce compte n'existe pas." msgid "User has been suspended." msgstr "L'utilisateur a été suspendu." msgid "Your account is suspended and is not permitted to access this feature" msgstr "" "Votre compte est suspendue et ne peut pas accéder à cette fonctionnalité" msgid "Rate limit exceeded" msgstr "Taux limite dépassé" msgid "Invalid or expired token" msgstr "Jeton invalide ou expiré" msgid "The specified user is not a subscriber of this list." msgstr "L'utilisateur spécifier n'est pas un abonné à la liste." msgid "The user you are trying to remove from the list is not a member." msgstr "" "L'utilisateur que vous essayé de retire de la liste n'en est pas membre." msgid "Account update failed: value is too long." msgstr "La Mise à jour du compte à échoué: valeur trop longue." msgid "Over capacity" msgstr "Surcapacité" msgid "Internal error" msgstr "Erreur interne" msgid "You have already favorited this status." msgstr "Vous avez déjà favorisé ce status." msgid "No status found with that ID." msgstr "Pas de status trouvez avec cette ID." msgid "You cannot send messages to users who are not following you." msgstr "" "Vous ne pouvez pas envoyer de message à des utilisateurs qui ne vous suivent " "pas." msgid "There was an error sending your message." msgstr "Il y a eu une erreur lors de l'envoi de votre message." msgid "You've already requested to follow this user." msgstr "Vous avez déjà demander de suivre cette utilisateur." msgid "You are unable to follow more people at this time" msgstr "Vous ne pouvez pas suivre d'autres personnes pour le moment" msgid "Sorry, you are not authorized to see this status" msgstr "Désolé, vous n'êtes pas autorisé à voir ce statut" msgid "User is over daily status update limit" msgstr "" "L'utilisateur a dépassé la limite de mise à jour quotidienne de son statut" msgid "Tweet needs to be a bit shorter." msgstr "Le tweet doit être un peut plus petit." msgid "Status is a duplicate" msgstr "Le statut est un doublon" msgid "Owner must allow dms from anyone." msgstr "Le propriétaire doit autoriser les messages direct de n'importe qui." msgid "Bad authentication data" msgstr "Mauvaise donnée d'authentification" msgid "Your credentials do not allow access to this resource." msgstr "Vos identifiants ne permettent pas d'accéder à cette ressource." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Cette demande semble pouvoir être automatisée. Pour protéger nos " "utilisateurs du spam et d'autres activités malveillantes, nous ne pouvons " "pas finir cette action maintenant." msgid "User must verify login" msgstr "L'utilisateur doit vérifier sa connexion" msgid "Application cannot perform write actions." msgstr "L'application ne peut pas effectuer des actions d'écriture." msgid "You can’t mute yourself." msgstr "Vous ne pouvez pas vous auto-bloquer." msgid "You are not muting the specified user." msgstr "Vous ne bloqué pas l'utilisateur spécifié." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" "Les GIF animé ne sont pas autorisé lors de l'envoie de multiples images." msgid "The validation of media ids failed." msgstr "La validation des identifiants du média a échoué." msgid "A media id was not found." msgstr "L'identifiant du media n'a pas été trouvé." msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" "Pour protéger nos utilisateur du spam et d'autre activités malicieuse, ce " "compte est temporairement bloqué." msgid "You have already retweeted this Tweet." msgstr "Vous avez déjà retweeté ce tweet." msgid "You cannot send messages to this user." msgstr "Vous ne pouvez pas envoyer de message à cet utilisateur." msgid "The text of your direct message is over the max character limit." msgstr "" "Le texte de votre message privé dépasse la limite maximum de caractères." msgid "Subscription already exists." msgstr "L'abonnement existes déjà." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "Vous tentez de répondre à un Tweet supprimé ou non visible par vous." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "Le Tweet dépasse le nombre de types de pièces jointes autorisés." msgid "The given URL is invalid." msgstr "L'URL donné est invalide." msgid "Invalid / suspended application" msgstr "Invalide / application suspendue" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "L'auteur du Tweet original a restreint qui peut répondre à ce Tweet." #, fuzzy msgid "Invalid media file" msgstr "La validation des identifiants du média a échoué." #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Maintenant" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "Il y a %d minute" msgstr[1] "Il y a %d minutes" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "Il y a %d heure" msgstr[1] "Il y a %d heures" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, fuzzy, c-format msgid "Replying to %s" msgstr "En réponse à %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "En réponse à %s et %d autres" msgstr[1] "En réponse à %s et %d autres" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "En réponse à %s et %s" msgid "Don't have a Twitter account yet?" msgstr "Pas encore de compte Twitter ?" msgid "Create one" msgstr "Créer un compte" #, c-format msgid "Could not open %s" msgstr "Impossible d'ouvrir %s" msgid "Failed to retrieve request token" msgstr "Impossible de récupérer le jeton demandé" msgid "Wrong PIN" msgstr "PIN erroné" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Compte déjà en utilisation" msgid "Failed to retrieve access token" msgstr "Impossible de récupérer le jeton d'accès" msgid "Select Image" msgstr "Sélectionner une image" msgid "Unfollow" msgstr "Se désabonner" msgid "Follow" msgstr "Suivre" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Aucun utilisateur trouvé" msgid "Retry" msgstr "Réessayer" msgid "Copy URL" msgstr "Copier l'URL" #, fuzzy msgid "Reload image" msgstr "Erreur lors du chargement de l'image" msgid "Save as…" msgstr "Enregistrer sous…" msgid "Save Video" msgstr "Enregistrer la vidéo" msgid "Save Image" msgstr "Enregistrer l'image" msgid "Save" msgstr "Enregistrer" msgid "Select Banner Image" msgstr "Sélectionner une image de bannière" msgid "Image does not meet minimum size requirements:" msgstr "L'image ne répond pas aux caractéristiques minimales de taille :" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Largeur minimale : %d pixel" msgstr[1] "Largeur minimale : %d pixels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Hauteur minimale : %d pixel" msgstr[1] "Hauteur minimale : %d pixels" msgid "Pick" msgstr "Sélectionner" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Erreur lors du chargement de l'image" msgstr[1] "Erreur lors du chargement de %u images" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Retour" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citer le tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Seulement un GIF par tweet est autorisé." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "%d caractère restant" msgstr[1] "%d caractères restant" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Image %d de %d" msgstr[1] "Image %d de %d" msgid "Modify Filter" msgstr "Modifier les filtres" msgid "Matches" msgstr "Correspond" msgid "Doesn't match" msgstr "Ne correspond pas" msgid "Modify Snippet" msgstr "Modifier le Snippet" msgid "Snippet can't be empty" msgstr "Le fragment de code ne peut pas être vide" msgid "Replacement can't be empty" msgstr "Le texte de remplacement ne peut pas être vide" msgid "Snippet may not contain whitespace" msgstr "Un snippet en peut contenir d'espace vide" msgid "Snippet already exists" msgstr "Le snippet existe déjà" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Ajouter ou supprimer des utilisateurs de la liste" msgid "You have no lists." msgstr "Vous n'avez aucune liste." msgid "About Cawbird" msgstr "À propos de Cawbird" msgid "New Account" msgstr "Nouveau compte" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Pour autoriser Cawbird, vous devez récupérer un code PIN de twitter.com, " "pour le compte que vous souhaitez ajouter" msgid "Request PIN" msgstr "Demander le PIN" msgid "Enter PIN from twitter.com below:" msgstr "Entrez le code PIN de twitter.com ci-dessous :" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirmer" msgid "Account Settings" msgstr "Paramètres du compte" msgid "Name" msgstr "Nom" msgid "Website" msgstr "Site web" msgid "Autostart" msgstr "Démarrage automatique" msgid "Remove Account" msgstr "Retirer le compte" msgid "Do you really want to remove this account?" msgstr "Voulez vraiment retirer ce compte ?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys et personnes" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Corp et Tenue" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animaux et nature" msgctxt "emoji category" msgid "Food & Drink" msgstr "Nourriture et Boisson" msgctxt "emoji category" msgid "Travel & Places" msgstr "Voyages et Endroits" msgctxt "emoji category" msgid "Activities" msgstr "Activités" msgctxt "emoji category" msgid "Objects" msgstr "Objets" msgctxt "emoji category" msgid "Symbols" msgstr "Symboles" msgctxt "emoji category" msgid "Flags" msgstr "Drapeaux" msgid "No Results Found" msgstr "Aucun Résultat" msgid "Try a different search" msgstr "Essayez d'effectuer une autre recherche" msgid "Send" msgstr "Envoyer" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Montrer les images favorites" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "Mots masqués" msgid "Filtered users" msgstr "Comptes masqués" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Utilisateurs" msgid "Set image description" msgstr "Ajouter la description de l'image" msgid "Subscribe" msgstr "S'abonner" msgid "Unsubscribe" msgstr "Se désabonner" msgid "Subscribers:" msgstr "Abonnés :" msgid "Members:" msgstr "Membres :" msgid "Creator:" msgstr "Créateur :" msgid "Created at:" msgstr "Créé à :" msgid "Edit" msgstr "Éditer" msgid "Mode:" msgstr "Mode :" msgid "Description" msgstr "Description" msgid "Settings" msgstr "Préférences" msgid "Shortcuts" msgstr "Raccourcis" msgid "About" msgstr "À propos" msgid "Quit" msgstr "Quitter" msgid "Add New Filter" msgstr "Ajouter un nouveau filtre" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Ajouter un nouveau snippet" msgid "Keyword" msgstr "Mot-clé" msgid "Replacement" msgstr "Substitution" msgid "Create New List" msgstr "Créer une nouvelle liste" msgid "Name:" msgstr "Nom :" msgid "Create" msgstr "Créer" msgid "Write Direct Message" msgstr "Écrire un message direct" msgid "Add to/Remove from List" msgstr "Ajouter/Retirer de la Liste" msgid "Blocked" msgstr "Bloqué" msgid "Muted" msgstr "Muet" msgid "Retweets disabled" msgstr "Retweets désactivés" msgid "More actions" msgstr "Plus d'actions" msgid "Follows you" msgstr "Vous suit" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Abonnés" msgid "Following" msgstr "Abonnements" msgid "Use dark theme" msgstr "Utiliser un thème sombre" #, fuzzy msgid "Shortcut key" msgstr "Raccourcis" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "%s timeline" msgid "Show inline media" msgstr "Afficher les médias sur une ligne" msgid "Always show" msgstr "Toujours afficher" msgid "Always hide" msgstr "Toujours cacher" msgid "Hide in timeline" msgstr "Cacher dans la timeline" msgid "Auto scroll on new tweets" msgstr "Défilement automatique des nouveaux tweets" msgid "Double-click activation" msgstr "Activation par double clic" msgid "Notifications" msgstr "Notifications" msgid "On New Tweets" msgstr "Lors de nouveaux tweets" msgid "Never" msgstr "Jamais" msgid "Every" msgstr "À chaque fois" msgid "Stack 5" msgstr "Tous les 5" msgid "Stack 10" msgstr "Tous les 10" msgid "Stack 25" msgstr "Tous les 25" msgid "Stack 50" msgstr "Tous les 50" msgid "On New Mentions" msgstr "Lors de nouvelles mentions" msgid "On New Messages" msgstr "Lors de nouveaux messages" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Avatars ronds" msgid "Remove trailing hashtags" msgstr "Supprimer les hashtags superflus" msgid "Remove media links" msgstr "Supprimer les liens vers les média" msgid "Hide inappropriate content" msgstr "Cacher le contenu inapproprié" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Pas de snippets configurés." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Vous pouvez activer des snippets en écrivant le mot-clé puis tabulation." msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "Général" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Écrire un nouveau tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Paramètres du compte" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Afficher le compte Popover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Afficher les paramètres de l'application" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "(Dés)Activer la barre supérieure" msgctxt "shortcuts window" msgid "Go Back" msgstr "Retour" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Avancer" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Aller à la énième page" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweeter" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Ajouter aux favoris" msgctxt "shortcuts window" msgid "Reply" msgstr "Répondre" msgctxt "shortcuts window" msgid "Quote" msgstr "Citer" msgctxt "shortcuts window" msgid "Show Details" msgstr "Montrer les détails" msgctxt "shortcuts window" msgid "Delete" msgstr "Supprimer" msgctxt "shortcuts window" msgid "Compose" msgstr "Écrire un nouveau tweet" msgid "Show Emoji Chooser" msgstr "Afficher le Sélecteur d'Emoji" msgid "Start new conversation" msgstr "Démarrer une nouvelle conversation" msgid "With:" msgstr "Avec :" msgid "Go" msgstr "Exécuter" msgid "Quote" msgstr "Citer" msgid "Retweet tweet" msgstr "Retweeter" #, fuzzy msgid "Like tweet" msgstr "Ajouter aux favoris" msgid "Reply to tweet" msgstr "Répondre au tweet" msgid "More" msgstr "Plus" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favori" msgid "Reply" msgstr "Répondre" msgid "Liked" msgstr "Aimé par" msgid "Retweeted" msgstr "Retweeté par" msgid "Unblock" msgstr "Débloquer" msgid "Show settings of this account" msgstr "Afficher les paramètres de ce compte" msgid "Open in new window" msgstr "Ouvrir dans une nouvelle fenêtre" msgid "Go to profile" msgstr "Se rendre sur le profil" msgid "Created" msgstr "Créé" msgid "Subscribed to" msgstr "S'inscrire" #~ msgid "Replying to" #~ msgstr "En réponse à" #~ msgid "and" #~ msgstr "et" #~ msgid "Actions" #~ msgstr "Actions" #~ msgid "Add Image" #~ msgstr "Ajouter une image" cawbird-1.4.2/po/ga.po000066400000000000000000000506241416632607600145200ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Aoibhe Agnew (Aoibhe Ní Ghnímh), 2017 # Aoibhe Ní Ghnímh , 2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-11-30 16:57+0000\n" "Last-Translator: Aoibhe Ní Ghnímh \n" "Language-Team: Irish (http://www.transifex.com/cawbird/cawbird/language/" "ga/)\n" "Language: ga\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : " "4);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Teachtaireachtaí Díreacha" msgid "Select Media" msgstr "" msgid "Open" msgstr "Oscail" msgid "Cancel" msgstr "" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "Teachtaireachtaí Díreacha" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #, c-format msgid "New direct message from %s" msgstr "" msgid "Direct Messages" msgstr "Teachtaireachtaí Díreacha" #, fuzzy msgid "Liked tweets timeline" msgstr "Croíthe" #, fuzzy msgid "Likes" msgstr "Croíthe" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "" #, c-format msgid "%s tweeted" msgstr "" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgid "Home" msgstr "Baile" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "" msgid "Private" msgstr "" msgid "Public" msgstr "" msgid "Lists" msgstr "Liostaí" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "" msgid "Add new Account" msgstr "" msgid "Mentions timeline" msgstr "" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "" #, c-format msgid "Tweet to @%s" msgstr "" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "Atweetáil" msgstr[1] "Atweetáil" msgstr[2] "Atweetáil" msgstr[3] "Atweetáil" msgstr[4] "Atweetáil" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "à Leanúint" msgstr[1] "à Leanúint" msgstr[2] "à Leanúint" msgstr[3] "à Leanúint" msgstr[4] "à Leanúint" #, c-format msgid "Location: %s" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Croíthe" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Leantóirí" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "à Leanúint" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Leantóirí" msgstr[1] "Leantóirí" msgstr[2] "Leantóirí" msgstr[3] "Leantóirí" msgstr[4] "Leantóirí" msgid "No tweets found" msgstr "" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "Cuardaigh" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "" msgid "Open in Browser" msgstr "" msgid "Source" msgstr "" msgid "Tweet Details" msgstr "" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgid "Delete" msgstr "Scrois" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Atweetáil" msgid "Are you sure you want to delete this tweet?" msgstr "" #, c-format msgid "Block %s" msgstr "Coisc %s" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Anois" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, c-format msgid "Replying to %s" msgstr "" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "à Leanúint" msgid "Follow" msgstr "Lean" msgid "Loading…" msgstr "" msgid "No entries found" msgstr "" msgid "Retry" msgstr "" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Sábháil" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgstr[4] "" msgid "Modify Filter" msgstr "" msgid "Matches" msgstr "" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "" msgid "New Account" msgstr "" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "" msgid "Account Settings" msgstr "" msgid "Name" msgstr "" msgid "Website" msgstr "" msgid "Autostart" msgstr "" msgid "Remove Account" msgstr "Scrois" msgid "Do you really want to remove this account?" msgstr "" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "Bratacha" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Seol" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "" msgid "Unsubscribe" msgstr "" msgid "Subscribers:" msgstr "" msgid "Members:" msgstr "" msgid "Creator:" msgstr "" msgid "Created at:" msgstr "" msgid "Edit" msgstr "" msgid "Mode:" msgstr "" msgid "Description" msgstr "" msgid "Settings" msgstr "" msgid "Shortcuts" msgstr "" msgid "About" msgstr "" msgid "Quit" msgstr "" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "" msgid "Name:" msgstr "" msgid "Create" msgstr "" msgid "Write Direct Message" msgstr "" msgid "Add to/Remove from List" msgstr "" msgid "Blocked" msgstr "" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" msgid "More actions" msgstr "" msgid "Follows you" msgstr "" msgid "Tweets" msgstr "Tweetanna" msgid "Followers" msgstr "Leantóirí" msgid "Following" msgstr "à Leanúint" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Croíthe" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "" msgid "Every" msgstr "" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweetanna" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweetanna" msgctxt "shortcuts window" msgid "Retweet" msgstr "Atweetáil" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "Freagra" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "" msgid "Go" msgstr "" msgid "Quote" msgstr "" msgid "Retweet tweet" msgstr "" msgid "Like tweet" msgstr "" msgid "Reply to tweet" msgstr "" msgid "More" msgstr "" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "Freagra" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Atweetáil" msgid "Unblock" msgstr "Díchoisc" msgid "Show settings of this account" msgstr "" msgid "Open in new window" msgstr "" msgid "Go to profile" msgstr "" msgid "Created" msgstr "" msgid "Subscribed to" msgstr "" #~ msgid "and" #~ msgstr "agus" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Leantóirí" #~ msgid "List" #~ msgstr "Liosta" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/gd.po000066400000000000000000000752471416632607600145330ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # GunChleoc, 2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: GunChleoc, 2021\n" "Language-Team: Gaelic, Scottish (https://www.transifex.com/cawbird/" "teams/107135/gd/)\n" "Language: gd\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : " "(n > 2 && n < 20) ? 2 : 3;\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliant Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as" " Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "’S e cliant Twitter stèidhichte air GTK+ tùsail a th’ ann an Cawbird a bheir" " bun-ghleusan dhut mar teachdaireachdan dìreach (DMs), brathan mu tweets is " "seallaidhean còmhraidh." msgid "" "Additional features include local viewing of videos, multiple inline images," " Lists, Filters, multiple accounts, etc." msgstr "" "Am measg nan gleusan eile, tha coimhead air videothan gu h-ionadail, iomadh " "dealbh am broinn na loidhne, liostaichean, criathragan, iomadh cunntas is " "msaa." msgid "Generic timeline view when using Cawbird" msgstr "Sealladh loidhne-ama coitcheann nuair a chleachdas tu Cawbird" msgid "Typical Twitter profile" msgstr "Pròifil Twitter dualtach" msgid "Account settings can be configured" msgstr "Gabhaidh roghainnean a’ chunntais rèiteachadh" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Tha iomadh ùrlar aig Cawbird (Adwaita, Adwaita dorcha, iomsgaradh àrd agus " "Adwaita Dark Green)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Cleachd Twitter am broinn aplacaid deasga àbhaisteach" #. TRANSLATORS: Search terms to find this application. Do NOT translate or #. localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" "Cha sheall e ach uinneag “Sgrìobh Tweet†dhan chunntas shònraichte gun dad " "sam bith eile." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` #. output msgid "account-name" msgstr "ainm-cunntais" #. TRANSLATORS: Description of the `--start-service` option for the command- #. line msgid "Start service" msgstr "Tòisich air an t-seirbheis" #. TRANSLATORS: Description of the `--stop-service` option for the command- #. line msgid "Stop service, if it has been started as a service" msgstr "" "Cuir crìoch air an t-seirbheis ma chaidh a thòiseachadh ’na sheirbheis" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the #. command-line msgid "Print configured startup accounts" msgstr "Seall na cunntasan tòiseachaidh a chaidh a rèiteachadh" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Sheall uinneag a’ chunntais shònraichte" #, c-format msgid "Direct messages with %s" msgstr "Teachdaireachdan dìreach le %s" msgid "Select Media" msgstr "Tagh meadhanan" msgid "Open" msgstr "Fosgail" msgid "Cancel" msgstr "Sguir dheth" msgid "Selected file is not an image or video." msgstr "Chan e dealbh no video a tha san fhaidhle a thagh thu." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Tha a’ video a thagh thu ro mhòr. Chan fhaod meud faidhle video a bhith nas " "motha na %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Tha an dealbh a thagh thu ro mhòr. Chan fhaod meud faidhle deilbh a bhith " "nas motha na %'d MB" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Tha an GIF a thagh thu ro mhòr. Chan fhaod meud faidhle GIF a bhith nas " "motha na %'d MB" msgid "Insert Emoji" msgstr "Cuir a-steach Emoji" msgid "Direct Conversation" msgstr "Còmhradh dìreach" msgid "Direct message threads" msgstr "Snàithleanan theachdaireachdan dìreach" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d teachdaireachd ùr o %s" msgstr[1] "%d theachdaireachd ùr o %s" msgstr[2] "%d teachdaireachdan ùra o %s" msgstr[3] "%d teachdaireachd ùr o %s" #, c-format msgid "New direct message from %s" msgstr "Teachdaireachd dhìreach ùr o %s" msgid "Direct Messages" msgstr "Teachdaireachdan dìreach" msgid "Liked tweets timeline" msgstr "Loidhne-ama nan tweets as toigh leat" msgid "Likes" msgstr "’S toil" msgid "Add new Filter" msgstr "Cuir criathrag ùr ris" msgid "Filters" msgstr "Criathragan" msgid "Home timeline" msgstr "Loidhne-ama na dachaighe" #, c-format msgid "%s retweeted %s" msgstr "Rinn %s ath-thweeteadh air %s" #, c-format msgid "%s tweeted" msgstr "Sgrìobh %s tweet" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d tweet ùr!" msgstr[1] "%d thweet ùr!" msgstr[2] "%d tweetaichean ùra!" msgstr[3] "%d tweet ùr!" msgid "Home" msgstr "Dachaigh" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" #. when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "Tweetaichean liosta %s" msgid "Private" msgstr "Prìobhaideach" msgid "Public" msgstr "Poblach" msgid "Lists" msgstr "Liostaichean" msgid "Show configured accounts" msgstr "Seall na cunntasan rèitichte" msgid "Compose Tweet" msgstr "Sgrìobh Tweet" msgid "Add new Account" msgstr "Cuir cunntas ùr ris" msgid "Mentions timeline" msgstr "Loidhne-ama nan iomraidhean" #, c-format msgid "%s mentioned %s" msgstr "Thug %s iomradh air %s" msgid "Mentions" msgstr "Iomraidhean" msgid "Suspended Account" msgstr "Cunntas à rèim" msgid "Protected profile" msgstr "Pròifil dhìonta" #, c-format msgid "Tweet to @%s" msgstr "Cuir tweet gu @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d thweet" msgstr[2] "%d tweetaichean" msgstr[3] "%d tweet" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "A’ leantainn air %d chunntas" msgstr[1] "A’ leantainn air %d chunntas" msgstr[2] "A’ leantainn air %d cunntasan" msgstr[3] "A’ leantainn air %d cunntas" #, c-format msgid "Location: %s" msgstr "Ionad: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile #. timeline view #, c-format msgid "%s timeline" msgstr "An loidhne-ama aig %s" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users following the user #, c-format msgid "%s followers" msgstr "An luchd-leantainn aig %s" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users followed by the user #, c-format msgid "%s following" msgstr "A’ leantainn air %s" msgid "Protected Profile" msgstr "Pròifil dhìonta" msgid "User is blocked" msgstr "Chaidh an cleachdaiche a bhacadh" msgid "User is muted" msgstr "Chaidh an cleachdaiche a mhùthadh" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d neach-leantainn" msgstr[1] "%d luchd-leantainn" msgstr[2] "%d luchd-leantainn" msgstr[3] "%d luchd-leantainn" msgid "No tweets found" msgstr "Cha deach tweet a lorg" msgid "No users found" msgstr "Cha deach cleachdaiche a lorg" #, c-format msgid "Users matching \"%s\"" msgstr "Cleachdaichean a fhreagras ri “%sâ€" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweetaichean a fhreagras ri “%sâ€" msgid "Search" msgstr "Lorg" msgid "Load More" msgstr "Luchdaich barrachd dheth" msgid "Could not show tweet" msgstr "Cha b’ urrainn dhuinn an tweet a shealltainn" msgid "This tweet is hidden by the author" msgstr "Chaidh an tweet seo fhalach leis an ùghdar" msgid "This tweet is unavailable" msgstr "Chan eil an tweet seo ri fhaighinn" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. #. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "Eadar-theangaich dhan Ghàidhlig" msgid "Retweets" msgstr "Air ath-thweeteadh" msgid "Open in Browser" msgstr "Fosgail sa bhrabhsair" msgid "Source" msgstr "Tùs" msgid "Tweet Details" msgstr "Mion-fhiosrachadh a’ tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when #. the client is blank msgid "an unknown client" msgstr "cliant nach aithne dhuinn" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d gun leughadh)" msgstr[1] "(%d gun leughadh)" msgstr[2] "(%d gun leughadh)" msgstr[3] "(%d gun leughadh)" msgid "Delete" msgstr "Sguab às" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Air ath-thweeteadh le %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "A bheil thu cinnteach gu bheil thu airson an tweet seo a sguabadh às?" #, c-format msgid "Block %s" msgstr "Bac %s" msgid "This tweet contains images marked as inappropriate" msgstr "Tha dealbh san tweet seo a chaidh a chomharradh mar dhroch-dhealbh" msgid "Show anyway" msgstr "Seall e co-dhiù" msgid "Could not authenticate you" msgstr "Cha b’ urrainn dhuinn dearbhadh cò thusa" msgid "Sorry, that page does not exist" msgstr "Tha sinn duilich ach chan eil an duilleag seo ann" msgid "User not found." msgstr "Cha deach an cleachdaiche a lorg." msgid "User has been suspended." msgstr "Chaidh an cleachdaiche a chur à rèim." msgid "Your account is suspended and is not permitted to access this feature" msgstr "" "Chaidh an cunntas agad a chur à rèim is chan fhaod thu an gleus seo " "inntrigeadh" msgid "Rate limit exceeded" msgstr "Chaidh thu thar crìoch na triceid" msgid "Invalid or expired token" msgstr "Chan eil an tòcan dligheach no dh’fhalbh an ùine air" msgid "The specified user is not a subscriber of this list." msgstr "" "Cha do dh’fho-sgrìobh an cleachdaiche a shònraich thu air an liosta seo." msgid "The user you are trying to remove from the list is not a member." msgstr "" "Chan eil an cleachdaiche a tha thu airson toirt air falbh on liosta ’na " "bhall." msgid "Account update failed: value is too long." msgstr "Dh’fhàillig le ùrachadh a’ chunntais: tha an luach ro fhada." msgid "Over capacity" msgstr "Thar an tomhais" msgid "Internal error" msgstr "Mearachd inntearnail" msgid "You have already favorited this status." msgstr "Chomharraich thu cheana gur toigh leat an staid seo." msgid "No status found with that ID." msgstr "Cha deach staid leis an ID seo a lorg." msgid "You cannot send messages to users who are not following you." msgstr "" "Chan urrainn dhut teachdaireachd a chur gu cleachdaiche nach eil a’ " "leantainn ort." msgid "There was an error sending your message." msgstr "Thachair mearachd nuair a bha sinn a’ cur do theachdaireachd." msgid "You've already requested to follow this user." msgstr "Dh’iarr thu leantainn air a’ chleachdaiche seo cheana." msgid "You are unable to follow more people at this time" msgstr "Chan urrainn dhut leantainn air barrachd daoine aig an àm seo" msgid "Sorry, you are not authorized to see this status" msgstr "" "Tha sinn duilich ach chan eil ùghdarrachadh agad airson an staid seo " "fhaicinn" msgid "User is over daily status update limit" msgstr "" "Chaidh an cleachdaiche thar crìoch làitheil nan ùrachaidhean air staid" msgid "Tweet needs to be a bit shorter." msgstr "Feumaidh an tweet a bhith beagan nas giorra." msgid "Status is a duplicate" msgstr "Tha an staid ’na dùblachadh" msgid "Owner must allow dms from anyone." msgstr "" "Feumaidh an seilbheadair teachdaireachdan dìreach a cheadachadh o dhuine sam" " bith." msgid "Bad authentication data" msgstr "Droch-dhàta dearbhaidh" msgid "Your credentials do not allow access to this resource." msgstr "Cha cheadaich an teisteas agad gun inntrig thu an goireas seo." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Cha coltas fèin-obrachail air an iarrtas seo. Airson an luchd-cleachdaidh " "agad a dhìon o spama is gnìomhachdan droch-rùnach eile, chan urrainn dhuinn " "an t-iarrtas seo a choileanadh an-dràsta." msgid "User must verify login" msgstr "Feumaidh an cleachdaiche an clàradh a-steach a dhearbhadh" msgid "Application cannot perform write actions." msgstr "Chan fhaod an aplacaid sgrìobhadh." msgid "You can’t mute yourself." msgstr "Chan urrainn dhut thu fhèin a mhùchadh." msgid "You are not muting the specified user." msgstr "Chan eil thu a’ mhùchadh an cleachdaiche sònraichte." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" "Chan eil GIF beòthaichte ceadaichte nuair a luchdaicheas tu suas iomadh " "dealbh." msgid "The validation of media ids failed." msgstr "Dh’fhàillig le dearbhadh IDan nam meadhanan." msgid "A media id was not found." msgstr "Cha deach ID mheadhanan a lorg." msgid "" "To protect our users from spam and other malicious activity, this account is" " temporarily locked." msgstr "" "Airson an luchd-cleachdaidh agad a dhìon o spama is gnìomhachdan droch-" "rùnach eile, chaidh an cunntas seo a ghlasadh rè seal." msgid "You have already retweeted this Tweet." msgstr "Rinn thu ath-thweeteadh air an tweet seo cheana." msgid "You cannot send messages to this user." msgstr "Chan urrainn dhut teachdaireachd a chur dhan chleachdaiche seo." msgid "The text of your direct message is over the max character limit." msgstr "Tha teacsa na teachdaireachd dìriche agad ro fhada." msgid "Subscription already exists." msgstr "Tha am fo-sgrìobhadh ann mu thràth." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" "Dh’fheuch thu ri freagairt do thweet a chaidh a sguabadh às no nach fhaic " "thu." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" "Tha barrachd seòrsaichean de cheanglachain ris an tweet na tha ceadaichte." msgid "The given URL is invalid." msgstr "Tha an URL a thug thu seachad mì-dhligheach." msgid "Invalid / suspended application" msgstr "Aplacaid mhì-dhligheach / à rèim" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" "Chuingich ùghdar an tweet tùsail cò dh’fhaodas freagairt a thoirt dhan tweet" " seo." msgid "Invalid media file" msgstr "Faidhle meadhain mì-dhligheach" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "Còd mearachd %lld nach aithne dhuinn rè an luchdaidh suas: %s" #, c-format msgid "Unknown error code %lld during upload" msgstr "Còd mearachd %lld nach aithne dhuinn rè an luchdaidh suas" msgid "Now" msgstr "An-dràsta" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d mhionaid air ais" msgstr[1] "%d mhionaid air ais" msgstr[2] "%d mionaidean air ais" msgstr[3] "%d mionaid air ais" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d uair a thìde air ais" msgstr[1] "%d uair a thìde air ais" msgstr[2] "%d uairean a thìde air ais" msgstr[3] "%d uair a thìde air ais" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%du" #. TRANSLATORS: Full-text date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, c-format msgid "Replying to %s" msgstr "A’ freagairt dha %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use #. the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "A’ freagairt dha %s ’s %d eile" msgstr[1] "A’ freagairt dha %s ’s %d eile" msgstr[2] "A’ freagairt dha %s ’s %d eile" msgstr[3] "A’ freagairt dha %s ’s %d eile" #. TRANSLATORS: The first %s is a list of one or more comma-separated user #. names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "A’ freagairt dha %s ’s %s" msgid "Don't have a Twitter account yet?" msgstr "Nach eil cunntas Twitter agad fhathast?" msgid "Create one" msgstr "Cruthaich fear" #, c-format msgid "Could not open %s" msgstr "Cha deach leinn %s fhosgladh" msgid "Failed to retrieve request token" msgstr "Cha b’ urrainn dhuinn tòcan iarrtais fhaighinn" msgid "Wrong PIN" msgstr "PIN ceàrr" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" "Tha an cunntas %s ann mu thràth le iuchraichean eile.\n" "\n" "A bheil thu airson seo a chur ’na àite?" msgid "Account already in use" msgstr "Tha an cunntas seo ga chleachdadh mu thràth" msgid "Failed to retrieve access token" msgstr "Cha b’ urrainn dhuinn tòcan inntrigidh fhaighinn" msgid "Select Image" msgstr "Tagh dealbh" msgid "Unfollow" msgstr "Na lean tuilleadh" msgid "Follow" msgstr "Lean air" msgid "Loading…" msgstr "’Ga luchdadh…" msgid "No entries found" msgstr "Cha deach nì sam bith a lorg" msgid "Retry" msgstr "Feuch ris a-rithist" msgid "Copy URL" msgstr "Dèan lethbhreac dhen URL" msgid "Reload image" msgstr "Ath-luchdaich an dealbh" msgid "Save as…" msgstr "Sàbhail mar…" msgid "Save Video" msgstr "Sàbhail a’ video" msgid "Save Image" msgstr "Sabhail an dealbh" msgid "Save" msgstr "Sàbhail" msgid "Select Banner Image" msgstr "Tagh dealbh na bataich" msgid "Image does not meet minimum size requirements:" msgstr "Tha an dealbh ro bheag agus seo na dh’fheumas tu:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "An leud as lugha: %d phiogsail" msgstr[1] "An leud as lugha: %d phiogsail" msgstr[2] "An leud as lugha: %d piogsailean" msgstr[3] "An leud as lugha: %d piogsail" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "An àirde as lugha: %d phiogsail" msgstr[1] "An àirde as lugha: %d phiogsail" msgstr[2] "An àirde as lugha: %d piogsailean" msgstr[3] "An àirde as lugha: %d piogsail" msgid "Pick" msgstr "Tagh" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Dh’fhàillig le luchdadh %u dhealbh" msgstr[1] "Dh’fhàillig le luchdadh %u dhealbh" msgstr[2] "Dh’fhàillig le luchdadh %u dealbhan" msgstr[3] "Dh’fhàillig le luchdadh %u dealbh" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , #. , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Air ais" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Mearachd a’ faighinn iomradh às an tweet: %s\n" "\n" "A bheil thu airson an tweet nach deach a chur a shàbhaladh?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Mearachd a’ faighinn tweet na freagairte: %s\n" "\n" "A bheil thu airson an tweet nach deach a chur a shàbhaladh?" msgid "Quote tweet" msgstr "Dèan iomradh dhen tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the #. "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "A bheil thu airson dreachd dhen Tweet a chumail?" msgid "Only one animated GIF file per tweet is allowed." msgstr "" "Chan eil barrachd air aon faidhle GIF beòthaichte ceadaichte ann an tweet." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "Tha %d charactar air fhàgail" msgstr[1] "Tha %d charactar air fhàgail" msgstr[2] "Tha %d caractaran air fhàgail" msgstr[3] "Tha %d caractar air fhàgail" #. TRANSLATORS: Values are current image index (1-based) and total image #. count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Dealbh %d à %d" msgstr[1] "Dealbh %d à %d" msgstr[2] "Dealbh %d à %d" msgstr[3] "Dealbh %d à %d" msgid "Modify Filter" msgstr "Atharraich a’ chriathrag" msgid "Matches" msgstr "Tha maids ann" msgid "Doesn't match" msgstr "Chan eil e ’na mhaids" msgid "Modify Snippet" msgstr "Atharraich an snippet" msgid "Snippet can't be empty" msgstr "Chan fhaod snippet a bhith bàn" msgid "Replacement can't be empty" msgstr "Chan fhaod cur an àite a bhith bàn" msgid "Snippet may not contain whitespace" msgstr "Chan fhaod geal-spàs a bhith am broinn snippet" msgid "Snippet already exists" msgstr "Tha an snippet seo ann mu thràth" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" "Feumaidh glèidheadairean-àite {SOURCE_LANG}, {TARGET_LANG} agus {CONTENT} a bhith am broinn URL eadar-theangachaidh\n" "\n" "Thèid URL “%s†a chleachdadh ’na àite" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgstr "" "Nach doir thu sùil air an tionndadh ùr seo dhe #Cawbird! \\ (•◡•) / #cool " "#newisalwaysbetter #gàidhlig" msgid "Add to or Remove User From List" msgstr "Cuir an cleachdaiche ris an liosta no thoir air falbh e" msgid "You have no lists." msgstr "Chan eil liosta agad." msgid "About Cawbird" msgstr "Mu Cawbird" msgid "New Account" msgstr "Cunntas ùr" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Airson Cawbird a dhearbhadh, feumaidh tu PIN fhaighinn o twitter.com leis a’" " chunntas a tha thu airson cur ris" msgid "Request PIN" msgstr "Iarr PIN" msgid "Enter PIN from twitter.com below:" msgstr "Cuir a-steach am PIN o twitter.com gu h-ìosal:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Dearbh" msgid "Account Settings" msgstr "Roghainnean a’ chunntais" msgid "Name" msgstr "Ainm" msgid "Website" msgstr "Làrach-lìn" msgid "Autostart" msgstr "Cuir gu dol gu fèin-obrachail" msgid "Remove Account" msgstr "Thoir air falbh an cunntas" msgid "Do you really want to remove this account?" msgstr "" "A bheil thu cinnteach gu bheil thu airson an cunntas seo a thoirt air falbh?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Samhlaidhean-gnùis ⊠daoine" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Bodhaig ⊠aodach" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Beathaichean ⊠an nàdar" msgctxt "emoji category" msgid "Food & Drink" msgstr "Biadh ⊠deoch" msgctxt "emoji category" msgid "Travel & Places" msgstr "Siubhal ⊠àitichean" msgctxt "emoji category" msgid "Activities" msgstr "Gnìomhachdan" msgctxt "emoji category" msgid "Objects" msgstr "Nithean" msgctxt "emoji category" msgid "Symbols" msgstr "Samhlaidhean" msgctxt "emoji category" msgid "Flags" msgstr "Brataichean" msgid "No Results Found" msgstr "Cha deach toradh a lorg" msgid "Try a different search" msgstr "Feuch lorg eile" msgid "Send" msgstr "Cuir" msgid "Add Media" msgstr "Cuir meadhan ris" msgid "Show favorite images" msgstr "Seall na dealbhan as toigh leat" msgid "Click to edit" msgstr "Briog gus a dheasachadh" msgid "Remove this Filter" msgstr "Thoir air falbh a’ chriathrag seo" msgid "Filtered terms" msgstr "Faclan criathraichte" msgid "Filtered users" msgstr "Cleachdaichean criathraichte" msgid "You can block users in their profile" msgstr "’S urrainn dhut cleachdaichean a bhacadh sna pròifilean aca" msgid "Users" msgstr "Cleachdaichean" msgid "Set image description" msgstr "Suidhich tuairisgeul an deilbh" msgid "Subscribe" msgstr "Fo-sgrìobh" msgid "Unsubscribe" msgstr "Cuir crìoch air an fho-sgrìobhadh" msgid "Subscribers:" msgstr "Fo-sgrìobhadairean:" msgid "Members:" msgstr "Buill:" msgid "Creator:" msgstr "Cruthadair:" msgid "Created at:" msgstr "Air a chruthachadh:" msgid "Edit" msgstr "Deasaich" msgid "Mode:" msgstr "Modh:" msgid "Description" msgstr "Tuairisgeul" msgid "Settings" msgstr "Roghainnean" msgid "Shortcuts" msgstr "Ath-ghoiridean" msgid "About" msgstr "Mu dhèidhinn" msgid "Quit" msgstr "Fàg an-seo" msgid "Add New Filter" msgstr "Cuir criathrag ùr ris" msgid "Regular Expression:" msgstr "Eas-preisean riaghailteach:" msgid "Test:" msgstr "Deuchainn:" msgid "Add New Snippet" msgstr "Cuir snippet ùr ris" msgid "Keyword" msgstr "Facal-luirg" msgid "Replacement" msgstr "Cur an àite" msgid "Create New List" msgstr "Cruthaich liosta ùr" msgid "Name:" msgstr "Ainm:" msgid "Create" msgstr "Cruthaich" msgid "Write Direct Message" msgstr "Sgrìobh teachdaireachd dhìreach" msgid "Add to/Remove from List" msgstr "Cuir ris/Thoir air falbh on liosta" msgid "Blocked" msgstr "’Ga bhacadh" msgid "Muted" msgstr "’Ga mhùchadh" msgid "Retweets disabled" msgstr "Tha ath-thweeteadh à comas" msgid "More actions" msgstr "Barrachd ghnìomhan" msgid "Follows you" msgstr "’Gad leantainn" msgid "Tweets" msgstr "Tweetaichean" msgid "Followers" msgstr "Luchd-leantainn" msgid "Following" msgstr "A’ leantainn" msgid "Use dark theme" msgstr "Cleachd an t-ùrlar dorcha" msgid "Shortcut key" msgstr "Iuchair ath-ghoirid" msgid "Alt" msgstr "Alt" msgid "Ctrl" msgstr "Ctrl" msgid "Shift" msgstr "Shift" msgid "Super" msgstr "Super" msgid "Primary" msgstr "Primary" msgid "Timelines" msgstr "Loidhnichean-ama" msgid "Show inline media" msgstr "Seall meadhanan am broinn na loidhne" msgid "Always show" msgstr "Seall an-còmhnaidh" msgid "Always hide" msgstr "Falaich an-còmhnaidh" msgid "Hide in timeline" msgstr "Falaich air an loidhne-ama" msgid "Auto scroll on new tweets" msgstr "Sgrolaich gu tweetaichean ùra gu fèin-obrachail" msgid "Double-click activation" msgstr "Dèan gnìomhachadh le briogadh dùbailte" msgid "Notifications" msgstr "Brathan" msgid "On New Tweets" msgstr "Air tweetaichean ùra" msgid "Never" msgstr "Chan ann idir" msgid "Every" msgstr "Gach turas" msgid "Stack 5" msgstr "5 ri chèile" msgid "Stack 10" msgstr "10 ri chèile" msgid "Stack 25" msgstr "25 ri chèile" msgid "Stack 50" msgstr "50 ri chèile" msgid "On New Mentions" msgstr "Air iomraidhean ùra" msgid "On New Messages" msgstr "Air teachdaireachdan ùra" msgid "Interface" msgstr "Eadar-aghaidh" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "Àbhaisteach" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "Mòr" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "Glè mhòr" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "Nas motha buileach" msgid "Tweet scale" msgstr "Sgèile nan tweetaichean" msgid "Round avatars" msgstr "Avataran cruinne" msgid "Remove trailing hashtags" msgstr "Thoir air falbh tagaichean hais deiridh" msgid "Remove media links" msgstr "Thoir air falbh ceanglaichean gu meadhanan" msgid "Hide inappropriate content" msgstr "Falaich susbaint frionasach" msgid "Translation" msgstr "Eadar-theangachadh" msgid "Translation service" msgstr "Seirbheis eadar-theangachaidh" msgid "Google" msgstr "Google" msgid "Bing" msgstr "Bing" msgid "DeepL" msgstr "DeepL" msgid "Custom" msgstr "Gnàthaichte" msgid "Custom translation URL" msgstr "URL eadar-theangachaidh gnàthaichte" msgid "No snippets configured." msgstr "Cha deach snippets a rèiteachadh." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "’S urrainn dhut snippets a ghnìomhachadh is tu a’ sgrìobhadh facal-luirg ’s " "a’ brùthadh air TAB an uairsin." msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "Coitcheann" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Sgrìobh Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Seall roghainnean a’ chunntais" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Seall priob-uinneag shealach nan cunntasan" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Seall roghainnean na h-aplacaid" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Toglaich bàr a’ bharra" msgctxt "shortcuts window" msgid "Go Back" msgstr "Air ais" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Air adhart" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Rach gun náµÊ° duilleag" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweetaichean" msgctxt "shortcuts window" msgid "Retweet" msgstr "Dèan ath-thweet" msgctxt "shortcuts window" msgid "Like" msgstr "’S toigh leam seo" msgctxt "shortcuts window" msgid "Reply" msgstr "Freagair" msgctxt "shortcuts window" msgid "Quote" msgstr "Thoir iomradh" msgctxt "shortcuts window" msgid "Show Details" msgstr "Seall am mion-fhiosrachadh" msgctxt "shortcuts window" msgid "Delete" msgstr "Sguab às" msgctxt "shortcuts window" msgid "Compose" msgstr "Sgrìobhadh" msgid "Show Emoji Chooser" msgstr "Seall roghnaichear nan Emoji" msgid "Start new conversation" msgstr "Tòisich air còmhradh ùr" msgid "With:" msgstr "Le:" msgid "Go" msgstr "Siuthad" msgid "Quote" msgstr "Thoir iomradh" msgid "Retweet tweet" msgstr "Dèan ath-thweet dheth" msgid "Like tweet" msgstr "’S toigh leam an tweet" msgid "Reply to tweet" msgstr "Freagair dhan tweet" msgid "More" msgstr "Barrachd" msgid "Translate" msgstr "Eadar-theangaich" msgid "Like" msgstr "’S toigh leam seo" msgid "Reply" msgstr "Freagair" msgid "Liked" msgstr "’S toil leat seo" msgid "Retweeted" msgstr "Air ath-thweeteadh" msgid "Unblock" msgstr "Na bac tuilleadh" msgid "Show settings of this account" msgstr "Seall roghainnean a’ chunntais seo" msgid "Open in new window" msgstr "Fosgail ann an uinneag ùr" msgid "Go to profile" msgstr "Tadhail air a’ phròifil" msgid "Created" msgstr "Air a chruthachadh" msgid "Subscribed to" msgstr "Air fho-sgrìobhadh air" cawbird-1.4.2/po/gl.po000066400000000000000000000606461416632607600145400ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # antiparvos, 2016-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-12-22 02:49+0000\n" "Last-Translator: antiparvos\n" "Language-Team: Galician (http://www.transifex.com/cawbird/cawbird/language/" "gl/)\n" "Language: gl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliente para o Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird é un cliente GTK+ nativo para o Twitter que fornece funcionalidades " "fundamentais como mensaxes directas (MD), notificacións de chíos ou vistas " "de conversas." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Inclúe funcionalidades adicionais como visualización local de vídeos, varias " "imaxes inseridas, listaxes, filtros, múltiples contas, etc." msgid "Generic timeline view when using Cawbird" msgstr "Cronoloxía xenérica ao usar Cawbird" msgid "Typical Twitter profile" msgstr "Perfil típico co Twitter" msgid "Account settings can be configured" msgstr "A conta pode configurarse" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Use Twitter desde un aplicativo de escritorio" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar contas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensaxes directas" #, fuzzy msgid "Select Media" msgstr "Seleccionar imaxe" msgid "Open" msgstr "Abrir" msgid "Cancel" msgstr "Cancelar" #, fuzzy msgid "Selected file is not an image or video." msgstr "O ficheiro seleccionado non é unha imaxe." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "A imaxe é grande de máis. O tamaño máximo permitido son %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "A imaxe é grande de máis. O tamaño máximo permitido son %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "A imaxe é grande de máis. O tamaño máximo permitido son %'d MB" msgid "Insert Emoji" msgstr "Inserir emoji" msgid "Direct Conversation" msgstr "Conversa directa" #, fuzzy msgid "Direct message threads" msgstr "Mensaxes directas" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nova mensaxe de %s" msgstr[1] "%d novas mensaxes de %s" #, c-format msgid "New direct message from %s" msgstr "Nova mensaxe directa de %s" msgid "Direct Messages" msgstr "Mensaxes directas" #, fuzzy msgid "Liked tweets timeline" msgstr "Marcar favorito" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "Engadir un filtro" msgid "Filters" msgstr "Filtros" #, fuzzy msgid "Home timeline" msgstr "Ocultar da cronoloxía" #, c-format msgid "%s retweeted %s" msgstr "%s rechouchiou a %s" #, c-format msgid "%s tweeted" msgstr "%s chiou" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d chío novo" msgstr[1] "%d chíos novos" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s chiou" msgid "Private" msgstr "Privada" msgid "Public" msgstr "Pública" msgid "Lists" msgstr "Listaxes" msgid "Show configured accounts" msgstr "Mostrar contas configuradas" msgid "Compose Tweet" msgstr "Escribir un chío" msgid "Add new Account" msgstr "Engadir conta" #, fuzzy msgid "Mentions timeline" msgstr "Ocultar da cronoloxía" #, c-format msgid "%s mentioned %s" msgstr "%s mencionou a %s" msgid "Mentions" msgstr "Mencións" msgid "Suspended Account" msgstr "Conta suspendida" msgid "Protected profile" msgstr "Perfil protexido" #, c-format msgid "Tweet to @%s" msgstr "Chiar a @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s chiou" msgstr[1] "%s chiou" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seguindo" msgstr[1] "Seguindo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificacións" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Ocultar da cronoloxía" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seguindo" msgid "Protected Profile" msgstr "Perfil protexido" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "Non se atoparon entradas" msgid "No users found" msgstr "Non se atoparon usuarios" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Chiar a @%s" msgid "Search" msgstr "Buscar" msgid "Load More" msgstr "Cargar máis" msgid "Could not show tweet" msgstr "Non foi posíbel mostrar o chío" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Rechouchíos" msgid "Open in Browser" msgstr "Abrir no navegador" msgid "Source" msgstr "Orixe" msgid "Tweet Details" msgstr "Detalles do chío" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d sen ler)" msgstr[1] "(%d sen ler)" msgid "Delete" msgstr "Eliminar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Rechouchiar" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Confirma a eliminación desta conta?" #, c-format msgid "Block %s" msgstr "Bloquear a %s" msgid "This tweet contains images marked as inappropriate" msgstr "Este chío contén imaxes marcadas como inapropiadas" msgid "Show anyway" msgstr "Mostrar" #, fuzzy msgid "Could not authenticate you" msgstr "Non foi posíbel abrir %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Non se atoparon usuarios" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interface" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "A cadea xa existe" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Agora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "En resposta a %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "En resposta a %s e %d máis" msgstr[1] "En resposta a %s e %d máis" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "En resposta a %s e %s" msgid "Don't have a Twitter account yet?" msgstr "Aínda non ten unha conta no Twitter?" msgid "Create one" msgstr "Crear unha" #, c-format msgid "Could not open %s" msgstr "Non foi posíbel abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN equivocado" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Xa se está usando esta conta" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Seleccionar imaxe" msgid "Unfollow" msgstr "Non seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "Cargando..." #, fuzzy msgid "No entries found" msgstr "Non se atoparon usuarios" msgid "Retry" msgstr "Reintentar" msgid "Copy URL" msgstr "Copiar URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Gardar como..." msgid "Save Video" msgstr "Gardar vídeo" msgid "Save Image" msgstr "Gardar imaxe" msgid "Save" msgstr "Gardar" msgid "Select Banner Image" msgstr "Seleccionar imaxe da cabeceira" msgid "Image does not meet minimum size requirements:" msgstr "A imaxe non reúne os requisitos do tamaño mínimo:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Largura mínima: %d píxel" msgstr[1] "Largura mínima: %d píxeles" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Altura mínima: %d píxel" msgstr[1] "Altura mínima: %d píxeles" msgid "Pick" msgstr "Escolla" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Volver" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar chío" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Só se permite un ficheiro GIF por chío." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar filtro" msgid "Matches" msgstr "Resultados" msgid "Doesn't match" msgstr "Sen resultados" msgid "Modify Snippet" msgstr "Modificar as cadeas" msgid "Snippet can't be empty" msgstr "A cadea non pode estar baleira" msgid "Replacement can't be empty" msgstr "A cadea substituta non pode estar baleira" msgid "Snippet may not contain whitespace" msgstr "A cadea non pode conter espazos" msgid "Snippet already exists" msgstr "A cadea xa existe" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Ei, probe esta nova versión do #Cawbird ! \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Engadir ou eliminar dunha listaxe" msgid "You have no lists." msgstr "Non ten listaxes." msgid "About Cawbird" msgstr "Sobre Cawbird" msgid "New Account" msgstr "Nova conta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Para autenticar o Cawbird, precisa un PIN de twitter.com coa conta que " "desexa engadir" msgid "Request PIN" msgstr "Solicitar PIN" msgid "Enter PIN from twitter.com below:" msgstr "Introduza debaixo o PIN de twitter.com:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Axustes da conta" msgid "Name" msgstr "Nome" msgid "Website" msgstr "Sitio web" msgid "Autostart" msgstr "Inicio automático" #, fuzzy msgid "Remove Account" msgstr "Eliminar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Confirma a eliminación desta conta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Emoticonos e persoas" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Corpo e roupa" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animais e natureza" msgctxt "emoji category" msgid "Food & Drink" msgstr "Comida e bebida" msgctxt "emoji category" msgid "Travel & Places" msgstr "Viaxar e lugares" msgctxt "emoji category" msgid "Activities" msgstr "Actividades" msgctxt "emoji category" msgid "Objects" msgstr "Obxectos" msgctxt "emoji category" msgid "Symbols" msgstr "Símbolos" msgctxt "emoji category" msgid "Flags" msgstr "Bandeiras" msgid "No Results Found" msgstr "Sen resultados" msgid "Try a different search" msgstr "Tente unha busca diferente" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Mostrar as imaxes favoritas" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtros" #, fuzzy msgid "Filtered users" msgstr "Filtros" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuarios" #, fuzzy msgid "Set image description" msgstr "Descrición" msgid "Subscribe" msgstr "Subscribir" msgid "Unsubscribe" msgstr "Non subscribir" msgid "Subscribers:" msgstr "Subscritores:" msgid "Members:" msgstr "Membros:" msgid "Creator:" msgstr "Creador:" msgid "Created at:" msgstr "Creada o:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "Descrición" msgid "Settings" msgstr "Axustes" msgid "Shortcuts" msgstr "Atallos" msgid "About" msgstr "Sobre" msgid "Quit" msgstr "Saír" msgid "Add New Filter" msgstr "Engadir un filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Engadir cadea" msgid "Keyword" msgstr "Palabra clave" msgid "Replacement" msgstr "Cadea substituta" msgid "Create New List" msgstr "Crear unha listaxe" msgid "Name:" msgstr "Nome:" msgid "Create" msgstr "Crear" msgid "Write Direct Message" msgstr "Escribir mensaxe directa" msgid "Add to/Remove from List" msgstr "Engadir/Eliminar dunha listaxe" msgid "Blocked" msgstr "Bloqueado" msgid "Muted" msgstr "Silenciado" msgid "Retweets disabled" msgstr "Rechouchíos desactivados" #, fuzzy msgid "More actions" msgstr "Mencións" msgid "Follows you" msgstr "Séguete" msgid "Tweets" msgstr "Chíos" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Seguindo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atallos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Ocultar da cronoloxía" msgid "Show inline media" msgstr "Mostrar medios inseridos" msgid "Always show" msgstr "Mostrar sempre" msgid "Always hide" msgstr "Ocultar sempre" msgid "Hide in timeline" msgstr "Ocultar da cronoloxía" msgid "Auto scroll on new tweets" msgstr "Desprazamento auto. nos chíos novos" msgid "Double-click activation" msgstr "Activación por dobre clic" msgid "Notifications" msgstr "Notificacións" msgid "On New Tweets" msgstr "Dos novos chíos" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Todos" msgid "Stack 5" msgstr "Cada 5" msgid "Stack 10" msgstr "Cada 10" msgid "Stack 25" msgstr "Cada 25" msgid "Stack 50" msgstr "Cada 50" msgid "On New Mentions" msgstr "Das novas mencións" msgid "On New Messages" msgstr "Das novas mensaxes" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Chíos" msgid "Round avatars" msgstr "Avatares redondos" msgid "Remove trailing hashtags" msgstr "Retirar cancelos no final dos chíos" msgid "Remove media links" msgstr "Retirar ligazóns multimedia" msgid "Hide inappropriate content" msgstr "Ocultar contido inapropiado" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Non hai cadeas configuradas" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Pode activar unha cadea escribindo a palabra clave e premendo TAB." msgid "Snippets" msgstr "Cadeas para inserir" msgctxt "shortcuts window" msgid "General" msgstr "Xeral" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Escribir un chío" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostrar os axustes das contas" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostrar información emerxente das contas" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostrar os axustes dos aplicativos" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Cambiar a barra superior" msgctxt "shortcuts window" msgid "Go Back" msgstr "Volver" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Seguir" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ir á páxina" msgctxt "shortcuts window" msgid "Tweets" msgstr "Chíos" msgctxt "shortcuts window" msgid "Retweet" msgstr "Rechouchiar" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Marcar favorito" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "Citar" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostrar detalles" msgctxt "shortcuts window" msgid "Delete" msgstr "Eliminar" msgctxt "shortcuts window" msgid "Compose" msgstr "Escribir" msgid "Show Emoji Chooser" msgstr "Mostrar o selector de emojis" msgid "Start new conversation" msgstr "Iniciar unha conversa nova" msgid "With:" msgstr "Con:" msgid "Go" msgstr "Ir" msgid "Quote" msgstr "Citar" msgid "Retweet tweet" msgstr "Rechouchiar" #, fuzzy msgid "Like tweet" msgstr "Marcar favorito" msgid "Reply to tweet" msgstr "Responder" msgid "More" msgstr "Máis" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Marcar favorito" msgid "Reply" msgstr "Responder" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Rechouchiar" msgid "Unblock" msgstr "Desbloquear" msgid "Show settings of this account" msgstr "Mostrar axustes da conta" msgid "Open in new window" msgstr "Abrir nunha xanela" msgid "Go to profile" msgstr "Ir ao perfil" msgid "Created" msgstr "Creada" msgid "Subscribed to" msgstr "Subscrito a" #~ msgid "Replying to" #~ msgstr "En resposta a" #~ msgid "and" #~ msgstr "e" #~ msgid "Actions" #~ msgstr "Accións" #~ msgid "Add Image" #~ msgstr "Engadir imaxe" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Listaxe" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Non autorizado. Normalmente significa que algo funciona mal nos " #~ "servidores do Twitter e debería tentalo máis tarde" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Non foi posíbel cargar os chíos" cawbird-1.4.2/po/hi.po000066400000000000000000000513701416632607600145300ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # vm, 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Hindi (India) (http://www.transifex.com/cawbird/cawbird/" "language/hi_IN/)\n" "Language: hi_IN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "पà¥à¤°à¤¤à¥à¤¯à¤•à¥à¤· संदेश" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "रदà¥à¤¦ करें" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "पà¥à¤°à¤¤à¥à¤¯à¤•à¥à¤· संदेश" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" msgstr[1] "" #, c-format msgid "New direct message from %s" msgstr "%s दà¥à¤µà¤¾à¤°à¤¾ पà¥à¤°à¤¤à¥à¤¯à¤•à¥à¤· संदेश" msgid "Direct Messages" msgstr "पà¥à¤°à¤¤à¥à¤¯à¤•à¥à¤· संदेश" msgid "Liked tweets timeline" msgstr "" msgid "Likes" msgstr "" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s ने यह रीटà¥à¤µà¥€à¤Ÿ किया %s" #, c-format msgid "%s tweeted" msgstr "%s ने टà¥à¤µà¥€à¤Ÿ किया" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d नया टà¥à¤µà¥€à¤Ÿ" msgstr[1] "%d नठटà¥à¤µà¥€à¤Ÿ!" msgid "Home" msgstr "होम" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s ने टà¥à¤µà¥€à¤Ÿ किया" msgid "Private" msgstr "" msgid "Public" msgstr "" msgid "Lists" msgstr "सूचियाà¤" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "" msgid "Add new Account" msgstr "" msgid "Mentions timeline" msgstr "" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "" #, c-format msgid "Tweet to @%s" msgstr "" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s ने टà¥à¤µà¥€à¤Ÿ किया" msgstr[1] "%s ने टà¥à¤µà¥€à¤Ÿ किया" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "" msgstr[1] "" #, c-format msgid "Location: %s" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, c-format msgid "%s timeline" msgstr "" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "फ़ॉलोअरà¥à¤¸" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "फ़ॉलोअरà¥à¤¸" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "फ़ॉलोअरà¥à¤¸" msgstr[1] "फ़ॉलोअरà¥à¤¸" #, fuzzy msgid "No tweets found" msgstr "कोई अभिलेख नही मिला" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "" msgid "Open in Browser" msgstr "" msgid "Source" msgstr "" msgid "Tweet Details" msgstr "" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgid "Delete" msgstr "" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "%s ने यह रीटà¥à¤µà¥€à¤Ÿ किया %s" msgid "Are you sure you want to delete this tweet?" msgstr "" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "%s नहीं खोल पाये" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, c-format msgid "Replying to %s" msgstr "" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "अभि बनाये" #, c-format msgid "Could not open %s" msgstr "%s नहीं खोल पाये" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "गलत पिन" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "यह खता पहले से उपयोग में है" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "अनफॉलो करे" msgid "Follow" msgstr "फॉलो करे" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "कोई अभिलेख नही मिला" msgid "Retry" msgstr "पà¥à¤¨: पà¥à¤°à¤¯à¤¾à¤¸ करें" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "सहेजें" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "फ़िलà¥à¤Ÿà¤° करे संशोधित करे" msgid "Matches" msgstr "" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "उपयोगकरà¥à¤¤à¤¾ को सूची से जोड़ें या निकालें" msgid "You have no lists." msgstr "आपके पास कोई सूची नही है" msgid "About Cawbird" msgstr "" msgid "New Account" msgstr "" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "" msgid "Account Settings" msgstr "खाते की सेटिंगà¥à¤¸" msgid "Name" msgstr "" msgid "Website" msgstr "" msgid "Autostart" msgstr "" msgid "Remove Account" msgstr "" msgid "Do you really want to remove this account?" msgstr "" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "" msgid "Unsubscribe" msgstr "" msgid "Subscribers:" msgstr "" msgid "Members:" msgstr "" msgid "Creator:" msgstr "" msgid "Created at:" msgstr "" msgid "Edit" msgstr "" msgid "Mode:" msgstr "" msgid "Description" msgstr "" msgid "Settings" msgstr "सेटिंगà¥à¤¸" msgid "Shortcuts" msgstr "" msgid "About" msgstr "" msgid "Quit" msgstr "" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "नयी सूचि बनाये " msgid "Name:" msgstr "" msgid "Create" msgstr "" msgid "Write Direct Message" msgstr "" msgid "Add to/Remove from List" msgstr "" msgid "Blocked" msgstr "" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" msgid "More actions" msgstr "" msgid "Follows you" msgstr "" msgid "Tweets" msgstr "" msgid "Followers" msgstr "फ़ॉलोअरà¥à¤¸" msgid "Following" msgstr "" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" msgid "Timelines" msgstr "" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "" msgid "Every" msgstr "" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" msgid "Tweet scale" msgstr "" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "" msgid "Go" msgstr "" msgid "Quote" msgstr "" msgid "Retweet tweet" msgstr "" msgid "Like tweet" msgstr "" msgid "Reply to tweet" msgstr "" msgid "More" msgstr "" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "%s ने यह रीटà¥à¤µà¥€à¤Ÿ किया %s" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "इस खाते की सेटिंगà¥à¤¸ दिखाà¤" msgid "Open in new window" msgstr "" msgid "Go to profile" msgstr "" msgid "Created" msgstr "" msgid "Subscribed to" msgstr "" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "फ़ॉलोअरà¥à¤¸" #~ msgid "List" #~ msgstr "सूचि" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/hu.po000066400000000000000000000500251416632607600145400ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Peter Borsa , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Hungarian (http://www.transifex.com/cawbird/cawbird/language/" "hu/)\n" "Language: hu\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Twitter kliens" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Személyes üzenetek" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Mégse" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "Személyes üzenetek" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d új üzenet %s felhasználótól" msgstr[1] "%d új üzenet %s felhasználótól" #, c-format msgid "New direct message from %s" msgstr "Új személyes üzenet %s felhasználótól" msgid "Direct Messages" msgstr "Személyes üzenetek" #, fuzzy msgid "Liked tweets timeline" msgstr "Kedvencek" #, fuzzy msgid "Likes" msgstr "Kedvencek" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "" #, c-format msgid "%s tweeted" msgstr "" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgstr[1] "" msgid "Home" msgstr "Home" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "" msgid "Private" msgstr "Privát" msgid "Public" msgstr "Publikus" msgid "Lists" msgstr "Listák" msgid "Show configured accounts" msgstr "" msgid "Compose Tweet" msgstr "" msgid "Add new Account" msgstr "" msgid "Mentions timeline" msgstr "" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Védett profil" #, c-format msgid "Tweet to @%s" msgstr "" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "" msgstr[1] "" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Követve" msgstr[1] "Követve" #, fuzzy, c-format msgid "Location: %s" msgstr "Értesítések" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Kedvencek" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "KövetÅ‘k" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Követve" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "KövetÅ‘k" msgstr[1] "KövetÅ‘k" msgid "No tweets found" msgstr "" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "Keresés" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "" msgid "Open in Browser" msgstr "Megnyitás a böngészÅ‘ben" msgid "Source" msgstr "Forrás" msgid "Tweet Details" msgstr "Tweet részletek" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "" msgstr[1] "" msgid "Delete" msgstr "Törlés" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "" msgid "Are you sure you want to delete this tweet?" msgstr "" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" msgid "Could not authenticate you" msgstr "" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" msgid "Internal error" msgstr "" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Most" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, c-format msgid "Replying to %s" msgstr "" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "" #, c-format msgid "Could not open %s" msgstr "" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Hibás PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "A felhasználói fiók már használatban" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "" msgid "Follow" msgstr "Követés" msgid "Loading…" msgstr "" msgid "No entries found" msgstr "" msgid "Retry" msgstr "Újra" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Mentés" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Vissza" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "SzűrÅ‘ módosítása" msgid "Matches" msgstr "" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "" msgid "You have no lists." msgstr "" msgid "About Cawbird" msgstr "Cawbird-rÅ‘l" msgid "New Account" msgstr "Új felhasználói fiók" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "PIN kérése" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "MegerÅ‘sítés" msgid "Account Settings" msgstr "" msgid "Name" msgstr "Név" msgid "Website" msgstr "Weboldal" msgid "Autostart" msgstr "" #, fuzzy msgid "Remove Account" msgstr "Törlés" msgid "Do you really want to remove this account?" msgstr "" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Küldés" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Felhasználók" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Feliratkozás" msgid "Unsubscribe" msgstr "Leiratkozás" msgid "Subscribers:" msgstr "Feliratkozók:" msgid "Members:" msgstr "Tagok:" msgid "Creator:" msgstr "" msgid "Created at:" msgstr "" msgid "Edit" msgstr "Szerkesztés" msgid "Mode:" msgstr "" msgid "Description" msgstr "" msgid "Settings" msgstr "Beállítások" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Névjegy" msgid "Quit" msgstr "Kilépés" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "Új lista létrehozása" msgid "Name:" msgstr "Név:" msgid "Create" msgstr "Létrehozás" msgid "Write Direct Message" msgstr "" msgid "Add to/Remove from List" msgstr "" msgid "Blocked" msgstr "Blokkolva" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" msgid "More actions" msgstr "" msgid "Follows you" msgstr "" msgid "Tweets" msgstr "Tweetek" msgid "Followers" msgstr "KövetÅ‘k" msgid "Following" msgstr "Követve" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Kedvencek" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "Értesítések" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "Soha" msgid "Every" msgstr "Minden" msgid "Stack 5" msgstr "" msgid "Stack 10" msgstr "" msgid "Stack 25" msgstr "" msgid "Stack 50" msgstr "" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweetek" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "" msgid "With:" msgstr "" msgid "Go" msgstr "" msgid "Quote" msgstr "" msgid "Retweet tweet" msgstr "" msgid "Like tweet" msgstr "" msgid "Reply to tweet" msgstr "" msgid "More" msgstr "" msgid "Translate" msgstr "" msgid "Like" msgstr "" msgid "Reply" msgstr "Válasz" msgid "Liked" msgstr "" msgid "Retweeted" msgstr "" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "" msgid "Open in new window" msgstr "Megnyitás új ablakban" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Létrehozva" msgid "Subscribed to" msgstr "" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "KövetÅ‘k" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/id.po000066400000000000000000000575461416632607600145370ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Krisan Alfa Timur , 2015 # Mahyuddin , 2015-2016-2016 # Reza Faiz A. Rahman , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Indonesian (Indonesia) (http://www.transifex.com/cawbird/" "cawbird/language/id_ID/)\n" "Language: id_ID\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter Klien" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird adalah GTK+ twitter klien native yang menyediakan fitur penting " "seperti Pesan Langsung (DMS), tweet pemberitahuan, pemandangan percakapan." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Fitur tambahan termasuk melihat video lokal, beberapa gambar di baris, " "Daftar, Penyaring, beberapa akun, dll" msgid "Generic timeline view when using Cawbird" msgstr "Tampilan timeline generik ketika menggunakan Cawbird" msgid "Typical Twitter profile" msgstr "Khas Profil Twitter" msgid "Account settings can be configured" msgstr "Setelan akun dapat dikonfigurasi" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Gunakan Twitter dari aplikasi desktop normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Tampilakn pengaturan akun" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Pesan Langsung" #, fuzzy msgid "Select Media" msgstr "Pilih Gambar" msgid "Open" msgstr "Buka" msgid "Cancel" msgstr "Batal" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Gambar yang dipilih terlalu besar. Ukuran berkas maksimum per gambar adalah" "%'d'd MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Gambar yang dipilih terlalu besar. Ukuran berkas maksimum per gambar adalah" "%'d'd MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Gambar yang dipilih terlalu besar. Ukuran berkas maksimum per gambar adalah" "%'d'd MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Percakapan Langsung" #, fuzzy msgid "Direct message threads" msgstr "Pesan Langsung" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d Pesan baru dari %s" #, c-format msgid "New direct message from %s" msgstr "Pesan baru langsung dari %s" msgid "Direct Messages" msgstr "Pesan Langsung" #, fuzzy msgid "Liked tweets timeline" msgstr "Tweet kesukaan" #, fuzzy msgid "Likes" msgstr "Kesukaan" msgid "Add new Filter" msgstr "Tambah Penyaringan baru" msgid "Filters" msgstr "Penyaringan" #, fuzzy msgid "Home timeline" msgstr "Sembunyi di beranda" #, c-format msgid "%s retweeted %s" msgstr "%s retweeted %s" #, c-format msgid "%s tweeted" msgstr "%s tweeted" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d Tweets! baru" msgid "Home" msgstr "Halaman depan" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweeted" msgid "Private" msgstr "Rahasia" msgid "Public" msgstr "Umum" msgid "Lists" msgstr "Daftar" msgid "Show configured accounts" msgstr "Tampilakn pengaturan akun" msgid "Compose Tweet" msgstr "Menulis tweet" msgid "Add new Account" msgstr "Tambah Akun baru" #, fuzzy msgid "Mentions timeline" msgstr "Sembunyi di beranda" #, c-format msgid "%s mentioned %s" msgstr "%s mentioned %s" msgid "Mentions" msgstr "Menyebutkan" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Profil dilindungi" #, c-format msgid "Tweet to @%s" msgstr "Tweet untuk @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweeted" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Mengikuti" #, fuzzy, c-format msgid "Location: %s" msgstr "Notifikasi" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Sembunyi di beranda" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Pengikut" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Mengikuti" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Pengikut" #, fuzzy msgid "No tweets found" msgstr "Entri tidak ditemukan" msgid "No users found" msgstr "User tidak ditemukan" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet untuk @%s" msgid "Search" msgstr "Pencarian" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Tidak bisa menampilkan tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Buka di Peladen" msgid "Source" msgstr "Sumber" msgid "Tweet Details" msgstr "Tweet Detil" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d belum dibaca)" msgid "Delete" msgstr "Hapus" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweet" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Anda yakin akan menghapus akun ini?" #, c-format msgid "Block %s" msgstr "Blok %s" msgid "This tweet contains images marked as inappropriate" msgstr "Tweet ini berisi gambar yang ditandai sebagai tidak sesuai" msgid "Show anyway" msgstr "Tampilkan apa saja" #, fuzzy msgid "Could not authenticate you" msgstr "Tidak bisa membuka %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "User tidak ditemukan" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Antarmuka" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Cuplikan sudah ada" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Sekarang" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dj" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Membalas ke %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Membalas ke %s dan 1%d lainnya" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Membalas ke %s dan %s" msgid "Don't have a Twitter account yet?" msgstr "Belum mempunyai akun Twitter?" msgid "Create one" msgstr "Buat satu" #, c-format msgid "Could not open %s" msgstr "Tidak bisa membuka %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Salah PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Akun sudah digunakan" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Pilih Gambar" msgid "Unfollow" msgstr "Batal mengikuti" msgid "Follow" msgstr "Mengikuti" msgid "Loading…" msgstr "Sedang memuatkan..." #, fuzzy msgid "No entries found" msgstr "User tidak ditemukan" msgid "Retry" msgstr "Coba lagi" msgid "Copy URL" msgstr "Salin URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Simpan sebagai..." msgid "Save Video" msgstr "SImpan Vidio" msgid "Save Image" msgstr "SImpan Gambar" msgid "Save" msgstr "Simpan" msgid "Select Banner Image" msgstr "Pilih Gambar Spanduk" msgid "Image does not meet minimum size requirements:" msgstr "Gambar tidak memenuhi persyaratan ukuran minimum:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Lebar minimum: %d piksel" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Tinggi minimum: %d piksel" msgid "Pick" msgstr "Memilih" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Kembali" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Kutip tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Hanya satu file GIF per tweet yang diizinkan." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "Ubah Penyaringan" msgid "Matches" msgstr "Cocok" msgid "Doesn't match" msgstr "Tidak cocok" msgid "Modify Snippet" msgstr "Ubah Cuplikan" msgid "Snippet can't be empty" msgstr "Cuplikan tidak boleh kosong" msgid "Replacement can't be empty" msgstr "Penggantian tidak boleh kosong" msgid "Snippet may not contain whitespace" msgstr "Cuplikan mungkin tidak mengandung spasi" msgid "Snippet already exists" msgstr "Cuplikan sudah ada" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hai, coba lihat versi baru dari #Cawbird! \\ (•◡•) / #keren " "#yangbaruselalulebihbaik" msgid "Add to or Remove User From List" msgstr "Tambah ke atau Hapus Pengguna Dari Daftar" msgid "You have no lists." msgstr "Anda tidak memiliki daftar." msgid "About Cawbird" msgstr "Tentang Cawbird" msgid "New Account" msgstr "Akun Baru" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Untuk mengotentikasi Cawbird, Anda memerlukan PIN dari twitter.com dengan " "akun yang ingin Anda tambahkan" msgid "Request PIN" msgstr "Permintaan PIN:" msgid "Enter PIN from twitter.com below:" msgstr "Masukkan PIN dari twitter.com dibawah ini:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Konfirmasi" msgid "Account Settings" msgstr "Pengaturan Akun" msgid "Name" msgstr "Nama" msgid "Website" msgstr "Situs" msgid "Autostart" msgstr "Mulai Otomatis" #, fuzzy msgid "Remove Account" msgstr "Hapus" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Anda yakin akan menghapus akun ini?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Kirim" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Tampilkan gambar favorit" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Penyaringan" #, fuzzy msgid "Filtered users" msgstr "Penyaringan" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Pengguna" #, fuzzy msgid "Set image description" msgstr "Deskripsi" msgid "Subscribe" msgstr "Langganan" msgid "Unsubscribe" msgstr "Berhenti langganan" msgid "Subscribers:" msgstr "Pelanggan:" msgid "Members:" msgstr "Anggota:" msgid "Creator:" msgstr "Pembuat:" msgid "Created at:" msgstr "Dibuat di:" msgid "Edit" msgstr "Ubah" msgid "Mode:" msgstr "Mode:" msgid "Description" msgstr "Deskripsi" msgid "Settings" msgstr "Pengaturan" msgid "Shortcuts" msgstr "Pintasan" msgid "About" msgstr "Tentang" msgid "Quit" msgstr "Berhenti" msgid "Add New Filter" msgstr "Tambah Penyaringan Baru" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Tambah Potongan Baru" msgid "Keyword" msgstr "Kata Kunci" msgid "Replacement" msgstr "Penggantian" msgid "Create New List" msgstr "Buat Daftar Baru" msgid "Name:" msgstr "Nama:" msgid "Create" msgstr "Membuat" msgid "Write Direct Message" msgstr "Tulis Pesan Langsung" msgid "Add to/Remove from List" msgstr "Tambah ke/Hapus dari daftar" msgid "Blocked" msgstr "Diblokir" msgid "Muted" msgstr "Meredam" msgid "Retweets disabled" msgstr "Retweets dinonaktifkan" #, fuzzy msgid "More actions" msgstr "Menyebutkan" msgid "Follows you" msgstr "Mengikuti anda" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Pengikut" msgid "Following" msgstr "Mengikuti" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Pintasan" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Sembunyi di beranda" msgid "Show inline media" msgstr "Tampilkan media dalam baris" msgid "Always show" msgstr "Selalu muncul" msgid "Always hide" msgstr "Selalu sembunyi" msgid "Hide in timeline" msgstr "Sembunyi di beranda" msgid "Auto scroll on new tweets" msgstr "Auto scroll pada tweet baru" msgid "Double-click activation" msgstr "Aktivasi klik-ganda" msgid "Notifications" msgstr "Notifikasi" msgid "On New Tweets" msgstr "Pada Tweets Baru" msgid "Never" msgstr "Tidak pernah" msgid "Every" msgstr "Setiap" msgid "Stack 5" msgstr "Stack 5" msgid "Stack 10" msgstr "Stack 10" msgid "Stack 25" msgstr "Stack 25" msgid "Stack 50" msgstr "Stack 50" msgid "On New Mentions" msgstr "Pada Menyebutkan Baru" msgid "On New Messages" msgstr "Pada Pesan Baru" msgid "Interface" msgstr "Antarmuka" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Avatar bulat" msgid "Remove trailing hashtags" msgstr "Hapus hastag sisa" msgid "Remove media links" msgstr "Hapus tautan media" msgid "Hide inappropriate content" msgstr "Sembunyikan konten yang tidak sesuai" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Tidak ada potongan yang terkonfigurasi." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Kamu bisa mengaktifkan potongan dengan menuliskan kata kuncinya dan tekan " "tombol TAB." msgid "Snippets" msgstr "Potongan" msgctxt "shortcuts window" msgid "General" msgstr "Umum" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Menulis Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Tampilkan Pengaturan Akun" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Tampilkan Popover Akun" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Tampilkan Pengaturan Aplikasi" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Alih Topbar" msgctxt "shortcuts window" msgid "Go Back" msgstr "Kembali" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Lanjut" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ke halaman nth" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Kesukaan" msgctxt "shortcuts window" msgid "Reply" msgstr "Balas" msgctxt "shortcuts window" msgid "Quote" msgstr "Kutipan" msgctxt "shortcuts window" msgid "Show Details" msgstr "Tampilkan Detil" msgctxt "shortcuts window" msgid "Delete" msgstr "Hapus" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Mulai percakapan baru" msgid "With:" msgstr "Dengan:" msgid "Go" msgstr "Pergi" msgid "Quote" msgstr "Kutipan" msgid "Retweet tweet" msgstr "Retweet tweet" #, fuzzy msgid "Like tweet" msgstr "Tweet kesukaan" msgid "Reply to tweet" msgstr "Balas ke tweet" msgid "More" msgstr "Lebih banyak" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Kesukaan" msgid "Reply" msgstr "Balas" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweet" msgid "Unblock" msgstr "Lepaskan blokir" msgid "Show settings of this account" msgstr "Tampilkan pengaturan akun ini" msgid "Open in new window" msgstr "Buka di window baru" msgid "Go to profile" msgstr "Pergi ke profil" msgid "Created" msgstr "Dibuat" msgid "Subscribed to" msgstr "Berlangganan ke" #~ msgid "Replying to" #~ msgstr "Membalas ke" #~ msgid "and" #~ msgstr "dan" #~ msgid "Actions" #~ msgstr "Aksi" #~ msgid "Add Image" #~ msgstr "Tambah Gambar" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Pengikut" #~ msgid "List" #~ msgstr "Daftar" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Tidak sah, ini berarti ada yang tidak beres dengan server Twitter dan " #~ "Anda harus mencoba lagi nanti" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Tidak bisa menampilkan tweet" cawbird-1.4.2/po/it.po000066400000000000000000000665311416632607600145510ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # Daniele Napolitano , 2014 # Emanuele Rocco Petrone , 2018 # Matteo Castelli , 2016 # Nicola Stanislao Vitale , 2015 # Tassoman, 2017 # ScardracS , 2020 # Dieg 0 , 2020 # Albano Battistella , 2019-2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: albanobattistella , 2021\n" "Language-Team: Italian (https://www.transifex.com/cawbird/teams/107135/it/)\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Client Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird è un client twitter nativo GTK+ che fornisce funzioni vitali come " "messaggi diretti (DM), notifiche dei tweet e vista delle conversazioni." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Funzionalità aggiuntive includono riproduzione locale dei video, più " "immagini in una riga, liste, filtri, account multipli ecc." msgid "Generic timeline view when using Cawbird" msgstr "Vista generica della timeline quando usi Cawbird" msgid "Typical Twitter profile" msgstr "Tipico profilo twitter" msgid "Account settings can be configured" msgstr "Le impostazioni dell'account possono essere configurate" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Cawbird in diversi temi (Adwaita, Adwaita dark variant, High Contrast e " "Adwaita Dark Green)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Usa Twitter come una normale applicazione desktop" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "Twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" "Mostra solo la finestra di \"composizione del tweet\" per l'account " "specificato, nient'altro." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "nome-account" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "Avvia servizio" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "Interrompi il servizio, se è stato avviato come as a service" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "Stampa account di avvio configurati" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Apri la finestra per l'account specificato" #, c-format msgid "Direct messages with %s" msgstr "Messaggi diretti con %s" msgid "Select Media" msgstr "Seleziona Media" msgid "Open" msgstr "Aprire" msgid "Cancel" msgstr "Annulla" msgid "Selected file is not an image or video." msgstr "Il file selezionato non è un'immagine o un video." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Il video selezionato è troppo grande. La dimensione massima del file per " "video è%'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "L'immagine selezionata è troppo grossa. La dimensione massima consentita è " "%'d MB" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Il GIF selezionato è troppo grande. La dimensione massima per GIF è di %'d MB" msgid "Insert Emoji" msgstr "Inserisci emoji" msgid "Direct Conversation" msgstr "Conversazione diretta" msgid "Direct message threads" msgstr "Thread dei messaggi diretti" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nuovo messaggio da %s" msgstr[1] "%d nuovi messaggi da %s" #, c-format msgid "New direct message from %s" msgstr "Nuovo messaggio diretto da %s" msgid "Direct Messages" msgstr "Messaggi diretti" #, fuzzy msgid "Liked tweets timeline" msgstr "Timeline dei favoriti" #, fuzzy msgid "Likes" msgstr "Preferiti" msgid "Add new Filter" msgstr "Aggiungi un nuovo filtro" msgid "Filters" msgstr "Filtri" msgid "Home timeline" msgstr "Timeline della home" #, c-format msgid "%s retweeted %s" msgstr "%s ha ritwittato %s" #, c-format msgid "%s tweeted" msgstr "%s ritwittato" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nuovo Twett!" msgstr[1] "%d nuovi Tweet!" msgid "Home" msgstr "Home" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "%s lista dei tweet" msgid "Private" msgstr "Privata" msgid "Public" msgstr "Pubblica" msgid "Lists" msgstr "Liste" msgid "Show configured accounts" msgstr "Mostra gli account configurati" msgid "Compose Tweet" msgstr "Componi tweet" msgid "Add new Account" msgstr "Aggiungi un nuovo account" msgid "Mentions timeline" msgstr "Timeline delle mezioni" #, c-format msgid "%s mentioned %s" msgstr "%s ha menzionato %s" msgid "Mentions" msgstr "Menzioni" msgid "Suspended Account" msgstr "Account sospeso" msgid "Protected profile" msgstr "Profilo protetto" #, c-format msgid "Tweet to @%s" msgstr "Twitta a @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d tweets" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "%d account seguito" msgstr[1] "%d account seguiti" #, c-format msgid "Location: %s" msgstr "Da: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, c-format msgid "%s timeline" msgstr "Timeline di %s" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, c-format msgid "%s followers" msgstr "%s followers " #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, c-format msgid "%s following" msgstr "Seguiti da %s" msgid "Protected Profile" msgstr "Profilo protetto" msgid "User is blocked" msgstr "L'utente è bloccato" msgid "User is muted" msgstr "L'utente è mutato" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d follower" msgstr[1] "%d followers" msgid "No tweets found" msgstr "Nessun tweet trovato" msgid "No users found" msgstr "Nessun utente" #, c-format msgid "Users matching \"%s\"" msgstr "Profili trovati: \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweets trovati: \"%s\"" msgid "Search" msgstr "Cerca" msgid "Load More" msgstr "Mostra altri" msgid "Could not show tweet" msgstr "Impossibile mostrare il tweet" msgid "This tweet is hidden by the author" msgstr "Questo tweet è nascosto dall'autore" msgid "This tweet is unavailable" msgstr "Questo tweet non è disponibile" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweet" msgid "Open in Browser" msgstr "Apri nel browser" msgid "Source" msgstr "Sorgente" msgid "Tweet Details" msgstr "Dettagli tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d non letto)" msgstr[1] "(%d non letti)" msgid "Delete" msgstr "Elimina" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Ritwittato da %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "Sei sicuro di voler eliminare questo tweet?" #, c-format msgid "Block %s" msgstr "Blocca %s" msgid "This tweet contains images marked as inappropriate" msgstr "Questo tweet contiene immagini segnalate come inappropriate" msgid "Show anyway" msgstr "Mostrare comunque" msgid "Could not authenticate you" msgstr "non possiamo autenticarti" msgid "Sorry, that page does not exist" msgstr "Mi dispiace, quella pagina non esiste" msgid "User not found." msgstr "Utente non trovato." msgid "User has been suspended." msgstr "L'utente è stato sospeso." msgid "Your account is suspended and is not permitted to access this feature" msgstr "Il tuo account è sospeso e non è consentito accedere a questa funzione" msgid "Rate limit exceeded" msgstr "Tasso limite superato" msgid "Invalid or expired token" msgstr "Token non valido o scaduto" msgid "The specified user is not a subscriber of this list." msgstr "L'utente specificato non è un abbonato di questo elenco." msgid "The user you are trying to remove from the list is not a member." msgstr "L'utente che si sta tentando di rimuovere dall'elenco non è un membro." msgid "Account update failed: value is too long." msgstr "Aggiornamento dell'account non riuscito: il valore è troppo lungo." msgid "Over capacity" msgstr "Sopra la capacità" msgid "Internal error" msgstr "Errore interno" msgid "You have already favorited this status." msgstr "Hai già inserito tra i preferiti questo status." msgid "No status found with that ID." msgstr "Nessuno stato trovato con quell'ID." msgid "You cannot send messages to users who are not following you." msgstr "Non è possibile inviare messaggi agli utenti che non ti seguono." msgid "There was an error sending your message." msgstr "Si è verificato un errore nell'invio del messaggio." msgid "You've already requested to follow this user." msgstr "Hai già richiesto di seguire questo utente." msgid "You are unable to follow more people at this time" msgstr "Non puoi seguire più persone in questo momento" msgid "Sorry, you are not authorized to see this status" msgstr "Spiacenti, non sei autorizzato a visualizzare questo stato" msgid "User is over daily status update limit" msgstr "" "L'utente ha superato il limite di aggiornamento dello stato giornaliero" msgid "Tweet needs to be a bit shorter." msgstr "Il Tweet deve essere un po 'più breve." msgid "Status is a duplicate" msgstr "Lo stato è un duplicato" msgid "Owner must allow dms from anyone." msgstr "Il proprietario deve consentire dms da chiunque." msgid "Bad authentication data" msgstr "Dati di autenticazione errati" msgid "Your credentials do not allow access to this resource." msgstr "Le tue credenziali non consentono l'accesso a questa risorsa." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Questa richiesta sembra essere automatizzata. Per proteggere i nostri utenti " "da spam e altre attività dannose, al momento non possiamo completare questa " "azione." msgid "User must verify login" msgstr "L'utente deve verificare l'accesso" msgid "Application cannot perform write actions." msgstr "L'applicazione non può eseguire azioni di scrittura." msgid "You can’t mute yourself." msgstr "Non puoi silenziare te stesso." msgid "You are not muting the specified user." msgstr "Non stai silenziando l'utente specificato." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "Le GIF animate non sono consentite quando si caricano più immagini." msgid "The validation of media ids failed." msgstr "La convalida degli ID media non è riuscita." msgid "A media id was not found." msgstr "Non è stato trovato un ID media." msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" "Per proteggere i nostri utenti da spam e altre attività dannose, questo " "account è temporaneamente bloccato." msgid "You have already retweeted this Tweet." msgstr "Hai già ritwittato questo Tweet." msgid "You cannot send messages to this user." msgstr "Non è possibile inviare messaggi a questo utente." msgid "The text of your direct message is over the max character limit." msgstr "" "Il testo del tuo messaggio diretto supera il limite massimo di caratteri." msgid "Subscription already exists." msgstr "La sottoscrizione esiste già." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "Hai tentato di rispondere a un Tweet eliminato o non visibile a te." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "Il Tweet supera il numero di tipi di allegati consentiti." msgid "The given URL is invalid." msgstr "L'URL indicato non è valido." msgid "Invalid / suspended application" msgstr "Applicazione non valida / sospesa" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" "L'autore originale del Tweet ha limitato chi può rispondere a questo Tweet." msgid "Invalid media file" msgstr "File multimediale non valido" #, fuzzy, c-format msgid "Unknown error code %lld during upload: %s" msgstr "Codice di errore sconosciuto %lld durante il caricamento: %s" #, c-format msgid "Unknown error code %lld during upload" msgstr "Codice di errore sconosciuto %lld durante il caricamento" msgid "Now" msgstr "Adesso" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d minuto fa" msgstr[1] "%d minuti fa" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d ora fa" msgstr[1] "%d ore fa" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e di %B" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, fuzzy, c-format msgid "Replying to %s" msgstr "Rispondendo a %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Rispondendo a %s e %d altri" msgstr[1] "Rispondendo a %s e %d altri" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Rispondendo a %s e %s" msgid "Don't have a Twitter account yet?" msgstr "Non hai ancora un account Twitter?" msgid "Create one" msgstr "Creane uno" #, c-format msgid "Could not open %s" msgstr "Non posso aprire %s" msgid "Failed to retrieve request token" msgstr "Richiesta del token non trovata" msgid "Wrong PIN" msgstr "PIN sbagliato" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Account già in uso" msgid "Failed to retrieve access token" msgstr "Token d'accesso non trovato" msgid "Select Image" msgstr "Sfoglia" msgid "Unfollow" msgstr "Non seguire" msgid "Follow" msgstr "Segui" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Nessun utente" msgid "Retry" msgstr "Riprova" msgid "Copy URL" msgstr "Copia URL" #, fuzzy msgid "Reload image" msgstr "Impossibile caricare l'immagine" msgid "Save as…" msgstr "Salva come..." msgid "Save Video" msgstr "Salva Video" msgid "Save Image" msgstr "Salva immagine" msgid "Save" msgstr "Salva" msgid "Select Banner Image" msgstr "Seleziona l'immagine banner" msgid "Image does not meet minimum size requirements:" msgstr "L'immagine non rispetta le dimensioni minime richieste" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Larghezza minima: %d pixel" msgstr[1] "Larghezze minime: %d pixel" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Altezza minima: %d pixel" msgstr[1] "Altezze minime: %d pixel" msgid "Pick" msgstr "Seleziona" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Impossibile caricare l'immagine" msgstr[1] "Carcamento immagini %u fallito." #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Indietro" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Errore durante il recupero del tweet citato: %s\n" "\n" "Salva tweet non inviati?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Errore durante il recupero del tweet di risposta: %s\n" "\n" "Salvare il tweet non inviato?" msgid "Quote tweet" msgstr "Cita il tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "È consentito un solo file GIF animato per tweet." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "%d lettera rimasta" msgstr[1] "%d lettere rimaste" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Immagine %d su %d" msgstr[1] "Immagine %d su %d" msgid "Modify Filter" msgstr "Modifica filtro" msgid "Matches" msgstr "Corrispondenze" msgid "Doesn't match" msgstr "Non corrisponde" msgid "Modify Snippet" msgstr "Modifica Snippet" msgid "Snippet can't be empty" msgstr "Lo snippet non può essere vuoto" msgid "Replacement can't be empty" msgstr "Il sostituto non può essere vuoto" msgid "Snippet may not contain whitespace" msgstr "Lo snippat non può contenere spazi" msgid "Snippet already exists" msgstr "Lo snippet esiste già" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Ehi, scarica la nuova versione di #Cawbird \\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Aggiungi o rimuovi un utente dalla lista" msgid "You have no lists." msgstr "Non hai nessuna lista" msgid "About Cawbird" msgstr "Informazioni su Cawbird" msgid "New Account" msgstr "Nuovo account" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Per autenticarti su Cawbird hai bisogno di un PIN da twitter.com " "dell'account che desideri aggiungere" msgid "Request PIN" msgstr "Richiedi PIN" msgid "Enter PIN from twitter.com below:" msgstr "Inserisci il pin da Twitter.com qui sotto" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Conferma" msgid "Account Settings" msgstr "Impostazioni account" msgid "Name" msgstr "Nome" msgid "Website" msgstr "Sito web" msgid "Autostart" msgstr "Avvio automatico" msgid "Remove Account" msgstr "Rimuovi account" msgid "Do you really want to remove this account?" msgstr "Vuoi VERAMENTE rimuovere questo account?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smiles e Persone" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Corpo e vestiti" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Animali e natura" msgctxt "emoji category" msgid "Food & Drink" msgstr "Cibo e bevande" msgctxt "emoji category" msgid "Travel & Places" msgstr "Viaggi e posti" msgctxt "emoji category" msgid "Activities" msgstr "Attività" msgctxt "emoji category" msgid "Objects" msgstr "Oggetti" msgctxt "emoji category" msgid "Symbols" msgstr "Simboli" msgctxt "emoji category" msgid "Flags" msgstr "Bandiere" msgid "No Results Found" msgstr "Nessun risultato trovato" msgid "Try a different search" msgstr "Prova una ricerca differente" msgid "Send" msgstr "Invia" msgid "Add Media" msgstr "Aggiungi Media" msgid "Show favorite images" msgstr "Mostra le immagini preferite" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "Parole filtrate" msgid "Filtered users" msgstr "Utenti filtrati" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Utenti" msgid "Set image description" msgstr "Aggiungi una descrizione" msgid "Subscribe" msgstr "Sottoscrivi" msgid "Unsubscribe" msgstr "Rimuovi sottoscrizione" msgid "Subscribers:" msgstr "Iscritti:" msgid "Members:" msgstr "Membri:" msgid "Creator:" msgstr "Creatore:" msgid "Created at:" msgstr "Creata il:" msgid "Edit" msgstr "Modifica" msgid "Mode:" msgstr "Modalità:" msgid "Description" msgstr "Descrizione" msgid "Settings" msgstr "Impostazioni" msgid "Shortcuts" msgstr "Scorciatoie" msgid "About" msgstr "Informazioni su..." msgid "Quit" msgstr "Esci" msgid "Add New Filter" msgstr "Aggiungi nuovo filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Aggiungi un nuovo snippet" msgid "Keyword" msgstr "Parola chiave" msgid "Replacement" msgstr "Sostituto" msgid "Create New List" msgstr "Crea una nuova lista" msgid "Name:" msgstr "Nome:" msgid "Create" msgstr "Crea" msgid "Write Direct Message" msgstr "Scrivi un messaggio diretto" msgid "Add to/Remove from List" msgstr "Aggiunti/rimuovi dalla lista" msgid "Blocked" msgstr "Bloccato" msgid "Muted" msgstr "Silenziato" msgid "Retweets disabled" msgstr "Retweet disabilitati" msgid "More actions" msgstr "Altro" msgid "Follows you" msgstr "Ti segue" msgid "Tweets" msgstr "Tweet" msgid "Followers" msgstr "Followers" msgid "Following" msgstr "Following" msgid "Use dark theme" msgstr "Usa il tema scuro" #, fuzzy msgid "Shortcut key" msgstr "Scorciatoie" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Timeline di %s" msgid "Show inline media" msgstr "Mostrare i media contenuti" msgid "Always show" msgstr "Mostrare sempre" msgid "Always hide" msgstr "Nascondere sempre" msgid "Hide in timeline" msgstr "Nascondere nella timeline" msgid "Auto scroll on new tweets" msgstr "Scorrimento automatico con nuovi tweet" msgid "Double-click activation" msgstr "Doppio-click per attivare" msgid "Notifications" msgstr "Notifiche" msgid "On New Tweets" msgstr "Per Nuovi Tweet" msgid "Never" msgstr "Mai" msgid "Every" msgstr "Sempre" msgid "Stack 5" msgstr "Ogni 5" msgid "Stack 10" msgstr "Ogni 10" msgid "Stack 25" msgstr "Ogni 25" msgid "Stack 50" msgstr "Ogni 50" msgid "On New Mentions" msgstr "Per Nuove Menzioni" msgid "On New Messages" msgstr "Per Nuovi Messaggi" msgid "Interface" msgstr "Interfaccia" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweet" msgid "Round avatars" msgstr "Avatar arrotondati" msgid "Remove trailing hashtags" msgstr "Rimuovere gli hastag finali" msgid "Remove media links" msgstr "Rimuovi link multimediali" msgid "Hide inappropriate content" msgstr "Contenuto inappropriato" msgid "Translation" msgstr "" #, fuzzy msgid "Translation service" msgstr "Avvia servizio" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nessuno snippet configurato." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Gli snippet si attivano digitando la parola chiave con la tastiera e " "premendo TAB." msgid "Snippets" msgstr "Snippet" msgctxt "shortcuts window" msgid "General" msgstr "Generale" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Componi tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostra Preferenze Account" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostra Account sovrimpresso" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostra Preferenze Applicazione" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Attiva/disattiva la barra superiore" msgctxt "shortcuts window" msgid "Go Back" msgstr "Indietro" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Avanti" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Vai alla pagina successiva" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweet" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Preferito" msgctxt "shortcuts window" msgid "Reply" msgstr "Rispondi" msgctxt "shortcuts window" msgid "Quote" msgstr "Cita" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostra Dettagli" msgctxt "shortcuts window" msgid "Delete" msgstr "Elimina" msgctxt "shortcuts window" msgid "Compose" msgstr "Componi" msgid "Show Emoji Chooser" msgstr "Mostra emoji picker" msgid "Start new conversation" msgstr "Inizia una nuova conversazione" msgid "With:" msgstr "Con:" msgid "Go" msgstr "Vai" msgid "Quote" msgstr "Cita" msgid "Retweet tweet" msgstr "Ritwitta il tweet" #, fuzzy msgid "Like tweet" msgstr "Tweet preferito" msgid "Reply to tweet" msgstr "Rispondi al tweet" msgid "More" msgstr "Altro" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Mi piace" msgid "Reply" msgstr "Rispondi" msgid "Liked" msgstr "Piaciuti" msgid "Retweeted" msgstr "Ritwittato" msgid "Unblock" msgstr "Sblocca" msgid "Show settings of this account" msgstr "Mostra le impostazioni di questo account" msgid "Open in new window" msgstr "Apri in una nuova finestra" msgid "Go to profile" msgstr "Vai al profilo" msgid "Created" msgstr "Creato" msgid "Subscribed to" msgstr "Sottoscritto a" #~ msgid "Replying to" #~ msgstr "Rispondendo a" #~ msgid "and" #~ msgstr "e" #~ msgid "Actions" #~ msgstr "Azioni" cawbird-1.4.2/po/ja.po000066400000000000000000000561571416632607600145320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # kumar8600 , 2014 # MTD TR , 2017 # Yuki Katsura , 2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-11-12 23:35+0000\n" "Last-Translator: MTD TR \n" "Language-Team: Japanese (Japan) (http://www.transifex.com/cawbird/cawbird/" "language/ja_JP/)\n" "Language: ja_JP\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter Client" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird ã¯GTK+ãƒã‚¤ãƒ†ã‚£ãƒ–ãªTwitterクライアントã§ã€ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ (DM) ・" "ツイート通知・会話ビューã¨ã„ã£ãŸé‡è¦æ©Ÿèƒ½ã‚’æä¾›ã—ã¾ã™ã€‚" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "加ãˆã¦ã€ãƒ“デオã®è¡¨ç¤ºãƒ»ã‚¤ãƒ³ãƒ©ã‚¤ãƒ³ã§ã®ç”»åƒã®è¡¨ç¤ºãƒ»ãƒªã‚¹ãƒˆãƒ»ãƒ•ィルター・複数ã®ã‚¢" "カウントãªã©ã‚’サãƒãƒ¼ãƒˆã—ã¾ã™" msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "通常ã®ãƒ‡ã‚¹ã‚¯ãƒˆãƒƒãƒ—アプリケーションã‹ã‚‰Twitterを使用ã™ã‚‹" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "アカウントã®è¨­å®šã‚’表示" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "ダイレクトメッセージ" #, fuzzy msgid "Select Media" msgstr "ç”»åƒã‚’é¸æŠž" msgid "Open" msgstr "" msgid "Cancel" msgstr "キャンセル" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "é¸æŠžã•れãŸç”»åƒã¯å¤§ãã™ãŽã¾ã™ã€‚最大ファイルサイズã¯%'dMBã§ã™" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "é¸æŠžã•れãŸç”»åƒã¯å¤§ãã™ãŽã¾ã™ã€‚最大ファイルサイズã¯%'dMBã§ã™" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "é¸æŠžã•れãŸç”»åƒã¯å¤§ãã™ãŽã¾ã™ã€‚最大ファイルサイズã¯%'dMBã§ã™" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "" #, fuzzy msgid "Direct message threads" msgstr "ダイレクトメッセージ" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%2$s ã‹ã‚‰ %1$d ä»¶ã®æ–°ç€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸" #, c-format msgid "New direct message from %s" msgstr "%s ã‹ã‚‰ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸" msgid "Direct Messages" msgstr "ダイレクトメッセージ" #, fuzzy msgid "Liked tweets timeline" msgstr "ãŠæ°—ã«å…¥ã‚Šã«ç™»éŒ²ã™ã‚‹" #, fuzzy msgid "Likes" msgstr "ãŠæ°—ã«å…¥ã‚Š" msgid "Add new Filter" msgstr "フィルターを追加ã™ã‚‹" msgid "Filters" msgstr "フィルター" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s ㌠%s をリツイート" #, c-format msgid "%s tweeted" msgstr "%s ãŒãƒ„イート" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d ä»¶ã®æ–°ç€ãƒ„イートï¼" msgid "Home" msgstr "ホーム" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s ãŒãƒ„イート" msgid "Private" msgstr "éžå…¬é–‹" msgid "Public" msgstr "公開" msgid "Lists" msgstr "リスト" msgid "Show configured accounts" msgstr "アカウントã®è¨­å®šã‚’表示" msgid "Compose Tweet" msgstr "ツイートã®ç·¨é›†" msgid "Add new Account" msgstr "アカウントを追加ã™ã‚‹" #, fuzzy msgid "Mentions timeline" msgstr "メンション" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "メンション" msgid "Suspended Account" msgstr "å‡çµã•れãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆ" msgid "Protected profile" msgstr "éžå…¬é–‹ãƒ—ロフィール" #, c-format msgid "Tweet to @%s" msgstr "@%s ã«ãƒ„イート" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s ãŒãƒ„イート" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "" #, fuzzy, c-format msgid "Location: %s" msgstr "通知" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "メンション" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "フォロー解除" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "フォロー解除" msgid "Protected Profile" msgstr "éžå…¬é–‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "フォロー解除" #, fuzzy msgid "No tweets found" msgstr "エントリーãŒã‚りã¾ã›ã‚“" msgid "No users found" msgstr "ユーザãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "@%s ã«ãƒ„イート" msgid "Search" msgstr "検索" msgid "Load More" msgstr "æ›´ã«èª­ã¿è¾¼ã‚€" msgid "Could not show tweet" msgstr "ツイートを表示ã§ãã¾ã›ã‚“ã§ã—ãŸ" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "リツイート" msgid "Open in Browser" msgstr "ブラウザーã§é–‹ã" msgid "Source" msgstr "ソース" msgid "Tweet Details" msgstr "ツイートã®è©³ç´°" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ä»¶ã®æœªèª­)" msgid "Delete" msgstr "削除" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "リツイート" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" #, c-format msgid "Block %s" msgstr "%sをブロック" msgid "This tweet contains images marked as inappropriate" msgstr "ã“ã®ãƒ„イートã¯ä¸é©åˆ‡ãªç”»åƒã‚’å«ã‚“ã§ã„ã¾ã™" msgid "Show anyway" msgstr "常ã«è¡¨ç¤ºã™ã‚‹" #, fuzzy msgid "Could not authenticate you" msgstr "%sã‚’é–‹ã‘ã¾ã›ã‚“ã§ã—ãŸ" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "ユーザãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "インターフェイス" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "ç¾åœ¨" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "返信ã™ã‚‹" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "ã¾ã Twitterã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’ãŠæŒã¡ã§ã¯ã‚りã¾ã›ã‚“ã‹?" msgid "Create one" msgstr "" #, c-format msgid "Could not open %s" msgstr "%sã‚’é–‹ã‘ã¾ã›ã‚“ã§ã—ãŸ" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "誤ã£ãŸPIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "アカウントã¯ã™ã§ã«ä½¿ã‚れã¦ã„ã¾ã™" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "ç”»åƒã‚’é¸æŠž" msgid "Unfollow" msgstr "フォロー解除" msgid "Follow" msgstr "フォロー" msgid "Loading…" msgstr "読ã¿è¾¼ã¿ä¸­..." #, fuzzy msgid "No entries found" msgstr "ユーザãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" msgid "Retry" msgstr "å†è©¦è¡Œ" msgid "Copy URL" msgstr "URLをコピー" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "åå‰ã‚’付ã‘ã¦ä¿å­˜" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "ä¿å­˜" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "戻る" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "1ã¤ã®ãƒ„イートã«ã¯1ã¤ã®GIFファイルã—ã‹æ·»ä»˜ã§ãã¾ã›ã‚“。" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "フィルターを編集" msgid "Matches" msgstr "一致" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "ã•ã‚ã€æ–°ã—ã„#Cawbird ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ãƒã‚§ãƒƒã‚¯ã—ã¦ãã ã•ã„ï¼\\ (•◡•) / #cool " "#newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "リストã‹ã‚‰ãƒ¦ãƒ¼ã‚¶ã‚’追加ã€å‰Šé™¤ã™ã‚‹" msgid "You have no lists." msgstr "リストãŒã‚りã¾ã›ã‚“" msgid "About Cawbird" msgstr "Cawbird ã«ã¤ã„ã¦" msgid "New Account" msgstr "æ–°ã—ã„アカウント" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "PINã‚’è¦æ±‚" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "確èª" msgid "Account Settings" msgstr "アカウントã®è¨­å®š" msgid "Name" msgstr "åå‰" msgid "Website" msgstr "" msgid "Autostart" msgstr "自動起動" #, fuzzy msgid "Remove Account" msgstr "削除" #, fuzzy msgid "Do you really want to remove this account?" msgstr "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "é€ä¿¡" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "フィルター" #, fuzzy msgid "Filtered users" msgstr "フィルター" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "ユーザー" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "購読" msgid "Unsubscribe" msgstr "購読解除" msgid "Subscribers:" msgstr "購読者:" msgid "Members:" msgstr "メンãƒãƒ¼:" msgid "Creator:" msgstr "作æˆè€…:" msgid "Created at:" msgstr "ä½œæˆæ™‚刻:" msgid "Edit" msgstr "編集" msgid "Mode:" msgstr "モード:" msgid "Description" msgstr "" msgid "Settings" msgstr "設定" msgid "Shortcuts" msgstr "ショートカット" msgid "About" msgstr "ã“ã®ã‚¢ãƒ—リケーションã«ã¤ã„ã¦" msgid "Quit" msgstr "終了" msgid "Add New Filter" msgstr "" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "" msgid "Keyword" msgstr "" msgid "Replacement" msgstr "" msgid "Create New List" msgstr "ãƒªã‚¹ãƒˆã®æ–°è¦ä½œæˆ" msgid "Name:" msgstr "åå‰:" msgid "Create" msgstr "作æˆ" msgid "Write Direct Message" msgstr "ダイレクトメッセージを書ã" msgid "Add to/Remove from List" msgstr "リストã¸è¿½åŠ ã€å‰Šé™¤ã™ã‚‹" msgid "Blocked" msgstr "ブロック済ã¿" msgid "Muted" msgstr "ミュート済ã¿" msgid "Retweets disabled" msgstr "リツイートã¯ã§ãã¾ã›ã‚“" #, fuzzy msgid "More actions" msgstr "メンション" msgid "Follows you" msgstr "フォローã•れã¦ã„ã‚‹" msgid "Tweets" msgstr "ツイート" msgid "Followers" msgstr "" msgid "Following" msgstr "" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "ショートカット" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "メンション" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "" msgid "Double-click activation" msgstr "" msgid "Notifications" msgstr "通知" msgid "On New Tweets" msgstr "" msgid "Never" msgstr "ã—ãªã„" msgid "Every" msgstr "å…¨ã¦" msgid "Stack 5" msgstr "5 件溜ã¾ã£ãŸã‚‰" msgid "Stack 10" msgstr "10 件溜ã¾ã£ãŸã‚‰" msgid "Stack 25" msgstr "25 件溜ã¾ã£ãŸã‚‰" msgid "Stack 50" msgstr "50 件溜ã¾ã£ãŸã‚‰" msgid "On New Mentions" msgstr "" msgid "On New Messages" msgstr "" msgid "Interface" msgstr "インターフェイス" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "ツイート" msgid "Round avatars" msgstr "" msgid "Remove trailing hashtags" msgstr "" msgid "Remove media links" msgstr "" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" msgid "Snippets" msgstr "" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "ツイート" msgctxt "shortcuts window" msgid "Retweet" msgstr "リツイート" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "ãŠæ°—ã«å…¥ã‚Š" msgctxt "shortcuts window" msgid "Reply" msgstr "返信" msgctxt "shortcuts window" msgid "Quote" msgstr "引用" msgctxt "shortcuts window" msgid "Show Details" msgstr "詳細を表示" msgctxt "shortcuts window" msgid "Delete" msgstr "削除" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "æ–°ãŸãªä¼šè©±ã‚’å§‹ã‚ã‚‹" msgid "With:" msgstr "相手:" msgid "Go" msgstr "å§‹ã‚ã‚‹" msgid "Quote" msgstr "引用ã™ã‚‹" msgid "Retweet tweet" msgstr "リツイートã™ã‚‹" #, fuzzy msgid "Like tweet" msgstr "ãŠæ°—ã«å…¥ã‚Šã«ç™»éŒ²ã™ã‚‹" msgid "Reply to tweet" msgstr "返信ã™ã‚‹" msgid "More" msgstr "詳細" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "ãŠæ°—ã«å…¥ã‚Š" msgid "Reply" msgstr "リプライ" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "リツイート" msgid "Unblock" msgstr "" msgid "Show settings of this account" msgstr "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®è¨­å®šã‚’表示" msgid "Open in new window" msgstr "æ–°ã—ã„ウィンドウã§é–‹ã" msgid "Go to profile" msgstr "プロファイルを開ã" msgid "Created" msgstr "作æˆ" msgid "Subscribed to" msgstr "購読中" #~ msgid "Actions" #~ msgstr "アクション" #~ msgid "Add Image" #~ msgstr "ç”»åƒã‚’追加" #~ msgid "List" #~ msgstr "リスト" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "ツイートをロードã§ãã¾ã›ã‚“ã§ã—ãŸ" cawbird-1.4.2/po/ko.po000066400000000000000000000557131416632607600145460ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # irus6, 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Korean (Korea) (http://www.transifex.com/cawbird/cawbird/" "language/ko_KR/)\n" "Language: ko_KR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "코어버드" msgid "Twitter Client" msgstr "트위터 í´ë¼ì´ì–¸íЏ" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird 는 다ì´ë ‰íЏ 메시지 (DMs), 트윗 알림, 대화 보기 ë“±ì˜ ì¤‘ìš” íŠ¹ì§•ì„ ì œê³µ" "하는 순수 GTK+ 트위터 í´ë¼ì´ì–¸íЏ 입니다." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "추가ì ì¸ 특징으로는 비디오 보기, ë³µìˆ˜ì˜ ì´ë¯¸ì§€, 리스트, í•„í„°, 멀티 계정, 등등" "ì´ í¬í•¨ë©ë‹ˆë‹¤." msgid "Generic timeline view when using Cawbird" msgstr "코어버드 사용시 ì¼ë°˜ 타임ë¼ì¸ 보기" msgid "Typical Twitter profile" msgstr "전형ì ì¸ 트위터 프로필" msgid "Account settings can be configured" msgstr "계정 í™˜ê²½ì´ ì„¤ì • ë  ìˆ˜ 있ìŒ" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "ì¼ë°˜ ë°ìФí¬íƒ‘ 앱ì—서 트위터 사용하기" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "트위터;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "êµ¬ì„±ëœ ê³„ì • 보여주기" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "다ì´ë ‰íЏ 메시지" #, fuzzy msgid "Select Media" msgstr "ì´ë¯¸ì§€ ì„ íƒ" msgid "Open" msgstr "열기" msgid "Cancel" msgstr "취소" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "다ì´ë ‰íЏ 대화" #, fuzzy msgid "Direct message threads" msgstr "다ì´ë ‰íЏ 메시지" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%2$s 로부터 %1$d ê°œì˜ ìƒˆë¡œìš´ 메시지" #, c-format msgid "New direct message from %s" msgstr "%s 로부터 새로운 다ì´ë ‰íЏ 메시지" msgid "Direct Messages" msgstr "다ì´ë ‰íЏ 메시지" #, fuzzy msgid "Liked tweets timeline" msgstr "íŠ¸ìœ—ì„ ì¦ê²¨ì°¾ê¸°" #, fuzzy msgid "Likes" msgstr "ì¦ê²¨ì°¾ê¸°" msgid "Add new Filter" msgstr "새로운 í•„í„° 추가" msgid "Filters" msgstr "í•„í„°" #, fuzzy msgid "Home timeline" msgstr "타임ë¼ì¸ì—서 가리기" #, c-format msgid "%s retweeted %s" msgstr "%s ê°€ %s 를 리트윗 함" #, c-format msgid "%s tweeted" msgstr "%s ê°€ 트윗함" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d ê°œì˜ ìƒˆë¡œìš´ 트윗!" msgid "Home" msgstr "홈" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s ê°€ 트윗함" msgid "Private" msgstr "ì „ìš©" msgid "Public" msgstr "공용" msgid "Lists" msgstr "리스트" msgid "Show configured accounts" msgstr "êµ¬ì„±ëœ ê³„ì • 보여주기" msgid "Compose Tweet" msgstr "트윗 작성" msgid "Add new Account" msgstr "새로운 계정 추가" #, fuzzy msgid "Mentions timeline" msgstr "타임ë¼ì¸ì—서 가리기" #, c-format msgid "%s mentioned %s" msgstr "%s ê°€(ì´) %s ì„(를) 멘션함" msgid "Mentions" msgstr "멘션" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "보호ë˜ëŠ” 프로필" #, c-format msgid "Tweet to @%s" msgstr "@%s ì—게 트윗" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s ê°€ 트윗함" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "팔로우 중" #, fuzzy, c-format msgid "Location: %s" msgstr "알림" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "타임ë¼ì¸ì—서 가리기" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "팔로워" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "팔로우 중" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "팔로워" #, fuzzy msgid "No tweets found" msgstr "ì°¾ì„수 ì—†ìŒ" msgid "No users found" msgstr "ì‚¬ìš©ìž ì—†ìŒ" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "@%s ì—게 트윗" msgid "Search" msgstr "찾기" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "íŠ¸ìœ—ì„ ë³¼ 수 ì—†ìŒ" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "리트윗" msgid "Open in Browser" msgstr "브ë¼ìš°ì €ì—서 열기" msgid "Source" msgstr "ì›ë³¸" msgid "Tweet Details" msgstr "ìƒì„¸ 트윗" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ê°œì˜ ì½ì§€ 않ìŒ)" msgid "Delete" msgstr "지우기" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "리트윗" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "ì •ë§ ì´ ê³„ì •ì„ ì§€ìš¸ê±´ê°€ìš”?" #, c-format msgid "Block %s" msgstr "%s 차단" msgid "This tweet contains images marked as inappropriate" msgstr "ì´ íŠ¸ìœ—ì— í¬í•¨ëœ ì´ë¯¸ì§€ëŠ” ì ì ˆí•˜ì§€ 않다고 표시 ë˜ì—ˆìŠµë‹ˆë‹¤" msgid "Show anyway" msgstr "아무튼 보기" #, fuzzy msgid "Could not authenticate you" msgstr "%s ì„ ì—´ 수 ì—†ìŒ" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "ì‚¬ìš©ìž ì—†ìŒ" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "ì¸í„°íŽ˜ì´ìФ" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "ì¡°ê°ì´ ì´ë¯¸ 존재함" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "현재" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dë¶„" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%d시" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "íŠ¸ìœ—ì— ë‹µìž¥" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "하나 만들기" #, c-format msgid "Could not open %s" msgstr "%s ì„ ì—´ 수 ì—†ìŒ" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "올바르지 ì•Šì€ PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "ê³„ì •ì´ ì´ë¯¸ 사용 중" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "ì´ë¯¸ì§€ ì„ íƒ" msgid "Unfollow" msgstr "언팔로우" msgid "Follow" msgstr "팔로우" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "ì‚¬ìš©ìž ì—†ìŒ" msgid "Retry" msgstr "재시ë„" msgid "Copy URL" msgstr "URL 복사" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "저장" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "뒤로" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "트윗 ì¸ìš©" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "í•„í„° 수정" msgid "Matches" msgstr "ì¼ì¹˜í•¨" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "ì¡°ê° ìˆ˜ì •" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "ì¡°ê°ì´ ê³µë°±ì„ í¬í•¨ 하지 않ìŒ" msgid "Snippet already exists" msgstr "ì¡°ê°ì´ ì´ë¯¸ 존재함" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "í—¤ì´, 새로운 #Cawbird ë²„ì ¼ì„ í™•ì¸í•˜ì„¸ìš”! \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "리스트ì—서 ì‚¬ìš©ìž ì¶”ê°€ ë˜ëŠ” ì‚­ì œ" msgid "You have no lists." msgstr "리스트 ì—†ìŒ." msgid "About Cawbird" msgstr "Cawbird ì— ê´€í•´" msgid "New Account" msgstr "새 계정" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "PIN 요청" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "확ì¸" msgid "Account Settings" msgstr "계정 설정" msgid "Name" msgstr "ì´ë¦„" msgid "Website" msgstr "웹사ì´íЏ" msgid "Autostart" msgstr "ìžë™ 시작" #, fuzzy msgid "Remove Account" msgstr "지우기" #, fuzzy msgid "Do you really want to remove this account?" msgstr "ì •ë§ ì´ ê³„ì •ì„ ì§€ìš¸ê±´ê°€ìš”?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "보내기" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "í•„í„°" #, fuzzy msgid "Filtered users" msgstr "í•„í„°" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "사용ìž" #, fuzzy msgid "Set image description" msgstr "설명" msgid "Subscribe" msgstr "구ë…하기" msgid "Unsubscribe" msgstr "êµ¬ë… ì·¨ì†Œ" msgid "Subscribers:" msgstr "구ë…ìž:" msgid "Members:" msgstr "멤버:" msgid "Creator:" msgstr "작성ìž:" msgid "Created at:" msgstr "작성 장소:" msgid "Edit" msgstr "수정" msgid "Mode:" msgstr "모드:" msgid "Description" msgstr "설명" msgid "Settings" msgstr "설정" msgid "Shortcuts" msgstr "바로가기" msgid "About" msgstr "About" msgid "Quit" msgstr "종료" msgid "Add New Filter" msgstr "새로운 í•„í„° 추가" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "새로운 스니펫 추가" msgid "Keyword" msgstr "키워드" msgid "Replacement" msgstr "대체" msgid "Create New List" msgstr "새로운 리스트 ìƒì„±" msgid "Name:" msgstr "ì´ë¦„:" msgid "Create" msgstr "ìƒì„±" msgid "Write Direct Message" msgstr "다ì´ë ‰íЏ 메시지 작성" msgid "Add to/Remove from List" msgstr "ë¦¬ìŠ¤íŠ¸ì— ì¶”ê°€/ì‚­ì œ" msgid "Blocked" msgstr "차단ë¨" msgid "Muted" msgstr "ìŒì†Œê±°ë¨" msgid "Retweets disabled" msgstr "리트윗 불가" #, fuzzy msgid "More actions" msgstr "멘션" msgid "Follows you" msgstr "ë‹¹ì‹ ì„ íŒ”ë¡œìš°í•¨" msgid "Tweets" msgstr "트윗" msgid "Followers" msgstr "팔로워" msgid "Following" msgstr "팔로우 중" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "바로가기" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "타임ë¼ì¸ì—서 가리기" msgid "Show inline media" msgstr "ì¸ë¼ì¸ 미디어 보기" msgid "Always show" msgstr "í•­ìƒ ë³´ì´ê¸°" msgid "Always hide" msgstr "í•­ìƒ ê°€ë¦¬ê¸°" msgid "Hide in timeline" msgstr "타임ë¼ì¸ì—서 가리기" msgid "Auto scroll on new tweets" msgstr "새로운 트윗으로 ìžë™ 스í¬ë¡¤" msgid "Double-click activation" msgstr "ë”블 í´ë¦­ 활성화" msgid "Notifications" msgstr "알림" msgid "On New Tweets" msgstr "새로운 트윗" msgid "Never" msgstr "절대 안함" msgid "Every" msgstr "매번" msgid "Stack 5" msgstr "ìŠ¤íƒ 5" msgid "Stack 10" msgstr "ìŠ¤íƒ 10" msgid "Stack 25" msgstr "ìŠ¤íƒ 25" msgid "Stack 50" msgstr "ìŠ¤íƒ 50" msgid "On New Mentions" msgstr "새로운 멘션" msgid "On New Messages" msgstr "새로운 메시지" msgid "Interface" msgstr "ì¸í„°íŽ˜ì´ìФ" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "트윗" msgid "Round avatars" msgstr "둥근 아바타" msgid "Remove trailing hashtags" msgstr "ì¶”ì í•˜ëŠ” 해쉬태그 ì‚­ì œ" msgid "Remove media links" msgstr "미디어 ë§í¬ ì‚­ì œ" msgid "Hide inappropriate content" msgstr "ë¶€ì ì ˆí•œ 컨í…츠 숨기기" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "ìŠ¤ë‹ˆíŽ«ì´ êµ¬ì„±ë˜ì§€ 않ìŒ." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "키워드를 ì“°ê³  TAB ì„ ëˆŒëŸ¬ì„œ ìŠ¤ë‹ˆíŽ«ì„ í™œì„±í™”í•  수 있ìŒ." msgid "Snippets" msgstr "스니펫" msgctxt "shortcuts window" msgid "General" msgstr "ì¼ë°˜" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "트윗 작성" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "계정 설정 보기" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "계정 íŒì˜¤ë²„ 보기" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "앱 설정 보기" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "ìƒë‹¨ ë°” 토글" msgctxt "shortcuts window" msgid "Go Back" msgstr "뒤로 가기" msgctxt "shortcuts window" msgid "Go Forward" msgstr "앞으로 가기" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "특정 페ì´ì§€ 가기" msgctxt "shortcuts window" msgid "Tweets" msgstr "트윗" msgctxt "shortcuts window" msgid "Retweet" msgstr "리트윗" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "ì¦ê²¨ì°¾ê¸°" msgctxt "shortcuts window" msgid "Reply" msgstr "답장" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "ìƒì„¸ 보기" msgctxt "shortcuts window" msgid "Delete" msgstr "지우기" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "새로운 대화 시작" msgid "With:" msgstr "함께:" msgid "Go" msgstr "가기" msgid "Quote" msgstr "ì¸ìš©" msgid "Retweet tweet" msgstr "íŠ¸ìœ—ì„ ë¦¬íŠ¸ìœ—" #, fuzzy msgid "Like tweet" msgstr "íŠ¸ìœ—ì„ ì¦ê²¨ì°¾ê¸°" msgid "Reply to tweet" msgstr "íŠ¸ìœ—ì— ë‹µìž¥" msgid "More" msgstr "ë” ë³´ê¸°" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "ì¦ê²¨ì°¾ê¸°" msgid "Reply" msgstr "답장" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "리트윗" msgid "Unblock" msgstr "ë¸”ë½ í•´ì œ" msgid "Show settings of this account" msgstr "ì´ ê³„ì •ì˜ ì„¤ì •ì„ ë³´ì—¬ì£¼ê¸°" msgid "Open in new window" msgstr "새 ì°½ì—서 열기" msgid "Go to profile" msgstr "프로필로 가기" msgid "Created" msgstr "만들어ì§" msgid "Subscribed to" msgstr "ì„ êµ¬ë…함" #~ msgid "Actions" #~ msgstr "ì•¡ì…˜" #~ msgid "Add Image" #~ msgstr "ì´ë¯¸ì§€ 추가" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "팔로워" #~ msgid "List" #~ msgstr "리스트" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "íŠ¸ìœ—ì„ ë¶ˆëŸ¬ì˜¬ 수 없습니다." cawbird-1.4.2/po/lt.po000066400000000000000000000633661416632607600145570ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Moo, 2014-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 18:18+0000\n" "Last-Translator: Moo\n" "Language-Team: Lithuanian (http://www.transifex.com/cawbird/cawbird/language/" "lt/)\n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n" "%100<10 || n%100>=20) ? 1 : 2);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter klientas" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird yra savas GTK+ twitter klientas, kuris suteikia svarbiausias " "ypatybes, tokias kaip TiesioginÄ—s ŽinutÄ—s (TŽ), tauÅ¡kalų praneÅ¡imai, " "pokalbių rodiniai." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Papildomas ypatybes sudaro vietinÄ— vaizdo įrašų peržiÅ«ra, kelių įterptų " "paveikslÄ—lių galimybÄ—, SÄ…raÅ¡ai, filtrai, kelių paskyrų galimybÄ— ir t. t." msgid "Generic timeline view when using Cawbird" msgstr "Bendrinis laiko juostos rodinys, naudojant Cawbird" msgid "Typical Twitter profile" msgstr "Tipinis Twitter profilis" msgid "Account settings can be configured" msgstr "Paskyros nustatymai gali bÅ«ti konfigÅ«ruojami" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "NaudokitÄ—s Twitter normalioje darbalaukio programoje" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Rodyti konfigÅ«ruotas paskyras" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "TiesioginÄ—s žinutÄ—s" #, fuzzy msgid "Select Media" msgstr "Pasirinkite paveikslÄ…" msgid "Open" msgstr "Atverti" msgid "Cancel" msgstr "Atsisakyti" #, fuzzy msgid "Selected file is not an image or video." msgstr "Pasirinktas failas nÄ—ra paveikslas." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Pasirinktas paveikslas yra per didelis. Didžiausias vieno paveikslo failo " "dydis yra %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Pasirinktas paveikslas yra per didelis. Didžiausias vieno paveikslo failo " "dydis yra %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Pasirinktas paveikslas yra per didelis. Didžiausias vieno paveikslo failo " "dydis yra %'d MB" msgid "Insert Emoji" msgstr "Ä®terpti jaustukÄ…" msgid "Direct Conversation" msgstr "Tiesioginis pokalbis" #, fuzzy msgid "Direct message threads" msgstr "TiesioginÄ—s žinutÄ—s" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nauja žinutÄ— nuo %s" msgstr[1] "%d naujos žinutÄ—s nuo %s" msgstr[2] "%d naujų žinuÄių nuo %s" msgstr[3] "%d naujų žinuÄių nuo %s" #, c-format msgid "New direct message from %s" msgstr "Nauja tiesioginÄ— žinutÄ— nuo %s" msgid "Direct Messages" msgstr "TiesioginÄ—s žinutÄ—s" #, fuzzy msgid "Liked tweets timeline" msgstr "MÄ—gstamas tauÅ¡kalas" #, fuzzy msgid "Likes" msgstr "MÄ—gstami" msgid "Add new Filter" msgstr "PridÄ—ti naujÄ… filtrÄ…" msgid "Filters" msgstr "Filtrai" #, fuzzy msgid "Home timeline" msgstr "SlÄ—pti laiko juostoje" #, c-format msgid "%s retweeted %s" msgstr "%s persiuntÄ— naudotojo %s tauÅ¡kalÄ…" #, c-format msgid "%s tweeted" msgstr "%s iÅ¡siuntÄ— tauÅ¡kalÄ…" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d naujas tauÅ¡kalas!" msgstr[1] "%d nauji tauÅ¡kalai!" msgstr[2] "%d naujų tauÅ¡kalų!" msgstr[3] "%d naujų tauÅ¡kalų!" msgid "Home" msgstr "Pradžia" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s iÅ¡siuntÄ— tauÅ¡kalÄ…" msgid "Private" msgstr "Privatus" msgid "Public" msgstr "VieÅ¡as" msgid "Lists" msgstr "SÄ…raÅ¡ai" msgid "Show configured accounts" msgstr "Rodyti konfigÅ«ruotas paskyras" msgid "Compose Tweet" msgstr "RaÅ¡yti tauÅ¡kalÄ…" msgid "Add new Account" msgstr "PridÄ—ti naujÄ… paskyrÄ…" #, fuzzy msgid "Mentions timeline" msgstr "SlÄ—pti laiko juostoje" #, c-format msgid "%s mentioned %s" msgstr "%s paminÄ—jo %s" msgid "Mentions" msgstr "PaminÄ—jimai" msgid "Suspended Account" msgstr "Pristabdyta paskyra" msgid "Protected profile" msgstr "Apsaugotas profilis" #, c-format msgid "Tweet to @%s" msgstr "Siųsti tauÅ¡kalÄ… naudotojui @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s iÅ¡siuntÄ— tauÅ¡kalÄ…" msgstr[1] "%s iÅ¡siuntÄ— tauÅ¡kalÄ…" msgstr[2] "%s iÅ¡siuntÄ— tauÅ¡kalÄ…" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seka" msgstr[1] "Seka" msgstr[2] "Seka" #, fuzzy, c-format msgid "Location: %s" msgstr "PraneÅ¡imai" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "SlÄ—pti laiko juostoje" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "SekÄ—jų" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seka" msgid "Protected Profile" msgstr "Apsaugotas profilis" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "SekÄ—jų" msgstr[1] "SekÄ—jų" msgstr[2] "SekÄ—jų" #, fuzzy msgid "No tweets found" msgstr "Nerasta įrašų" msgid "No users found" msgstr "Naudotojų nerasta" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Siųsti tauÅ¡kalÄ… naudotojui @%s" msgid "Search" msgstr "PaieÅ¡ka" msgid "Load More" msgstr "Ä®kelti daugiau" msgid "Could not show tweet" msgstr "Nepavyko parodyti tauÅ¡kalo" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Persiųsti tauÅ¡kalai" msgid "Open in Browser" msgstr "Atverti narÅ¡yklÄ—je" msgid "Source" msgstr "Å altinis" msgid "Tweet Details" msgstr "IÅ¡samiau apie tauÅ¡kalÄ…" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d neperskaitytas)" msgstr[1] "(%d neperskaityti)" msgstr[2] "(%d neperskaitytų)" msgstr[3] "(%d neperskaitytų)" msgid "Delete" msgstr "IÅ¡trinti" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Persiųsti" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Ar tikrai norite iÅ¡trinti Å¡iÄ… paskyrÄ…?" #, c-format msgid "Block %s" msgstr "Blokuoti %s" msgid "This tweet contains images marked as inappropriate" msgstr "Å iame tauÅ¡kale yra paveikslų, kurie yra pažymÄ—ti kaip netinkami" msgid "Show anyway" msgstr "Vis tiek rodyti" #, fuzzy msgid "Could not authenticate you" msgstr "Nepavyko atverti %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Naudotojų nerasta" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "SÄ…saja" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "IÅ¡karpa jau yra" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Dabar" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dmin." #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dval." #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Atsakoma" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "ir %d kitiems" msgstr[1] "ir %d kitiems" msgstr[2] "ir %d kitiems" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Atsakoma" msgid "Don't have a Twitter account yet?" msgstr "Dar neturite Twitter paskyros?" msgid "Create one" msgstr "Susikurkite paskyrÄ…" #, c-format msgid "Could not open %s" msgstr "Nepavyko atverti %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Neteisingas PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Paskyra jau yra naudojama" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Pasirinkite paveikslÄ…" msgid "Unfollow" msgstr "Nustoti sekti" msgid "Follow" msgstr "Sekti" msgid "Loading…" msgstr "Ä®keliama…" #, fuzzy msgid "No entries found" msgstr "Naudotojų nerasta" msgid "Retry" msgstr "Pakartoti" msgid "Copy URL" msgstr "Kopijuoti URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Ä®raÅ¡yti kaip…" msgid "Save Video" msgstr "Ä®raÅ¡yti vaizdo įrašą" msgid "Save Image" msgstr "Ä®raÅ¡yti paveikslÄ…" msgid "Save" msgstr "Ä®raÅ¡yti" msgid "Select Banner Image" msgstr "Pasirinkite reklamjuostÄ—s paveikslÄ…" msgid "Image does not meet minimum size requirements:" msgstr "Paveikslas neatitinka minimalių dydžio reikalavimų:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimalus plotis: %d taÅ¡kas" msgstr[1] "Minimalus plotis: %d taÅ¡kai" msgstr[2] "Minimalus plotis: %d taÅ¡kų" msgstr[3] "Minimalus plotis: %d taÅ¡kų" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimalus aukÅ¡tis: %d taÅ¡kas" msgstr[1] "Minimalus aukÅ¡tis: %d taÅ¡kai" msgstr[2] "Minimalus aukÅ¡tis: %d taÅ¡kų" msgstr[3] "Minimalus aukÅ¡tis: %d taÅ¡kų" msgid "Pick" msgstr "Pasirinkti" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Grįžti" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Cituoti tauÅ¡kalÄ…" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Viename tauÅ¡kale leidžiamas tik vienas GIF failas." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Modify Filter" msgstr "Keisti filtrÄ…" msgid "Matches" msgstr "Sutampa" msgid "Doesn't match" msgstr "Nesutampa" msgid "Modify Snippet" msgstr "Keisti iÅ¡karpÄ…" msgid "Snippet can't be empty" msgstr "IÅ¡karpa negali bÅ«ti tuÅ¡Äia" msgid "Replacement can't be empty" msgstr "Pakeitimas negali bÅ«ti tuÅ¡Äias" msgid "Snippet may not contain whitespace" msgstr "IÅ¡karpoje negali bÅ«ti tarpų" msgid "Snippet already exists" msgstr "IÅ¡karpa jau yra" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Labas, pažvelk į Å¡iÄ… naujÄ… #Cawbird versijÄ…! \\ (•◡•) / #jega " "#naujayravisadageriau" msgid "Add to or Remove User From List" msgstr "PridÄ—ti į arba Å¡alinti naudotojÄ… iÅ¡ sÄ…raÅ¡o" msgid "You have no lists." msgstr "JÅ«s neturite sÄ…rašų." msgid "About Cawbird" msgstr "Apie Cawbird" msgid "New Account" msgstr "Nauja paskyra" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Norint programoje Cawbird nustatyti tapatybÄ™, yra reikalingas PIN iÅ¡ twitter." "com su paskyra, kuriÄ… norite pridÄ—ti" msgid "Request PIN" msgstr "Užklausti PIN" msgid "Enter PIN from twitter.com below:" msgstr "Žemiau įveskite PIN iÅ¡ twitter.com:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Patvirtinti" msgid "Account Settings" msgstr "Paskyros nustatymai" msgid "Name" msgstr "Vardas" msgid "Website" msgstr "SvetainÄ—" msgid "Autostart" msgstr "Paleisti įjungus" #, fuzzy msgid "Remove Account" msgstr "IÅ¡trinti" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Ar tikrai norite iÅ¡trinti Å¡iÄ… paskyrÄ…?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Å ypsenÄ—lÄ—s ir žmonÄ—s" msgctxt "emoji category" msgid "Body & Clothing" msgstr "KÅ«nas ir apranga" msgctxt "emoji category" msgid "Animals & Nature" msgstr "GyvÅ«nai ir gamta" msgctxt "emoji category" msgid "Food & Drink" msgstr "Maistas ir gÄ—rimai" msgctxt "emoji category" msgid "Travel & Places" msgstr "KelionÄ—s ir vietos" msgctxt "emoji category" msgid "Activities" msgstr "Veiklos" msgctxt "emoji category" msgid "Objects" msgstr "Objektai" msgctxt "emoji category" msgid "Symbols" msgstr "Simboliai" msgctxt "emoji category" msgid "Flags" msgstr "VÄ—liavos" msgid "No Results Found" msgstr "Rezultatų nerasta" msgid "Try a different search" msgstr "Bandykite kitÄ… paieÅ¡kÄ…" msgid "Send" msgstr "Siųsti" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Rodyti mÄ—gstamus paveikslus" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtrai" #, fuzzy msgid "Filtered users" msgstr "Filtrai" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Naudotojai" #, fuzzy msgid "Set image description" msgstr "ApraÅ¡as" msgid "Subscribe" msgstr "Prenumeruoti" msgid "Unsubscribe" msgstr "Atsisakyti prenumeratos" msgid "Subscribers:" msgstr "Prenumeratoriai:" msgid "Members:" msgstr "Nariai:" msgid "Creator:" msgstr "KÅ«rÄ—jas:" msgid "Created at:" msgstr "Sukurta:" msgid "Edit" msgstr "Redaguoti" msgid "Mode:" msgstr "Veiksena:" msgid "Description" msgstr "ApraÅ¡as" msgid "Settings" msgstr "Nustatymai" msgid "Shortcuts" msgstr "Trumpiniai" msgid "About" msgstr "Apie" msgid "Quit" msgstr "IÅ¡eiti" msgid "Add New Filter" msgstr "PridÄ—ti naujÄ… filtrÄ…" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "PridÄ—ti naujÄ… iÅ¡karpÄ…" msgid "Keyword" msgstr "Raktažodis" msgid "Replacement" msgstr "Pakeitimas" msgid "Create New List" msgstr "Sukurti naujÄ… sÄ…rašą" msgid "Name:" msgstr "Pavadinimas:" msgid "Create" msgstr "Sukurti" msgid "Write Direct Message" msgstr "RaÅ¡yti tiesioginÄ™ žinutÄ™" msgid "Add to/Remove from List" msgstr "PridÄ—ti į/PaÅ¡alinti iÅ¡ sÄ…raÅ¡o" msgid "Blocked" msgstr "Užblokuotas" msgid "Muted" msgstr "Nutildytas" msgid "Retweets disabled" msgstr "TauÅ¡kalų persiuntimas iÅ¡jungtas" #, fuzzy msgid "More actions" msgstr "PaminÄ—jimai" msgid "Follows you" msgstr "Seka paskui jus" msgid "Tweets" msgstr "TauÅ¡kalai" msgid "Followers" msgstr "SekÄ—jų" msgid "Following" msgstr "Seka" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Trumpiniai" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "SlÄ—pti laiko juostoje" msgid "Show inline media" msgstr "Rodyti įterptÄ…jÄ… medijÄ…" msgid "Always show" msgstr "Visada rodyti" msgid "Always hide" msgstr "Visada slÄ—pti" msgid "Hide in timeline" msgstr "SlÄ—pti laiko juostoje" msgid "Auto scroll on new tweets" msgstr "AutomatiÅ¡kai slinkti, esant naujiems tauÅ¡kalams" msgid "Double-click activation" msgstr "Aktyvavimas dvikarÄiu spustelÄ—jimu" msgid "Notifications" msgstr "PraneÅ¡imai" msgid "On New Tweets" msgstr "Esant naujiems tauÅ¡kalams" msgid "Never" msgstr "Niekada" msgid "Every" msgstr "Kiekvienam" msgid "Stack 5" msgstr "DÄ—klui iÅ¡ 5" msgid "Stack 10" msgstr "DÄ—klui iÅ¡ 10" msgid "Stack 25" msgstr "DÄ—klui iÅ¡ 25" msgid "Stack 50" msgstr "DÄ—klui iÅ¡ 50" msgid "On New Mentions" msgstr "Esant naujiems paminÄ—jimams" msgid "On New Messages" msgstr "Esant naujoms žinutÄ—ms" msgid "Interface" msgstr "SÄ…saja" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "TauÅ¡kalai" msgid "Round avatars" msgstr "Apvalinti avatarus" msgid "Remove trailing hashtags" msgstr "Å alinti besivelkanÄius saitažodžius" msgid "Remove media links" msgstr "Å alinti medijos nuorodas" msgid "Hide inappropriate content" msgstr "SlÄ—pti netinkamÄ… turinį" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "NÄ—ra sukonfigÅ«ruotų iÅ¡karpų." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Aktyvinkite iÅ¡karpas, įrašę raktažodį ir nuspausdami TAB klavišą." msgid "Snippets" msgstr "IÅ¡karpos" msgctxt "shortcuts window" msgid "General" msgstr "Bendra" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "RaÅ¡yti tauÅ¡kalÄ…" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Rodyti paskyros nustatymus" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Rodyti iÅ¡kylanÄiuosius paskyrų langus" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Rodyti programos nustatymus" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Perjungti virÅ¡utinÄ™ juostÄ…" msgctxt "shortcuts window" msgid "Go Back" msgstr "Grįžti atgal" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Eiti pirmyn" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Eiti į n-tÄ… puslapį" msgctxt "shortcuts window" msgid "Tweets" msgstr "TauÅ¡kalai" msgctxt "shortcuts window" msgid "Retweet" msgstr "Persiųsti" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "MÄ—gstamas" msgctxt "shortcuts window" msgid "Reply" msgstr "Atsakyti" msgctxt "shortcuts window" msgid "Quote" msgstr "Cituoti" msgctxt "shortcuts window" msgid "Show Details" msgstr "Rodyti iÅ¡samesnÄ™ informacijÄ…" msgctxt "shortcuts window" msgid "Delete" msgstr "IÅ¡trinti" msgctxt "shortcuts window" msgid "Compose" msgstr "RaÅ¡yti" msgid "Show Emoji Chooser" msgstr "Rodyti jaustukų parinkiklį" msgid "Start new conversation" msgstr "PradÄ—ti naujÄ… pokalbį" msgid "With:" msgstr "Su:" msgid "Go" msgstr "Pirmyn" msgid "Quote" msgstr "Cituoti" msgid "Retweet tweet" msgstr "Persiųsti tauÅ¡kalÄ…" #, fuzzy msgid "Like tweet" msgstr "MÄ—gstamas tauÅ¡kalas" msgid "Reply to tweet" msgstr "Atsakyti į tauÅ¡kalÄ…" msgid "More" msgstr "Daugiau" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "MÄ—gstamas" msgid "Reply" msgstr "Atsakyti" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Persiųsti" msgid "Unblock" msgstr "Atblokuoti" msgid "Show settings of this account" msgstr "Rodyti Å¡ios paskyros nustatymus" msgid "Open in new window" msgstr "Atidaryti naujame lange" msgid "Go to profile" msgstr "Pereiti į profilį" msgid "Created" msgstr "Sukurta" msgid "Subscribed to" msgstr "JÅ«sų prenumeratos" #~ msgid "Replying to" #~ msgstr "Atsakoma" #~ msgid "and" #~ msgstr "ir" #~ msgid "Actions" #~ msgstr "Veiksmai" #~ msgid "Add Image" #~ msgstr "PridÄ—ti paveikslÄ…" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "SekÄ—jų" #~ msgid "List" #~ msgstr "SÄ…raÅ¡as" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Neleistina prieiga. Daugelyje atvejų, tai reiÅ¡kia Twitter serveriuose " #~ "įvykusius nesklandumus, tad vÄ—liau, turÄ—tumÄ—te pabandyti dar kartÄ…" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Nepavyko įkelti tauÅ¡kalų" cawbird-1.4.2/po/meson.build000066400000000000000000000005361416632607600157300ustar00rootroot00000000000000i18n = import('i18n') i18n.gettext('cawbird', args: [ '--keyword=_', '--keyword=N_', '--flag=N_:1:pass-c-format', '--from-code=UTF-8', '--add-comments=TRANSLATORS', '--no-location', '--msgid-bugs-address=https://github.com/ibboard/cawbird/issues/new', '--copyright-holder=2013-2018 Timm Bäder, 2018-2020 IBBoard' ]) cawbird-1.4.2/po/nb.po000066400000000000000000000575601416632607600145360ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Kim Malmo , 2018 # Ã…ka Sikrom, 2016-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2018-02-09 06:34+0000\n" "Last-Translator: Kim Malmo \n" "Language-Team: Norwegian BokmÃ¥l (http://www.transifex.com/cawbird/cawbird/" "language/nb/)\n" "Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter-klient" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird er en GTK+-basert Twitter-klient som tilbyr viktige funksjoner som " "direktemeldinger, tweet-varslinger og samtalevisning." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Du kan ogsÃ¥ se videoer, flerbilde-tweets, lister, filtre og bruke ulike " "kontoer samtidig i programmet." msgid "Generic timeline view when using Cawbird" msgstr "Generisk tidslinje-visning ved bruk av Cawbird" msgid "Typical Twitter profile" msgstr "Typisk Twitter-profil" msgid "Account settings can be configured" msgstr "Kontoinnstillinger kan endres" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Bruk Twitter som et vanlig skrivebordsprogram" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Vis oppsatte kontoer" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Direktemeldinger" #, fuzzy msgid "Select Media" msgstr "Velg bild" msgid "Open" msgstr "Ã…pne" msgid "Cancel" msgstr "Avbryt" #, fuzzy msgid "Selected file is not an image or video." msgstr "Valgte fil er ikke et bilde." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "Valgt bilde er for stort. Maksimal filstørrelse per bilde er %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "Valgt bilde er for stort. Maksimal filstørrelse per bilde er %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "Valgt bilde er for stort. Maksimal filstørrelse per bilde er %'d MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Direktesamtale" #, fuzzy msgid "Direct message threads" msgstr "Direktemeldinger" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d ny melding fra %s" msgstr[1] "%d nye meldinger fra %s" #, c-format msgid "New direct message from %s" msgstr "Ny direktemelding fra %s" msgid "Direct Messages" msgstr "Direktemeldinger" #, fuzzy msgid "Liked tweets timeline" msgstr "Merk tweet som favoritt" #, fuzzy msgid "Likes" msgstr "Favoritter" msgid "Add new Filter" msgstr "Legg til nytt filter" msgid "Filters" msgstr "Filtre" #, fuzzy msgid "Home timeline" msgstr "Skjul fra tidslinje" #, c-format msgid "%s retweeted %s" msgstr "%s retweetet %s" #, c-format msgid "%s tweeted" msgstr "%s tweetet" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d ny tweet!" msgstr[1] "%d nye tweets!" msgid "Home" msgstr "Hjem" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s tweetet" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Offentlig" msgid "Lists" msgstr "Lister" msgid "Show configured accounts" msgstr "Vis oppsatte kontoer" msgid "Compose Tweet" msgstr "Skriv tweet" msgid "Add new Account" msgstr "Legg til ny konto" #, fuzzy msgid "Mentions timeline" msgstr "Skjul fra tidslinje" #, c-format msgid "%s mentioned %s" msgstr "%s nevnte %s" msgid "Mentions" msgstr "Nevnelser" msgid "Suspended Account" msgstr "Suspendert konto" msgid "Protected profile" msgstr "Beskyttet profil" #, c-format msgid "Tweet to @%s" msgstr "Tweet til @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s tweetet" msgstr[1] "%s tweetet" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Følger" msgstr[1] "Følger" #, fuzzy, c-format msgid "Location: %s" msgstr "Varslinger" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Skjul fra tidslinje" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Følgere" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Følger" msgid "Protected Profile" msgstr "Beskyttet profil" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Følgere" msgstr[1] "Følgere" #, fuzzy msgid "No tweets found" msgstr "Fant ingen elementer" msgid "No users found" msgstr "Fant ingen brukere" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet til @%s" msgid "Search" msgstr "Søk" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Klarte ikke Ã¥ vise tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Ã…pne i nettleser" msgid "Source" msgstr "Kilde" msgid "Tweet Details" msgstr "Tweet-detaljer" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ulest)" msgstr[1] "(%d ulest)" msgid "Delete" msgstr "Slett" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweet" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Er du sikker pÃ¥ at du vil slette denne kontoen?" #, c-format msgid "Block %s" msgstr "Blokker %s" msgid "This tweet contains images marked as inappropriate" msgstr "Denne tweet-en inneholder bilder som er merket som upassende" msgid "Show anyway" msgstr "Vis likevel" #, fuzzy msgid "Could not authenticate you" msgstr "Klarte ikke Ã¥ Ã¥pne %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Fant ingen brukere" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Grensesnitt" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Kodesnutt finnes allerede" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "NÃ¥" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Svarer %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Svarer %s og %d andre" msgstr[1] "Svarer %s og %d andre" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Svarer %s og %s" msgid "Don't have a Twitter account yet?" msgstr "Har du ikke en Twitter-konto enda?" msgid "Create one" msgstr "Lag konto" #, c-format msgid "Could not open %s" msgstr "Klarte ikke Ã¥ Ã¥pne %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Feil PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Konto allerede i bruk" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Velg bild" msgid "Unfollow" msgstr "Avfølg" msgid "Follow" msgstr "Følg" msgid "Loading…" msgstr "Laster inn …" #, fuzzy msgid "No entries found" msgstr "Fant ingen brukere" msgid "Retry" msgstr "Prøv pÃ¥ nyt" msgid "Copy URL" msgstr "Kopier nettadresse" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Lagre som …" msgid "Save Video" msgstr "Lagre video" msgid "Save Image" msgstr "Lagre bilde" msgid "Save" msgstr "Lagre" msgid "Select Banner Image" msgstr "Velg fanebilde" msgid "Image does not meet minimum size requirements:" msgstr "Bildet tilfredsstiller ikke størrelseskrav:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimumsbredde: %d piksel" msgstr[1] "Minimumsbredde: %d piksler" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimumshøyde: %d piksel" msgstr[1] "Minimumshøyde: %d piksler" msgid "Pick" msgstr "Velg" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Tilbake" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Siter tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Du kan bare legge til én GIF-fil per tweet." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Endre filter" msgid "Matches" msgstr "Treff" msgid "Doesn't match" msgstr "Samsvarer ikke" msgid "Modify Snippet" msgstr "Endre kodesnut" msgid "Snippet can't be empty" msgstr "Kodesnutt kan ikke være tom" msgid "Replacement can't be empty" msgstr "Erstatning kan ikke være tom" msgid "Snippet may not contain whitespace" msgstr "Kodesnutt kan ikke inneholde mellomrom" msgid "Snippet already exists" msgstr "Kodesnutt finnes allerede" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "Sjekk ut den nye versjonen av #Cawbird ! \\ (•◡•) / #cool" msgid "Add to or Remove User From List" msgstr "Legg til eller fjern bruker fra liste" msgid "You have no lists." msgstr "Du har ingen lister." msgid "About Cawbird" msgstr "Om Cawbird" msgid "New Account" msgstr "Ny konto" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "For Ã¥ autentisere Cawbird mÃ¥ du logge inn pÃ¥ twitter.com med kontoen du vil " "bruke og hente en PIN-kode" msgid "Request PIN" msgstr "Be om PIN" msgid "Enter PIN from twitter.com below:" msgstr "Skriv inn PIN fra twitter.com nedenfor:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Bekreft" msgid "Account Settings" msgstr "Kontoinnstillinger" msgid "Name" msgstr "Navn" msgid "Website" msgstr "Nettside" msgid "Autostart" msgstr "Autostart" #, fuzzy msgid "Remove Account" msgstr "Slett" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Er du sikker pÃ¥ at du vil slette denne kontoen?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Send" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Vis favorittbilder" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtre" #, fuzzy msgid "Filtered users" msgstr "Filtre" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Brukere" #, fuzzy msgid "Set image description" msgstr "Beskrivelse" msgid "Subscribe" msgstr "Abonner" msgid "Unsubscribe" msgstr "Avslutt abonnement" msgid "Subscribers:" msgstr "Abonnenter:" msgid "Members:" msgstr "Medlemmer:" msgid "Creator:" msgstr "Laget av:" msgid "Created at:" msgstr "Laget:" msgid "Edit" msgstr "Rediger" msgid "Mode:" msgstr "Modus:" msgid "Description" msgstr "Beskrivelse" msgid "Settings" msgstr "Innstillinger" msgid "Shortcuts" msgstr "Snarveier" msgid "About" msgstr "Om" msgid "Quit" msgstr "Avslutt" msgid "Add New Filter" msgstr "Legg til nytt filter" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Legg til ny kodesnut" msgid "Keyword" msgstr "Nøkkelord" msgid "Replacement" msgstr "Erstatning" msgid "Create New List" msgstr "Lag ny liste" msgid "Name:" msgstr "Navn:" msgid "Create" msgstr "Lag" msgid "Write Direct Message" msgstr "Skriv direktemelding" msgid "Add to/Remove from List" msgstr "Legg til/fjern fra liste" msgid "Blocked" msgstr "Blokkert" msgid "Muted" msgstr "Skjult" msgid "Retweets disabled" msgstr "Retweets slÃ¥tt av" #, fuzzy msgid "More actions" msgstr "Nevnelser" msgid "Follows you" msgstr "Følger deg" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Følgere" msgid "Following" msgstr "Følger" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Snarveier" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Skjul fra tidslinje" msgid "Show inline media" msgstr "Vis multimedier" msgid "Always show" msgstr "Vis alltid" msgid "Always hide" msgstr "Skjul allti" msgid "Hide in timeline" msgstr "Skjul fra tidslinje" msgid "Auto scroll on new tweets" msgstr "Rull automatisk ved nye tweets" msgid "Double-click activation" msgstr "Dobbeltklikk-trigging" msgid "Notifications" msgstr "Varslinger" msgid "On New Tweets" msgstr "Ved nye tweets" msgid "Never" msgstr "Aldri" msgid "Every" msgstr "Hver" msgid "Stack 5" msgstr "Stabel 5" msgid "Stack 10" msgstr "Stabel 10" msgid "Stack 25" msgstr "Stabel 25" msgid "Stack 50" msgstr "Stabel 50" msgid "On New Mentions" msgstr "Ved nye nevnelser" msgid "On New Messages" msgstr "Ved nye meldinger" msgid "Interface" msgstr "Grensesnitt" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Runde profilbilder" msgid "Remove trailing hashtags" msgstr "Fjern avsluttende hasjtagger" msgid "Remove media links" msgstr "Fjern medielenker" msgid "Hide inappropriate content" msgstr "Skjul upassende innhold" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Ingen kodesnutter satt opp." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "SlÃ¥ pÃ¥ kodesnutter ved Ã¥ skrive med tastaturet og trykk TAB." msgid "Snippets" msgstr "Kodesnutter" msgctxt "shortcuts window" msgid "General" msgstr "Generelt" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Skriv tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Vis kontoinnstillinger" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Vis pop-over med kontoer" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Vis programinnstillinger" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "SlÃ¥ pÃ¥/av topplinje" msgctxt "shortcuts window" msgid "Go Back" msgstr "GÃ¥ tilbake" msgctxt "shortcuts window" msgid "Go Forward" msgstr "GÃ¥ framover" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "GÃ¥ til n-te sid" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favoritt" msgctxt "shortcuts window" msgid "Reply" msgstr "Svar" msgctxt "shortcuts window" msgid "Quote" msgstr "Sitat" msgctxt "shortcuts window" msgid "Show Details" msgstr "Vis detaljer" msgctxt "shortcuts window" msgid "Delete" msgstr "Slett" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Start ny samtale" msgid "With:" msgstr "Med:" msgid "Go" msgstr "Kjør" msgid "Quote" msgstr "Siter" msgid "Retweet tweet" msgstr "Retweet tweet" #, fuzzy msgid "Like tweet" msgstr "Merk tweet som favoritt" msgid "Reply to tweet" msgstr "Svar pÃ¥ tweet" msgid "More" msgstr "Mer" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favoritt" msgid "Reply" msgstr "Svar" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweet" msgid "Unblock" msgstr "Fjern blokkering" msgid "Show settings of this account" msgstr "Vis innstillinger for denne kontoen" msgid "Open in new window" msgstr "Ã…pne i nytt vindu" msgid "Go to profile" msgstr "GÃ¥ til profil" msgid "Created" msgstr "Laget" msgid "Subscribed to" msgstr "Abonnert pÃ¥" #~ msgid "Replying to" #~ msgstr "Svarer" #~ msgid "and" #~ msgstr "og" #~ msgid "Actions" #~ msgstr "Handlinger" #~ msgid "Add Image" #~ msgstr "Legg til bild" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Følgere" #~ msgid "List" #~ msgstr "Liste" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Ikke autentisert. Dette betyr som regel at det er noe galt med Twitter-" #~ "tjenerne. Prøv igjen senere" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Klarte ikke Ã¥ laste inn tweets" cawbird-1.4.2/po/nl.po000066400000000000000000000674441416632607600145520ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # Geert Wirken , 2014 # Nathan Follens, 2017 # Heimen Stoffels , 2014-2015, 2020-2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Heimen Stoffels , 2021\n" "Language-Team: Dutch (https://www.transifex.com/cawbird/teams/107135/nl/)\n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twittertoepassing" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as" " Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird is een geïntegreerde GTK+ Twittertoepassing met ondersteuning voor " "de belangrijkste functionaliteiten, zoals privéberichten (DM's), " "tweetmeldingen en gespreksweergaven." msgid "" "Additional features include local viewing of videos, multiple inline images," " Lists, Filters, multiple accounts, etc." msgstr "" "Daarnaast kun je video's afspelen, meerdere bijgevoegde afbeeldingen " "bekijken en is er ondersteuning voor lijsten, filters, meerdere accounts en " "nog veel meer." msgid "Generic timeline view when using Cawbird" msgstr "Algemene tijdlijnweergave gebruiken" msgid "Typical Twitter profile" msgstr "Doorsnee Twitter-profiel" msgid "Account settings can be configured" msgstr "Accountinstellingen kunnen worden ingesteld" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Cawbird met verschillende thema's (Adwaita, Adwaita donker, Hoog contrast en" " Adwaita donkergroen)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Gebruik Twitter vanuit een normale bureaubladtoepassing" #. TRANSLATORS: Search terms to find this application. Do NOT translate or #. localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" "Toont alleen het venster 'Tweet opstellen' van het opgegeven account en niks" " anders." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` #. output msgid "account-name" msgstr "accountnaam" #. TRANSLATORS: Description of the `--start-service` option for the command- #. line msgid "Start service" msgstr "Start de dienst" #. TRANSLATORS: Description of the `--stop-service` option for the command- #. line msgid "Stop service, if it has been started as a service" msgstr "Stop de dienst indien deze gestart is" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the #. command-line msgid "Print configured startup accounts" msgstr "Toon de ingestelde accounts die automatisch moeten worden geladen" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Open het venster met het opgegeven account" #, c-format msgid "Direct messages with %s" msgstr "Privégesprek met %s" msgid "Select Media" msgstr "Media kiezen" msgid "Open" msgstr "Openen" msgid "Cancel" msgstr "Annuleren" msgid "Selected file is not an image or video." msgstr "Het gekozen bestand is geen afbeelding of video." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "De gekozen video is te groot. De maximale bestandsgrootte is %'d MB per " "video." #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "De gekozen afbeelding is te groot. De maximale bestandsgrootte is %'d MB per" " afbeelding." #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Het gekozen gif-bestand is te groot. De maximale bestandsgrootte is %'d MB." msgid "Insert Emoji" msgstr "Emoji invoegen" msgid "Direct Conversation" msgstr "Privégesprek" msgid "Direct message threads" msgstr "Privégesprekken" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nieuw bericht van %s" msgstr[1] "%d nieuwe berichten van %s" #, c-format msgid "New direct message from %s" msgstr "Nieuw privébericht van %s" msgid "Direct Messages" msgstr "Privéberichten" msgid "Liked tweets timeline" msgstr "Vind-ik-leukstijdlijn" msgid "Likes" msgstr "Vind-ik-leuks" msgid "Add new Filter" msgstr "Filter toevoegen" msgid "Filters" msgstr "Filters" msgid "Home timeline" msgstr "Startpaginatijdlijn" #, c-format msgid "%s retweeted %s" msgstr "%s heeft %s geretweet" #, c-format msgid "%s tweeted" msgstr "%s tweette" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nieuwe tweet!" msgstr[1] "%d nieuwe tweets!" msgid "Home" msgstr "Startpagina" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" #. when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "Tweets op de %s-lijst" msgid "Private" msgstr "Privé" msgid "Public" msgstr "Openbaar" msgid "Lists" msgstr "Lijsten" msgid "Show configured accounts" msgstr "Ingestelde accounts bekijken" msgid "Compose Tweet" msgstr "Tweet opstellen" msgid "Add new Account" msgstr "Account toevoegen" msgid "Mentions timeline" msgstr "Vermeldingentijdlijn" #, c-format msgid "%s mentioned %s" msgstr "%s heeft %s vermeld" msgid "Mentions" msgstr "Vermeldingen" msgid "Suspended Account" msgstr "Opgeschort account" msgid "Protected profile" msgstr "Afgeschermd profiel" #, c-format msgid "Tweet to @%s" msgstr "Verstuur een tweet aan @%s" #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d tweet" msgstr[1] "%d tweets" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Volgt %d account" msgstr[1] "Volgt %d accounts" #, c-format msgid "Location: %s" msgstr "Locatie: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile #. timeline view #, c-format msgid "%s timeline" msgstr "%s's tijdlijn" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users following the user #, c-format msgid "%s followers" msgstr "%s's volgers" #. TRANSLATORS: Value is user's name - used for accessibility text for list of #. users followed by the user #, c-format msgid "%s following" msgstr "Gevolgd door %s" msgid "Protected Profile" msgstr "Afgeschermd profiel" msgid "User is blocked" msgstr "De gebruiker is geblokkeerd" msgid "User is muted" msgstr "De gebruiker wordt genegeerd" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d volger" msgstr[1] "%d volgers" msgid "No tweets found" msgstr "Er zijn geen tweets gevonden" msgid "No users found" msgstr "Er zijn geen gebruikers gevonden" #, c-format msgid "Users matching \"%s\"" msgstr "Gebruikers die overeenkomen met \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Tweets die overeenkomen met \"%s\"" msgid "Search" msgstr "Zoeken" msgid "Load More" msgstr "Meer laden" msgid "Could not show tweet" msgstr "De tweet kan niet worden getoond" msgid "This tweet is hidden by the author" msgstr "Deze tweet is onzichtbaar gemaakt door de auteur" msgid "This tweet is unavailable" msgstr "Deze tweet is niet beschikbaar" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. #. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "Vertalen naar het Nederlands" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Openen in webbrowser" msgid "Source" msgstr "Bron" msgid "Tweet Details" msgstr "Details" #. TRANSLATORS: Used for the "via " line in Tweet Info view when #. the client is blank msgid "an unknown client" msgstr "een onbekende Twitter-toepassing" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d ongelezen)" msgstr[1] "(%d ongelezen)" msgid "Delete" msgstr "Verwijderen" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Geretweet door %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "Weet je zeker dat je deze tweet wilt verwijderen?" #, c-format msgid "Block %s" msgstr "%s blokkeren" msgid "This tweet contains images marked as inappropriate" msgstr "Deze tweet bevat afbeeldingen die gemarkeerd zijn als gevoelig" msgid "Show anyway" msgstr "Tóch bekijken" msgid "Could not authenticate you" msgstr "Het autoriseren in mislukt" msgid "Sorry, that page does not exist" msgstr "Sorry, die pagina bestaat niet" msgid "User not found." msgstr "Deze gebruiker is niet gevonden." msgid "User has been suspended." msgstr "Deze gebruiker is tijdelijk verbannen." msgid "Your account is suspended and is not permitted to access this feature" msgstr "" "Je kunt deze functie niet gebruiken omdat je account tijdelijk verbannen is." msgid "Rate limit exceeded" msgstr "Gebruikspercentage overschreden" msgid "Invalid or expired token" msgstr "Ongeldige of verlopen toegangssleutel" msgid "The specified user is not a subscriber of this list." msgstr "De opgegeven gebruiker is niet geabonneerd op deze lijst." msgid "The user you are trying to remove from the list is not a member." msgstr "" "De gebruiker die je wilt verwijderen is niet geabonneerd op deze lijst." msgid "Account update failed: value is too long." msgstr "Account bijwerken mislukt: de waarde is te lang." msgid "Over capacity" msgstr "Overbelast" msgid "Internal error" msgstr "Interne fout" msgid "You have already favorited this status." msgstr "Je hebt deze status al leuk gevonden." msgid "No status found with that ID." msgstr "Er is geen status met die id." msgid "You cannot send messages to users who are not following you." msgstr "Je kunt geen berichten versturen aan gebruikers die jou niet volgen." msgid "There was an error sending your message." msgstr "Er is een fout opgetreden tijdens het versturen." msgid "You've already requested to follow this user." msgstr "Je hebt al een volgverzoek ingediend." msgid "You are unable to follow more people at this time" msgstr "Je kunt momenteel niet méér mensen volgen" msgid "Sorry, you are not authorized to see this status" msgstr "Je hebt geen toestemming om deze status te bekijken" msgid "User is over daily status update limit" msgstr "Deze gebruiker heeft het dagelijkse statuslimiet bereikt" msgid "Tweet needs to be a bit shorter." msgstr "Je tweet moet worden ingekort." msgid "Status is a duplicate" msgstr "Je hebt deze status al verstuurd" msgid "Owner must allow dms from anyone." msgstr "De eigenaar moet privéberichten van iedereen toestaan." msgid "Bad authentication data" msgstr "Onjuiste autorisatiegegevens" msgid "Your credentials do not allow access to this resource." msgstr "Je inloggegevens geven je geen toegang tot deze bron." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Dit verzoek lijkt geautomatiseerd. Om gebruikers te beschermen tegen spam en" " andere schadelijke activiteit, kunnen we deze handeling nu niet uitvoeren." msgid "User must verify login" msgstr "Je moet je inlogverzoek goedkeuren" msgid "Application cannot perform write actions." msgstr "Cawbird beschikt niet over schrijfrechten." msgid "You can’t mute yourself." msgstr "Je kunt jezelf niet negeren." msgid "You are not muting the specified user." msgstr "Je negeert de opgegeven gebruiker niet." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" "Bewegende gif's zijn niet toegestaan bij het uploaden van meerdere " "afbeeldingen." msgid "The validation of media ids failed." msgstr "Het goedkeuren van de media-id's is mislukt." msgid "A media id was not found." msgstr "Er is geen media-id gevonden." msgid "" "To protect our users from spam and other malicious activity, this account is" " temporarily locked." msgstr "" "Om gebruikers te beschermen tegen spam en andere schadelijke activiteit, is " "dit account tijdelijk geblokkeerd." msgid "You have already retweeted this Tweet." msgstr "Je hebt deze tweet al geretweet." msgid "You cannot send messages to this user." msgstr "Je kunt geen berichten versturen aan deze gebruiker." msgid "The text of your direct message is over the max character limit." msgstr "Het aantal tekens van je privébericht overschrijdt het limiet." msgid "Subscription already exists." msgstr "Je bent al geabonneerd." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" "Je probeerde een tweet te beantwoorden die verwijderd is, of niet zichtbaar " "voor jou." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "De tweet overschrijdt het bijlagelimiet." msgid "The given URL is invalid." msgstr "De opgegeven url is onjuist." msgid "Invalid / suspended application" msgstr "Ongeldige/Geblokkeerde toepassing" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" "De tweetauteur heeft een beperking ingesteld op het beantwoorden van deze " "tweet." msgid "Invalid media file" msgstr "Ongeldig mediabestand" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" "Er deed zich een onbekende fout met code '%s' voor tijdens het uploaden" #, c-format msgid "Unknown error code %lld during upload" msgstr "" "Er deed zich een onbekende fout met code '%lld' voor tijdens het uploaden" msgid "Now" msgstr "Nu" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d minuut geleden" msgstr[1] "%d minuten geleden" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d uur geleden" msgstr[1] "%d uur geleden" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%du" #. TRANSLATORS: Full-text date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see #. https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, c-format msgid "Replying to %s" msgstr "In antwoord op %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use #. the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "In antwoord op %s en %d ander" msgstr[1] "In antwoord op %s en %d anderen" #. TRANSLATORS: The first %s is a list of one or more comma-separated user #. names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "In antwoord op %s en %s" msgid "Don't have a Twitter account yet?" msgstr "Heb je nog geen Twitter-account?" msgid "Create one" msgstr "Maak een account aan" #, c-format msgid "Could not open %s" msgstr "Het openen van %s is mislukt" msgid "Failed to retrieve request token" msgstr "De verzoeksleutel kan niet worden opgehaald" msgid "Wrong PIN" msgstr "De pincode is onjuist" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" "%s is al toegevoegd, maar bevat andere sleutels.\n" "\n" "Wil je dat account vervangen?" msgid "Account already in use" msgstr "Dit account is al in gebruik" msgid "Failed to retrieve access token" msgstr "De toegangssleutel kan niet worden opgehaald" msgid "Select Image" msgstr "Afbeelding kiezen" msgid "Unfollow" msgstr "Ontvolgen" msgid "Follow" msgstr "Volgen" msgid "Loading…" msgstr "Bezig met laden…" msgid "No entries found" msgstr "Geen items gevonden" msgid "Retry" msgstr "Opnieuw proberen" msgid "Copy URL" msgstr "URL kopiëren" msgid "Reload image" msgstr "Afbeelding herladen" msgid "Save as…" msgstr "Opslaan als…" msgid "Save Video" msgstr "Video opslaan" msgid "Save Image" msgstr "Afbeelding opslaan" msgid "Save" msgstr "Opslaan" msgid "Select Banner Image" msgstr "Omslagfoto kiezen" msgid "Image does not meet minimum size requirements:" msgstr "De afbeelding voldoet niet aan de minimumafmetingen:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimale breedte: %d pixel" msgstr[1] "Minimale breedte: %d pixels" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimale hoogte: %d pixel" msgstr[1] "Minimale hoogte: %d pixels" msgid "Pick" msgstr "Kiezen" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "De afbeelding kan niet worden geladen" msgstr[1] "%u afbeeldingen kunnen niet worden geladen" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , #. , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Terug" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "De tweet kan niet worden geciteerd: %s\n" "\n" "Wil je de onverstuurde tweet bewaren?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "De tweet kan niet worden beantwoord: %s\n" "\n" "Wil je de onverstuurde tweet bewaren?" msgid "Quote tweet" msgstr "Tweet citeren" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the #. "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "Wil je de tweet bewaren als concept?" msgid "Only one animated GIF file per tweet is allowed." msgstr "Er wordt slechts één gif-bestand per tweet toegestaan." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "Nog %d teken mogelijk" msgstr[1] "Nog %d tekens mogelijk" #. TRANSLATORS: Values are current image index (1-based) and total image #. count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Afbeelding %d van %d" msgstr[1] "Afbeelding %d van %d" msgid "Modify Filter" msgstr "Filter aanpassen" msgid "Matches" msgstr "Overeenkomsten" msgid "Doesn't match" msgstr "Komt niet overeen" msgid "Modify Snippet" msgstr "Fragment bewerken" msgid "Snippet can't be empty" msgstr "Het fragment mag niet blanco zijn" msgid "Replacement can't be empty" msgstr "De vervanging mag niet blanco zijn" msgid "Snippet may not contain whitespace" msgstr "Het fragment mag geen witruimte bevatten" msgid "Snippet already exists" msgstr "Dit fragment bestaat al" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" "De vertaal-url moet de plaatshouders {SOURCE_LANG}, {TARGET_LANG} en {CONTENT} bevatten\n" "\n" "In plaats daarvan wordt de url '%s' gebruikt" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool " "#newisalwaysbetter" msgstr "" "Hé, kijk eens naar deze nieuwe #Cawbird-versie! \\ (•◡•) / #cool " "#nieuwisaltijdbeter" msgid "Add to or Remove User From List" msgstr "Gebruiker toevoegen aan of verwijderen van lijst" msgid "You have no lists." msgstr "Je hebt geen lijsten." msgid "About Cawbird" msgstr "Over Cawbird" msgid "New Account" msgstr "Nieuw account" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Geef Cawbird toestemming door op twitter.com een pincode op te halen van het" " toe te voegen account" msgid "Request PIN" msgstr "Pincode aanvragen" msgid "Enter PIN from twitter.com below:" msgstr "Voer de pincode hieronder in:" msgid "PIN" msgstr "Pincode" msgid "Confirm" msgstr "Bevestigen" msgid "Account Settings" msgstr "Accountinstellingen" msgid "Name" msgstr "Naam" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Automatisch laden" msgid "Remove Account" msgstr "Account verwijderen" msgid "Do you really want to remove this account?" msgstr "Weet je zeker dat je dit account wilt verwijderen?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Smileys en personen" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Lichaam en kledij" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Dieren en natuur" msgctxt "emoji category" msgid "Food & Drink" msgstr "Eten en drinken" msgctxt "emoji category" msgid "Travel & Places" msgstr "Reizen en locaties" msgctxt "emoji category" msgid "Activities" msgstr "Activiteiten" msgctxt "emoji category" msgid "Objects" msgstr "Objecten" msgctxt "emoji category" msgid "Symbols" msgstr "Symbolen" msgctxt "emoji category" msgid "Flags" msgstr "Vlaggen" msgid "No Results Found" msgstr "Geen zoekresultaten" msgid "Try a different search" msgstr "Probeer een andere zoekopdracht" msgid "Send" msgstr "Verzenden" msgid "Add Media" msgstr "Media toevoegen" msgid "Show favorite images" msgstr "Leuk gevonden afbeeldingen tonen" msgid "Click to edit" msgstr "Klik om te bewerken" msgid "Remove this Filter" msgstr "Filter verwijderen" msgid "Filtered terms" msgstr "Gefilterde zoektermen" msgid "Filtered users" msgstr "Gefilterde gebruikers" msgid "You can block users in their profile" msgstr "Je gebruikers blokkeren via hun profiel" msgid "Users" msgstr "Gebruikers" msgid "Set image description" msgstr "Afbeeldingsomschrijving instellen" msgid "Subscribe" msgstr "Abonneren" msgid "Unsubscribe" msgstr "Opzeggen" msgid "Subscribers:" msgstr "Aantal abonnees:" msgid "Members:" msgstr "Aantal leden:" msgid "Creator:" msgstr "Auteur:" msgid "Created at:" msgstr "Gemaakt op:" msgid "Edit" msgstr "Aanpassen" msgid "Mode:" msgstr "Modus:" msgid "Description" msgstr "Omschrijving" msgid "Settings" msgstr "Instellingen" msgid "Shortcuts" msgstr "Sneltoetsen" msgid "About" msgstr "Over" msgid "Quit" msgstr "Afsluiten" msgid "Add New Filter" msgstr "Filter toevoegen" msgid "Regular Expression:" msgstr "Reguliere uitdrukking:" msgid "Test:" msgstr "Test:" msgid "Add New Snippet" msgstr "Fragment toevoegen" msgid "Keyword" msgstr "Trefwoord" msgid "Replacement" msgstr "Vervangen door" msgid "Create New List" msgstr "Lijst aanmaken" msgid "Name:" msgstr "Naam:" msgid "Create" msgstr "Aanmaken" msgid "Write Direct Message" msgstr "Privébericht opstellen" msgid "Add to/Remove from List" msgstr "Toevoegen aan of verwijderen van lijst" msgid "Blocked" msgstr "Geblokkeerd" msgid "Muted" msgstr "Genegeerd" msgid "Retweets disabled" msgstr "Retweets uitschakelen" msgid "More actions" msgstr "Meer acties" msgid "Follows you" msgstr "Volgt je" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Volgers" msgid "Following" msgstr "Volgend" msgid "Use dark theme" msgstr "Donker thema gebruiken" msgid "Shortcut key" msgstr "Sneltoets" msgid "Alt" msgstr "Alt" msgid "Ctrl" msgstr "Ctrl" msgid "Shift" msgstr "Shift" msgid "Super" msgstr "Super" msgid "Primary" msgstr "Primair" msgid "Timelines" msgstr "Tijdlijnen" msgid "Show inline media" msgstr "Media tonen" msgid "Always show" msgstr "Altijd tonen" msgid "Always hide" msgstr "Altijd verbergen" msgid "Hide in timeline" msgstr "Verbergen op tijdlijn" msgid "Auto scroll on new tweets" msgstr "Automatisch scrollen naar nieuwste tweets" msgid "Double-click activation" msgstr "Tweet openen door te dubbelklikken" msgid "Notifications" msgstr "Meldingen" msgid "On New Tweets" msgstr "Bij nieuwe tweets" msgid "Never" msgstr "Nooit" msgid "Every" msgstr "Elke" msgid "Stack 5" msgstr "5 opstapelen" msgid "Stack 10" msgstr "10 opstapelen" msgid "Stack 25" msgstr "25 opstapelen" msgid "Stack 50" msgstr "50 opstapelen" msgid "On New Mentions" msgstr "Bij nieuwe vermeldingen" msgid "On New Messages" msgstr "Bij nieuwe berichten" msgid "Interface" msgstr "Vormgeving" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "Normaal" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "Groot" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "Extra groot" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "Enorm" msgid "Tweet scale" msgstr "Lettergrootte van tweets" msgid "Round avatars" msgstr "Ronde profielfoto's" msgid "Remove trailing hashtags" msgstr "Bijgevoegde hashtags verwijderen" msgid "Remove media links" msgstr "Medialinks verwijderen" msgid "Hide inappropriate content" msgstr "Gevoelige inhoud verbergen" msgid "Translation" msgstr "Vertaling" msgid "Translation service" msgstr "Vertaaldienst" msgid "Google" msgstr "Google" msgid "Bing" msgstr "Bing" msgid "DeepL" msgstr "DeepL" msgid "Custom" msgstr "Aangepast" msgid "Custom translation URL" msgstr "Aangepaste vertaal-url" msgid "No snippets configured." msgstr "Je hebt geen fragmenten ingesteld." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Activeer fragmenten door het trefwoord te typen en op de tabtoets te " "drukken." msgid "Snippets" msgstr "Fragmenten" msgctxt "shortcuts window" msgid "General" msgstr "Algemeen" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Tweet opstellen" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Accountinstellingen tonen" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Accountsmenu tonen" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Toepassingsinstellingen tonen" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Bovenste balk tonen/verbergen" msgctxt "shortcuts window" msgid "Go Back" msgstr "Terug" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Vooruit" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ga naar pagina x" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweeten" msgctxt "shortcuts window" msgid "Like" msgstr "Vind ik leuk" msgctxt "shortcuts window" msgid "Reply" msgstr "Beantwoorden" msgctxt "shortcuts window" msgid "Quote" msgstr "Citeren" msgctxt "shortcuts window" msgid "Show Details" msgstr "Details bekijken" msgctxt "shortcuts window" msgid "Delete" msgstr "Verwijderen" msgctxt "shortcuts window" msgid "Compose" msgstr "Opstellen" msgid "Show Emoji Chooser" msgstr "Emojikiezer tonen" msgid "Start new conversation" msgstr "Gesprek starten" msgid "With:" msgstr "Met:" msgid "Go" msgstr "Gaan" msgid "Quote" msgstr "Tweet citeren" msgid "Retweet tweet" msgstr "Tweet retweeten" msgid "Like tweet" msgstr "Tweet leuk vinden" msgid "Reply to tweet" msgstr "Tweet beantwoorden" msgid "More" msgstr "Meer" msgid "Translate" msgstr "Vertalen" msgid "Like" msgstr "Vind ik leuk" msgid "Reply" msgstr "Beantwoorden" msgid "Liked" msgstr "Leuk gevonden" msgid "Retweeted" msgstr "Geretweet" msgid "Unblock" msgstr "Deblokkeren" msgid "Show settings of this account" msgstr "Accountinstellingen tonen" msgid "Open in new window" msgstr "Openen in nieuw venster" msgid "Go to profile" msgstr "Ga naar profiel" msgid "Created" msgstr "Aangemaakt" msgid "Subscribed to" msgstr "Geabonneerd op" cawbird-1.4.2/po/pl.po000066400000000000000000000640611416632607600145440ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Piotr DrÄ…g , 2014-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 15:14+0000\n" "Last-Translator: Piotr DrÄ…g \n" "Language-Team: Polish (http://www.transifex.com/cawbird/cawbird/language/" "pl/)\n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" "%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Klient Twittera" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird jest klientem Twittera natywnym dla biblioteki GTK+ dostarczajÄ…cym " "takie ważne funkcje, jak prywatne wiadomoÅ›ci, powiadomienia o tweetach oraz " "widok rozmowy." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Dodatkowe funkcje obejmujÄ… lokalne wyÅ›wietlanie filmów i wielu obrazów " "osadzonych w tweetach oraz obsÅ‚ugÄ™ list, filtrów i wielu kont na raz." msgid "Generic timeline view when using Cawbird" msgstr "OÅ› czasu programu Cawbird" msgid "Typical Twitter profile" msgstr "Typowy profil Twittera" msgid "Account settings can be configured" msgstr "Można konfigurować ustawienia konta" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Korzystanie z Twittera w zwykÅ‚ym programie" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "WyÅ›wietla skonfigurowane konta" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "WiadomoÅ›ci prywatne" #, fuzzy msgid "Select Media" msgstr "Wybór obrazu" msgid "Open" msgstr "Otwórz" msgid "Cancel" msgstr "Anuluj" #, fuzzy msgid "Selected file is not an image or video." msgstr "Wybrany plik nie jest obrazem." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Wybrany obraz jest za duży. Maksymalny rozmiar pliku na obraz to %'d MB." #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Wybrany obraz jest za duży. Maksymalny rozmiar pliku na obraz to %'d MB." #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Wybrany obraz jest za duży. Maksymalny rozmiar pliku na obraz to %'d MB." msgid "Insert Emoji" msgstr "Wstaw emoji" msgid "Direct Conversation" msgstr "Prywatna rozmowa" #, fuzzy msgid "Direct message threads" msgstr "WiadomoÅ›ci prywatne" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nowa wiadomość od %s" msgstr[1] "%d nowe wiadomoÅ›ci od %s" msgstr[2] "%d nowych wiadomoÅ›ci od %s" msgstr[3] "%d nowych wiadomoÅ›ci od %s" #, c-format msgid "New direct message from %s" msgstr "Nowa wiadomość prywatna od %s" msgid "Direct Messages" msgstr "WiadomoÅ›ci prywatne" #, fuzzy msgid "Liked tweets timeline" msgstr "Dodaj tweet do ulubionych" #, fuzzy msgid "Likes" msgstr "Ulubione" msgid "Add new Filter" msgstr "Dodanie nowego filtru" msgid "Filters" msgstr "Filtry" #, fuzzy msgid "Home timeline" msgstr "Ukrywanie na osi czasu" #, c-format msgid "%s retweeted %s" msgstr "Użytkownik %s podaÅ‚ dalej tweet użytkownika %s" #, c-format msgid "%s tweeted" msgstr "Użytkownik %s napisaÅ‚ tweet" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d nowy tweet" msgstr[1] "%d nowe tweety" msgstr[2] "%d nowych tweetów" msgstr[3] "%d nowych tweetów" msgid "Home" msgstr "Główna" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "Użytkownik %s napisaÅ‚ tweet" msgid "Private" msgstr "Prywatny" msgid "Public" msgstr "Publiczny" msgid "Lists" msgstr "Listy" msgid "Show configured accounts" msgstr "WyÅ›wietla skonfigurowane konta" msgid "Compose Tweet" msgstr "Napisz tweet" msgid "Add new Account" msgstr "Dodanie nowego konta" #, fuzzy msgid "Mentions timeline" msgstr "Ukrywanie na osi czasu" #, c-format msgid "%s mentioned %s" msgstr "Użytkownik %s wspomniaÅ‚ użytkownika %s" msgid "Mentions" msgstr "Wzmianki" msgid "Suspended Account" msgstr "Zawieszone konto" msgid "Protected profile" msgstr "Profil chroniony" #, c-format msgid "Tweet to @%s" msgstr "Tweet do @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "Użytkownik %s napisaÅ‚ tweet" msgstr[1] "Użytkownik %s napisaÅ‚ tweet" msgstr[2] "Użytkownik %s napisaÅ‚ tweet" msgstr[3] "Użytkownik %s napisaÅ‚ tweet" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Obserwowanie" msgstr[1] "Obserwowanie" msgstr[2] "Obserwowanie" msgstr[3] "Obserwowanie" #, fuzzy, c-format msgid "Location: %s" msgstr "Powiadomienia" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Ukrywanie na osi czasu" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "ObserwujÄ…cy" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Obserwowanie" msgid "Protected Profile" msgstr "Profil chroniony" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "ObserwujÄ…cy" msgstr[1] "ObserwujÄ…cy" msgstr[2] "ObserwujÄ…cy" msgstr[3] "ObserwujÄ…cy" #, fuzzy msgid "No tweets found" msgstr "Nie odnaleziono żadnych wpisów" msgid "No users found" msgstr "Nie odnaleziono żadnych użytkowników" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet do @%s" msgid "Search" msgstr "Wyszukaj" msgid "Load More" msgstr "Wczytaj wiÄ™cej" msgid "Could not show tweet" msgstr "Nie można wyÅ›wietlić tweeta" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Podane dalej" msgid "Open in Browser" msgstr "Otwórz w przeglÄ…darce" msgid "Source" msgstr "ŹródÅ‚o" msgid "Tweet Details" msgstr "Szczegóły tweeta" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d nieprzeczytana)" msgstr[1] "(%d nieprzeczytane)" msgstr[2] "(%d nieprzeczytanych)" msgstr[3] "(%d nieprzeczytanych)" msgid "Delete" msgstr "UsuÅ„" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Podanie tweeta dalej" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Na pewno usunąć to konto?" #, c-format msgid "Block %s" msgstr "Zablokuj użytkownika %s" msgid "This tweet contains images marked as inappropriate" msgstr "Ten tweet zawiera obrazy oznaczone jako nieodpowiednie" msgid "Show anyway" msgstr "WyÅ›wietl mimo to" #, fuzzy msgid "Could not authenticate you" msgstr "Nie można otworzyć %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Nie odnaleziono żadnych użytkowników" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfejs" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Wstawka już istnieje" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Teraz" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%d min" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%d godz." #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "W odpowiedzi do %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "W odpowiedzi do %s i %d innych" msgstr[1] "W odpowiedzi do %s i %d innych" msgstr[2] "W odpowiedzi do %s i %d innych" msgstr[3] "W odpowiedzi do %s i %d innych" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "W odpowiedzi do %s i %s" msgid "Don't have a Twitter account yet?" msgstr "Nie masz jeszcze konta serwisu Twitter?" msgid "Create one" msgstr "Utwórz konto" #, c-format msgid "Could not open %s" msgstr "Nie można otworzyć %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Błędny numer PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Konto jest już używane" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Wybór obrazu" msgid "Unfollow" msgstr "PrzestaÅ„ obserwować" msgid "Follow" msgstr "Obserwuj" msgid "Loading…" msgstr "Wczytywanie…" #, fuzzy msgid "No entries found" msgstr "Nie odnaleziono żadnych użytkowników" msgid "Retry" msgstr "Ponów" msgid "Copy URL" msgstr "Skopiuj adres URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Zapisz jako…" msgid "Save Video" msgstr "Zapis filmu" msgid "Save Image" msgstr "Zapis obrazu" msgid "Save" msgstr "Zapisz" msgid "Select Banner Image" msgstr "Wybór obrazu banera" msgid "Image does not meet minimum size requirements:" msgstr "Obraz nie speÅ‚nia minimalnych wymaganych wymiarów:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimalna szerokość: %d piksel" msgstr[1] "Minimalna szerokość: %d piksele" msgstr[2] "Minimalna szerokość: %d pikseli" msgstr[3] "Minimalna szerokość: %d pikseli" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimalna wysokość: %d piksel" msgstr[1] "Minimalna wysokość: %d piksele" msgstr[2] "Minimalna wysokość: %d pikseli" msgstr[3] "Minimalna wysokość: %d pikseli" msgid "Pick" msgstr "Wybierz" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Wstecz" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Cytowanie tweeta" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Dozwolony jest tylko jeden plik GIF na tweet." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgid "Modify Filter" msgstr "Modyfikacja filtru" msgid "Matches" msgstr "PasujÄ…ce" msgid "Doesn't match" msgstr "NiepasujÄ…ce" msgid "Modify Snippet" msgstr "Modyfikacja wstawki" msgid "Snippet can't be empty" msgstr "Wstawka nie może być pusta" msgid "Replacement can't be empty" msgstr "Zamiennik nie może być pusty" msgid "Snippet may not contain whitespace" msgstr "Wstawka nie może zawierać spacji" msgid "Snippet already exists" msgstr "Wstawka już istnieje" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "Hej, #Cawbird ma nowÄ… wersjÄ™! \\ (•◡•) / #super #nowejestzawszelepsze" msgid "Add to or Remove User From List" msgstr "Dodawanie lub usuwanie użytkowników z listy" msgid "You have no lists." msgstr "Nie ma żadnych list." msgid "About Cawbird" msgstr "O programie Cawbird" msgid "New Account" msgstr "Nowe konto" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Aby uwierzytelnić program Cawbird, należy podać PIN z serwisu twitter.com " "dla dodawanego konta" msgid "Request PIN" msgstr "Zażądaj kod PIN" msgid "Enter PIN from twitter.com below:" msgstr "ProszÄ™ podać kod PIN z serwisu twitter.com poniżej:" msgid "PIN" msgstr "Kod PIN" msgid "Confirm" msgstr "Potwierdź" msgid "Account Settings" msgstr "Ustawienia konta" msgid "Name" msgstr "Nazwa" msgid "Website" msgstr "Strona WWW" msgid "Autostart" msgstr "Automatyczne uruchamianie" #, fuzzy msgid "Remove Account" msgstr "UsuÅ„" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Na pewno usunąć to konto?" msgctxt "emoji category" msgid "Smileys & People" msgstr "UÅ›mieszki i osoby" msgctxt "emoji category" msgid "Body & Clothing" msgstr "CiaÅ‚o i ubrania" msgctxt "emoji category" msgid "Animals & Nature" msgstr "ZwierzÄ™ta i przyroda" msgctxt "emoji category" msgid "Food & Drink" msgstr "Jedzenie i napoje" msgctxt "emoji category" msgid "Travel & Places" msgstr "Podróże i miejsca" msgctxt "emoji category" msgid "Activities" msgstr "Sport" msgctxt "emoji category" msgid "Objects" msgstr "Rzeczy" msgctxt "emoji category" msgid "Symbols" msgstr "Symbole" msgctxt "emoji category" msgid "Flags" msgstr "Flagi" msgid "No Results Found" msgstr "Brak wyników" msgid "Try a different search" msgstr "ProszÄ™ spróbować innych słów" msgid "Send" msgstr "WyÅ›lij" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "WyÅ›wietla ulubione obrazy" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtry" #, fuzzy msgid "Filtered users" msgstr "Filtry" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Użytkownicy" #, fuzzy msgid "Set image description" msgstr "Opis" msgid "Subscribe" msgstr "Subskrybuj" msgid "Unsubscribe" msgstr "UsuÅ„ subskrypcjÄ™" msgid "Subscribers:" msgstr "Subskrybenci:" msgid "Members:" msgstr "CzÅ‚onkowie:" msgid "Creator:" msgstr "Twórca:" msgid "Created at:" msgstr "Utworzono:" msgid "Edit" msgstr "Modyfikuj" msgid "Mode:" msgstr "Tryb:" msgid "Description" msgstr "Opis" msgid "Settings" msgstr "Ustawienia" msgid "Shortcuts" msgstr "Skróty" msgid "About" msgstr "O programie" msgid "Quit" msgstr "ZakoÅ„cz" msgid "Add New Filter" msgstr "Dodanie nowego filtru" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Dodanie nowej wstawki" msgid "Keyword" msgstr "SÅ‚owo kluczowe" msgid "Replacement" msgstr "Zamiennik" msgid "Create New List" msgstr "Utwórz nowÄ… listÄ™" msgid "Name:" msgstr "Nazwa:" msgid "Create" msgstr "Utwórz" msgid "Write Direct Message" msgstr "Napisz prywatnÄ… wiadomość" msgid "Add to/Remove from List" msgstr "Dodaj/usuÅ„ z listy" msgid "Blocked" msgstr "Zablokowany" msgid "Muted" msgstr "Wyciszony" msgid "Retweets disabled" msgstr "Wyłączono podawanie dalej" #, fuzzy msgid "More actions" msgstr "Wzmianki" msgid "Follows you" msgstr "Obserwuje ciÄ™" msgid "Tweets" msgstr "Tweety" msgid "Followers" msgstr "ObserwujÄ…cy" msgid "Following" msgstr "Obserwowanie" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Skróty" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Ukrywanie na osi czasu" msgid "Show inline media" msgstr "Multimedia w wiadomoÅ›ciach" msgid "Always show" msgstr "WyÅ›wietlanie" msgid "Always hide" msgstr "Ukrywanie" msgid "Hide in timeline" msgstr "Ukrywanie na osi czasu" msgid "Auto scroll on new tweets" msgstr "Automatyczne przewijanie do nowych tweetów" msgid "Double-click activation" msgstr "Aktywacja podwójnym klikniÄ™ciem" msgid "Notifications" msgstr "Powiadomienia" msgid "On New Tweets" msgstr "Po otrzymaniu nowych tweetów" msgid "Never" msgstr "Nigdy" msgid "Every" msgstr "Każdy" msgid "Stack 5" msgstr "Co pięć" msgid "Stack 10" msgstr "Co dziesięć" msgid "Stack 25" msgstr "Co dwadzieÅ›cia pięć" msgid "Stack 50" msgstr "Co pięćdziesiÄ…t" msgid "On New Mentions" msgstr "Po otrzymaniu nowych wzmianek" msgid "On New Messages" msgstr "Po otrzymaniu nowych wiadomoÅ›ci" msgid "Interface" msgstr "Interfejs" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweety" msgid "Round avatars" msgstr "OkrÄ…gÅ‚e awatary" msgid "Remove trailing hashtags" msgstr "Usuwanie koÅ„cowych znaczników #" msgid "Remove media links" msgstr "Usuwanie odnoÅ›ników do multimediów" msgid "Hide inappropriate content" msgstr "Ukrywanie nieodpowiednich treÅ›ci" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nie skonfigurowano żadnych wstawek." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Można aktywować wstawkÄ™ piszÄ…c sÅ‚owo kluczowe i naciskajÄ…c klawisz Tab." msgid "Snippets" msgstr "Wstawki" msgctxt "shortcuts window" msgid "General" msgstr "Ogólne" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Napisanie tweeta" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "WyÅ›wietlenie ustawieÅ„ konta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "WyÅ›wietlenie okienka kont" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "WyÅ›wietlenie ustawieÅ„ programu" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Przełączenie górnego paska" msgctxt "shortcuts window" msgid "Go Back" msgstr "Wstecz" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Dalej" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "PrzejÅ›cie do n-tej strony" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweety" msgctxt "shortcuts window" msgid "Retweet" msgstr "Podanie tweeta dalej" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Dodanie do ulubionych" msgctxt "shortcuts window" msgid "Reply" msgstr "Odpowiadanie" msgctxt "shortcuts window" msgid "Quote" msgstr "Cytowanie" msgctxt "shortcuts window" msgid "Show Details" msgstr "WyÅ›wietlenie szczegółów" msgctxt "shortcuts window" msgid "Delete" msgstr "UsuniÄ™cie" msgctxt "shortcuts window" msgid "Compose" msgstr "Pisanie" msgid "Show Emoji Chooser" msgstr "WyÅ›wietlenie okna wyboru emoji" msgid "Start new conversation" msgstr "Rozpocznij nowÄ… rozmowÄ™" msgid "With:" msgstr "Z:" msgid "Go" msgstr "Przejdź" msgid "Quote" msgstr "Cytuj" msgid "Retweet tweet" msgstr "Podaj tweet dalej" #, fuzzy msgid "Like tweet" msgstr "Dodaj tweet do ulubionych" msgid "Reply to tweet" msgstr "Odpowiedz na tweet" msgid "More" msgstr "WiÄ™cej" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Dodaje do ulubionych" msgid "Reply" msgstr "Odpowiada" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Podanie tweeta dalej" msgid "Unblock" msgstr "Odblokuj" msgid "Show settings of this account" msgstr "WyÅ›wietla ustawienia tego konta" msgid "Open in new window" msgstr "Otwiera w nowym oknie" msgid "Go to profile" msgstr "Przejdź do profilu" msgid "Created" msgstr "Utworzono" msgid "Subscribed to" msgstr "Subskrybowano do" #~ msgid "Replying to" #~ msgstr "W odpowiedzi do" #~ msgid "and" #~ msgstr "i" #~ msgid "Actions" #~ msgstr "DziaÅ‚ania" #~ msgid "Add Image" #~ msgstr "Dodaj obraz" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "ObserwujÄ…cy" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Brak upoważnienia. Zwykle oznacza to, że coÅ› jest nie tak z serwerami " #~ "Twittera i powinno siÄ™ spróbować później" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Nie można wczytać tweetów" cawbird-1.4.2/po/pt.po000066400000000000000000000564441416632607600145620ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Bruno Guerreiro , 2015 # Steeven Lopes , 2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Portuguese (Portugal) (http://www.transifex.com/cawbird/" "cawbird/language/pt_PT/)\n" "Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliente de Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird é um cliente twitter nativo GTK+ que fornece recursos vitais como " "Mensagens Diretas (DMs), notificações de tweet, exibição de conversas." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Funcionalidades adicionais incluem visualização de vídeos locais, múltiplas " "imagens em linha, Listas, Filtros, múltiplas contas, entre outros." msgid "Generic timeline view when using Cawbird" msgstr "Mostrar timeline genérica ao usar o Cawbird" msgid "Typical Twitter profile" msgstr "Perfil típico do Twitter" msgid "Account settings can be configured" msgstr "Definições da conta podem ser configuradas" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Usar o Twitter de uma aplicação de desktop normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar contas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensagens diretas" #, fuzzy msgid "Select Media" msgstr "Selecionar imagens" msgid "Open" msgstr "Abrir" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo por imagem é de %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo por imagem é de %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo por imagem é de %'d MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversa Direta" #, fuzzy msgid "Direct message threads" msgstr "Mensagens diretas" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nova Mensagem de %s" msgstr[1] "%d novas Mensagens de %s" #, c-format msgid "New direct message from %s" msgstr "Nova mensagem direta de %s" msgid "Direct Messages" msgstr "Mensagens diretas" #, fuzzy msgid "Liked tweets timeline" msgstr "Tweet favorito" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "Adicionar um novo filtro" msgid "Filters" msgstr "Filtros" #, fuzzy msgid "Home timeline" msgstr "Esconder no timeline" #, c-format msgid "%s retweeted %s" msgstr "%s retweetou %s" #, c-format msgid "%s tweeted" msgstr "%s twittou" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d novo Tweet!" msgstr[1] "%d novos Tweets!" msgid "Home" msgstr "Inicio" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s twittou" msgid "Private" msgstr "Privado" msgid "Public" msgstr "Publico" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "Mostrar contas configuradas" msgid "Compose Tweet" msgstr "Compor Tweet" msgid "Add new Account" msgstr "Adicionar uma nova conta" #, fuzzy msgid "Mentions timeline" msgstr "Esconder no timeline" #, c-format msgid "%s mentioned %s" msgstr "%s mencionado %s" msgid "Mentions" msgstr "Citações" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Perfil protegido" #, c-format msgid "Tweet to @%s" msgstr "Tweet para @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s twittou" msgstr[1] "%s twittou" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seguindo" msgstr[1] "Seguindo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificações" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Esconder no timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seguindo" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "Nenhuma entrada encontrada" msgid "No users found" msgstr "Não foram encontrados usuários " #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet para @%s" msgid "Search" msgstr "Procurar" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Não possível mostrar o tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Abrir no navegador" msgid "Source" msgstr "Fonte" msgid "Tweet Details" msgstr "Detalhes do Tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d não lido)" msgstr[1] "(%d não lidos)" msgid "Delete" msgstr "Apagar" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweet" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Quer realmente pagar a conta?" #, c-format msgid "Block %s" msgstr "Bloco %s" msgid "This tweet contains images marked as inappropriate" msgstr "Este tweet contém imagens marcadas como impróprio" msgid "Show anyway" msgstr "Mostrar de qualquer forma" #, fuzzy msgid "Could not authenticate you" msgstr "Não foi possível abrir %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Não foram encontrados usuários " msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interface" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Snippet já existe" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Agora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Responder a tweet" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Criar uma" #, c-format msgid "Could not open %s" msgstr "Não foi possível abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN errado" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Conta já me uso" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Selecionar imagens" msgid "Unfollow" msgstr "Deixar de seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Não foram encontrados usuários " msgid "Retry" msgstr "Tentar novamente" msgid "Copy URL" msgstr "Copiar URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Gravar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Voltar" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar Filtro" msgid "Matches" msgstr "Corresponde" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "Modificar o snippet" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "Snippet não pode conter espaços em branco" msgid "Snippet already exists" msgstr "Snippet já existe" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Ei, confira essa nova versão do #Cawbird! \\ (•◡•) / #cool #novoesempremelhor" msgid "Add to or Remove User From List" msgstr "Adicionar ou Remover Utilizador Da Lista" msgid "You have no lists." msgstr "Você não tem listas." msgid "About Cawbird" msgstr "Sobre Cawbird" msgid "New Account" msgstr "Nova Conta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Pedir PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Definições de conta" msgid "Name" msgstr "Nome" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Iniciar automaticamente" #, fuzzy msgid "Remove Account" msgstr "Apagar" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Quer realmente pagar a conta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtros" #, fuzzy msgid "Filtered users" msgstr "Filtros" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Utilizadores" #, fuzzy msgid "Set image description" msgstr "Descrição" msgid "Subscribe" msgstr "Subscrever" msgid "Unsubscribe" msgstr "Cancelar subscrição" msgid "Subscribers:" msgstr "Inscritos:" msgid "Members:" msgstr "Membros:" msgid "Creator:" msgstr "Criador:" msgid "Created at:" msgstr "Criado em:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "Descrição" msgid "Settings" msgstr "Definições" msgid "Shortcuts" msgstr "Atalhos" msgid "About" msgstr "Sobre" msgid "Quit" msgstr "Sair" msgid "Add New Filter" msgstr "Adicionar Novo Filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Adicionar novo fragmento" msgid "Keyword" msgstr "Palavra-Chave" msgid "Replacement" msgstr "Substituição" msgid "Create New List" msgstr "Criar nova lista" msgid "Name:" msgstr "Nome:" msgid "Create" msgstr "Criado" msgid "Write Direct Message" msgstr "Escrever Mensagem Direta" msgid "Add to/Remove from List" msgstr "Adicionar/remover da lista" msgid "Blocked" msgstr "Bloquado" msgid "Muted" msgstr "Mudo" msgid "Retweets disabled" msgstr "Retweets desativados" #, fuzzy msgid "More actions" msgstr "Citações" msgid "Follows you" msgstr "Segue-te" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Seguindo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atalhos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Esconder no timeline" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "Mostrar sempre" msgid "Always hide" msgstr "Esconder sempre" msgid "Hide in timeline" msgstr "Esconder no timeline" msgid "Auto scroll on new tweets" msgstr "Deslocação automática de novos tweets" msgid "Double-click activation" msgstr "Ativação do clique-duplo" msgid "Notifications" msgstr "Notificações" msgid "On New Tweets" msgstr "Em novos tweets" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Todo" msgid "Stack 5" msgstr "Pilha 5" msgid "Stack 10" msgstr "Pilha 10" msgid "Stack 25" msgstr "Pilha 25" msgid "Stack 50" msgstr "Pilha 50" msgid "On New Mentions" msgstr "Em novas citações" msgid "On New Messages" msgstr "Em novas mensagens" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Avatares redondos" msgid "Remove trailing hashtags" msgstr "Remover hashtags à direita" msgid "Remove media links" msgstr "Remover ligações de mídia" msgid "Hide inappropriate content" msgstr "Ocultar conteúdo impróprio" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nenhum fragmento configurado." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Pode ativar fragmentos escrevendo a palavra-chave e depois pressionar TAB." msgid "Snippets" msgstr "Fragmentos" msgctxt "shortcuts window" msgid "General" msgstr "Geral" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Compor Tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostrar definições da conta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostrar Contas Popover" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostrar Definições da Aplicação" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Alternar para a barra superior" msgctxt "shortcuts window" msgid "Go Back" msgstr "Voltar atrás" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Ir para frente" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ir para a página nth" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favorito" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "Citar" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostrar Detalhes" msgctxt "shortcuts window" msgid "Delete" msgstr "Eliminar" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Iniciar nova conversa" msgid "With:" msgstr "Com:" msgid "Go" msgstr "Ir" msgid "Quote" msgstr "Citar" msgid "Retweet tweet" msgstr "Retweetar tweet" #, fuzzy msgid "Like tweet" msgstr "Tweet favorito" msgid "Reply to tweet" msgstr "Responder a tweet" msgid "More" msgstr "Mais" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorito" msgid "Reply" msgstr "Responder" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweet" msgid "Unblock" msgstr "Desbloquear" msgid "Show settings of this account" msgstr "Mostrar definições desta conta" msgid "Open in new window" msgstr "Abrir em numa nova janela" msgid "Go to profile" msgstr "Ir para o perfil" msgid "Created" msgstr "Criado" msgid "Subscribed to" msgstr "Subscrito" #~ msgid "Actions" #~ msgstr "Ações" #~ msgid "Add Image" #~ msgstr "Adicionar Imagem" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "Não foi possível obter os tweets" cawbird-1.4.2/po/pt_BR.po000066400000000000000000000605201416632607600151330ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Anders Bateva , 2016 # Evertton de Lima , 2015 # Luderson Costa , 2016 # Mauro Pereira Junior , 2015 # Ricardo Borges Jr. , 2015-2017 # Salomão Carneiro de Brito , 2015 # Vanderlei Ventura , 2018 # x_root , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2018-01-02 18:35+0000\n" "Last-Translator: Vanderlei Ventura \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/cawbird/cawbird/" "language/pt_BR/)\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Cliente Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird é um cliente twitter nativo GTK+ que fornece recursos vitais como " "mensagens diretas (DMs), notificações de tweet, exibição de conversas." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Funcionalidades adicionais incluem visualização de vídeos locais, múltiplas " "imagens em linha, Listas, Filtros, múltiplas contas, entre outros." msgid "Generic timeline view when using Cawbird" msgstr "Timeline genérica quando visualizada usando Cawbird" msgid "Typical Twitter profile" msgstr "perfil do Twitter típico" msgid "Account settings can be configured" msgstr "Configuração de conta pode ser configurada" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Use o Twitter de um aplicativo desktop normal" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter:" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Mostrar contas configuradas" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mensagens diretas" #, fuzzy msgid "Select Media" msgstr "Selecione a image" msgid "Open" msgstr "Abrir" msgid "Cancel" msgstr "Cancelar" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo do arquivo por imagem " "é %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo do arquivo por imagem " "é %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "A imagem selecionada é muito grande. O tamanho máximo do arquivo por imagem " "é %'d MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "Conversa direta" #, fuzzy msgid "Direct message threads" msgstr "Mensagens diretas" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nova mensagem de %s" msgstr[1] "%d novas mensagens de %s" #, c-format msgid "New direct message from %s" msgstr "Nova mensagem direta de %s" msgid "Direct Messages" msgstr "Mensagens diretas" #, fuzzy msgid "Liked tweets timeline" msgstr "Curtir" #, fuzzy msgid "Likes" msgstr "Favoritos" msgid "Add new Filter" msgstr "Adicionar novo Filtro" msgid "Filters" msgstr "Filtros" #, fuzzy msgid "Home timeline" msgstr "Esconder na timeline" #, c-format msgid "%s retweeted %s" msgstr "%s retweetou %s" #, c-format msgid "%s tweeted" msgstr "%s twittou" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d novo tweet!" msgstr[1] "%d novos tweets!" msgid "Home" msgstr "Início" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s twittou" msgid "Private" msgstr "Privado" msgid "Public" msgstr "Público" msgid "Lists" msgstr "Listas" msgid "Show configured accounts" msgstr "Mostrar contas configuradas" msgid "Compose Tweet" msgstr "Compor tweet" msgid "Add new Account" msgstr "Adicionar nova conta" #, fuzzy msgid "Mentions timeline" msgstr "Esconder na timeline" #, c-format msgid "%s mentioned %s" msgstr "%s mencionado %s " msgid "Mentions" msgstr "Menções" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Perfil protegido" #, c-format msgid "Tweet to @%s" msgstr "Tweet para @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s twittou" msgstr[1] "%s twittou" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Seguindo" msgstr[1] "Seguindo" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificações" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Esconder na timeline" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Seguidores" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Seguindo" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Seguidores" msgstr[1] "Seguidores" #, fuzzy msgid "No tweets found" msgstr "Nenhuma entrada encontrada" msgid "No users found" msgstr "Usuário não encontrado" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tweet para @%s" msgid "Search" msgstr "Pesquisa" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Não foi possível mostrar o tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Retweets" msgid "Open in Browser" msgstr "Abrir no navegador" msgid "Source" msgstr "Fonte" msgid "Tweet Details" msgstr "Detalhes do tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d não lido)" msgstr[1] "(%d não lidos)" msgid "Delete" msgstr "Excluir" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Retweet" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Você realmente quer excluir esta conta?" #, c-format msgid "Block %s" msgstr "Bloquear%s" msgid "This tweet contains images marked as inappropriate" msgstr "Este tweet contém imagens marcadas como inapropriadas" msgid "Show anyway" msgstr "Mostrar de qualquer maneira" #, fuzzy msgid "Could not authenticate you" msgstr "Não foi possível abrir %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Usuário não encontrado" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interface" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "Snippet já existente" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Agora" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Respondendo a %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Respondendo a %s e %d outros" msgstr[1] "Respondendo a %s e %d outros" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Respondendo a %s e %s" msgid "Don't have a Twitter account yet?" msgstr "Ainda não tem uma conta do Twitter?" msgid "Create one" msgstr "Criar uma" #, c-format msgid "Could not open %s" msgstr "Não foi possível abrir %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN errado" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Conta já em uso" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Selecione a image" msgid "Unfollow" msgstr "Deixar de seguir" msgid "Follow" msgstr "Seguir" msgid "Loading…" msgstr "Carregando..." #, fuzzy msgid "No entries found" msgstr "Usuário não encontrado" msgid "Retry" msgstr "Tentar novamente" msgid "Copy URL" msgstr "Copiar URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Salvar como..." msgid "Save Video" msgstr "Salvar Vídeo" msgid "Save Image" msgstr "Salvar imagem" msgid "Save" msgstr "Salvar" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Voltar" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citar tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Somente um arquivo GIF por tweet é permitido." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgid "Modify Filter" msgstr "Modificar o filtro" msgid "Matches" msgstr "Corresponde" msgid "Doesn't match" msgstr "Não Corresponde" msgid "Modify Snippet" msgstr "Modificar snippet" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "O snippet não pode contém espaços" msgid "Snippet already exists" msgstr "Snippet já existente" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Ei, confira essa nova versão do #Cawbird! \\ (•◡•) / #legal " "#novoesempremelhor" msgid "Add to or Remove User From List" msgstr "Adicionar ou remover o usuário da lista" msgid "You have no lists." msgstr "Você não tem listas." msgid "About Cawbird" msgstr "Sobre o Cawbird" msgid "New Account" msgstr "Nova conta" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Pedido de PIN" msgid "Enter PIN from twitter.com below:" msgstr "Digite o PIN do twitter.com abaixo:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Confirmar" msgid "Account Settings" msgstr "Configurações da conta" msgid "Name" msgstr "Nome" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Início automático" #, fuzzy msgid "Remove Account" msgstr "Excluir" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Você realmente quer excluir esta conta?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Enviar" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filtros" #, fuzzy msgid "Filtered users" msgstr "Filtros" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Usuários" #, fuzzy msgid "Set image description" msgstr "Descrição" msgid "Subscribe" msgstr "Inscreva-se" msgid "Unsubscribe" msgstr "Cancelar inscrição" msgid "Subscribers:" msgstr "Inscritos:" msgid "Members:" msgstr "Membros:" msgid "Creator:" msgstr "Criador:" msgid "Created at:" msgstr "Criado em:" msgid "Edit" msgstr "Editar" msgid "Mode:" msgstr "Modo:" msgid "Description" msgstr "Descrição" msgid "Settings" msgstr "Configurações" msgid "Shortcuts" msgstr "Atalhos" msgid "About" msgstr "Sobre" msgid "Quit" msgstr "Sair" msgid "Add New Filter" msgstr "Adicionar novo filtro" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Adicionar novo snippet" msgid "Keyword" msgstr "Palavra-chave" msgid "Replacement" msgstr "Substituição" msgid "Create New List" msgstr "Criar nova lista" msgid "Name:" msgstr "Nome:" msgid "Create" msgstr "Criar" msgid "Write Direct Message" msgstr "Escrever mensagem direta" msgid "Add to/Remove from List" msgstr "Adicionar/remover da lista" msgid "Blocked" msgstr "Bloqueado" msgid "Muted" msgstr "Silenciado" msgid "Retweets disabled" msgstr "Retweets desativados" #, fuzzy msgid "More actions" msgstr "Menções" msgid "Follows you" msgstr "Você segue" msgid "Tweets" msgstr "Tweets" msgid "Followers" msgstr "Seguidores" msgid "Following" msgstr "Seguindo" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Atalhos" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Esconder na timeline" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "Sempre mostrar" msgid "Always hide" msgstr "Sempre esconder" msgid "Hide in timeline" msgstr "Esconder na timeline" msgid "Auto scroll on new tweets" msgstr "Rolagem automática de novos tweets" msgid "Double-click activation" msgstr "Ativar dois cliques" msgid "Notifications" msgstr "Notificações" msgid "On New Tweets" msgstr "Em novos tweets" msgid "Never" msgstr "Nunca" msgid "Every" msgstr "Todo" msgid "Stack 5" msgstr "Pilha 5" msgid "Stack 10" msgstr "Pilha 10" msgid "Stack 25" msgstr "Pilha 25" msgid "Stack 50" msgstr "Pilha 50" msgid "On New Mentions" msgstr "Em novas menções" msgid "On New Messages" msgstr "Em novas mensagens" msgid "Interface" msgstr "Interface" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweets" msgid "Round avatars" msgstr "Avatares redondos" msgid "Remove trailing hashtags" msgstr "Remover hashtags à direita" msgid "Remove media links" msgstr "Remover links de mídia" msgid "Hide inappropriate content" msgstr "Ocultar conteúdo inapropriado" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nenhum snippet configurado." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Você pode ativar snippets digitando a palavra-chave e depois pressionando " "TAB." msgid "Snippets" msgstr "Snippets" msgctxt "shortcuts window" msgid "General" msgstr "Geral" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Compor tweet" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Mostrar configurações da conta" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Mostrar popover das contas" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Mostrar configurações da aplicação" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Alterar barra superior" msgctxt "shortcuts window" msgid "Go Back" msgstr "Voltar" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Avançar" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Ir para página n°" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tweets" msgctxt "shortcuts window" msgid "Retweet" msgstr "Retweet" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Favorito" msgctxt "shortcuts window" msgid "Reply" msgstr "Responder" msgctxt "shortcuts window" msgid "Quote" msgstr "Citar" msgctxt "shortcuts window" msgid "Show Details" msgstr "Mostrar detalhes" msgctxt "shortcuts window" msgid "Delete" msgstr "Excluir" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Iniciar nova conversa" msgid "With:" msgstr "Com:" msgid "Go" msgstr "Ir" msgid "Quote" msgstr "Citação" msgid "Retweet tweet" msgstr "Retweetar" #, fuzzy msgid "Like tweet" msgstr "Curtir" msgid "Reply to tweet" msgstr "Responder" msgid "More" msgstr "Mais" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favorito" msgid "Reply" msgstr "Responder" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Retweet" msgid "Unblock" msgstr "Desbloquear" msgid "Show settings of this account" msgstr "Mostrar configurações dessa conta" msgid "Open in new window" msgstr "Abrir em uma nova janela" msgid "Go to profile" msgstr "Ir para o perfil" msgid "Created" msgstr "Criado" msgid "Subscribed to" msgstr "Inscrever-se" #~ msgid "Replying to" #~ msgstr "Respondendo a" #~ msgid "and" #~ msgstr "e" #~ msgid "Actions" #~ msgstr "Ações" #~ msgid "Add Image" #~ msgstr "Adicionar imagem" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Seguidores" #~ msgid "List" #~ msgstr "Lista" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Não autorizado. Na maioria das vezes, isso significa que há algo errado " #~ "com os servidores do Twitter e você deve tentar novamente mais tarde" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Não foi possível carrega os tweets" cawbird-1.4.2/po/ro.po000066400000000000000000000536531416632607600145560ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Iulian Vărzaru , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Romanian (Romania) (http://www.transifex.com/cawbird/cawbird/" "language/ro_RO/)\n" "Language: ro_RO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?" "2:1));\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Client Twitter" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird este un client de Twitter nativ GTK+ care furnizează caracteristici " "vitale ca Mesaje Directe (DM), notificări, pagina de conversaÅ£ii." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Caracteristici adiÅ£ionale includ vizualizarea videoclipurilor local, imagini " "in linie multiple, Liste, Filtre, conturi multiple, etc." msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Arată conturile configurate" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Mesaje directe" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "Anulează" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "ConversaÅ£ie directă" #, fuzzy msgid "Direct message threads" msgstr "Mesaje directe" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "" msgstr[1] "" msgstr[2] "" #, c-format msgid "New direct message from %s" msgstr "Un nou mesaj direct de la %s" msgid "Direct Messages" msgstr "Mesaje directe" #, fuzzy msgid "Liked tweets timeline" msgstr "Apreciază tweet" #, fuzzy msgid "Likes" msgstr "Apreciate" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "" #, c-format msgid "%s tweeted" msgstr "" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Home" msgstr "Pagina de pornire" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "" msgid "Private" msgstr "Privat" msgid "Public" msgstr "Public" msgid "Lists" msgstr "Liste" msgid "Show configured accounts" msgstr "Arată conturile configurate" msgid "Compose Tweet" msgstr "Compune Tweet" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "MenÅ£ionări" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "MenÅ£ionări" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Profil protejat" #, c-format msgid "Tweet to @%s" msgstr "" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "ÃŽncorporează tweet" msgstr[1] "ÃŽncorporează tweet" msgstr[2] "ÃŽncorporează tweet" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "UrmăriÅ£i" msgstr[1] "UrmăriÅ£i" msgstr[2] "UrmăriÅ£i" #, fuzzy, c-format msgid "Location: %s" msgstr "Notificări" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "MenÅ£ionări" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Urmăritori" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "UrmăriÅ£i" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Urmăritori" msgstr[1] "Urmăritori" msgstr[2] "Urmăritori" #, fuzzy msgid "No tweets found" msgstr "Nu a fost găsit nici un rezultat" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, c-format msgid "Tweets matching \"%s\"" msgstr "" msgid "Search" msgstr "Caută" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Nu am putut afiÅŸa tweet" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "" msgid "Open in Browser" msgstr "Deschide în Browser" msgid "Source" msgstr "Sursă" msgid "Tweet Details" msgstr "Detalii tweet" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d necitit)" msgstr[1] "(%d necitite)" msgstr[2] "(%d necitite)" msgid "Delete" msgstr "Åžterge" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "EÅŸti sigur că doreÅŸti să ÅŸtergi contul?" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "Nu s-a putut deschide %s" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfaţă" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Acum" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Răspunde la tweet" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Creează unul" #, c-format msgid "Could not open %s" msgstr "Nu s-a putut deschide %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN GreÅŸit" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Cont deja in folosinţă" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Nu mai urmări" msgid "Follow" msgstr "UrmăreÅŸte" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Nu a fost găsit nici un rezultat" msgid "Retry" msgstr "Reîncearcă" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Salvează" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "ÃŽnapoi" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "ÃŽncorporează tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Modify Filter" msgstr "Modifică Filtru" msgid "Matches" msgstr "Se potriveÅŸte" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" msgid "Add to or Remove User From List" msgstr "Adaugă sau Åžterge un utilizator din listă" msgid "You have no lists." msgstr "Nu ai nici o listă." msgid "About Cawbird" msgstr "Despre Cawbird" msgid "New Account" msgstr "Cont nou" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Cere PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Confirmă" msgid "Account Settings" msgstr "Setări cont" msgid "Name" msgstr "Nume" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Pornire automată" #, fuzzy msgid "Remove Account" msgstr "Åžterge" #, fuzzy msgid "Do you really want to remove this account?" msgstr "EÅŸti sigur că doreÅŸti să ÅŸtergi contul?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Trimite" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Utilizatori" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Abonează-te" msgid "Unsubscribe" msgstr "Dezabonează-te" msgid "Subscribers:" msgstr "AbonaÅ£i:" msgid "Members:" msgstr "Membrii:" msgid "Creator:" msgstr "Creator:" msgid "Created at:" msgstr "Creat la:" msgid "Edit" msgstr "Editează" msgid "Mode:" msgstr "Mod:" msgid "Description" msgstr "" msgid "Settings" msgstr "Setări" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Despre" msgid "Quit" msgstr "ÃŽnchide" msgid "Add New Filter" msgstr "Adaugă un filtru nou" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Adaugă un nou fragment de text" msgid "Keyword" msgstr "Cuvânt cheie" msgid "Replacement" msgstr "ÃŽnlocuitor" msgid "Create New List" msgstr "Creează o listă nouă" msgid "Name:" msgstr "Nume:" msgid "Create" msgstr "Creează" msgid "Write Direct Message" msgstr "Scrie mesaj direct" msgid "Add to/Remove from List" msgstr "Adaugă sau Åžterge din listă" msgid "Blocked" msgstr "Blocat" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "" #, fuzzy msgid "More actions" msgstr "MenÅ£ionări" msgid "Follows you" msgstr "Te urmăreÅŸte" msgid "Tweets" msgstr "Tweeturi" msgid "Followers" msgstr "Urmăritori" msgid "Following" msgstr "UrmăriÅ£i" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "MenÅ£ionări" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Derulare automată la tweeturi noi" msgid "Double-click activation" msgstr "Activare prin dublu click" msgid "Notifications" msgstr "Notificări" msgid "On New Tweets" msgstr "La tweeturi noi" msgid "Never" msgstr "Niciodată" msgid "Every" msgstr "Fiecare" msgid "Stack 5" msgstr "Câte 5" msgid "Stack 10" msgstr "Câte 10" msgid "Stack 25" msgstr "Câte 25" msgid "Stack 50" msgstr "Câte 50" msgid "On New Mentions" msgstr "La menÅ£ionări noi" msgid "On New Messages" msgstr "La mesaje noi" msgid "Interface" msgstr "Interfaţă" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweeturi" msgid "Round avatars" msgstr "Avatare rotunde" msgid "Remove trailing hashtags" msgstr "Åžterge hashtag-urile de la sfârÅŸit" msgid "Remove media links" msgstr "Åžterge linkurile media" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nici un fragment de text configurat." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "PoÅ£i activa fragmentele de text scriinde cuvântul cheie si apasând tasta TAB." msgid "Snippets" msgstr "Fragmente" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "ÃŽncepe o conversaÅ£ie nouă" msgid "With:" msgstr "Cu:" msgid "Go" msgstr "ÃŽncepe" msgid "Quote" msgstr "ÃŽncorporează" msgid "Retweet tweet" msgstr "" #, fuzzy msgid "Like tweet" msgstr "Apreciază tweet" msgid "Reply to tweet" msgstr "Răspunde la tweet" msgid "More" msgstr "Mai multe" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Apreciază" msgid "Reply" msgstr "Răspunde" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Răspunde la tweet" msgid "Unblock" msgstr "Deblochează" msgid "Show settings of this account" msgstr "Arată setările acestui cont" msgid "Open in new window" msgstr "Deschide intr-o pagină nouă" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Creat" msgid "Subscribed to" msgstr "Abonat la" #~ msgid "Actions" #~ msgstr "AcÅ£iuni" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Urmăritori" #~ msgid "List" #~ msgstr "Listă" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/ru.po000066400000000000000000001042101416632607600145460ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR 2013-2018 Timm Bäder, 2018-2020 IBBoard # This file is distributed under the same license as the cawbird package. # FIRST AUTHOR , YEAR. # # Translators: # Alex Baranov , 2016 # Alexander , 2016 # Andrew Makarov, 2015 # Dmitry Kireev , 2015 # Egor Erastov , 2018 # George - , 2018 # Ivlev Denis , 2015 # Max Koloskov , 2015 # Mikhail Ivanov , 2016 # netforhack, 2015 # Yevgeny Kvitchenko , 2017 # Даниил Пронин , 2021 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2020-02-05 20:32+0000\n" "Last-Translator: Даниил Пронин , 2021\n" "Language-Team: Russian (https://www.transifex.com/cawbird/teams/107135/ru/)\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Клиент Ð´Ð»Ñ Ð¢Ð²Ð¸Ñ‚Ñ‚ÐµÑ€Ð°" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird Ñто клиент Ð´Ð»Ñ Ð¢Ð²Ð¸Ñ‚Ñ‚ÐµÑ€Ð° в оболочке GTK+, который умеет отправлÑть " "личные ÑообщениÑ, ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¸ личные ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð² беÑедах." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Также можно: Ñмотреть видео и изображениÑ, наÑтраивать ÑпиÑки и фильтры, " "неÑколько профилей и Ñ‚.д." msgid "Generic timeline view when using Cawbird" msgstr "Общий вид ленты при иÑпользовании Cawbird" msgid "Typical Twitter profile" msgstr "Типичный профиль Twitter" msgid "Account settings can be configured" msgstr "ÐаÑтройки Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ Ð¼Ð¾Ð¶Ð½Ð¾ изменить" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" "Cawbird Ñ Ñ€Ð°Ð·Ð½Ñ‹Ð¼Ð¸ темами (Adwaita, тёмный вариант Adwaita, выÑоко " "контраÑÑ‚Ð½Ð°Ñ Ð¸ Adwaita Dark Green)" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "ПользуйтеÑÑŒ Твиттером как обычным приложением" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" "Показываем только окно 'ÐапиÑать твит' Ð´Ð»Ñ Ñтого профилÑ, и ничего больше." #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "account-name" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "ЗапуÑтить Ñлужбу" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "ОÑтановить, еÑли запущено как Ñлужба" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line msgid "Print configured startup accounts" msgstr "Ðапечатать наÑтроенные профили автозапуÑка" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "Открыть окно Ð´Ð»Ñ Ñтого профилÑ" #, c-format msgid "Direct messages with %s" msgstr "ПерепиÑка Ñ %s" msgid "Select Media" msgstr "Выбрать медиа" msgid "Open" msgstr "Открыть" msgid "Cancel" msgstr "Отмена" msgid "Selected file is not an image or video." msgstr "Выбранный файл должен быть картинкой или видео." #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "Выбранное видео Ñлишком большое. МакÑимальный размер %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "Выбранное изображение Ñлишком большое. МакÑимальный размер %'d MB" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "Выбранный GIF Ñлишком большой. МакÑимальный размер %'d MB" msgid "Insert Emoji" msgstr "Ð’Ñтавить Ñмодзи" msgid "Direct Conversation" msgstr "Ð›Ð¸Ñ‡Ð½Ð°Ñ Ð±ÐµÑеда" msgid "Direct message threads" msgstr "Личные беÑеды" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d новое Ñообщение от %s" msgstr[1] "%d новых ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾Ñ‚ %s" msgstr[2] "%d новых Ñообщений от %s" msgstr[3] "%d новых Ñообщений от %s" #, c-format msgid "New direct message from %s" msgstr "Ðовое Ñообщение от %s" msgid "Direct Messages" msgstr "Личные ÑообщениÑ" #, fuzzy msgid "Liked tweets timeline" msgstr "Лента избранного" #, fuzzy msgid "Likes" msgstr "Избранные" msgid "Add new Filter" msgstr "Добавить новый фильтр" msgid "Filters" msgstr "Фильтры" msgid "Home timeline" msgstr "ДомашнÑÑ Ð»ÐµÐ½Ñ‚Ð°" #, c-format msgid "%s retweeted %s" msgstr "%s ретвитнут %s" #, c-format msgid "%s tweeted" msgstr "%s твитнут" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d новый твит!" msgstr[1] "%d новых твита!" msgstr[2] "%d новых твитов!" msgstr[3] "%d новых твитов!" msgid "Home" msgstr "Домой" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, c-format msgid "%s list tweets" msgstr "Твиты из ÑпиÑка %s" msgid "Private" msgstr "Приватный" msgid "Public" msgstr "Открытый" msgid "Lists" msgstr "СпиÑки" msgid "Show configured accounts" msgstr "ÐаÑтройки профилей" msgid "Compose Tweet" msgstr "ÐапиÑать твит" msgid "Add new Account" msgstr "Добавить новый профиль" msgid "Mentions timeline" msgstr "Лента упоминаний" #, c-format msgid "%s mentioned %s" msgstr "%s упомÑнул %s" msgid "Mentions" msgstr "УпоминаниÑ" msgid "Suspended Account" msgstr "Заблокированный профиль" msgid "Protected profile" msgstr "Защищённый профиль" #, c-format msgid "Tweet to @%s" msgstr "Твитнуть @%s " #, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%d твит" msgstr[1] "%d твита" msgstr[2] "%d твитов" msgstr[3] "%d твит" #, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Читает %d профиль" msgstr[1] "Читает %d профилÑ" msgstr[2] "Читает %d профилей" msgstr[3] "Читает %d профилей" #, c-format msgid "Location: %s" msgstr "МеÑтоположение: %s" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, c-format msgid "%s timeline" msgstr "Лента %s" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, c-format msgid "%s followers" msgstr "%s подпиÑчиков" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, c-format msgid "%s following" msgstr "%s читает" msgid "Protected Profile" msgstr "Защищённый профиль" msgid "User is blocked" msgstr "Пользователь заблокирован" msgid "User is muted" msgstr "Пользователь заглушён" #, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "%d подпиÑчик" msgstr[1] "%d подпиÑчика" msgstr[2] "%d подпиÑчиков" msgstr[3] "%d подпиÑчиков" msgid "No tweets found" msgstr "Твиты не найдены" msgid "No users found" msgstr "Пользователи не найдены" #, c-format msgid "Users matching \"%s\"" msgstr "Пользователи, ÑоответÑтвующие \"%s\"" #, c-format msgid "Tweets matching \"%s\"" msgstr "Твиты, ÑоответÑтвующие \"%s\"" msgid "Search" msgstr "ПоиÑк" msgid "Load More" msgstr "Загрузить больше" msgid "Could not show tweet" msgstr "Ðе удалоÑÑŒ показать твит" msgid "This tweet is hidden by the author" msgstr "Этот твит Ñкрыт его автором" msgid "This tweet is unavailable" msgstr "Этот твит недоÑтупен" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "ПеревеÑти на РуÑÑкий" msgid "Retweets" msgstr "Ретвиты" msgid "Open in Browser" msgstr "Открыть в браузере" msgid "Source" msgstr "ИÑточник" msgid "Tweet Details" msgstr "ПодробноÑти твита" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d не прочитан)" msgstr[1] "(%d не прочитано)" msgstr[2] "(%d не прочитано)" msgstr[3] "(%d не прочитано)" msgid "Delete" msgstr "Удалить" #. TRANSLATORS: replacements are name and handle (without the "@") #, c-format msgid "Retweeted by %s (@%s)" msgstr "Ретвитнул %s (@%s)" msgid "Are you sure you want to delete this tweet?" msgstr "Ð’Ñ‹ дейÑтвительно хотите удалить Ñтот твит?" #, c-format msgid "Block %s" msgstr "Блокировать %s" msgid "This tweet contains images marked as inappropriate" msgstr "Медиафайлы могут ноÑить деликатный характер" msgid "Show anyway" msgstr "Ð’ÑÑ‘ равно показать" msgid "Could not authenticate you" msgstr "Ðе получаетÑÑ Ð²Ð¾Ð¹Ñ‚Ð¸" msgid "Sorry, that page does not exist" msgstr "ПроÑтите, Ñта Ñтраница не ÑущеÑтвует" msgid "User not found." msgstr "Пользователь не найден." msgid "User has been suspended." msgstr "Пользователь заблокирован." msgid "Your account is suspended and is not permitted to access this feature" msgstr "Ваш профиль заблокирован и не имеет доÑтупа к Ñтому" msgid "Rate limit exceeded" msgstr "Превышен предел ÑкороÑти" msgid "Invalid or expired token" msgstr "Ðеправильный или уÑтаревший токен" msgid "The specified user is not a subscriber of this list." msgstr "Выбранный пользователь не подпиÑан на Ñтот ÑпиÑок." msgid "The user you are trying to remove from the list is not a member." msgstr "" "Пользователь, которого вы пытаетеÑÑŒ убрать из ÑпиÑка, в нём не ÑоÑтоит." msgid "Account update failed: value is too long." msgstr "Обновление Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ Ð½ÐµÐ²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾: значение Ñлишком велико." msgid "Over capacity" msgstr "Превышение пропуÑкной ÑпоÑобноÑти" msgid "Internal error" msgstr "ВнутреннÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ°" msgid "You have already favorited this status." msgstr "Ð’Ñ‹ уже лайкнули Ñтот твит." msgid "No status found with that ID." msgstr "Твит Ñ Ñ‚Ð°ÐºÐ¸Ð¼ ID не найден." msgid "You cannot send messages to users who are not following you." msgstr "Ð’Ñ‹ не можете отправить Ñообщение человеку, который на Ð²Ð°Ñ Ð½Ðµ подпиÑан." msgid "There was an error sending your message." msgstr "При отправке ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð¾ÑˆÐ»Ð° ошибка." msgid "You've already requested to follow this user." msgstr "Ð’Ñ‹ уже запроÑили подпиÑатьÑÑ Ð½Ð° Ñтого пользователÑ." msgid "You are unable to follow more people at this time" msgstr "Ð¡ÐµÐ¹Ñ‡Ð°Ñ Ð²Ñ‹ не можете подпиÑатьÑÑ ÐµÑ‰Ñ‘ на кого-либо" msgid "Sorry, you are not authorized to see this status" msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ доÑтупа к Ñтому твиту" msgid "User is over daily status update limit" msgstr "Слишком много твитов за день" msgid "Tweet needs to be a bit shorter." msgstr "Твит должен быть немного короче." msgid "Status is a duplicate" msgstr "Это повтор твита" msgid "Owner must allow dms from anyone." msgstr "Пользователь должен разрешить пиÑать ему ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾Ñ‚ кого угодно." msgid "Bad authentication data" msgstr "Ðеправильные данные входа" msgid "Your credentials do not allow access to this resource." msgstr "Ваши Ð¿Ð¾Ð»Ð½Ð¾Ð¼Ð¾Ñ‡Ð¸Ñ Ð½ÐµÐ´Ð¾Ñтаточны Ð´Ð»Ñ Ð´Ð¾Ñтупа к Ñтому." msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" "Ваш Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¿Ð¾Ñ…Ð¾Ð¶ на автоматичеÑкий. Ð’ целÑÑ… защиты от Ñпама и других " "подозрительных дейÑтвий, вы не можем Ñовершить ваше дейÑтвие ÑейчаÑ." msgid "User must verify login" msgstr "Пользователь должен подтвердить логин" msgid "Application cannot perform write actions." msgstr "Приложение не может Ñовершать дейÑÑ‚Ð²Ð¸Ñ Ð·Ð°Ð¿Ð¸Ñи." msgid "You can’t mute yourself." msgstr "Ð’Ñ‹ не можете заглушить ÑебÑ." msgid "You are not muting the specified user." msgstr "Ð’Ñ‹ не заглушили Ñтого пользователÑ." msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "Ðнимированные GIF не разрешены при загрузке неÑкольких изображений." msgid "The validation of media ids failed." msgstr "Проверка ID медиафайлов не удалаÑÑŒ." msgid "A media id was not found." msgstr "ID медиафайла не найден." msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" "Ð’ целÑÑ… защиты от Ñпама и других подозрительных дейÑтвий, Ñтот профиль " "временно заблокирован." msgid "You have already retweeted this Tweet." msgstr "Ð’Ñ‹ уже ретвитнули Ñтот твит." msgid "You cannot send messages to this user." msgstr "Ð’Ñ‹ не можете напиÑать Ñтому профилю." msgid "The text of your direct message is over the max character limit." msgstr "ТекÑÑ‚ вашего ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñлишком большой." msgid "Subscription already exists." msgstr "ПодпиÑка уже оÑущеÑтвлена." msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "Ð’Ñ‹ пытаетеÑÑŒ ответить на твит, который удалён или недоÑтупен Ð´Ð»Ñ Ð²Ð°Ñ." msgid "The Tweet exceeds the number of allowed attachment types." msgstr "Твит превышает разрешённое количеÑтво типов вложений." msgid "The given URL is invalid." msgstr "Эта ÑÑылка нерабочаÑ." msgid "Invalid / suspended application" msgstr "Ðеправильное / приоÑтановленное приложение" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "Ðвтор твита ограничил возможноÑть ответа." msgid "Invalid media file" msgstr "Ðеправильный медиафайл" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "Во Ð²Ñ€ÐµÐ¼Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ произошла неизвеÑÑ‚Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° Ñ ÐºÐ¾Ð´Ð¾Ð¼ %lld: %s" #, c-format msgid "Unknown error code %lld during upload" msgstr "Во Ð²Ñ€ÐµÐ¼Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ произошла неизвеÑÑ‚Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° Ñ ÐºÐ¾Ð´Ð¾Ð¼ %lld" msgid "Now" msgstr "СейчаÑ" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "%d минуту назад" msgstr[1] "%d минуты назад" msgstr[2] "%d минут назад" msgstr[3] "%d минут назад" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dм" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "%d Ñ‡Ð°Ñ Ð½Ð°Ð·Ð°Ð´" msgstr[1] "%d чаÑа назад" msgstr[2] "%d чаÑов назад" msgstr[3] "%d чаÑов назад" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dч" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "%e %B" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "%e %b" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "%e %B %Y" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "%e %b %Y" #, fuzzy, c-format msgid "Replying to %s" msgstr "Отвечает на твит" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "и %d других" msgstr[1] "и %d других" msgstr[2] "и %d других" msgstr[3] "и %d других" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Отвечает на твит" msgid "Don't have a Twitter account yet?" msgstr "Ещё нет Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ Ð² Твиттере?" msgid "Create one" msgstr "Создать" #, c-format msgid "Could not open %s" msgstr "Ðе могу открыть %s" msgid "Failed to retrieve request token" msgstr "Ðе удалоÑÑŒ получить токен запроÑа" msgid "Wrong PIN" msgstr "Ðеверный PIN-код" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Профиль уже иÑпользуетÑÑ" msgid "Failed to retrieve access token" msgstr "Ðе удалоÑÑŒ получить токен" msgid "Select Image" msgstr "Выбрать изображение" msgid "Unfollow" msgstr "ОтпиÑатьÑÑ" msgid "Follow" msgstr "ПодпиÑатьÑÑ" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Пользователи не найдены" msgid "Retry" msgstr "Повтор" msgid "Copy URL" msgstr "Копировать URL" #, fuzzy msgid "Reload image" msgstr "Ðе удалоÑÑŒ загрузить изображение" msgid "Save as…" msgstr "Сохранить как…" msgid "Save Video" msgstr "Сохранить видео" msgid "Save Image" msgstr "Сохранить изображение" msgid "Save" msgstr "Сохранить" msgid "Select Banner Image" msgstr "Выберите изображение Ð´Ð»Ñ Ð·Ð°Ñтавки" msgid "Image does not meet minimum size requirements:" msgstr "Изображение не ÑоответÑтует минимальным размерам:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð°: %d пикÑель" msgstr[1] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð°: %d пикÑелÑ" msgstr[2] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð°: %d пикÑелей" msgstr[3] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð°: %d пикÑелей" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð²Ñ‹Ñота: %d пикÑель" msgstr[1] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð²Ñ‹Ñота: %d пикÑелÑ" msgstr[2] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð²Ñ‹Ñота: %d пикÑелей" msgstr[3] "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð²Ñ‹Ñота: %d пикÑелей" msgid "Pick" msgstr "Выбрать" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "Ðе удалоÑÑŒ загрузить изображение" msgstr[1] "Ðе удалоÑÑŒ загрузить %u изображениÑ" msgstr[2] "Ðе удалоÑÑŒ загрузить %u изображений" msgstr[3] "Ðе удалоÑÑŒ загрузить %u изображений" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "%s: %s" msgid "Back" msgstr "Ðазад" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Ðе удалоÑÑŒ загрузить цитируемый твит: %s\n" "\n" "Сохранить черновик?" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" "Ðе удалоÑÑŒ загрузить отвечаемый твит: %s\n" "\n" "Сохранить черновик?" msgid "Quote tweet" msgstr "Цитировать твит" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "Можно прикрепить только один анимированный GIF." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "%d знак оÑталÑÑ" msgstr[1] "%d знака оÑталоÑÑŒ" msgstr[2] "%d знаков оÑталÑÑŒ" msgstr[3] "%d знаков оÑталоÑÑŒ" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "Изображение %d из %d" msgstr[1] "Изображение %d из %d" msgstr[2] "Изображение %d из %d" msgstr[3] "Изображение %d из %d" msgid "Modify Filter" msgstr "Изменить фильтр" msgid "Matches" msgstr "СовпадениÑ" msgid "Doesn't match" msgstr "ÐеÑовпадениÑ" msgid "Modify Snippet" msgstr "Изменить Ñниппет" msgid "Snippet can't be empty" msgstr "Сниппет не может быть пуÑтым" msgid "Replacement can't be empty" msgstr "Замена не может быть пуÑтой" msgid "Snippet may not contain whitespace" msgstr "Сниппет не может Ñодержать пробелы" msgid "Snippet already exists" msgstr "Сниппет уже ÑущеÑтвует" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Привет, поÑвилаÑÑŒ Ð½Ð¾Ð²Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ #Cawbird! \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Добавить или удалить Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð· ÑпиÑка" msgid "You have no lists." msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ ÑпиÑков." msgid "About Cawbird" msgstr "О программе Cawbird" msgid "New Account" msgstr "Ðовый профиль" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Чтобы войти в Cawbird, вам нужно получить PIN-код Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ð° Ñ twitter.com " "Ð´Ð»Ñ Ð½ÑƒÐ¶Ð½Ð¾Ð³Ð¾ профилÑ" msgid "Request PIN" msgstr "ЗапроÑить PIN-код" msgid "Enter PIN from twitter.com below:" msgstr "Введите PIN-код Ñ twitter.com ниже:" msgid "PIN" msgstr "PIN-код" msgid "Confirm" msgstr "Подтвердить" msgid "Account Settings" msgstr "ÐаÑтройки профилÑ" msgid "Name" msgstr "ИмÑ" msgid "Website" msgstr "Сайт" msgid "Autostart" msgstr "ÐвтозапуÑк" msgid "Remove Account" msgstr "Убрать профиль" msgid "Do you really want to remove this account?" msgstr "Ð’Ñ‹ дейÑтвительно хотите убрать Ñтот профиль?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Улыбки и люди" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Тело и одежда" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Животные и природа" msgctxt "emoji category" msgid "Food & Drink" msgstr "Еда и напитки" msgctxt "emoji category" msgid "Travel & Places" msgstr "ПутешеÑÑ‚Ð²Ð¸Ñ Ð¸ меÑта" msgctxt "emoji category" msgid "Activities" msgstr "ЗанÑтиÑ" msgctxt "emoji category" msgid "Objects" msgstr "Вещи" msgctxt "emoji category" msgid "Symbols" msgstr "Знаки" msgctxt "emoji category" msgid "Flags" msgstr "Флаги" msgid "No Results Found" msgstr "Ðе найдено" msgid "Try a different search" msgstr "Попробуйте поиÑкать что-то другое" msgid "Send" msgstr "Отправить" msgid "Add Media" msgstr "Добавить медиа" msgid "Show favorite images" msgstr "Показать избранные изображениÑ" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "Отфильтрованные темы" msgid "Filtered users" msgstr "Отфильтрованные пользователи" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Пользователи" msgid "Set image description" msgstr "Добавить опиÑание изображениÑ" msgid "Subscribe" msgstr "ПодпиÑатьÑÑ" msgid "Unsubscribe" msgstr "ОтпиÑатьÑÑ" msgid "Subscribers:" msgstr "ПодпиÑчики:" msgid "Members:" msgstr "УчаÑтники:" msgid "Creator:" msgstr "Создатель:" msgid "Created at:" msgstr "Создан:" msgid "Edit" msgstr "Изменить" msgid "Mode:" msgstr "Режим:" msgid "Description" msgstr "ОпиÑание" msgid "Settings" msgstr "ÐаÑтройки" msgid "Shortcuts" msgstr "ГорÑчие клавиши" msgid "About" msgstr "О программе" msgid "Quit" msgstr "Выход" msgid "Add New Filter" msgstr "Добавить новый фильтр" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Добавить новый Ñниппет" msgid "Keyword" msgstr "Ключевое Ñлово" msgid "Replacement" msgstr "Замена" msgid "Create New List" msgstr "Создать новый ÑпиÑок" msgid "Name:" msgstr "ИмÑ:" msgid "Create" msgstr "Создать" msgid "Write Direct Message" msgstr "ÐапиÑать личное Ñообщение" msgid "Add to/Remove from List" msgstr "Добавить / Удалить из ÑпиÑка" msgid "Blocked" msgstr "Заблокирован" msgid "Muted" msgstr "Беззвучно" msgid "Retweets disabled" msgstr "Ретвиты отключены" msgid "More actions" msgstr "Ещё дейÑтвиÑ" msgid "Follows you" msgstr "ПодпиÑаны на тебÑ" msgid "Tweets" msgstr "Твиты" msgid "Followers" msgstr "Избранные" msgid "Following" msgstr "ПодпиÑан" msgid "Use dark theme" msgstr "ИÑпользовать тёмную тему" #, fuzzy msgid "Shortcut key" msgstr "ГорÑчие клавиши" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Лента %s" msgid "Show inline media" msgstr "Показать вÑтроенные медиа" msgid "Always show" msgstr "Ð’Ñегда показывать" msgid "Always hide" msgstr "Ð’Ñегда Ñкрывать" msgid "Hide in timeline" msgstr "Скрыть в ленте" msgid "Auto scroll on new tweets" msgstr "Ðвтопрокрутка к новым твитам" msgid "Double-click activation" msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ Ð¿Ð¾ двойному клику" msgid "Notifications" msgstr "УведомлениÑ" msgid "On New Tweets" msgstr "К новым твитам" msgid "Never" msgstr "Ðикогда" msgid "Every" msgstr "Каждый" msgid "Stack 5" msgstr "Стек 5" msgid "Stack 10" msgstr "Стек 10" msgid "Stack 25" msgstr "Стек 25" msgid "Stack 50" msgstr "Стек 50" msgid "On New Mentions" msgstr "К новым упоминаниÑм" msgid "On New Messages" msgstr "К новым ÑообщениÑм" msgid "Interface" msgstr "ИнтерфейÑ" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "Ðормальный" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "Большой" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "Очень большой" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "Огромный" msgid "Tweet scale" msgstr "Размер твита" msgid "Round avatars" msgstr "Круглые аватары" msgid "Remove trailing hashtags" msgstr "Убрать Ñ…Ñштеги в конце" msgid "Remove media links" msgstr "Убрать ÑÑылки на медиа" msgid "Hide inappropriate content" msgstr "СпрÑтать неумеÑтное" msgid "Translation" msgstr "Перевод" msgid "Translation service" msgstr "Служба переводов" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "ÐаÑтроить ÑÑылку Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ð¾Ð´Ð¾Ð²" msgid "No snippets configured." msgstr "ÐаÑтроенных Ñниппетов нет." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Ð’Ñ‹ можете отметить и перемеÑтить Ñлово в текÑте, нажав клавишу TAB" msgid "Snippets" msgstr "Сниппеты" msgctxt "shortcuts window" msgid "General" msgstr "Общее" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "ÐапиÑать твит" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Показать наÑтройки профилÑ" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Показать вÑплывающее окно профилей" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Показать наÑтройки приложениÑ" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Переключить панель" msgctxt "shortcuts window" msgid "Go Back" msgstr "Ðазад" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Вперёд" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Перейти на Ñтраницу nth" msgctxt "shortcuts window" msgid "Tweets" msgstr "Твиты" msgctxt "shortcuts window" msgid "Retweet" msgstr "Ретвит" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Избранные" msgctxt "shortcuts window" msgid "Reply" msgstr "Ответить" msgctxt "shortcuts window" msgid "Quote" msgstr "Цитировать" msgctxt "shortcuts window" msgid "Show Details" msgstr "Показать подробноÑти" msgctxt "shortcuts window" msgid "Delete" msgstr "Удалить" msgctxt "shortcuts window" msgid "Compose" msgstr "ÐапиÑать" msgid "Show Emoji Chooser" msgstr "Выбрать Ñмодзи" msgid "Start new conversation" msgstr "Ðачать новую беÑеду" msgid "With:" msgstr "С:" msgid "Go" msgstr "Перейти" msgid "Quote" msgstr "Цитата" msgid "Retweet tweet" msgstr "Ретвитнуть" #, fuzzy msgid "Like tweet" msgstr "Ð’ избранное" msgid "Reply to tweet" msgstr "Ответить" msgid "More" msgstr "Ещё" msgid "Translate" msgstr "ПеревеÑти" #, fuzzy msgid "Like" msgstr "Ð’ избранное" msgid "Reply" msgstr "Ответить" msgid "Liked" msgstr "Избранное" msgid "Retweeted" msgstr "Ретвитнуто" msgid "Unblock" msgstr "Разблокировать" msgid "Show settings of this account" msgstr "Показать наÑтройки профилÑ" msgid "Open in new window" msgstr "Открыть в новом окне" msgid "Go to profile" msgstr "Перейти к профилю" msgid "Created" msgstr "Создан" msgid "Subscribed to" msgstr "ПодпиÑан" #~ msgid "Replying to" #~ msgstr "Отвечает на твит" #~ msgid "and" #~ msgstr "и" cawbird-1.4.2/po/sr.po000066400000000000000000000717321416632607600145600ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Марко М. КоÑтић (Marko M. Kostić) , 2016-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-11-19 08:34+0000\n" "Last-Translator: Марко М. КоÑтић (Marko M. Kostić) \n" "Language-Team: Serbian (http://www.transifex.com/cawbird/cawbird/language/" "sr/)\n" "Language: sr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" msgid "Cawbird" msgstr "Корберд" msgid "Twitter Client" msgstr "Твитер клијент" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Корберд је домаћи ГТК+ твитер клијент који пружа оÑновне могућноÑти као што " "Ñу директне поруке, обавештења о твитовима и прегледе разговора." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Додатне могућноÑти Ñадрже локално прегледање видео запиÑа, вишеÑтруке " "угнежђене Ñлике, ÑпиÑкове, филтере, вишеÑтруке налоге и тако даље." msgid "Generic timeline view when using Cawbird" msgstr "Општи преглед временÑке линије када Ñе кориÑти Корберд" msgid "Typical Twitter profile" msgstr "Уобичајени Твитер профил" msgid "Account settings can be configured" msgstr "Подешавања налога Ñе могу мењати" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "КориÑтите Твитер помоћу обичног рачунарÑког програма" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;tviter;твитер;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Прикажи подешене налоге" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Директне поруке" #, fuzzy msgid "Select Media" msgstr "Изабери Ñлику" msgid "Open" msgstr "Отвори" msgid "Cancel" msgstr "Откажи" #, fuzzy msgid "Selected file is not an image or video." msgstr "Изабрана датотека није Ñлика." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Изабрана Ñлика је Ñувише велика. Ðајвећа дозвољена величина датотеке по " "Ñлици је %'d мегабајта" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Изабрана Ñлика је Ñувише велика. Ðајвећа дозвољена величина датотеке по " "Ñлици је %'d мегабајта" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Изабрана Ñлика је Ñувише велика. Ðајвећа дозвољена величина датотеке по " "Ñлици је %'d мегабајта" msgid "Insert Emoji" msgstr "Убаци емоџи" msgid "Direct Conversation" msgstr "Директни разговори" #, fuzzy msgid "Direct message threads" msgstr "Директне поруке" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d нова порука од %s" msgstr[1] "%d нове поруке од %s" msgstr[2] "%d нових порука од %s" #, c-format msgid "New direct message from %s" msgstr "Ðова директна порука од %s" msgid "Direct Messages" msgstr "Директне поруке" #, fuzzy msgid "Liked tweets timeline" msgstr "Означи твит омиљеним" #, fuzzy msgid "Likes" msgstr "Омиљено" msgid "Add new Filter" msgstr "Додај нови филтер" msgid "Filters" msgstr "Филтери" #, fuzzy msgid "Home timeline" msgstr "Сакриј Ñа временÑке линије" #, c-format msgid "%s retweeted %s" msgstr "%s је поново твитовао/ла %s" #, c-format msgid "%s tweeted" msgstr "%s је твитовао/ла" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d нови твит!" msgstr[1] "%d нова твита!" msgstr[2] "%d нових твитова!" msgid "Home" msgstr "Почетна" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s је твитовао/ла" msgid "Private" msgstr "Приватно" msgid "Public" msgstr "Јавно" msgid "Lists" msgstr "СпиÑкови" msgid "Show configured accounts" msgstr "Прикажи подешене налоге" msgid "Compose Tweet" msgstr "СаÑтави твит" msgid "Add new Account" msgstr "Додај нови налог" #, fuzzy msgid "Mentions timeline" msgstr "Сакриј Ñа временÑке линије" #, c-format msgid "%s mentioned %s" msgstr "%s Ñпомену %s" msgid "Mentions" msgstr "Спомињања" msgid "Suspended Account" msgstr "ОбуÑтављени налог" msgid "Protected profile" msgstr "Заштићен профил" #, c-format msgid "Tweet to @%s" msgstr "Твитуј ка @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s је твитовао/ла" msgstr[1] "%s је твитовао/ла" msgstr[2] "%s је твитовао/ла" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Прати" msgstr[1] "Прати" msgstr[2] "Прати" #, fuzzy, c-format msgid "Location: %s" msgstr "Обавештења" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Сакриј Ñа временÑке линије" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Пратиоци" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Прати" msgid "Protected Profile" msgstr "Заштићени профил" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Пратиоци" msgstr[1] "Пратиоци" msgstr[2] "Пратиоци" #, fuzzy msgid "No tweets found" msgstr "Ðема пронађених уноÑа" msgid "No users found" msgstr "Ðема пронађених кориÑника" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Твитуј ка @%s" msgid "Search" msgstr "Претрага" msgid "Load More" msgstr "Учитај још" msgid "Could not show tweet" msgstr "Ðе могу да прикажем твит" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "поновних твитова" msgid "Open in Browser" msgstr "Отвори у прегледачу" msgid "Source" msgstr "извор" msgid "Tweet Details" msgstr "појединоÑти твита" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d непрочитана)" msgstr[1] "(%d непрочитане)" msgstr[2] "(%d непрочитаних)" msgid "Delete" msgstr "Обриши" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Поново твитуј" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Да ли Ñтварно желите обриÑати овај налог?" #, c-format msgid "Block %s" msgstr "Блокирај %s" msgid "This tweet contains images marked as inappropriate" msgstr "Означено је да овај твит Ñадржи недоличне Ñлике" msgid "Show anyway" msgstr "Ипак прикажи" #, fuzzy msgid "Could not authenticate you" msgstr "Ðе могу да отворим %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Ðема пронађених кориÑника" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "ИнтерфејÑ" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "ИÑечак већ поÑтоји" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Ñада" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dм" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dч" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Одговор за %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Одговор за %s и %d других" msgstr[1] "Одговор за %s и %d других" msgstr[2] "Одговор за %s и %d других" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Одговор за %s и %s" msgid "Don't have a Twitter account yet?" msgstr "Још немате Твитер налог?" msgid "Create one" msgstr "Ðаправи налог" #, c-format msgid "Could not open %s" msgstr "Ðе могу да отворим %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Погрешан ПИÐ" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Ðалог Ñе већ кориÑти" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Изабери Ñлику" msgid "Unfollow" msgstr "ПреÑтани Ñа праћењем" msgid "Follow" msgstr "Прати" msgid "Loading…" msgstr "Учитавам…" #, fuzzy msgid "No entries found" msgstr "Ðема пронађених кориÑника" msgid "Retry" msgstr "Поново покушај" msgid "Copy URL" msgstr "Копирај УРЛ" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "Сачувај као…" msgid "Save Video" msgstr "Сачувај видео" msgid "Save Image" msgstr "Сачувај Ñлику" msgid "Save" msgstr "Сачувај" msgid "Select Banner Image" msgstr "Изаберите Ñлику барјака" msgid "Image does not meet minimum size requirements:" msgstr "Слика не иÑпуњава минималне захтеве о величини:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Минимална ширина: %d пикÑел" msgstr[1] "Минимална ширина: %d пикÑела" msgstr[2] "Минимална ширина: %d пикÑела" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Минимална виÑина: %d пикÑел" msgstr[1] "Минимална виÑина: %d пикÑела" msgstr[2] "Минимална виÑина: %d пикÑела" msgid "Pick" msgstr "Изабери" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Ðазад" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Цитирај твит" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Дозвољен је Ñамо један ГИФ по твиту." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Modify Filter" msgstr "Измени филтер" msgid "Matches" msgstr "подудара Ñе" msgid "Doesn't match" msgstr "Ðе поклапа Ñе" msgid "Modify Snippet" msgstr "Измени иÑечак" msgid "Snippet can't be empty" msgstr "ИÑечак не може бити празан" msgid "Replacement can't be empty" msgstr "Замена не може бити празна" msgid "Snippet may not contain whitespace" msgstr "ИÑечак не може Ñадржати размак" msgid "Snippet already exists" msgstr "ИÑечак већ поÑтоји" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Хеј, баци поглед на ново #Cawbird издање! \\ (•◡•) / #cool #новојеувекбоље" msgid "Add to or Remove User From List" msgstr "Додај или уклони кориÑника Ñа ÑпиÑка" msgid "You have no lists." msgstr "Ðемате ниједан ÑпиÑак." msgid "About Cawbird" msgstr "О Корберду" msgid "New Account" msgstr "Ðови налог" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Да биÑте овлаÑтили Корберд, морате унети ПИРÑа twitter.com преко налога " "којег желите додати" msgid "Request PIN" msgstr "Затражи ПИÐ" msgid "Enter PIN from twitter.com below:" msgstr "УнеÑите ПИРÑа twitter.com иÑпод:" msgid "PIN" msgstr "ПИÐ" msgid "Confirm" msgstr "Потврди" msgid "Account Settings" msgstr "Подешавања налога" msgid "Name" msgstr "Име" msgid "Website" msgstr "Веб Ñајт" msgid "Autostart" msgstr "СамоÑтално покрени" #, fuzzy msgid "Remove Account" msgstr "Обриши" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Да ли Ñтварно желите обриÑати овај налог?" msgctxt "emoji category" msgid "Smileys & People" msgstr "Смешци и људи" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Тело и одећа" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Животиње и природа" msgctxt "emoji category" msgid "Food & Drink" msgstr "Храна и пиће" msgctxt "emoji category" msgid "Travel & Places" msgstr "Путовање и меÑта" msgctxt "emoji category" msgid "Activities" msgstr "ÐктивноÑти" msgctxt "emoji category" msgid "Objects" msgstr "Ствари" msgctxt "emoji category" msgid "Symbols" msgstr "Знакови" msgctxt "emoji category" msgid "Flags" msgstr "ЗаÑтаве" msgid "No Results Found" msgstr "Ðема резултата" msgid "Try a different search" msgstr "Пробајте Ñа другачијом претрагом" msgid "Send" msgstr "Пошаљи" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Прикажи омиљене Ñлике" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Филтери" #, fuzzy msgid "Filtered users" msgstr "Филтери" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "КориÑници" #, fuzzy msgid "Set image description" msgstr "ОпиÑ" msgid "Subscribe" msgstr "Претплати Ñе" msgid "Unsubscribe" msgstr "Укини претплату" msgid "Subscribers:" msgstr "Претплатници:" msgid "Members:" msgstr "Чланови:" msgid "Creator:" msgstr "Творац:" msgid "Created at:" msgstr "Ðаправљено у:" msgid "Edit" msgstr "Уреди" msgid "Mode:" msgstr "Режим:" msgid "Description" msgstr "ОпиÑ" msgid "Settings" msgstr "Подешавања" msgid "Shortcuts" msgstr "Пречице" msgid "About" msgstr "О програму" msgid "Quit" msgstr "Изађи" msgid "Add New Filter" msgstr "Додај нови филтер" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Додај нови иÑечак" msgid "Keyword" msgstr "Кључна реч" msgid "Replacement" msgstr "Замена" msgid "Create New List" msgstr "Ðаправи нови ÑпиÑак" msgid "Name:" msgstr "Име:" msgid "Create" msgstr "Ðаправи" msgid "Write Direct Message" msgstr "Ðапиши директну поруку" msgid "Add to/Remove from List" msgstr "Додај или уклони Ñа ÑпиÑка" msgid "Blocked" msgstr "Блокиран(а)" msgid "Muted" msgstr "Пригушен(а)" msgid "Retweets disabled" msgstr "Поновни твитови онемогућени" #, fuzzy msgid "More actions" msgstr "Спомињања" msgid "Follows you" msgstr "Прати ваÑ" msgid "Tweets" msgstr "Твитови" msgid "Followers" msgstr "Пратиоци" msgid "Following" msgstr "Прати" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "Пречице" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Сакриј Ñа временÑке линије" msgid "Show inline media" msgstr "Прикажи угнежђени Ñадржај" msgid "Always show" msgstr "Увек прикажи" msgid "Always hide" msgstr "Увек Ñкривај" msgid "Hide in timeline" msgstr "Сакриј Ñа временÑке линије" msgid "Auto scroll on new tweets" msgstr "ÐутоматÑки клизај при новим твитовима" msgid "Double-click activation" msgstr "Ðктивирај на дупли клик" msgid "Notifications" msgstr "Обавештења" msgid "On New Tweets" msgstr "При новим твитовима" msgid "Never" msgstr "Ðикада" msgid "Every" msgstr "Сваки пут" msgid "Stack 5" msgstr "Сакупи 5" msgid "Stack 10" msgstr "Сакупи 10" msgid "Stack 25" msgstr "Сакуп 25" msgid "Stack 50" msgstr "Сакупи 50" msgid "On New Mentions" msgstr "При новим Ñпомињањима" msgid "On New Messages" msgstr "При новим порукама" msgid "Interface" msgstr "ИнтерфејÑ" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Твитови" msgid "Round avatars" msgstr "Округли аватари" msgid "Remove trailing hashtags" msgstr "Уклони пратеће тарабице" msgid "Remove media links" msgstr "Уклони мултимедијалне везе" msgid "Hide inappropriate content" msgstr "Сакриј неприкладни Ñадржај" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Ðема подешених иÑечака." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Увек можете активирати иÑечке пиÑањем кључне речи и притиÑком таÑтера TAB." msgid "Snippets" msgstr "ИÑечци" msgctxt "shortcuts window" msgid "General" msgstr "Опште" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "СаÑтави твит" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Прикажи подешавања налога" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Прикажи иÑкачући прозор налога" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Прикажи подешавања програма" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Укључи или иÑкључи горњу траку" msgctxt "shortcuts window" msgid "Go Back" msgstr "Иди назад" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Иди напред" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Иди на одређену Ñтрану" msgctxt "shortcuts window" msgid "Tweets" msgstr "Твитови" msgctxt "shortcuts window" msgid "Retweet" msgstr "Поново твитуј" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Означи као омиљено" msgctxt "shortcuts window" msgid "Reply" msgstr "Одговори" msgctxt "shortcuts window" msgid "Quote" msgstr "Цитирај" msgctxt "shortcuts window" msgid "Show Details" msgstr "Прикажи појединоÑти" msgctxt "shortcuts window" msgid "Delete" msgstr "Обриши" msgctxt "shortcuts window" msgid "Compose" msgstr "СаÑтави" msgid "Show Emoji Chooser" msgstr "Прикажи бирача емоџија" msgid "Start new conversation" msgstr "Започни нови разговор" msgid "With:" msgstr "Са:" msgid "Go" msgstr "Иди" msgid "Quote" msgstr "Цитирај" msgid "Retweet tweet" msgstr "Поново твитуј твит" #, fuzzy msgid "Like tweet" msgstr "Означи твит омиљеним" msgid "Reply to tweet" msgstr "Одговори на твит" msgid "More" msgstr "Више" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Стави као омиљено" msgid "Reply" msgstr "Одговори" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Поново твитуј" msgid "Unblock" msgstr "Деблокирај" msgid "Show settings of this account" msgstr "Прикажи подешавања овог налога" msgid "Open in new window" msgstr "Отвори у новом прозору" msgid "Go to profile" msgstr "Иди на профил" msgid "Created" msgstr "Ðаправи" msgid "Subscribed to" msgstr "Претплаћен(а) на" #~ msgid "Replying to" #~ msgstr "Одговор за" #~ msgid "and" #~ msgstr "и" #~ msgid "Actions" #~ msgstr "Радње" #~ msgid "Add Image" #~ msgstr "Додај Ñлику" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Пратиоци" #~ msgid "List" #~ msgstr "СпиÑак" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Ðеовлашћено. У већини Ñлучајева ово значи да нешто није у реду Ñа " #~ "Твитеровим Ñерверима и да требате покушати каÑније" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Ðе могу да учитам твитове" cawbird-1.4.2/po/sr_BA@latin.po000066400000000000000000000621121416632607600162420ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-11-20 17:05+0000\n" "Last-Translator: Slobodan Terzić \n" "Language-Team: Serbian (Latin) (http://www.transifex.com/cawbird/cawbird/" "language/sr%40latin/)\n" "Language: sr@latin\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter klijent" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird je domaći GTK+ tviter klijent koji pruža osnovne mogućnosti kao Å¡to " "su direktne poruke, obaveÅ¡tenja o tvitovima i preglede razgovora." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Dodatne mogućnosti sadrže lokalno pregledanje video zapisa, viÅ¡estruke " "ugnežđene slike, spiskove, filtere, viÅ¡estruke naloge i tako dalje." msgid "Generic timeline view when using Cawbird" msgstr "OpÅ¡ti pregled vremenske linije kada se koristi Cawbird" msgid "Typical Twitter profile" msgstr "UobiÄajeni Twitter profil" msgid "Account settings can be configured" msgstr "PodeÅ¡avanja naloga se mogu menjati" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "Koristite Twitter pomoću obiÄnog raÄunarskog programa" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;tviter;tviter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Prikaži podeÅ¡ene naloge" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Direktne poruke" #, fuzzy msgid "Select Media" msgstr "Izaberi sliku" msgid "Open" msgstr "Otvori" msgid "Cancel" msgstr "Otkaži" #, fuzzy msgid "Selected file is not an image or video." msgstr "Izabrana datoteka nije slika." #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" "Izabrana slika je suviÅ¡e velika. Najveća dozvoljena veliÄina datoteke po " "slici je %'d megabajta" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" "Izabrana slika je suviÅ¡e velika. Najveća dozvoljena veliÄina datoteke po " "slici je %'d megabajta" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" "Izabrana slika je suviÅ¡e velika. Najveća dozvoljena veliÄina datoteke po " "slici je %'d megabajta" msgid "Insert Emoji" msgstr "Ubaci emodži" msgid "Direct Conversation" msgstr "Direktni razgovori" #, fuzzy msgid "Direct message threads" msgstr "Direktne poruke" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d nova poruka od %s" msgstr[1] "%d nove poruke od %s" msgstr[2] "%d novih poruka od %s" #, c-format msgid "New direct message from %s" msgstr "Nova direktna poruka od %s" msgid "Direct Messages" msgstr "Direktne poruke" #, fuzzy msgid "Liked tweets timeline" msgstr "OznaÄi tvit omiljenim" #, fuzzy msgid "Likes" msgstr "Omiljeno" msgid "Add new Filter" msgstr "Dodaj novi filter" msgid "Filters" msgstr "Filteri" #, fuzzy msgid "Home timeline" msgstr "Sakrij sa vremenske linije" #, c-format msgid "%s retweeted %s" msgstr "%s je ponovo tvitovao/la %s" #, c-format msgid "%s tweeted" msgstr "%s je tvitovao/la" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d novi tvit!" msgstr[1] "%d nova tvita!" msgstr[2] "%d novih tvitova!" msgid "Home" msgstr "PoÄetna" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s je tvitovao/la" msgid "Private" msgstr "Privatno" msgid "Public" msgstr "Javno" msgid "Lists" msgstr "Spiskovi" msgid "Show configured accounts" msgstr "Prikaži podeÅ¡ene naloge" msgid "Compose Tweet" msgstr "Sastavi tvit" msgid "Add new Account" msgstr "Dodaj novi nalog" #, fuzzy msgid "Mentions timeline" msgstr "Sakrij sa vremenske linije" #, c-format msgid "%s mentioned %s" msgstr "%s spomenu %s" msgid "Mentions" msgstr "Spominjanja" msgid "Suspended Account" msgstr "Obustavljeni nalog" msgid "Protected profile" msgstr "ZaÅ¡tićen profil" #, c-format msgid "Tweet to @%s" msgstr "Tvituj ka @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s je tvitovao/la" msgstr[1] "%s je tvitovao/la" msgstr[2] "%s je tvitovao/la" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Prati" msgstr[1] "Prati" msgstr[2] "Prati" #, fuzzy, c-format msgid "Location: %s" msgstr "ObaveÅ¡tenja" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Sakrij sa vremenske linije" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Pratioci" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Prati" msgid "Protected Profile" msgstr "ZaÅ¡tićeni profil" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Pratioci" msgstr[1] "Pratioci" msgstr[2] "Pratioci" #, fuzzy msgid "No tweets found" msgstr "Nema pronaÄ‘enih unosa" msgid "No users found" msgstr "Nema pronaÄ‘enih korisnika" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Tvituj ka @%s" msgid "Search" msgstr "Pretraga" msgid "Load More" msgstr "UÄitaj joÅ¡" msgid "Could not show tweet" msgstr "Ne mogu da prikažem tvit" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "ponovnih tvitova" msgid "Open in Browser" msgstr "Otvori u pregledaÄu" msgid "Source" msgstr "izvor" msgid "Tweet Details" msgstr "pojedinosti tvita" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d neproÄitana)" msgstr[1] "(%d neproÄitane)" msgstr[2] "(%d neproÄitanih)" msgid "Delete" msgstr "ObriÅ¡i" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Ponovo tvituj" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Da li stvarno želite obrisati ovaj nalog?" #, c-format msgid "Block %s" msgstr "Blokiraj %s" msgid "This tweet contains images marked as inappropriate" msgstr "OznaÄeno je da ovaj tvit sadrži nedoliÄne slike" msgid "Show anyway" msgstr "Ipak prikaži" #, fuzzy msgid "Could not authenticate you" msgstr "Ne mogu da otvorim %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "Nema pronaÄ‘enih korisnika" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Interfejs" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "IseÄak već postoji" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "sada" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dÄ" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Odgovor za %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "Odgovor za %s i %d drugih" msgstr[1] "Odgovor za %s i %d drugih" msgstr[2] "Odgovor za %s i %d drugih" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "Odgovor za %s i %s" msgid "Don't have a Twitter account yet?" msgstr "JoÅ¡ nemate Twitter nalog?" msgid "Create one" msgstr "Napravi nalog" #, c-format msgid "Could not open %s" msgstr "Ne mogu da otvorim %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PogreÅ¡an PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Nalog se već koristi" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "Izaberi sliku" msgid "Unfollow" msgstr "Prestani sa praćenjem" msgid "Follow" msgstr "Prati" msgid "Loading…" msgstr "UÄitavam…" #, fuzzy msgid "No entries found" msgstr "Nema pronaÄ‘enih korisnika" msgid "Retry" msgstr "Ponovo pokuÅ¡aj" msgid "Copy URL" msgstr "Kopiraj URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "SaÄuvaj kao…" msgid "Save Video" msgstr "SaÄuvaj video" msgid "Save Image" msgstr "SaÄuvaj sliku" msgid "Save" msgstr "SaÄuvaj" msgid "Select Banner Image" msgstr "Izaberite sliku barjaka" msgid "Image does not meet minimum size requirements:" msgstr "Slika ne ispunjava minimalne zahteve o veliÄini:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "Minimalna Å¡irina: %d piksel" msgstr[1] "Minimalna Å¡irina: %d piksela" msgstr[2] "Minimalna Å¡irina: %d piksela" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "Minimalna visina: %d piksel" msgstr[1] "Minimalna visina: %d piksela" msgstr[2] "Minimalna visina: %d piksela" msgid "Pick" msgstr "Izaberi" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Nazad" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Citiraj tvit" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "Dozvoljen je samo jedan GIF po tvitu." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Modify Filter" msgstr "Izmeni filter" msgid "Matches" msgstr "podudara se" msgid "Doesn't match" msgstr "Ne poklapa se" msgid "Modify Snippet" msgstr "Izmeni iseÄak" msgid "Snippet can't be empty" msgstr "IseÄak ne može biti prazan" msgid "Replacement can't be empty" msgstr "Zamena ne može biti prazna" msgid "Snippet may not contain whitespace" msgstr "IseÄak ne može sadržati razmak" msgid "Snippet already exists" msgstr "IseÄak već postoji" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Hej, baci pogled na novo #Cawbird izdanje! \\ (•◡•) / #cool #novojeuvekbolje" msgid "Add to or Remove User From List" msgstr "Dodaj ili ukloni korisnika sa spiska" msgid "You have no lists." msgstr "Nemate nijedan spisak." msgid "About Cawbird" msgstr "O Cawbirdu" msgid "New Account" msgstr "Novi nalog" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" "Da biste ovlastili Cawbird, morate uneti PIN sa twitter.com preko naloga " "kojeg želite dodati" msgid "Request PIN" msgstr "Zatraži PIN" msgid "Enter PIN from twitter.com below:" msgstr "Unesite PIN sa twitter.com ispod:" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "Potvrdi" msgid "Account Settings" msgstr "PodeÅ¡avanja naloga" msgid "Name" msgstr "Ime" msgid "Website" msgstr "Veb sajt" msgid "Autostart" msgstr "Samostalno pokreni" #, fuzzy msgid "Remove Account" msgstr "ObriÅ¡i" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Da li stvarno želite obrisati ovaj nalog?" msgctxt "emoji category" msgid "Smileys & People" msgstr "SmeÅ¡ci i ljudi" msgctxt "emoji category" msgid "Body & Clothing" msgstr "Telo i odeća" msgctxt "emoji category" msgid "Animals & Nature" msgstr "Životinje i priroda" msgctxt "emoji category" msgid "Food & Drink" msgstr "Hrana i piće" msgctxt "emoji category" msgid "Travel & Places" msgstr "Putovanje i mesta" msgctxt "emoji category" msgid "Activities" msgstr "Aktivnosti" msgctxt "emoji category" msgid "Objects" msgstr "Stvari" msgctxt "emoji category" msgid "Symbols" msgstr "Znakovi" msgctxt "emoji category" msgid "Flags" msgstr "Zastave" msgid "No Results Found" msgstr "Nema rezultata" msgid "Try a different search" msgstr "Probajte sa drugaÄijom pretragom" msgid "Send" msgstr "PoÅ¡alji" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "Prikaži omiljene slike" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "Filteri" #, fuzzy msgid "Filtered users" msgstr "Filteri" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Korisnici" #, fuzzy msgid "Set image description" msgstr "Opis" msgid "Subscribe" msgstr "Pretplati se" msgid "Unsubscribe" msgstr "Ukini pretplatu" msgid "Subscribers:" msgstr "Pretplatnici:" msgid "Members:" msgstr "ÄŒlanovi:" msgid "Creator:" msgstr "Tvorac:" msgid "Created at:" msgstr "Napravljeno u:" msgid "Edit" msgstr "Uredi" msgid "Mode:" msgstr "Režim:" msgid "Description" msgstr "Opis" msgid "Settings" msgstr "PodeÅ¡avanja" msgid "Shortcuts" msgstr "PreÄice" msgid "About" msgstr "O programu" msgid "Quit" msgstr "IzaÄ‘i" msgid "Add New Filter" msgstr "Dodaj novi filter" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Dodaj novi iseÄak" msgid "Keyword" msgstr "KljuÄna reÄ" msgid "Replacement" msgstr "Zamena" msgid "Create New List" msgstr "Napravi novi spisak" msgid "Name:" msgstr "Ime:" msgid "Create" msgstr "Napravi" msgid "Write Direct Message" msgstr "NapiÅ¡i direktnu poruku" msgid "Add to/Remove from List" msgstr "Dodaj ili ukloni sa spiska" msgid "Blocked" msgstr "Blokiran(a)" msgid "Muted" msgstr "PriguÅ¡en(a)" msgid "Retweets disabled" msgstr "Ponovni tvitovi onemogućeni" #, fuzzy msgid "More actions" msgstr "Spominjanja" msgid "Follows you" msgstr "Prati vas" msgid "Tweets" msgstr "Tvitovi" msgid "Followers" msgstr "Pratioci" msgid "Following" msgstr "Prati" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "PreÄice" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Sakrij sa vremenske linije" msgid "Show inline media" msgstr "Prikaži ugnežđeni sadržaj" msgid "Always show" msgstr "Uvek prikaži" msgid "Always hide" msgstr "Uvek skrivaj" msgid "Hide in timeline" msgstr "Sakrij sa vremenske linije" msgid "Auto scroll on new tweets" msgstr "Automatski klizaj pri novim tvitovima" msgid "Double-click activation" msgstr "Aktiviraj na dupli klik" msgid "Notifications" msgstr "ObaveÅ¡tenja" msgid "On New Tweets" msgstr "Pri novim tvitovima" msgid "Never" msgstr "Nikada" msgid "Every" msgstr "Svaki put" msgid "Stack 5" msgstr "Sakupi 5" msgid "Stack 10" msgstr "Sakupi 10" msgid "Stack 25" msgstr "Sakup 25" msgid "Stack 50" msgstr "Sakupi 50" msgid "On New Mentions" msgstr "Pri novim spominjanjima" msgid "On New Messages" msgstr "Pri novim porukama" msgid "Interface" msgstr "Interfejs" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tvitovi" msgid "Round avatars" msgstr "Okrugli avatari" msgid "Remove trailing hashtags" msgstr "Ukloni prateće tarabice" msgid "Remove media links" msgstr "Ukloni multimedijalne veze" msgid "Hide inappropriate content" msgstr "Sakrij neprikladni sadržaj" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Nema podeÅ¡enih iseÄaka." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Uvek možete aktivirati iseÄke pisanjem kljuÄne reÄi i pritiskom tastera TAB." msgid "Snippets" msgstr "IseÄci" msgctxt "shortcuts window" msgid "General" msgstr "OpÅ¡te" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Sastavi tvit" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Prikaži podeÅ¡avanja naloga" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Prikaži iskaÄući prozor naloga" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Prikaži podeÅ¡avanja programa" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "UkljuÄi ili iskljuÄi gornju traku" msgctxt "shortcuts window" msgid "Go Back" msgstr "Idi nazad" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Idi napred" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Idi na odreÄ‘enu stranu" msgctxt "shortcuts window" msgid "Tweets" msgstr "Tvitovi" msgctxt "shortcuts window" msgid "Retweet" msgstr "Ponovo tvituj" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "OznaÄi kao omiljeno" msgctxt "shortcuts window" msgid "Reply" msgstr "Odgovori" msgctxt "shortcuts window" msgid "Quote" msgstr "Citiraj" msgctxt "shortcuts window" msgid "Show Details" msgstr "Prikaži pojedinosti" msgctxt "shortcuts window" msgid "Delete" msgstr "ObriÅ¡i" msgctxt "shortcuts window" msgid "Compose" msgstr "Sastavi" msgid "Show Emoji Chooser" msgstr "Prikaži biraÄa emodžija" msgid "Start new conversation" msgstr "ZapoÄni novi razgovor" msgid "With:" msgstr "Sa:" msgid "Go" msgstr "Idi" msgid "Quote" msgstr "Citiraj" msgid "Retweet tweet" msgstr "Ponovo tvituj tvit" #, fuzzy msgid "Like tweet" msgstr "OznaÄi tvit omiljenim" msgid "Reply to tweet" msgstr "Odgovori na tvit" msgid "More" msgstr "ViÅ¡e" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Stavi kao omiljeno" msgid "Reply" msgstr "Odgovori" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Ponovo tvituj" msgid "Unblock" msgstr "Deblokiraj" msgid "Show settings of this account" msgstr "Prikaži podeÅ¡avanja ovog naloga" msgid "Open in new window" msgstr "Otvori u novom prozoru" msgid "Go to profile" msgstr "Idi na profil" msgid "Created" msgstr "Napravi" msgid "Subscribed to" msgstr "Pretplaćen(a) na" #~ msgid "Replying to" #~ msgstr "Odgovor za" #~ msgid "and" #~ msgstr "i" #~ msgid "Actions" #~ msgstr "Radnje" #~ msgid "Add Image" #~ msgstr "Dodaj sliku" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Pratioci" #~ msgid "List" #~ msgstr "Spisak" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "" #~ "Neovlašćeno. U većini sluÄajeva ovo znaÄi da neÅ¡to nije u redu sa " #~ "Twitterovim serverima i da trebate pokuÅ¡ati kasnije" #~ msgid "Timm Bäder" #~ msgstr "Timm Bäder" #~ msgid "Could not load tweets" #~ msgstr "Ne mogu da uÄitam tvitove" cawbird-1.4.2/po/tr.po000066400000000000000000000536501416632607600145600ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # adem demir , 2015 # Ali GOREN , 2015 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Turkish (Turkey) (http://www.transifex.com/cawbird/cawbird/" "language/tr_TR/)\n" "Language: tr_TR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Twitter İstemcisi" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird, Direkt Mesajlar (DMS), Bildirimler, KonuÅŸma Görünümleri Gibi Hayati " "Özellikler Taşıyan GTK+ Kullanan Yerel Bir Twitter İstemcisidir." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Yerel Görüntüleme İçin Ek Özellikler Åžunlardır, Videolar, Çoklu Resimler, " "Listeler, Çoklu Hesaplar ve Dahası." msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Hesap Yapılandırmalarını Göster" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Direkt Mesajlar" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "İptal" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "DoÄŸrudan Görüşme" #, fuzzy msgid "Direct message threads" msgstr "Direkt Mesajlar" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%2$s Kullanıcısından %1$d Yeni Mesaj" msgstr[1] "%2$s Kullanıcısından %1$d Yeni Mesaj" #, c-format msgid "New direct message from %s" msgstr "%s Kullanıcısından Yeni Mesaj Var" msgid "Direct Messages" msgstr "Direkt Mesajlar" #, fuzzy msgid "Liked tweets timeline" msgstr "Tweet'i Favorile" #, fuzzy msgid "Likes" msgstr "Favoriler" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s Retweetlendi %s" #, c-format msgid "%s tweeted" msgstr "%s Tweetlendi" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d Yeni Tweet!" msgstr[1] "%d Yeni Tweet!" msgid "Home" msgstr "Anasayfa" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s Tweetlendi" msgid "Private" msgstr "Özel" msgid "Public" msgstr "Genel" msgid "Lists" msgstr "Listeler" msgid "Show configured accounts" msgstr "Hesap Yapılandırmalarını Göster" msgid "Compose Tweet" msgstr "Tweet OluÅŸtur" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "Bahsedilenler" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "Bahsedilenler" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Korumalı Hesap" #, c-format msgid "Tweet to @%s" msgstr "@%s Kullanıcısına Tweetle" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s Tweetlendi" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Takiptekiler" #, fuzzy, c-format msgid "Location: %s" msgstr "Bildirimler" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "Bahsedilenler" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Takipçiler" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Takiptekiler" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Takipçiler" #, fuzzy msgid "No tweets found" msgstr "Herhangi Bir Tweet Bulunamadı." msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "@%s Kullanıcısına Tweetle" msgid "Search" msgstr "Ara" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Tweet Gösterilemiyor" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Yeniden Tweetlenenler" msgid "Open in Browser" msgstr "Tarayıcıda Aç" msgid "Source" msgstr "Kaynak" msgid "Tweet Details" msgstr "Tweet Detayları" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d Okunmamış)" msgstr[1] "(%d Okunmamış)" msgid "Delete" msgstr "Sil" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Yeniden Tweetlenenler" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Gerçekten Bu Hesabı Silmek İstiyor Musunuz?" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "%s Açılamadı" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "Arayüz" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Åžimdi" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%dm" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%dh" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "Tweeti Cevapla" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Yeni Bir Tane OluÅŸtur." #, c-format msgid "Could not open %s" msgstr "%s Açılamadı" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Yanlış PİN." #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Bu Hesap Bir BaÅŸkası Tarafından Kullanılıyor." msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Takibi Bırak" msgid "Follow" msgstr "Takip Et" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "Herhangi Bir Tweet Bulunamadı." msgid "Retry" msgstr "Tekrar" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Kaydet" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Geri" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Alıntı Tweet" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "Filtreleri Düzenle." msgid "Matches" msgstr "EÅŸleÅŸenler" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "" "Merhaba, #Cawbird Uygulamasının Son Sürümüne Bakmanı Öneririm! \\ (•◡•) / " "#harika" msgid "Add to or Remove User From List" msgstr "Kullanıcı Listesinden Ekle ya da Kaldır" msgid "You have no lists." msgstr "Herhangi Bir Listeniz Yok." msgid "About Cawbird" msgstr "Cawbird Hakkında" msgid "New Account" msgstr "Yeni Hesap" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Yeni PIN İste" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "DoÄŸrula" msgid "Account Settings" msgstr "Hesap Ayarları" msgid "Name" msgstr "İsim" msgid "Website" msgstr "Website" msgid "Autostart" msgstr "Otomotik BaÅŸlat" #, fuzzy msgid "Remove Account" msgstr "Sil" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Gerçekten Bu Hesabı Silmek İstiyor Musunuz?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Gönder" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "Kullanıcılar" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "Abone Ol" msgid "Unsubscribe" msgstr "Abonelikten Çık" msgid "Subscribers:" msgstr "Aboneler:" msgid "Members:" msgstr "Üyeler:" msgid "Creator:" msgstr "Kurucular:" msgid "Created at:" msgstr "OluÅŸturulan:" msgid "Edit" msgstr "Düzenleme" msgid "Mode:" msgstr "Mod:" msgid "Description" msgstr "" msgid "Settings" msgstr "Ayarlar" msgid "Shortcuts" msgstr "" msgid "About" msgstr "Hakkında" msgid "Quit" msgstr "Çıkış" msgid "Add New Filter" msgstr "Yeni Filtre Ekle" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Yeni Parçacık Ekle" msgid "Keyword" msgstr "Anahtar Kelime" msgid "Replacement" msgstr "DeÄŸiÅŸtirme" msgid "Create New List" msgstr "Yeni Liste OluÅŸtur" msgid "Name:" msgstr "İsim:" msgid "Create" msgstr "OluÅŸtur" msgid "Write Direct Message" msgstr "Direkt Mesaj Gönder" msgid "Add to/Remove from List" msgstr "Listeden Kaldır/Ekle" msgid "Blocked" msgstr "EngellenmiÅŸ" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "Retweetler Kapalı" #, fuzzy msgid "More actions" msgstr "Bahsedilenler" msgid "Follows you" msgstr "Takip edenler" msgid "Tweets" msgstr "Tweetler" msgid "Followers" msgstr "Takipçiler" msgid "Following" msgstr "Takiptekiler" msgid "Use dark theme" msgstr "" msgid "Shortcut key" msgstr "" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "Bahsedilenler" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Yeni Tweetlerde Otomatik Kaydır" msgid "Double-click activation" msgstr "EtkinleÅŸtirmek İçin Çift Tıkla" msgid "Notifications" msgstr "Bildirimler" msgid "On New Tweets" msgstr "Yeni Tweetlerde" msgid "Never" msgstr "Asla" msgid "Every" msgstr "Her Zaman" msgid "Stack 5" msgstr "5 destek" msgid "Stack 10" msgstr "10 destek" msgid "Stack 25" msgstr "25 destek" msgid "Stack 50" msgstr "50 destek" msgid "On New Mentions" msgstr "Yeni Cevaplarda" msgid "On New Messages" msgstr "Yeni Mesajlarda" msgid "Interface" msgstr "Arayüz" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Tweetler" msgid "Round avatars" msgstr "Yuvarlak Avatarlar" msgid "Remove trailing hashtags" msgstr "Tekrarlayan Hashtagleri Kaldır" msgid "Remove media links" msgstr "Medya Linklerini Kaldır" msgid "Hide inappropriate content" msgstr "" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Yapılandırılmış Parçalar Yok." msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "" "Anahtar Kelimeleri Yazıp TAB TuÅŸuna Basabilir ve Parçacıkları Aktif " "Edebilirsiniz." msgid "Snippets" msgstr "Parçacıklar" msgctxt "shortcuts window" msgid "General" msgstr "" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "" msgctxt "shortcuts window" msgid "Go Back" msgstr "" msgctxt "shortcuts window" msgid "Go Forward" msgstr "" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "" msgctxt "shortcuts window" msgid "Tweets" msgstr "" msgctxt "shortcuts window" msgid "Retweet" msgstr "" msgctxt "shortcuts window" msgid "Like" msgstr "" msgctxt "shortcuts window" msgid "Reply" msgstr "" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "" msgctxt "shortcuts window" msgid "Delete" msgstr "" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Yeni Bir Sohbet BaÅŸlat" msgid "With:" msgstr "İle:" msgid "Go" msgstr "Git" msgid "Quote" msgstr "Alıntı" msgid "Retweet tweet" msgstr "Tweet'i Retweetle" #, fuzzy msgid "Like tweet" msgstr "Tweet'i Favorile" msgid "Reply to tweet" msgstr "Tweeti Cevapla" msgid "More" msgstr "Daha Fazla" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Favori" msgid "Reply" msgstr "Cevap" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Yeniden Tweetlenenler" msgid "Unblock" msgstr "Engeli Kaldır" msgid "Show settings of this account" msgstr "Hesap Ayarlarını Göster" msgid "Open in new window" msgstr "Yeni Pencerede Aç" msgid "Go to profile" msgstr "" msgid "Created" msgstr "OluÅŸturuldu" msgid "Subscribed to" msgstr "Abone Olundu" #~ msgid "Actions" #~ msgstr "EtkileÅŸimler" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Takipçiler" #~ msgid "List" #~ msgstr "Liste" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/uk_UA.po000066400000000000000000000614711416632607600151370ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # Illia Ratkevych , 2016 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Ukrainian (Ukraine) (http://www.transifex.com/cawbird/cawbird/" "language/uk_UA/)\n" "Language: uk_UA\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" msgid "Cawbird" msgstr "" msgid "Twitter Client" msgstr "Twitter-клієнт" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird - Twitter-клієнт заÑнований на GTK+, Ñкий реалізує такі необхідні " "функції Ñк приватні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ (ПП), ÑповіщеннÑ, переглÑд розмов." msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "Додаткові функції включають у Ñебе локальний переглÑд відео, вбудовані " "зображеннÑ, ÑпиÑки, фільтри, робота з кількома акаунтами, тощо." msgid "Generic timeline view when using Cawbird" msgstr "" msgid "Typical Twitter profile" msgstr "" msgid "Account settings can be configured" msgstr "" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "Показати акаунти" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "Приватне повідомленнÑ" msgid "Select Media" msgstr "" msgid "Open" msgstr "" msgid "Cancel" msgstr "СкаÑувати" msgid "Selected file is not an image or video." msgstr "" #, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "" #, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "ПрÑмі розмови" #, fuzzy msgid "Direct message threads" msgstr "Приватне повідомленнÑ" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%d нове Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ %s" msgstr[1] "%d нових повідомлень від %s" msgstr[2] "%d нових повідомлень від %s" msgstr[3] "%d нових повідомлень від %s" #, c-format msgid "New direct message from %s" msgstr "Ðове приватне Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ %s" msgid "Direct Messages" msgstr "Приватне повідомленнÑ" #, fuzzy msgid "Liked tweets timeline" msgstr "Вподобати твіт" #, fuzzy msgid "Likes" msgstr "ВподобаннÑ" msgid "Add new Filter" msgstr "" msgid "Filters" msgstr "" msgid "Home timeline" msgstr "" #, c-format msgid "%s retweeted %s" msgstr "%s ретвітнув %s" #, c-format msgid "%s tweeted" msgstr "%s твітнув" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d новий твіт" msgstr[1] "%d нових твітів" msgstr[2] "%d нових твітів" msgstr[3] "%d нових твітів" msgid "Home" msgstr "Головна" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s твітнув" msgid "Private" msgstr "Приватний" msgid "Public" msgstr "Публічний" msgid "Lists" msgstr "СпиÑки" msgid "Show configured accounts" msgstr "Показати акаунти" msgid "Compose Tweet" msgstr "ÐапиÑати новий твіт" msgid "Add new Account" msgstr "" #, fuzzy msgid "Mentions timeline" msgstr "ЗгадуваннÑ" #, c-format msgid "%s mentioned %s" msgstr "" msgid "Mentions" msgstr "ЗгадуваннÑ" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "Захищенний профайл" #, c-format msgid "Tweet to @%s" msgstr "Твіт до @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s твітнув" msgstr[1] "%s твітнув" msgstr[2] "%s твітнув" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "Читає" msgstr[1] "Читає" msgstr[2] "Читає" #, fuzzy, c-format msgid "Location: %s" msgstr "СповіщеннÑ" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "ЗгадуваннÑ" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "Читачі" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "Читає" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "Читачі" msgstr[1] "Читачі" msgstr[2] "Читачі" #, fuzzy msgid "No tweets found" msgstr "ЗапиÑів не знайдено" msgid "No users found" msgstr "" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "Твіт до @%s" msgid "Search" msgstr "Пошук" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "Ðе можу показати твіт" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "Ретвіти" msgid "Open in Browser" msgstr "Відкрити у браузері" msgid "Source" msgstr "Джерело" msgid "Tweet Details" msgstr "Деталі твіта" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d непрочитано)" msgstr[1] "(%d непрочитано)" msgstr[2] "(%d непрочитано)" msgstr[3] "(%d непрочитано)" msgid "Delete" msgstr "Видалити" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "Цитувати" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "Ви дійÑно бажаєте видалити цей акаунт?" #, c-format msgid "Block %s" msgstr "" msgid "This tweet contains images marked as inappropriate" msgstr "" msgid "Show anyway" msgstr "" #, fuzzy msgid "Could not authenticate you" msgstr "Ðе можливо відкрити %s" msgid "Sorry, that page does not exist" msgstr "" msgid "User not found." msgstr "" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "ІнтерфейÑ" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" msgid "Subscription already exists." msgstr "" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "Зараз" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%d хв" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%d г" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "ВідповіÑти на твіт" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "Створити акаунт" #, c-format msgid "Could not open %s" msgstr "Ðе можливо відкрити %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "Ðевірний PIN" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "Ðкаунт вже викориÑтовуєтьÑÑ" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "" msgid "Unfollow" msgstr "Ðе читати" msgid "Follow" msgstr "Читати" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "ЗапиÑів не знайдено" msgid "Retry" msgstr "Спробувати ще" msgid "Copy URL" msgstr "" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "Зберегти" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgstr[3] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "Ðазад" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "Цитувати твіт" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" msgstr[1] "" msgstr[2] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgstr[1] "" msgstr[2] "" msgid "Modify Filter" msgstr "Змінити фільтр" msgid "Matches" msgstr "Співпадає" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "" msgid "Snippet already exists" msgstr "" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "Вау, заціни нову верÑÑ–ÑŽ #Cawbird ! \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "Додати/видалити кориÑтувача зі ÑпиÑку" msgid "You have no lists." msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” ÑпиÑків" msgid "About Cawbird" msgstr "Про Cawbird" msgid "New Account" msgstr "Ðовий акаунт" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "Запит PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "Підтвердити" msgid "Account Settings" msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÐºÐ°ÑƒÐ½Ñ‚Ð°" msgid "Name" msgstr "Ім'Ñ" msgid "Website" msgstr "ВебÑайт" msgid "Autostart" msgstr "Ðвто-Ñтарт" #, fuzzy msgid "Remove Account" msgstr "Видалити" #, fuzzy msgid "Do you really want to remove this account?" msgstr "Ви дійÑно бажаєте видалити цей акаунт?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "Відправити" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" msgid "Filtered terms" msgstr "" msgid "Filtered users" msgstr "" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "КориÑтувачі" msgid "Set image description" msgstr "" msgid "Subscribe" msgstr "ПідпиÑатиÑÑŒ" msgid "Unsubscribe" msgstr "ВідпиÑатиÑÑŒ" msgid "Subscribers:" msgstr "ПідпиÑники:" msgid "Members:" msgstr "Члени:" msgid "Creator:" msgstr "Ðвтор:" msgid "Created at:" msgstr "Створено в:" msgid "Edit" msgstr "Редагувати" msgid "Mode:" msgstr "Режим:" msgid "Description" msgstr "" msgid "Settings" msgstr "ÐалаштуваннÑ" msgid "Shortcuts" msgstr "СкороченнÑ" msgid "About" msgstr "Про" msgid "Quit" msgstr "Вийти" msgid "Add New Filter" msgstr "Додати новий фільтр" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "Додати новий Ñніпет" msgid "Keyword" msgstr "Ключові Ñлова" msgid "Replacement" msgstr "Заміна" msgid "Create New List" msgstr "Створити новий ÑпиÑок" msgid "Name:" msgstr "Ім'Ñ:" msgid "Create" msgstr "Створити" msgid "Write Direct Message" msgstr "ÐапиÑати приватне повідомленнÑ" msgid "Add to/Remove from List" msgstr "Додати/видалити зі ÑпиÑку" msgid "Blocked" msgstr "Заблоковано" msgid "Muted" msgstr "" msgid "Retweets disabled" msgstr "Ретвіти вимкнуто" #, fuzzy msgid "More actions" msgstr "ЗгадуваннÑ" msgid "Follows you" msgstr "Читає ваÑ" msgid "Tweets" msgstr "Твіти" msgid "Followers" msgstr "Читачі" msgid "Following" msgstr "Читає" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "СкороченнÑ" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "ЗгадуваннÑ" msgid "Show inline media" msgstr "" msgid "Always show" msgstr "" msgid "Always hide" msgstr "" msgid "Hide in timeline" msgstr "" msgid "Auto scroll on new tweets" msgstr "Ðвто-Ñкрол при поÑві нових твітів" msgid "Double-click activation" msgstr "ÐÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¸ подвійному кліку" msgid "Notifications" msgstr "СповіщеннÑ" msgid "On New Tweets" msgstr "При отриманні нових твітів" msgid "Never" msgstr "Ðіколи" msgid "Every" msgstr "Кожний" msgid "Stack 5" msgstr "По 5" msgid "Stack 10" msgstr "По 10" msgid "Stack 25" msgstr "По 25" msgid "Stack 50" msgstr "По 50" msgid "On New Mentions" msgstr "При нових отриманні нових згадок" msgid "On New Messages" msgstr "При отриманні нових повідомлень" msgid "Interface" msgstr "ІнтерфейÑ" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "Твіти" msgid "Round avatars" msgstr "Круглі аватари" msgid "Remove trailing hashtags" msgstr "Прибрати хештеги наприкінці" msgid "Remove media links" msgstr "Прибрати поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° медіа" msgid "Hide inappropriate content" msgstr "Сховати недопуÑтимий контент" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "Ðе Ñконфігуровано жодного Ñніпета" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "Ви можете активувати Ñніпет напиÑавши ключове Ñлово Ñ– натиÑнувши TAB." msgid "Snippets" msgstr "Сніпети" msgctxt "shortcuts window" msgid "General" msgstr "Загальне" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "Створити новий твіт" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "Показати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÐºÐ°ÑƒÐ½Ñ‚Ð°" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "Показати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð¿Ð¾Ð²ÐµÑ€Ð°" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "Показати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "Перемкнути топбар" msgctxt "shortcuts window" msgid "Go Back" msgstr "Ðазад" msgctxt "shortcuts window" msgid "Go Forward" msgstr "Вперед" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "Перейти на Ñторінку" msgctxt "shortcuts window" msgid "Tweets" msgstr "Твіти" msgctxt "shortcuts window" msgid "Retweet" msgstr "Цитувати" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "Вподобане" msgctxt "shortcuts window" msgid "Reply" msgstr "ВідповіÑти" msgctxt "shortcuts window" msgid "Quote" msgstr "" msgctxt "shortcuts window" msgid "Show Details" msgstr "Показати деталі" msgctxt "shortcuts window" msgid "Delete" msgstr "Видалити" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "Почати нову розмову" msgid "With:" msgstr "З:" msgid "Go" msgstr "Go" msgid "Quote" msgstr "Цитувати" msgid "Retweet tweet" msgstr "Цитувати твіт" #, fuzzy msgid "Like tweet" msgstr "Вподобати твіт" msgid "Reply to tweet" msgstr "ВідповіÑти на твіт" msgid "More" msgstr "Більше" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "Вподобане" msgid "Reply" msgstr "ВідповіÑти" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "Цитувати" msgid "Unblock" msgstr "Розблокувати" msgid "Show settings of this account" msgstr "Показати Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÐºÐ°ÑƒÐ½Ñ‚Ð°" msgid "Open in new window" msgstr "Відкрити у новому вікні" msgid "Go to profile" msgstr "" msgid "Created" msgstr "Створено" msgid "Subscribed to" msgstr "ПідпиÑано на" #~ msgid "Actions" #~ msgstr "Дії" #~ msgid "Add Image" #~ msgstr "Додати зображеннÑ" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "Читачі" #~ msgid "List" #~ msgstr "СпиÑок" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" cawbird-1.4.2/po/zh_CN.po000066400000000000000000000546461416632607600151420ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # colindemian , 2015-2016-2016 # Dianjin Wang <1132321739qq@gmail.com>, 2014 # hmingming1222 , 2015 # S Shen, 2016 # Tong Hui , 2014 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2017-10-22 10:08+0000\n" "Last-Translator: baedert \n" "Language-Team: Chinese (China) (http://www.transifex.com/cawbird/cawbird/" "language/zh_CN/)\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter 客户端" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird 是一款原生的 GTK+ Twitter 客户端,能æä¾›å¤šç§åŸºç¡€åŠŸèƒ½å¦‚ç›´æŽ¥æ¶ˆæ¯(ç§" "ä¿¡)ã€æé†’ã€ä¼šè¯è§†å›¾ç­‰ã€‚" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "é«˜çº§åŠŸèƒ½åŒ…æ‹¬æœ¬åœ°è§‚çœ‹è§†é¢‘ã€æ’入多个图片ã€åˆ—表ã€è¿‡æ»¤ã€å¤šå¸å·ç­‰ã€‚" msgid "Generic timeline view when using Cawbird" msgstr "使用 Cawbird æ—¶å¯ç”¨å¸¸è§„时间线视图" msgid "Typical Twitter profile" msgstr "常规 Twitter 资料" msgid "Account settings can be configured" msgstr "账户设置å¯è‡ªå®šä¹‰" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "桌é¢åº”用般的 Twitter 使用体验" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "显示已é…置的账户" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "ç§ä¿¡" #, fuzzy msgid "Select Media" msgstr "选择图片" msgid "Open" msgstr "打开" msgid "Cancel" msgstr "å–æ¶ˆ" msgid "Selected file is not an image or video." msgstr "" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "所选图åƒè¿‡å¤§ï¼Œæ¯å¼ å›¾è±¡å¤§å°ä¸Šé™ä¸º %'d MB" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "所选图åƒè¿‡å¤§ï¼Œæ¯å¼ å›¾è±¡å¤§å°ä¸Šé™ä¸º %'d MB" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "所选图åƒè¿‡å¤§ï¼Œæ¯å¼ å›¾è±¡å¤§å°ä¸Šé™ä¸º %'d MB" msgid "Insert Emoji" msgstr "" msgid "Direct Conversation" msgstr "直接对è¯" #, fuzzy msgid "Direct message threads" msgstr "ç§ä¿¡" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "有æ¥è‡ª %2$s çš„ %1$d æ¡æ–°æ¶ˆæ¯" #, c-format msgid "New direct message from %s" msgstr "有æ¥è‡ª %s 的新ç§ä¿¡" msgid "Direct Messages" msgstr "ç§ä¿¡" #, fuzzy msgid "Liked tweets timeline" msgstr "æ”¶è—æŽ¨æ–‡" #, fuzzy msgid "Likes" msgstr "æ”¶è—" msgid "Add new Filter" msgstr "添加新过滤器" msgid "Filters" msgstr "过滤器" #, fuzzy msgid "Home timeline" msgstr "从时间线上éšè—" #, c-format msgid "%s retweeted %s" msgstr "%s 转推了 %s" #, c-format msgid "%s tweeted" msgstr "%s 已呿ލ" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d æ¡æ–°æŽ¨ï¼" msgid "Home" msgstr "主页" #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s 已呿ލ" msgid "Private" msgstr "ç§äºº" msgid "Public" msgstr "公开" msgid "Lists" msgstr "列表" msgid "Show configured accounts" msgstr "显示已é…置的账户" msgid "Compose Tweet" msgstr "撰写推文" msgid "Add new Account" msgstr "添加新账户" #, fuzzy msgid "Mentions timeline" msgstr "从时间线上éšè—" #, c-format msgid "%s mentioned %s" msgstr "%s æåŠäº† %s" msgid "Mentions" msgstr "æåŠæˆ‘çš„" msgid "Suspended Account" msgstr "" msgid "Protected profile" msgstr "资料已å—ä¿æŠ¤" #, c-format msgid "Tweet to @%s" msgstr "ç»™ @%s 呿ލ" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s 已呿ލ" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "已关注" #, fuzzy, c-format msgid "Location: %s" msgstr "通知" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "从时间线上éšè—" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "关注者" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "已关注" msgid "Protected Profile" msgstr "" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "关注者" #, fuzzy msgid "No tweets found" msgstr "未找到æ¡ç›®" msgid "No users found" msgstr "未找到用户" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "ç»™ @%s 呿ލ" msgid "Search" msgstr "æœç´¢" msgid "Load More" msgstr "" msgid "Could not show tweet" msgstr "无法显示推文" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "转推" msgid "Open in Browser" msgstr "æµè§ˆå™¨ä¸­æ‰“å¼€" msgid "Source" msgstr "æ¥æº" msgid "Tweet Details" msgstr "详情" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d æ¡æœªè¯»)" msgid "Delete" msgstr "删除" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "转推" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "您确定è¦åˆ é™¤æ­¤è´¦æˆ·å—?" #, c-format msgid "Block %s" msgstr "å±è”½ %s" msgid "This tweet contains images marked as inappropriate" msgstr "此推文包å«è¢«æ ‡è®°ä¸ºâ€œä¸é€‚宜â€çš„图片" msgid "Show anyway" msgstr "ä»ç„¶æ˜¾ç¤º" #, fuzzy msgid "Could not authenticate you" msgstr "无法打开 %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "未找到用户" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "外观" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "æ’件已存在" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "现在" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%d分钟" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%då°æ—¶" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "å›žå¤æŽ¨æ–‡" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, c-format msgid "Replying to %s and %s" msgstr "" msgid "Don't have a Twitter account yet?" msgstr "" msgid "Create one" msgstr "注册一个" #, c-format msgid "Could not open %s" msgstr "无法打开 %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN ç é”™è¯¯" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "账户已在使用中" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "选择图片" msgid "Unfollow" msgstr "å–æ¶ˆå…³æ³¨" msgid "Follow" msgstr "关注" msgid "Loading…" msgstr "" #, fuzzy msgid "No entries found" msgstr "未找到用户" msgid "Retry" msgstr "é‡è¯•" msgid "Copy URL" msgstr "å¤åˆ¶ URL" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "" msgid "Save Video" msgstr "" msgid "Save Image" msgstr "" msgid "Save" msgstr "ä¿å­˜" msgid "Select Banner Image" msgstr "" msgid "Image does not meet minimum size requirements:" msgstr "" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "" #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "" msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "返回" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "引用推文" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" msgid "Only one animated GIF file per tweet is allowed." msgstr "" #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "修改过滤器" msgid "Matches" msgstr "匹é…" msgid "Doesn't match" msgstr "" msgid "Modify Snippet" msgstr "修改æ’ä»¶" msgid "Snippet can't be empty" msgstr "" msgid "Replacement can't be empty" msgstr "" msgid "Snippet may not contain whitespace" msgstr "æ’ä»¶ä¸å¯åŒ…å«ç©ºå­—符" msgid "Snippet already exists" msgstr "æ’件已存在" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "新版的 #Cawbird å‘布啦ï¼\\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "添加或删除列表中的用户" msgid "You have no lists." msgstr "您还没有任何列表。" msgid "About Cawbird" msgstr "关于 Cawbird" msgid "New Account" msgstr "新账户" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "" msgid "Request PIN" msgstr "需è¦è¾“å…¥ PIN" msgid "Enter PIN from twitter.com below:" msgstr "" msgid "PIN" msgstr "" msgid "Confirm" msgstr "确认" msgid "Account Settings" msgstr "账户设置" msgid "Name" msgstr "å§“å" msgid "Website" msgstr "网站" msgid "Autostart" msgstr "自动登入" #, fuzzy msgid "Remove Account" msgstr "删除" #, fuzzy msgid "Do you really want to remove this account?" msgstr "您确定è¦åˆ é™¤æ­¤è´¦æˆ·å—?" msgctxt "emoji category" msgid "Smileys & People" msgstr "" msgctxt "emoji category" msgid "Body & Clothing" msgstr "" msgctxt "emoji category" msgid "Animals & Nature" msgstr "" msgctxt "emoji category" msgid "Food & Drink" msgstr "" msgctxt "emoji category" msgid "Travel & Places" msgstr "" msgctxt "emoji category" msgid "Activities" msgstr "" msgctxt "emoji category" msgid "Objects" msgstr "" msgctxt "emoji category" msgid "Symbols" msgstr "" msgctxt "emoji category" msgid "Flags" msgstr "" msgid "No Results Found" msgstr "" msgid "Try a different search" msgstr "" msgid "Send" msgstr "å‘é€" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "过滤器" #, fuzzy msgid "Filtered users" msgstr "过滤器" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "用户" #, fuzzy msgid "Set image description" msgstr "æè¿°" msgid "Subscribe" msgstr "订阅" msgid "Unsubscribe" msgstr "å–æ¶ˆè®¢é˜…" msgid "Subscribers:" msgstr "订阅者:" msgid "Members:" msgstr "æˆå‘˜ï¼š" msgid "Creator:" msgstr "创建人:" msgid "Created at:" msgstr "创建时间:" msgid "Edit" msgstr "编辑" msgid "Mode:" msgstr "模å¼ï¼š" msgid "Description" msgstr "æè¿°" msgid "Settings" msgstr "设置" msgid "Shortcuts" msgstr "å¿«æ·é”®" msgid "About" msgstr "关于" msgid "Quit" msgstr "退出" msgid "Add New Filter" msgstr "添加新过滤器" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "添加新æ’ä»¶" msgid "Keyword" msgstr "关键字" msgid "Replacement" msgstr "替æ¢å†…容" msgid "Create New List" msgstr "创建新列表" msgid "Name:" msgstr "å称:" msgid "Create" msgstr "创建" msgid "Write Direct Message" msgstr "呿¶ˆæ¯" msgid "Add to/Remove from List" msgstr "添加至列表/从列表删除" msgid "Blocked" msgstr "å·²å±è”½" msgid "Muted" msgstr "å·²é™éŸ³" msgid "Retweets disabled" msgstr "转推已ç¦ç”¨" #, fuzzy msgid "More actions" msgstr "æåŠæˆ‘çš„" msgid "Follows you" msgstr "关注您" msgid "Tweets" msgstr "推文" msgid "Followers" msgstr "关注者" msgid "Following" msgstr "已关注" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "å¿«æ·é”®" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "从时间线上éšè—" msgid "Show inline media" msgstr "显示内è”媒体" msgid "Always show" msgstr "总是显示" msgid "Always hide" msgstr "总是éšè—" msgid "Hide in timeline" msgstr "从时间线上éšè—" msgid "Auto scroll on new tweets" msgstr "自动载入新推文" msgid "Double-click activation" msgstr "åŒå‡»æ¿€æ´»" msgid "Notifications" msgstr "通知" msgid "On New Tweets" msgstr "新推文" msgid "Never" msgstr "从ä¸" msgid "Every" msgstr "é—´éš”" msgid "Stack 5" msgstr "5 æ¡" msgid "Stack 10" msgstr "10 æ¡" msgid "Stack 25" msgstr "25 æ¡" msgid "Stack 50" msgstr "50 æ¡" msgid "On New Mentions" msgstr "æ–°æåŠ" msgid "On New Messages" msgstr "新消æ¯" msgid "Interface" msgstr "外观" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "推文" msgid "Round avatars" msgstr "圆形头åƒ" msgid "Remove trailing hashtags" msgstr "移除尾部标签" msgid "Remove media links" msgstr "移除媒体链接" msgid "Hide inappropriate content" msgstr "éšè—ä¸é€‚宜内容" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "未é…置任何æ’ä»¶" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "您å¯ä»¥é€šè¿‡é”®å…¥å…³é”®è¯å’ŒæŒ‰ä¸‹ TAB é”®æ¥æ¿€æ´»æ’件。" msgid "Snippets" msgstr "æ’ä»¶" msgctxt "shortcuts window" msgid "General" msgstr "一般选项" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "撰写推文" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "显示账户设置" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "显示账户弹出框" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "显示应用设置" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "é¡¶æ å¼€å…³" msgctxt "shortcuts window" msgid "Go Back" msgstr "返回" msgctxt "shortcuts window" msgid "Go Forward" msgstr "å‰è¿›" msgctxt "shortcuts window" msgid "Go to nth page" msgstr "跳转页é¢" msgctxt "shortcuts window" msgid "Tweets" msgstr "推文" msgctxt "shortcuts window" msgid "Retweet" msgstr "转推" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "æ”¶è—" msgctxt "shortcuts window" msgid "Reply" msgstr "回å¤" msgctxt "shortcuts window" msgid "Quote" msgstr "引用" msgctxt "shortcuts window" msgid "Show Details" msgstr "显示详情" msgctxt "shortcuts window" msgid "Delete" msgstr "删除" msgctxt "shortcuts window" msgid "Compose" msgstr "" msgid "Show Emoji Chooser" msgstr "" msgid "Start new conversation" msgstr "开始新对è¯" msgid "With:" msgstr "和:" msgid "Go" msgstr "å‰å¾€" msgid "Quote" msgstr "引用" msgid "Retweet tweet" msgstr "转推" #, fuzzy msgid "Like tweet" msgstr "æ”¶è—æŽ¨æ–‡" msgid "Reply to tweet" msgstr "å›žå¤æŽ¨æ–‡" msgid "More" msgstr "更多" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "æ”¶è—" msgid "Reply" msgstr "回å¤" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "转推" msgid "Unblock" msgstr "å–æ¶ˆå±è”½" msgid "Show settings of this account" msgstr "显示此账户设置" msgid "Open in new window" msgstr "æ–°çª—å£æ‰“å¼€" msgid "Go to profile" msgstr "查看资料" msgid "Created" msgstr "已创建" msgid "Subscribed to" msgstr "订阅到" #~ msgid "Actions" #~ msgstr "行为" #~ msgid "Add Image" #~ msgstr "添加图åƒ" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "关注者" #~ msgid "List" #~ msgstr "列表" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "Could not load tweets" #~ msgstr "无法加载推文" cawbird-1.4.2/po/zh_TW.po000066400000000000000000000567031416632607600151700ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR Timm Bäder # This file is distributed under the same license as the cawbird package. # # Translators: # HybridGlucose , 2016-2017-2017 msgid "" msgstr "" "Project-Id-Version: cawbird\n" "Report-Msgid-Bugs-To: https://github.com/ibboard/cawbird/issues/new\n" "POT-Creation-Date: 2021-04-12 19:16+0100\n" "PO-Revision-Date: 2018-01-17 19:31+0000\n" "Last-Translator: HybridGlucose \n" "Language-Team: Chinese (Taiwan) (http://www.transifex.com/cawbird/cawbird/" "language/zh_TW/)\n" "Language: zh_TW\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" msgid "Cawbird" msgstr "Cawbird" msgid "Twitter Client" msgstr "Twitter用戶端" msgid "" "Cawbird is a native GTK+ twitter client that provides vital features such as " "Direct Messages (DMs), tweet notifications, conversation views." msgstr "" "Cawbird 是原生的 GTK+ Twitter 用戶端,具有訊æ¯ã€æŽ¨æ–‡é€šçŸ¥ã€ä»¥å°è©±å½¢å¼æª¢è¦–回覆" "等等功能。" msgid "" "Additional features include local viewing of videos, multiple inline images, " "Lists, Filters, multiple accounts, etc." msgstr "" "其他功能包括直接觀賞影片ã€é¡¯ç¤ºå¤šå¼µå½±åƒã€åˆ—表ã€éŽæ¿¾å™¨åŠå¤šé‡å¸³æˆ¶ç™»å…¥ç­‰ç­‰ã€‚" msgid "Generic timeline view when using Cawbird" msgstr "使用 Cawbird 以時間軸方å¼ç€è¦½" msgid "Typical Twitter profile" msgstr "典型的 Twitter 個人檔案" msgid "Account settings can be configured" msgstr "å¯ä»¥è¨­å®šå¸³æˆ¶è³‡æ–™" msgid "" "Cawbird in different themes (Adwaita, Adwaita dark variant, High Contrast " "and Adwaita Dark Green)" msgstr "" msgid "IBBoard" msgstr "IBBoard" msgid "Use Twitter from within a normal desktop application" msgstr "åœ¨æ™®é€šçš„æ¡Œé¢æ‡‰ç”¨ç¨‹å¼å…§ä½¿ç”¨ Twitter" #. TRANSLATORS: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! msgid "twitter;" msgstr "twitter;" #. TRANSLATORS: Description of the `--tweet` option for the command-line msgid "" "Shows only the 'compose tweet' window for the given account, nothing else." msgstr "" #. TRANSLATORS: Used as the placeholder for the account name in the `--help` output msgid "account-name" msgstr "" #. TRANSLATORS: Description of the `--start-service` option for the command-line msgid "Start service" msgstr "" #. TRANSLATORS: Description of the `--stop-service` option for the command-line msgid "Stop service, if it has been started as a service" msgstr "" #. TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line #, fuzzy msgid "Print configured startup accounts" msgstr "顯示帳戶" #. TRANSLATORS: Description of the `--account` option for the command-line msgid "Open the window for the given account" msgstr "" #, fuzzy, c-format msgid "Direct messages with %s" msgstr "訊æ¯" #, fuzzy msgid "Select Media" msgstr "鏿“‡åª’é«”" msgid "Open" msgstr "開啟" msgid "Cancel" msgstr "å–æ¶ˆ" #, fuzzy msgid "Selected file is not an image or video." msgstr "æ‰€é¸æ“‡çš„æª”案並éžå½±åƒæˆ–圖片。" #, fuzzy, c-format msgid "" "The selected video is too big. The maximum file size per video is %'d MB" msgstr "æ‰€é¸æ“‡çš„媒體檔案太大。大å°ä¸Šé™ç‚º %'d MB。" #, c-format msgid "" "The selected image is too big. The maximum file size per image is %'d MB" msgstr "æ‰€é¸æ“‡çš„媒體檔案太大。大å°ä¸Šé™ç‚º %'d MB。" #, fuzzy, c-format msgid "The selected GIF is too big. The maximum file size per GIF is %'d MB" msgstr "æ‰€é¸æ“‡çš„媒體檔案太大。大å°ä¸Šé™ç‚º %'d MB。" msgid "Insert Emoji" msgstr "æ’入顿–‡å­—" msgid "Direct Conversation" msgstr "å°è©±" #, fuzzy msgid "Direct message threads" msgstr "訊æ¯" #, c-format msgid "%d new Message from %s" msgid_plural "%d new Messages from %s" msgstr[0] "%2$s 傳é€äº† %1$d 則新訊æ¯" #, c-format msgid "New direct message from %s" msgstr "%s 傳é€äº†æ–°è¨Šæ¯" msgid "Direct Messages" msgstr "訊æ¯" #, fuzzy msgid "Liked tweets timeline" msgstr "個喜歡" #, fuzzy msgid "Likes" msgstr "喜歡" msgid "Add new Filter" msgstr "æ–°å¢žéŽæ¿¾å™¨" msgid "Filters" msgstr "éŽæ¿¾å™¨" #, fuzzy msgid "Home timeline" msgstr "在時間軸上隱è—" #, c-format msgid "%s retweeted %s" msgstr "%s 轉推了 %s" #, c-format msgid "%s tweeted" msgstr "%s 推文了" #, c-format msgid "%d new Tweet!" msgid_plural "%d new Tweets!" msgstr[0] "%d 則新推文ï¼" msgid "Home" msgstr "主é " #. TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 #, fuzzy, c-format msgid "%s list tweets" msgstr "%s 推文了" msgid "Private" msgstr "ç§äºº" msgid "Public" msgstr "公開" msgid "Lists" msgstr "列表" msgid "Show configured accounts" msgstr "顯示帳戶" msgid "Compose Tweet" msgstr "撰寫新推文" msgid "Add new Account" msgstr "加入新帳戶" #, fuzzy msgid "Mentions timeline" msgstr "在時間軸上隱è—" #, c-format msgid "%s mentioned %s" msgstr "%s æåŠ %s" msgid "Mentions" msgstr "æåŠ" msgid "Suspended Account" msgstr "被暫時åœç”¨çš„帳戶" msgid "Protected profile" msgstr "å—ä¿è­·çš„個人檔案" #, c-format msgid "Tweet to @%s" msgstr "推文給 @%s" #, fuzzy, c-format msgid "%d tweet" msgid_plural "%d tweets" msgstr[0] "%s 推文了" #, fuzzy, c-format msgid "Following %d account" msgid_plural "Following %d accounts" msgstr[0] "正在關注" #, fuzzy, c-format msgid "Location: %s" msgstr "通知" #. TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view #, fuzzy, c-format msgid "%s timeline" msgstr "在時間軸上隱è—" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user #, fuzzy, c-format msgid "%s followers" msgstr "跟隨者" #. TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user #, fuzzy, c-format msgid "%s following" msgstr "正在關注" msgid "Protected Profile" msgstr "å—ä¿è­·çš„個人檔案" msgid "User is blocked" msgstr "" msgid "User is muted" msgstr "" #, fuzzy, c-format msgid "%d follower" msgid_plural "%d followers" msgstr[0] "跟隨者" #, fuzzy msgid "No tweets found" msgstr "沒有找到æ¢ç›®" msgid "No users found" msgstr "沒有找到用戶" #, c-format msgid "Users matching \"%s\"" msgstr "" #, fuzzy, c-format msgid "Tweets matching \"%s\"" msgstr "推文給 @%s" msgid "Search" msgstr "æœå°‹" msgid "Load More" msgstr "載入更多" msgid "Could not show tweet" msgstr "無法顯示推文" msgid "This tweet is hidden by the author" msgstr "" msgid "This tweet is unavailable" msgstr "" #. TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" msgid "Translate to English" msgstr "" msgid "Retweets" msgstr "個轉推" msgid "Open in Browser" msgstr "在ç€è¦½å™¨ä¸­é–‹å•Ÿ" msgid "Source" msgstr "來æº" msgid "Tweet Details" msgstr "推文詳細" #. TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank msgid "an unknown client" msgstr "" #, c-format msgid "(%d unread)" msgid_plural "(%d unread)" msgstr[0] "(%d 未讀)" msgid "Delete" msgstr "刪除" #. TRANSLATORS: replacements are name and handle (without the "@") #, fuzzy, c-format msgid "Retweeted by %s (@%s)" msgstr "轉推" #, fuzzy msgid "Are you sure you want to delete this tweet?" msgstr "你確定è¦ç§»é™¤é€™å€‹å¸³æˆ¶ï¼Ÿ" #, c-format msgid "Block %s" msgstr "å°éŽ– %s" msgid "This tweet contains images marked as inappropriate" msgstr "é€™å‰‡æŽ¨æ–‡è¢«æ¨™è¨˜ç‚ºæ•æ„Ÿå…§å®¹" msgid "Show anyway" msgstr "顯示它" #, fuzzy msgid "Could not authenticate you" msgstr "無法開啟 %s" msgid "Sorry, that page does not exist" msgstr "" #, fuzzy msgid "User not found." msgstr "沒有找到用戶" msgid "User has been suspended." msgstr "" msgid "Your account is suspended and is not permitted to access this feature" msgstr "" msgid "Rate limit exceeded" msgstr "" msgid "Invalid or expired token" msgstr "" msgid "The specified user is not a subscriber of this list." msgstr "" msgid "The user you are trying to remove from the list is not a member." msgstr "" msgid "Account update failed: value is too long." msgstr "" msgid "Over capacity" msgstr "" #, fuzzy msgid "Internal error" msgstr "界é¢" msgid "You have already favorited this status." msgstr "" msgid "No status found with that ID." msgstr "" msgid "You cannot send messages to users who are not following you." msgstr "" msgid "There was an error sending your message." msgstr "" msgid "You've already requested to follow this user." msgstr "" msgid "You are unable to follow more people at this time" msgstr "" msgid "Sorry, you are not authorized to see this status" msgstr "" msgid "User is over daily status update limit" msgstr "" msgid "Tweet needs to be a bit shorter." msgstr "" msgid "Status is a duplicate" msgstr "" msgid "Owner must allow dms from anyone." msgstr "" msgid "Bad authentication data" msgstr "" msgid "Your credentials do not allow access to this resource." msgstr "" msgid "" "This request looks like it might be automated. To protect our users from " "spam and other malicious activity, we can’t complete this action right now." msgstr "" msgid "User must verify login" msgstr "" msgid "Application cannot perform write actions." msgstr "" msgid "You can’t mute yourself." msgstr "" msgid "You are not muting the specified user." msgstr "" msgid "Animated GIFs are not allowed when uploading multiple images." msgstr "" msgid "The validation of media ids failed." msgstr "" msgid "A media id was not found." msgstr "" msgid "" "To protect our users from spam and other malicious activity, this account is " "temporarily locked." msgstr "" msgid "You have already retweeted this Tweet." msgstr "" msgid "You cannot send messages to this user." msgstr "" msgid "The text of your direct message is over the max character limit." msgstr "" #, fuzzy msgid "Subscription already exists." msgstr "片語關éµå­—已存在" msgid "" "You attempted to reply to a Tweet that is deleted or not visible to you." msgstr "" msgid "The Tweet exceeds the number of allowed attachment types." msgstr "" msgid "The given URL is invalid." msgstr "" msgid "Invalid / suspended application" msgstr "" msgid "The original Tweet author restricted who can reply to this Tweet." msgstr "" msgid "Invalid media file" msgstr "" #, c-format msgid "Unknown error code %lld during upload: %s" msgstr "" #, c-format msgid "Unknown error code %lld during upload" msgstr "" msgid "Now" msgstr "ç¾åœ¨" #, c-format msgid "%d minute ago" msgid_plural "%d minutes ago" msgstr[0] "" #. TRANSLATORS: short-form "x minutes ago" #, c-format msgid "%dm" msgstr "%d分é˜" #, c-format msgid "%d hour ago" msgid_plural "%d hours ago" msgstr[0] "" #. TRANSLATORS: short-form "x hours ago" #, c-format msgid "%dh" msgstr "%då°æ™‚" #. TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B" msgstr "" #. TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b" msgstr "" #. TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %B %Y" msgstr "" #. TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html msgid "%e %b %Y" msgstr "" #, fuzzy, c-format msgid "Replying to %s" msgstr "回覆給 %s" #. TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string #, fuzzy, c-format msgid "Replying to %s and %d other" msgid_plural "Replying to %s and %d others" msgstr[0] "和其他 %d 人" #. TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list #, fuzzy, c-format msgid "Replying to %s and %s" msgstr "回覆給 %s å’Œ %s" msgid "Don't have a Twitter account yet?" msgstr "還沒有 Twitter 帳號嗎?" msgid "Create one" msgstr "註冊一個" #, c-format msgid "Could not open %s" msgstr "無法開啟 %s" msgid "Failed to retrieve request token" msgstr "" msgid "Wrong PIN" msgstr "PIN 錯誤" #, c-format msgid "" "The account %s already exists with different keys.\n" "\n" "Replace it?" msgstr "" msgid "Account already in use" msgstr "此帳戶已被加入éŽ" msgid "Failed to retrieve access token" msgstr "" msgid "Select Image" msgstr "鏿“‡åª’é«”" msgid "Unfollow" msgstr "å–æ¶ˆè·Ÿéš¨" msgid "Follow" msgstr "跟隨" msgid "Loading…" msgstr "載入中..." #, fuzzy msgid "No entries found" msgstr "沒有找到用戶" msgid "Retry" msgstr "é‡è©¦" msgid "Copy URL" msgstr "複製網å€" msgid "Reload image" msgstr "" msgid "Save as…" msgstr "儲存為..." msgid "Save Video" msgstr "儲存影片" msgid "Save Image" msgstr "儲存圖片" msgid "Save" msgstr "儲存" msgid "Select Banner Image" msgstr "鏿“‡é¦–é ç›¸ç‰‡" msgid "Image does not meet minimum size requirements:" msgstr "å½±åƒæ²’有é”到最低大å°:" #, c-format msgid "Minimum width: %d pixel" msgid_plural "Minimum width: %d pixels" msgstr[0] "最å°å¯¬åº¦: %d åƒç´ " #, c-format msgid "Minimum height: %d pixel" msgid_plural "Minimum height: %d pixels" msgstr[0] "最å°é«˜åº¦: %d åƒç´ " msgid "Pick" msgstr "" #, c-format msgid "Failed to load image" msgid_plural "Failed to load %u images" msgstr[0] "" #. TRANSLATORS: Combine plural "Failed to load image" and list of failed paths #. to make "Failed to load image: " or "Failed to load 3 images: , , " #, c-format msgid "%s: %s" msgstr "" msgid "Back" msgstr "返回" #. TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") #, c-format msgid "" "Error fetching quoted tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" #, c-format msgid "" "Error fetching reply tweet: %s\n" "\n" "Save unsent tweet?" msgstr "" msgid "Quote tweet" msgstr "引用推文" #. TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog msgid "Keep tweet as draft?" msgstr "" #, fuzzy msgid "Only one animated GIF file per tweet is allowed." msgstr "æ¯ä¸€å‰‡æŽ¨æ–‡åªå…許一個GIF檔案." #, c-format msgid "%d character remaining" msgid_plural "%d characters remaining" msgstr[0] "" #. TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. #. Should only be seen when image count is two or more. #, c-format msgid "Image %d of %d" msgid_plural "Image %d of %d" msgstr[0] "" msgid "Modify Filter" msgstr "ä¿®æ”¹éŽæ¿¾å™¨" msgid "Matches" msgstr "相符" msgid "Doesn't match" msgstr "ä¸ç¬¦åˆ" msgid "Modify Snippet" msgstr "編輯片語" msgid "Snippet can't be empty" msgstr "片語關éµå­—ä¸å¯ç©ºç™½" msgid "Replacement can't be empty" msgstr "替æ›å…§å®¹ä¸å¯ç©ºç™½" msgid "Snippet may not contain whitespace" msgstr "片語關éµå­—ä¸å¯åŒ…å«ç©ºç™½" msgid "Snippet already exists" msgstr "片語關éµå­—已存在" #, c-format msgid "" "Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} " "placeholders\n" "\n" "The URL \"%s\" will be used instead" msgstr "" msgid "" "Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter" msgstr "嘿,#Cawbird æŽ¨å‡ºäº†æ–°ç‰ˆæœ¬ï¼ \\ (•◡•) / #cool #newisalwaysbetter" msgid "Add to or Remove User From List" msgstr "從列表中加入或移除æˆå“¡" msgid "You have no lists." msgstr "你沒有任何列表" msgid "About Cawbird" msgstr "關於Cawbird" msgid "New Account" msgstr "新帳戶" msgid "" "To authenticate Cawbird, you need a PIN from twitter.com with the account " "you wish to add" msgstr "為了授權 Cawbird çš„ä½¿ç”¨ï¼Œæ‚¨å¿…é ˆå‘ twitter.com å–的欲新增帳戶之 PIN 。" msgid "Request PIN" msgstr "å–å¾— PIN" msgid "Enter PIN from twitter.com below:" msgstr "在下方輸入 twitter.com 所æä¾›çš„ PIN :" msgid "PIN" msgstr "PIN" msgid "Confirm" msgstr "確èª" msgid "Account Settings" msgstr "帳戶設定" msgid "Name" msgstr "å稱" msgid "Website" msgstr "網站" msgid "Autostart" msgstr "自動啟動" #, fuzzy msgid "Remove Account" msgstr "刪除" #, fuzzy msgid "Do you really want to remove this account?" msgstr "你確定è¦ç§»é™¤é€™å€‹å¸³æˆ¶ï¼Ÿ" msgctxt "emoji category" msgid "Smileys & People" msgstr "表情&人物" msgctxt "emoji category" msgid "Body & Clothing" msgstr "身體&衣æœ" msgctxt "emoji category" msgid "Animals & Nature" msgstr "動物&自然" msgctxt "emoji category" msgid "Food & Drink" msgstr "食物&飲å“" msgctxt "emoji category" msgid "Travel & Places" msgstr "旅行&地點" msgctxt "emoji category" msgid "Activities" msgstr "活動" msgctxt "emoji category" msgid "Objects" msgstr "物件" msgctxt "emoji category" msgid "Symbols" msgstr "象徵物" msgctxt "emoji category" msgid "Flags" msgstr "旗幟" msgid "No Results Found" msgstr "找ä¸åˆ°ç›¸ç¬¦çš„" msgid "Try a different search" msgstr "試試其他的å§" msgid "Send" msgstr "推文" msgid "Add Media" msgstr "" msgid "Show favorite images" msgstr "顯示加入常用的媒體" msgid "Click to edit" msgstr "" msgid "Remove this Filter" msgstr "" #, fuzzy msgid "Filtered terms" msgstr "éŽæ¿¾å™¨" #, fuzzy msgid "Filtered users" msgstr "éŽæ¿¾å™¨" msgid "You can block users in their profile" msgstr "" msgid "Users" msgstr "帳戶" #, fuzzy msgid "Set image description" msgstr "æè¿°" msgid "Subscribe" msgstr "訂閱" msgid "Unsubscribe" msgstr "å–æ¶ˆè¨‚é–±" msgid "Subscribers:" msgstr "訂閱者:" msgid "Members:" msgstr "æˆå“¡ï¼š" msgid "Creator:" msgstr "建立者:" msgid "Created at:" msgstr "建立時間:" msgid "Edit" msgstr "編輯" msgid "Mode:" msgstr "éš±ç§ï¼š" msgid "Description" msgstr "æè¿°" msgid "Settings" msgstr "設定" msgid "Shortcuts" msgstr "å¿«æ·éµ" msgid "About" msgstr "關於" msgid "Quit" msgstr "離開" msgid "Add New Filter" msgstr "æ–°å¢žéŽæ¿¾å™¨" msgid "Regular Expression:" msgstr "" msgid "Test:" msgstr "" msgid "Add New Snippet" msgstr "新增片語" msgid "Keyword" msgstr "é—œéµå­—" msgid "Replacement" msgstr "æ›¿æ›æˆ" msgid "Create New List" msgstr "建立新列表" msgid "Name:" msgstr "å稱:" msgid "Create" msgstr "建立" msgid "Write Direct Message" msgstr "寫新訊æ¯" msgid "Add to/Remove from List" msgstr "新增到列表中或從列表中移除" msgid "Blocked" msgstr "å°éŽ–" msgid "Muted" msgstr "éœéŸ³" msgid "Retweets disabled" msgstr "ç¦æ­¢è½‰æŽ¨" #, fuzzy msgid "More actions" msgstr "æåŠ" msgid "Follows you" msgstr "跟隨你" msgid "Tweets" msgstr "推文" msgid "Followers" msgstr "跟隨者" msgid "Following" msgstr "正在關注" msgid "Use dark theme" msgstr "" #, fuzzy msgid "Shortcut key" msgstr "å¿«æ·éµ" msgid "Alt" msgstr "" msgid "Ctrl" msgstr "" msgid "Shift" msgstr "" msgid "Super" msgstr "" msgid "Primary" msgstr "" #, fuzzy msgid "Timelines" msgstr "在時間軸上隱è—" msgid "Show inline media" msgstr "顯示推文中媒體" msgid "Always show" msgstr "æ°¸é é¡¯ç¤º" msgid "Always hide" msgstr "æ°¸é éš±è—" msgid "Hide in timeline" msgstr "在時間軸上隱è—" msgid "Auto scroll on new tweets" msgstr "有新推文時自動滾動" msgid "Double-click activation" msgstr "雙擊æ“作" msgid "Notifications" msgstr "通知" msgid "On New Tweets" msgstr "有新推文" msgid "Never" msgstr "從ä¸" msgid "Every" msgstr "æ¯å€‹" msgid "Stack 5" msgstr "æ¯5個" msgid "Stack 10" msgstr "æ¯10個" msgid "Stack 25" msgstr "æ¯25個" msgid "Stack 50" msgstr "æ¯50個" msgid "On New Mentions" msgstr "有新æåŠ" msgid "On New Messages" msgstr "有新訊æ¯" msgid "Interface" msgstr "界é¢" #. TRANSLATORS: 'Normal' font size combo box item in the settings msgid "Normal" msgstr "" #. TRANSLATORS: 'Large' font size combo box item in the settings msgid "Large" msgstr "" #. TRANSLATORS: 'X-Large' font size combo box item in the settings msgid "X-Large" msgstr "" #. TRANSLATORS: 'XX-Large' font size combo box item in the settings msgid "XX-Large" msgstr "" #, fuzzy msgid "Tweet scale" msgstr "推文" msgid "Round avatars" msgstr "圓形的大頭貼" msgid "Remove trailing hashtags" msgstr "移除貼文標籤" msgid "Remove media links" msgstr "移除媒體連çµ" msgid "Hide inappropriate content" msgstr "éš±è—æ¨™ç¤ºç‚ºæ•感內容之推文" msgid "Translation" msgstr "" msgid "Translation service" msgstr "" msgid "Google" msgstr "" msgid "Bing" msgstr "" msgid "DeepL" msgstr "" msgid "Custom" msgstr "" msgid "Custom translation URL" msgstr "" msgid "No snippets configured." msgstr "沒有設定片語" msgid "You can activate snippets by writing the keyword and pressing TAB." msgstr "ä½ å¯ä»¥åœ¨æ’°å¯«æ™‚輸入關éµå­—後按TABéµè½‰æ›æˆç‰‡èªžã€‚" msgid "Snippets" msgstr "片語" msgctxt "shortcuts window" msgid "General" msgstr "一般" msgctxt "shortcuts window" msgid "Compose Tweet" msgstr "撰寫新推文" msgctxt "shortcuts window" msgid "Show Account settings" msgstr "顯示帳戶設定" msgctxt "shortcuts window" msgid "Show Accounts Popover" msgstr "顯示帳戶列表" msgctxt "shortcuts window" msgid "Show Application Settings" msgstr "顯示程å¼è¨­å®š" msgctxt "shortcuts window" msgid "Toggle Topbar" msgstr "顯示/éš±è—頂端分é " msgctxt "shortcuts window" msgid "Go Back" msgstr "上一é " msgctxt "shortcuts window" msgid "Go Forward" msgstr "下一é " msgctxt "shortcuts window" msgid "Go to nth page" msgstr "切æ›ç¬¬...分é " msgctxt "shortcuts window" msgid "Tweets" msgstr "推文" msgctxt "shortcuts window" msgid "Retweet" msgstr "轉推" #, fuzzy msgctxt "shortcuts window" msgid "Like" msgstr "喜歡" msgctxt "shortcuts window" msgid "Reply" msgstr "回覆" msgctxt "shortcuts window" msgid "Quote" msgstr "引用" msgctxt "shortcuts window" msgid "Show Details" msgstr "顯示詳細資料" msgctxt "shortcuts window" msgid "Delete" msgstr "刪除" msgctxt "shortcuts window" msgid "Compose" msgstr "撰寫新推文" msgid "Show Emoji Chooser" msgstr "é¡¯ç¤ºé¡æ–‡å­—清單" msgid "Start new conversation" msgstr "é–‹å§‹æ–°å°è©±" msgid "With:" msgstr "跟:" msgid "Go" msgstr "é–‹å§‹" msgid "Quote" msgstr "引用" msgid "Retweet tweet" msgstr "轉推" #, fuzzy msgid "Like tweet" msgstr "個喜歡" msgid "Reply to tweet" msgstr "回覆" msgid "More" msgstr "更多" msgid "Translate" msgstr "" #, fuzzy msgid "Like" msgstr "喜歡" msgid "Reply" msgstr "回覆" msgid "Liked" msgstr "" #, fuzzy msgid "Retweeted" msgstr "轉推" msgid "Unblock" msgstr "解除å°éŽ–" msgid "Show settings of this account" msgstr "顯示這個帳號的設定" msgid "Open in new window" msgstr "在新視窗中開啟" msgid "Go to profile" msgstr "å‰å¾€å€‹äººæª”案" msgid "Created" msgstr "建立" msgid "Subscribed to" msgstr "訂閱的" #~ msgid "Replying to" #~ msgstr "回覆給" #~ msgid "and" #~ msgstr "å’Œ" #~ msgid "Actions" #~ msgstr "動作" #~ msgid "Add Image" #~ msgstr "新增媒體" #, fuzzy, c-format #~ msgid "%s (%s) followers" #~ msgstr "跟隨者" #~ msgid "List" #~ msgstr "列表" #~ msgid "uk.co.ibboard.cawbird" #~ msgstr "uk.co.ibboard.cawbird" #~ msgid "" #~ "Unauthorized. Most of the time, this means that there's something wrong " #~ "with the Twitter servers and you should try again later" #~ msgstr "授權失敗。通常這是因為Twitter伺æœå™¨å‡ºéŒ¯ï¼Œè«‹ç¨å¾Œå†è©¦ã€‚" #~ msgid "Could not load tweets" #~ msgstr "無法載入推文" cawbird-1.4.2/screenshot1.jpg000066400000000000000000002242301416632607600161070ustar00rootroot00000000000000ÿØÿàJFIFHHÿâ°ICC_PROFILE lcms0mntrRGB XYZ ä 4acspAPPLöÖÓ-lcms desc @cprt`6wtpt˜chad¬,rXYZØbXYZìgXYZrTRC gTRC bTRC chrm4$dmndX$dmdd|$mluc enUS$GIMP built-in sRGBmluc enUSPublic DomainXYZ öÖÓ-sf32 BÞÿÿó%“ýÿÿû¡ÿÿý¢ÜÀnXYZ o 8õXYZ $Ÿ„¶ÄXYZ b—·‡Ùparaffò§ YÐ [chrm£×T|LÍ™š&g\mluc enUSGIMPmluc enUSsRGBÿÛC     ÿÛC   ÿÂiÿÄ ÿÄÿÚ ÷üjå0PC KA0-RˆU…Ae©~(é—Gѹ¾ž·Ó˜³fjbòCU3s r¥’ʆ˜Ò±®mÙÃYÝK…*•€²§Â»Ì‡Óy¾¥..³¬²fÚ©!S7.<ûRÑE/¬ñ wy {ž]sêÙÖ6j™!§‡žê!ØËªL%¢ØJ[  r¹(J´ hØÏMJbYšlÐiÜîX}gZºœõžÁ+¹ïZڀǪ§E‡ë>³5b“³EvàŒÎä1SœÔïs Ô¹Û°<òÎÙnÀcW:Ü çÖu‹³UNŠ8«;Y@9û!:i@0åQ 4(ÅžmžÖîuŒM%¸ÞÆÀ?:ø}yõ|Bå|‡èøý çÜ>·ÉÒÑtÀMµŒŒÔ7Œ…è ‘Ä‚ŠJi—@æKÅs ?¦Ê¢Xá±-nú²–`ùwj‹cRAGÊû·¸ù%èËç¨å±sÔvÏ!㇣¢ÍË’X¥­WšïãîÅ4>E -é«Þ\ï˜ù¹Ï'Jù-(MæŽ5¨<*[ ÍmO&—\äò¶âVÉ’eJ—Bú}×(y¢|?cDW a,ûšýóclO.¨ó-«bCìEnkÙ’–×YoS;d¢_Öz_W>sÇÒ]æŒÝž¹œÉã´§ØÌßB—î¹@ÍáûM”Z®E,[¯êó¬~†RS!W"@zÛÜ¥‚+i«a‡á7I]¨M.™¡ËILÍvŽò_ºå4>Ô–e)sYVPÕÕïYSŸ‡?Óâ×çÑ^ÛÿCéóý¿=ÉgóÈȺÅ>£óï×8z~jvòġNþ_¸î€Íáô“1º«GWú5z¤³7ޱùKVÀ0DzîcéöÞÔùßÒüGMÝWæ}½¯³ð¸Ÿ~—ÏúùºÞ_¯ä¨çÄÁÕ)êüõõǾ ÇÕó¿¯ÃÓ›åïõŸ¹¦ÀÍâìâ®´Ë ef»rÛÒ÷ÍLÜ69#VRÿ¥kÏô.ý/‘Ó|¿Òæxþµ__Ë£ÛÍ6½ïÑ[¿ËæçæåIcb_¹øôóo7·æ/_“ ·‘vNÃzû®PóCâFbT(âEßï%®Íj\¹TµœÅ¢7½Ëô“ù}k¥9壿%^Þ"Ç·§±Ðtú|„ü“/‚[~¾ç½/³åîøóßO¢z ÷d æéðå‰+‘£iÔ½diƒÂÕ±ÂÅb¶^þ›«Èðyùÿoçiuñ$…,+[~_Ðlwú<–¿!#—´ò×Òœ}0ÎÞ?ìùðÑb+“¿_»%4> ²I[LKÇ-ÜÝ‘º’,LÄGç®÷ÔÃÆó¾¿Ë6â\÷RË2òô\ãö×ÑÏ ·æ÷q>ããÓÂøúþ}öüú{“óÌ;«£cУÍO‡)aEHÅ–ã[}PÆgÔÚM&©E«-Ýçõ™æúy±SÑóonn³çõçÏLŽUúø*oçýÃY<½_1z|™{ç¶1šú®¤;åû¶PóSáÛdÎQ[jĺt{Ó#œ«r²²” zn Û±Ïïäy·S2®¼uºð¿Ó¬~n¯µæwŸSåü·äôÔïÄ+KܡБßå÷B€šR¢Â*ÖïM[ÔDÄæÎÌj¨ƒ‰2뼿³èﶯ£ÏWž³1çÈ¿:yß]íÜùfáë±èóóŸcóü?£æ»RQæáEW÷;´ûG7‰&¢AH"سwZaŸ™Nb¤K|úö~è±6«ddVœáÏ(7ñnfU¸£ßÃG×òáÖZGJ«˜hÓл%?ξ,ÆàÆYR]MŠéOލkœ¾ÎžZ1BjϱÓN½ôèöÎO-ãg•|øúýuÏøoÓçÄ÷|ÌwÄŽå¶<ˆAlQ@>í”8lëænš+ ²fçn7._·{b¡Ê²/ÏoSÏõ^íßóº^ïòO‚Ö—é>¹ê}¾Ž>6ðû,ôáæ¼üGx»ñ|~“ÏKW?Ùâ«èø°ê5#©r,*%ô ~íÏ#ä}-g[·pµ¥š«•ߎGNtR÷—Ñ«Ÿ¥/Öcqòzg³ó½÷Õù~uÁäþnczNôtá1ëá¾³¡ôù|7ÏàìþwÕ·›™ÜƒŸØIëw_/Gçéôç4.d—1ÕM_@_ºå7>)Í|ŽjyÖÛRŒé,³&u>=óù?a‹ÏÀžŸÎt]|>¯¿c»cŠååô>Ü»¿n~pñ{!áÛ|ü›>êì–X¡Ÿ[|ÿ¤ƒ×ù–k„±,ÌW-¶•½æŸt´5ñß.ð‰R·d}’J«>;Úùÿ¬çºpo³òºø"oÚ¹}MκôžgV?£}Ï|Œç[–¹^4’ydÔi_–ÝQëçvsbR¡±ó0Ý÷vý¿ªFz|³Ë×M#–ݪÌÊÂ\uØø§âý|¡úžƒ§ÎŠ„èï_HãïÜôrê=¾jV:ò¡§Œø1Äqã …¬t†È·‰³ÒÎbÍSÔeÎ~ów7Ð5>ÕÞ€ÏO™óì³›oã}ˆóÕŸkáCs?>ÓüÓp¾žZ¿Sâköñº)£d•f–Ã[=§aé˺òf§—ù¸ùÏŸæÄÝÌêævªÛÉò-#¬lÖ¸‡y†oÑo?³ú€˜ÏOåì‰_p?7ìèðôtCåÒù?s…ôã¤ú²ïóÚÓ¡â#êÚb—:OYú¾-mòäùtñ“é«"&ãµù¦3%vkZíf“)³|¦ÏN›~´;à9¬ôñ¾^ÈÖf`ÎøÓæüÞêýžƒìù]£Ãoªð°‡”e¶nzÏo›¡õy¹§^/ãûà•,¬,°” ¹·5+ÓRɘ–ë¢sú·Óã ,ïË8úÚ¯™Šßòý.“|;GŠZ—0XÖDR!Ãá”ãcÑööx¹wnkåûcQ ¹±:¤W]McènþÃÎü»®Ycf6¤5+)M¥Ì-Ž %G+E##•w=/è|úÝ3Àü¯¤Èª¼¤+ ¥uÛ¸öîþ æsÓÅ8û\Id²Ù‡Å°’J$r¼b k.‡S"ŽÓéxdíÏŠùŸB.zRKD6›-„ #5µcíão=¹üNËÔ°…0…§Ð‹f¦¹ú¯ sØéã{EG,u]‹´g)l̶˜ÔƒêDm-w_KÁ\ñlžN–ìtŒU±Â¤ÂÔJû"XÍ;\ôx€ŸÇOóû¤–"YbiTnF ±ˆ<˜®‘g3¥Àk¿µû~üúƒž¬£ Y hë# ª·l#QŸWôø€›ÇOóû묌ºi´’½\‰”TÊ‘cHÈìrà ·Êø~—#^wëò$¹šYnUK ‹V8]+Ó#£×?\ôx€ŸÇOó{ÖÔf)µD¦#Z©$:ÄÓ¢ÄQFÄ‘­IN*cÓä¼þ·Eßâúmâë'Ì­µÑÂTƒÉb5ˆÒß?mïás=ÈÇO8òûQ[#mA””âLÅP}‚ È™ë`q jï–ÞùôÚá4·5•‡QJ62xôâ±éæ¯KQV·o?Vôø€#<ëÉíe­¹…YªÜ¦’Zt:G+¡iHe%uM•ÇUUè{y»p¼’"«¬d°ç\·>ù“\åèùu5Xôø@žÏ_ ó{¬Ã,ad“(ÉéÑZ/,ˆÀ$E¨Ò6¥‰ –]’µ´ÖÕ­ÍrÔß-}s±‹_žóô^øÏ³‰óûYj럳z| Ïc¯ù}ÏW"gNÜt#+lñk0WÂ*S‘Õ$ ™U·%{Yœþí¤½’Ôý3¿ÓÏÏÝerîcQˆoÞ~»êð€yqñÑ@Q´èJl-I<h£†Š<4$6ƒ†E(…bÁi67€ˆ @·«Ï[yì¼^¬4ÑV“’ÂÈ ¥$º²–ÈI€ˆ” Å<€¢v]9ïï78uó¢é!®sæÍY±.™…O‚E°4œhãLÕ9Ñås§*\Z.Œ 0xÑÀ4Pp(   &+–ãÎpòê’±Še‹ÆY8⩦f &i¬U3Í䤠3ÀÑ9ó "3…*!–k Œlò¬W$<”„˜ ʼn†T%Ö9áàfHF‡Í½|™Ð­kúxúlëÞøý\êt+â"’^V—‡’0a(„$à|GèùÝïƒê÷¾O¡âŸcáz~>ñâ÷r% 9k/K`çlÖ—¸\ãTÊjéx¦_5#9ñçB|EèùÝ—ƒëzéxÔø]Ç^?Oqö  Ñâ G  ÎÜ1Õù}Ú8ïK^l¿OO_ôø¹R¸âsLÉ$-•Š"Æx¨_9“LÊ:£SXì>ñöµž';¾¿Ÿ¥vñ€ÉÅ7Ï3L¤Ý[†yT×,եᦄN^5@¬sÅÃ\œtß3‘ê©"€õHG<”€Ž7Ï<+–ÍóÔ2SÜ-æI®^#-ñF‡:j 8Æ6 #;ñÀ:o‘o/Šú–±¤,Â%uÌ6ÆÆŒ&$I@ªHNV,€é¾r?GÆT™º¾~¹^ŽH(•³³åïÌy»U,– †ÁPÉh‰Ì"Ù¢@BNg–ÊbŠ:o˜eáIHÈK&Y¤8¦H Λç"g›$¤Å“Ÿ/•ÈE4ΘçMóÅÊy%=MT¦ÃFˆ ( €4A ~¿§Ðùw:yããëÏòû«sëgÑå]󇇢‡‡è&HÇgC@Î›çÆ¸ÝFkõ-ÙÍXâ1ËR&ªˆ¨0el}oƒ³õ¾$Ý3SÅõ"óz¡áÞ.^Œÿ7¦é3§ôã{Ýàmf|O­wžö€7ω¼_S•ÆçÆ¥ôx»o_…ŠÍVÄp”JÜÖ* ¿¡òúOÑ~1yúr¾_螸¿/ë'’ú¸³:«Ãªæº'ãk÷‘¥¯£óWxËùWëg¼°9Ó|ø›ÃõyŽ‹ÙÔ~¿ŸÛû>sUº­¨¢@š%d%Gú?Äî}ŸÊWó}¼¿›úkâþ‚.}'ì~jµàóÿÈ~ÃWÑæ²Ç=â÷K÷A÷>g7ò½Ù¸Ó¦4~¯Éú{çýß7Ï‹~Ôãùöžk_N‹ê|(Ø•ØI[4Õ!7ŽûößÊŸÇÛ—âû™¾«CËî¡æõzÚü\¿ ù\Oáÿ¢éöóXÖ9Ÿ¾ï7_öþ7ð>톉gÕýyw é¾|Wáú\‡>ˬٺ¥ðö7”¤É’²Ö•¹­VÊÛŸSýwóŠÕ›äú¸?'îjû†7ƒëô>ï•CÏìËñ}&sé_—[ý9AÏhAÖÆ«gOFZ„C#ê^œû{7ÏŠüžîG—£zç~BJ­U¬^¼´}>NŸ·VMC5tøj™6T‚UU‡Jª¹4Œ€)$)©Œ¯©{qîwçMóæ|öó.Ž»•ôN1&šªž;Û—-Ñ×z¼“K‘ÏÔÎ>‡ã]W%¢ljÊ[•e¤‘‘®clé]ØšŠ!’/'=/Ÿº?Jíz·×gVµŸWõy7:stß<“§˜ížS“ãcÑ/^‡ÑÒñ«Ï-Œ×8n{<úß¾ö"L[2:YzâÏ~O\ÙmK‹9ò<|ðtÖÇKÚóíï1áó\ùØêþ§–ýv·}ý6yú·9óõÏÎÛíÈ9Ó|ù«ÇìÀš“ž:¾|v^þý¯§‡¹ä\Ôc+§=ÿ?^χ}ÿ/©:Þ^jq}þ…¡æglW±«ÎÞ|†x9­N}æ¸ì¾gJ>..õ-xûã}N]Ï›ô}ü½o£ý_Ç~AÿÄ5!0 "1265#3@P$%&AB`ÿÚ]x¦\S#ä$‹ŠeÅ2>HÐrJ¸g\3£äù rä%ÈK—!.B\„¹ rä%ÈK—!.B\„¹ rä%ÈK—!.B\„¹ qL¸¦O̹ rä%ÈIËrÚj¿ù±ñ¶O%—¢ØèzS›¢j~:¬oPA–»oñ¥ø´c^v³߿ϒËÕÄ€u9)n°[©œ«vp½^HûúÛ_+}éôr `=cUì113xMj*꽨­…óR¼5rÙì¬vOÙçèÕÕ9ò±ãèA‹©kñ£þ;•Zäž^+Þ‡QbìXÈ×ÀO_X)Ï1G¦,E,X[qסƒ:Óc°SÓ¿éõb>x=Í•:1¶Ào ¦=²•±ÔýßSÀOŒ¡:tå“¡:vW¡J¶2Œsq.ñ—xÊI¹U{ wÀ»àV'iŸýžñ—x˼dß6óu©­Mjkq­MT‹¸]‹.Å—b˱eزìYv,»]‹.Å—b˱eزìYv,»]‹.Å—b˱eزìYv,»]‹.Å—b˱eزìYv,»]‹.Å—b˱eزìYv,£üVG(r³Ñ›Ãã7Pž¸ëz·ÉÕ½Sý3 Úf³!–À®JJ¸|—½±ß&RñcàÅe"_#ý¥ë®ÞÍr ù'»vÎG|²4ücüVRœ¶Ç€³ñÆøØéÓuN¨SƒäËbkæiôÏIVéÀù w…|F¸b)>É”¢Y1X³ÇËg§Ú~§ùn᣷>#.$¼}Õ+/u̽×2÷\ËÝr¯u̪R*ë×®7\n¸ÝqºãuÆë×®7\n¸ÝqºãuÆë×®7\n¸ÝqºãuÆë×®7\n¸ÝqºãuÆë×®7\n¸Ýqºã ~Ñ0­RëÜ&F~pÛk¨)R’ŽržJÇüOS‹—Tãû~‰¥ÊYÌÄ}kZLÏHõ|ÏTJ‘óÜ]ž¨»^¾g%xðÔÖ»«}K`-¿R\(­u]±œ2WýᄾyegM'wVHqå“—¼Çäd–h34>÷qŒ/Í$µ,wÿ¡rôµ¬=ÒÝY§“Ǩ¤áêçêêiq¹ªPZÑŠ”¾Åÿh]œdÇÕš>Ö±õY¬à)X°tçšj¬‘ѯ$WŠ´aN(äÚÚðG·´ƒiU„ÔTaˆ § vpmŽ „Ð(@× |ƒbþ=OûGD@6mãë•‚ê‹3M˜ö/ûM©犴ÔuÞfÌ‹©ò†týù«Ù9+ çâŠ2í.%ºŠ°XÈQÕïñ¹@ÊCQA1\Ïqй“jrd8áê:óMW;ZåÚýJ2Å&tN_ê:Í8å#*Gši1Å›Î%Ž¥«R+Y!«<ÙXâz™î_N¿JEÔ]O¶*;0 ˜ì¿FãúªŸCtÌÝ-×Fƒ&"¬­œEk2*”µ&ÅVb³N;hñðÈOX ¡¼˜ø$p¡R‡^—Zhä«¥&: ]±Xb©ø:nÞè«ÈX8b©ØDõB„! ¸ºó!£¯wéVŒå‹^ЏCêWÀP©f\uy”Xêð(èÁíbîlÈñW7ʼyd‘ïØv;’ Ê¡•™­Á•±P¿8¼ù2%4-ï)ùéÌS×ÿû­¬¶›—sG°W®1Õâmƒ«F"™´ÿ‚ê̼ØŸk™–_³ âöa|^Ì/‹Ù•ñ{0‹Úæ]{SÊÇíG,rµÜÀ—ÅüÂø½˜_³ âöa|^̯‹ù”Þ×ó âî_qû\Ì ü]̯‹¹}>/eôø»™_³ âöa|]Ì!öµ—Ûñw1©{Zʳ|\Ë/‹¹…ñs0£ö¯™4þÕó;¤ö­—‰¾.æL{JÉæ³¾‡´ÓþM|Cø„­²>ãùmó8öxk« èå ̨×î­dªv–H\<" ç—Ã>ßwóQâ5'hÓ18Í+ÉáÐ?·úÒ?Kù>ʼ]ÌÒãô!ƒAÚÏ;m—婵ÌßRñ-5‚b€æ°s›¾¾î ÖBŸþ裺 >g 0:wúº.ý¿Ðö‘ú_Žž_uö}æÍ»!7Ž[@M"Ñ7—€§ðfL™µ-›°¼§ƒ}䙥W%­ÀXvÈÙ™ê÷2Ó»x¹xtåÞ‡´Òõ×ÀÃh­®É‡sh-®Èd>[r™È¶êß.ï ˆìîƒì„w9Ž™´Z<ˆbÞì.T³Û±}Ä7;¶ˆ]Ùnã¢ÂáæÍd1Øø0΢ÌíYl“ÜäóŠèÿËýi¥òhÎú§dëU¯“¾¡ë·Ÿòø€rÄò8Ô`Ž:ÂàL)¯"¸»‡×¹$ŒTæ'µ­¶Ù¶è!tL¶žÊñQfJĺüû¡íôçó}4[Ó­<Ê:á¬vX…L%â2!B¦fZ2ç²2Ý4ò–Ø8‘Ù#‘ï“„J6{„Æ#xú"óØêCÚ+kÑ®­‹c·ÉµÞ8ûR9Æó–ó‘þXêytn}N»eÎë›FäÕotòÊJ8%qç}ö¤Õ§<Ãîˆ\|:öÿC¯£åé9c qdìæœtM ¦¯ÈÛ6€6a·1퉴òÑGHôúW-}¯a®b¥/¥wNÀÆ~ìÝ+n_¥µ ¸0‚ä8ñ•4ÕéÛ‡³¼%D¦}T­µÛêr_uöO÷ð×_€ýÃÐëwv鈱"cN¼vìßÄÅLG DÕå·Ž¡)%üZWúL^WâwQQ)Õ3÷XtGQI=.¢³Ô¦¨Tìo?f¸~ßr䳫)>¹›¥YãkY,GKôý5Ö KG#¹r"ƒW6¶×dí§€â$!åªèÜ=hÇÒ!’Û»áQY¢ï6HñG¥[CaY”b¸ÜyÇ樮 •Æšvdr¹.q‚¤fàeŽkQAô¬'Pâîc¦=liVãzx+%ÚÁ»ûµs£•É=¨_iŽ/£ÉL¤fdr‰¢WÛ±èÛ½h¿¦éæèL™1“¿,Œ"n·¢nš»ÌCWKΈœŸf‹o†ØÏô;4Ñâº\Ž—è‰p7å¯9™Žlƒu6®Ý¿Üã_ëmÚ8»h«;>åöZorêŒ4^lï;”{ nô:õ·t™W]ºh´Aâv\>|h!CÏAžç<„¢sÖ…æ¿^3X+„Ã%˜¥’ÜòÂÖ:jœÑ[è×v·âq ƒ'ÑìúYÛzvÑnÑ9ê„45·GätãçУ·«½µý2õY”q°@\\`9Utð“&Ýö…îZyÌj–Þ2\NðèØƒNVóTlpÍbxîLîÅç€#]CaŽ×>‹vö!qA¹üÜ‘jÄõ·°Ö&Í6»¹ù¯1xt@kÕ^‡X>Þœä&¯¼!@3IµÅ3“&$d5Bí˜éâÞ8Þ®öj`ÉèjOŽC‹m䬃*B ÏÀ¨%w¸ Ñ´SVãLnNÚ36Ðs6@Å©Wå]‹Š*å½À™ËÉl[RvÜ᪆]£uG¡Ô¡É„jc§ ö¼ÃvŠRûHÂa,ãY®Þy‹…Ñ»&ªë¶}J&d06ƒ :ìJLpk.3S§Wµš:ðHÁ®¦ Öhž(Ýš—Ñ0œ¢Ä‹!Ç©*lnÜ” î›d†¡ƒbw ®Š=D˜z$º§Ðê6rÃ3¾Ã'i6©æWðÒÔC8Ùç–áâzy«0 :x“§òZ1i±M›[[Wˆ€ŠxÙÙÁ82ÚÉáT‘ , Zç= ÛéŒcÕõóߣ‘:Ñ´8†FafoºØ¸üô\nœ]Ÿo‡ÿ[Vš1y4&ìõeä†Ìlð³ù»j{4-VZ'e·Í…™lEáfO¨«³¶! §¡Ô…³ 7df,„›»™ û­Ž×…Ø,oLz6ôÎεd+U½3¶öÛ¢mIö-ŒJ¹qE?÷â8Ý…¼7.O%÷M ¹hâ›ÍöèÏäœ\@3dý£þðccÑÇW!CêCô±HKi¨÷’3(‚9›•£M#<º‚ÛÈÜZ&mVÒtìê¶Öžäõ¶•ø!i,w?å½}ÖÇ\n¶ºÚ…ÈX‹ë&Z¾Œé›VÅiïC:Ú⎻náÑ0;§bC\Sƒ2f_šÚ¥ÜKväã0ˆIªbÈìgdØâ•öó"•ÝÀþ§ÆÉ+Ë@¼õf[Ö©™“²ÚëGwfó!d,ζ§a[V)¿î>†oøÂ&uø³ætÓ ¶¬OäÉÉЙ:ü“FÍààÄ…Ó» /7óÜ̉÷3šæ}ŽÜPhîúÕ8y3m^Kïâiˆ„·»§ÕneŒ6|¡Ô¦#ožåä›Dò‹?8òFLe¹™ÃÍ1 ‘ÉÜ–ŒëO'dÚ'Bí§žœ£ §={ðäY˜L]Ÿì¹<·-ËvÉ¢cÕÙ?’v^KWÔ¼Ö$tÈzïâ\D–¢Ìò>šò» jÂâÂ+k²òÛöO#8î&mÄèdܘˆ\õ#Õ;¦7Ï\á«C5k6??6R¦ôHk—DϢݪó×VÜÚ&qC0±šÄIµv#òÅ3wþ‡Q>Ü8‹’ØÛÜ]G+¢ÜèH…?›nv—@ß«ry¼Ì,G«îòÞ+¸\»Ê·¬·³<=›Å£®úGô-ÌIä˜mÄÇ5’Õ›Ri7!6”uÐ@–älj1p,IÿÜ} ÿñ?f×EÈ(KDÇ£‰ "òv-IrÉÇy»” w5.WdĘ÷F2Ñv&ÉIÃJB]2ÝÞ:h茀a°r¾ó×ÉßoÕæÈ4•nvt&Búù>£,½}¡&<ö‹)„S޽èÍ6æ$Ö¢Xù6H0Í"ÛY‘ ÎE#‹3ˆu˜ Ú‹¹4K?kV”¼ð× «Z<¤ Í– %Œet{Á«I1?“Áä,È´fDz ù¦o¤Å´ÂÆÃ‘ô3¯¦(t‘ly ÖnÜ]̳È{QJ¥75šw ¢m§'™Ï 9ا’æc^í“||p‰·Ó^mLr“ Á{7³®rÕärf›EÜ»Ùg~t6XúžÎ¬RêøFÛ’ô3~XÆf0ÀW2•Ü]I¹ËE$$Ó#†­dµg‘“1ï"e×…K¤ÉçfG&ôhÅT&*±G›Zʱ€1:øB›[a¡Ä#²qòãvs[\…Ãê&Ú\Z¦m„Nî™·´Éz¿âô&}Tß@ÌeÇie†s–jPÚ«mE  šÌ̈ö¿Zj?S¢æOöì֑ƾ<"EHSӕôWÖ,úi½jì£} ™¤MÔã ÿíŸGÛåæÌÑíLÚ–_yz¯ã7×Eæš7\.ÁÝGÑŽ‘B+êfxÜ‚GëI"޳”ˆÐǵmó)<‰ÐtÈ‹¾2¸¼˜ÈÙ{•´&QEˆ²È±vT”¦…0Ž»\—­Ãç‡ 2†_øý²ÿßÝ:gðgdNúèki:fÑ}Öš)]‚ÎE¼U#­¾«ÖÛæéÅ™q³§…“6Œõ„Ü*FiƒÆÂŠ!v.9†å>Öm»ÇFo¶+GÈúo<~ŵ²ë¤Ú³ÊéÜÙ Œ^i…8­«F[Ál[šFÚ¶1-ŽÈøÆ=ìH‰‰ ,±yY`Qψ˹˜ôL{›WeÈÊ]®äJ&8}êîJÌËû²>†|Ü1!`ÊB” ŒÈdtÓm™;›¿3ºä&¥˜‰ŽA\†è¬œkº‘×&ä$îŠMƒsŒ[Q·Ðê]¬Ï.!¿•£m…ZXÈs Ï г¢ŠGRÅ`—g(¦¯jÞR("”ŠÁ¬1¼½ ûk‰0#µy:vQŽÅ¹Ý;r/Å hûдÕiªó_uæ¶ê¢LZ£qÖCqvû¦fdï¢"(¤-9A š¨.ÍT¿¨·1å呤¹$䌶¯ÉbY½ãèX€-Eî*KÜT—¸©¯qÓM‚¤ËÜtÙ{Š’÷ %îI°µ{’¦¾çª½ÏU{žª÷-DØjŒ½ÏU{š£7¹ê¢ÃÔ&÷ %îzº{–¢÷-Eîj‰°T…{Š–¯ƒ¦èðdÁÓ|%7^࢟IÓà):.˜Çl!PâjÁ'‡. \@¸qâÄ ˆ. \@¸qâÄ ˆ. \@¸qâÄ ˆ. \@¸qâÄ ˆ. \@¸qâÄ ˆ. \@¸q⑉‰gr+,.Fs´f1…\í+…-Èâ²§6³AZ¥Š½ý~í®@MÞ@ŠÜqå«JÃn§ŽÀ¨£$Œž!#ú/b1?ši%êiñ¸ˆZ¦0Ç©1˜Ü<µ©ßЛw'wÜw§{;4Ðá1râ©EÓZx)«_¯Ñ¶bÇæzX®Û÷,¾ù>™·&6ïOö•:L  †®a‰ùÆAùum|˜YŸU¯žæÓç BGùØQ˜ÆÈ2‚c&ŽAb)$ù¯à#²öºCº =C’è;KÏD9hAM÷qZƾR:ÓW¶gÚÙÆgg’¼¦\WÜQeJ™P±ÓArÄÏZÙËS¹÷©S¼×#¡'=ZÖ ÂðÞ{Õ`Ì“X©j~Ÿîµü…K’˜nP­’†Æ&+QÅZ¥ÂŠxnŒvÚx"–Õ‰iË6¡´†­‰”V«G5+“V¢Ç’ìe€1PZ×籉­fuö]Ä[yCRÉÖ ^ö­ÈDÀÛ¡ÛË?qÞPÝã!¯ hk7ú7k½ªpâMìŽHäÈáe·1áOe¨&·H°vYpö¬Ïî‹o}Å;™ažÍY1Õ-cìv—ª^¹rÅŒ0ãêϓݶ¶Wbµ˜£bÔã…³Z+˜ë6–N¤Ò´pw”k⦎|u ! ~Pœqö ‡z’aíÈÞ§P…Œ¸Aeò5E‹!X Kp ~±Y:ö ‚äPçj­v™î@3ÖÊÖ²”««äëñÿ¿ÔÄàùdžŸL8P³†)«æhÙœCÁ âLqôqƒEëôÉCLUµÁÞ˜Ù^ÇL½‘‹Mcýþ þ ? ·´q¹/ˆó/‰!ö3¯ˆ³/ˆÓ/ˆó/‰/‰3/‰S/‰s/‰s/‰“/‰“/‰“¯‰³¯‰³/‰³¯‰ó¯‰ó¯Š¨½¢ÜG×7[’~»ÑKí7´£Mí!?´"t~Ð.ƒ|N|O|O|P™|PtþQóXC¨?ˆê&oêr\¢Ê"b}«jqN+E¢Ñh´Z-‹E¢ÑWÄË3EZµb’ƨíp·uÈ/6çàÞˆ''r"A!ÆýÞõÚÇ2’"‰ü:õ_C¨?ˆë?gyŒœcrUœ£¶–‰Ù;xh´NËE¢Ñ;*ôd´ñT†’ŽAÐcbš>=$´rR–$IÇO»“Ô”E; ´ûJ ÈÚ.†ý[ÐêâðêÏÙ£ ©Ÿé‡Îë'ðvNËE§†‹jÚ¶ª¸¶ÞÌž'–_ªfúßíŒÆÕa³CbáÑh´NËE¢Ñiòw”øö=×äy¦áŠZZ!€ëÎÖa³+[.I«[ ’[Â̼ÒRã6³a«eÛv‹O D»¿LzAü@~YçÔ»QÇ e4M—–·É¢Óåt_JfmóÈ1µx^IJmÞã$œ"¥›TñhiµÙm'gy¤!`}‹E¢èŸÖ= þ ?«vþ¥©X/ìkýªÓœ@]5½¼4NËE¢Ñ;'dsBÊ[jBöep„Øu-@œu‹Œµìòλ»L¹çg±É,œd™›Œ—§×®‹m:gÐêâðêÝG©Ä>X_¦%ã.kÿN[a.¸è¹ªž?!Ê~$L+_ÇE¢ÓÃE§ÌâéÅhœVž.º#_éC¨?ˆëñör¹bÂH%Ó9x×ãMɇý|Õ•; –ôq®êÜÏî{ö±F ¬% A^2(àj’î(m³½[fÈÊ*úvµô“ˆ×,J •ØJ¢Úˆv€Päi뼎áàíFÝ:Ânlg¡ÔÄá“Ù߯•<¶Kv?*6[jvY(ÿêÞq”¶4VÞ'‚îøã·J­r©¯É¹¤ŸGîIÈ­Á¡Ò¾éi*IÝÀ-LÃ:P¾Ÿ‡q¨÷FE5HîÉàJg¤&I«²ú„%Þ¦‹G0€ÔSZ¥~­‡¶Ñä;dÖ— c?5øv–¢¤üh` õ øGëµø©1aßòì±M ŒÿDÆ)_^‘Ö=%^X@ul¡­;)§á‚—é#EšXq¤'oOqqß!¶ê6ü§¿@Dß·ó6G´SJ»Lab©´ÚZPÝpוjØ(!%Ëa°X‡†ÇH -4Z$0Z.ÔoÜžf‹*šßSŠf-²$-›p¿ö•Á!®øQá«ÉXnÍAÖí+&§V_ @‹Ov³y×·<×]ÿ…Gšl¨Ym_(Ú|…½Ó$Ôäöè~¤é šX|+ñ.ß²Æá Û²mݧ»Wذ_O;ºŸ%¶Q°1øR¤Òt2Ë[(°R È_ƒÖàç!„&ÒÅ(l£KÂÄá›Õ0GÚžhŸ–Òâ¬"å`­`. =Ñyî’¾é©ÂV·SJdúÖ.Oì£ìZ,ªTªÕ-*­R9¹)RNíQýIͱh}H|„ÜsJ~8ÿe7H²ˆu©1féE6¢ ÐÑmNÿ¥=¼0^Ь»sì›ß!º*‚ùE«å;*µKƒ~#ÅCôøaw+ê¶7êgÊm¯7NÙ4 WjO;Ú>§.›ÙC9`EǺ¢§Á’uDˆ-Øû åtšm^VµQN7“Ô°˜GNDÖ¶ÓT³Vêwñݲ#tæl]Ê &ê(îäOu{¨±ŠíìÔÌK˜)DY Ô5ÙK‚l¤½†Šstš<—Õ¥H*Tr-ZjÂá3öMk`n¦œ7w,f-¯¬QKE^ëJ1éDRšŽm¡0 O#MåyZ†s´Ç‰¨"íÔÏ<¸fî«{­+J®JT¡…Ó;HQÂÝ RÊ$¬V!Ó;nÙvQ?PN*@ãõZZ{-¾S€=‘ô¶4S¾%.)òíìcòU ¹¤v–¨¡l ¤é7µŠ™Òšoe¥hZTVÓyÕ­jZ‚è| Äbˆ­@ÿKƒ{•akoæ» AÜ}©Ív²W Û¥ÀRu¸i5Ö¸f–‡¡Û#œ(¦ ÿ©R¥J•*T¦ÅAïIÿW`ýÓ —ô¬çû¯õÿdÜtçûµò?»Qt¿'K‹˜ùÿ²É›ûÈOùn™ŽçNª?®ÙR¥Jºš ^TQëÝ,WÖ0øK}Ný˜Œv-º¼Gä8õH£Áë=öC  úP†½OAõØ!ºªT¤†9EHÛGÃßòßõ ñSAýa›~aG+%˜o­¶-  B”Pëc>£zÎÿ’›‰úÒ=-Xl4q»aºÏBꢙh_Á|åJ²Óœ˜êâBt»ùî1nˆˆñBçðWtzpxgˆíÕú‡× ˜°¿ùB'Êu8ÙP44nT-Û(ê¶T±¸§IŠü 4›(Š_Ù;pším(ËF“'Óäm1üAa§uõ¨ž9lQî{õ0—49Ýósñ¥ÃdY.xýLü¾GðQÈÙZÎǧ ôjíI„t±ëfù_IýŠ€ikma£ÔBŠ+¸Žì™5íKP_Qút‚gã#7kéxCÜ\ð£hcCB’ð„N¾ÉÒ)HG¥FÙ\ýÐ+—Hnç€ÉÆ›i’=Û²~›»&2‚e‡Œ¼éR¼BÍ-Qß’J¥¨k>HÕ…²Û zpøäÿ¾®îŸ odZG~‹»!û¢Õe4²X\G¸ú“d¡ºcšàƒ…­mUî¿d´DUF†–Š PZ‚Ô…aXG§ ½*Ô‡eÄAÈ?óN-+†Â¤mvêVUÏJ•gIÝúq­H›C’éX(ÓZ\„A !lQm*T9í¾Mc0-PÉþ]0Ý–ášZœšûÌ…IÈQ›[ªq\2´Ã)ÌÒ/•çlCdÊCºvA58µÞ-F”¾g¦ß‹ulœÝ²a±™4œ>rk´¡;”Òº–!®v•­çå[¿4|S¹i9´iC”ÐN%1šÓÂdeæ”јÅ&Ÿ„È™§²”nÝIåŸÿÄ:!1 0A"@Q2Pa¡#Bq±ábp‘Á3CR€ÑÿÚ?ý¶ûìuã}Œ]_Ye–_ÛY}l¾¶_ÝÙeù¨¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢¾Æ»«Ë^kê¿h¢Š(¢Šý¦Ñ}!’ZcÉ8K©~ÕóFŠC”¬ôõ Þ›=C†)Ûý©º‘ë³Î){bÉ•ÆþOE’YÕû\¹%ñK߃5Ž;~Ö÷1¸%õ }_Wœ~ÇíTR/¬©$Ç \l<1ŠQ|ïgµ›øþbÇù¯üãçÿ#ôñÑK›ʉ`ƒžß5þ[XñAñü?š#9iÃþâà TVÜ©ìÃK›¾?óFH¨Ê—ìv[-–ø#‘Åê-–Ëe²ÙmþÖ±¦{h~ ð×E܇öQàº}v6˜•º%8è¹%Å[ì}oì’·BÂh¤8žè«éC“l}8%‘Ën‹~”>•Õý†¦.Éø–çªìÿrǦ£ÑG¤+–dqrØ}¿5öüÉ?’®‹³äQ±_HËIÈ•ôNŠû|–6…W¹ôÙ-?FˆÆF1cŒlx×À ’ÜÒ™ öhŽGµ½ JÅŠOƒÚ•\ÆÒ±E¶8J<š$icO£v9»ØNר¡¡óÙ},ÖÑî6);5ئѭÝ#b›Ddìy"äãddÔ‰ÍÝ3ÞØŽZ%”ÉŸm1'/„E^ÂÑUö+‚L}ϱnX‡ØËÚ‘©·»ë7Dž”Qެ¿±ˆÇÞÄï‚y%‹‘ç­Ñîjà„Út)^æµð$úÐÅÑ¿“þf7©Û!ܳo:41DžËÁ7CϧàÉ?yR!‡,¶h¥Kä—¥Üd{Šä†O§cûùdå©‘œl¼Œ]‹c[Hm>‹¶[!åW¦ˆBöDð莦Åê!{˜±ãË Q&´½&=ˆ?‚«²É~ ʾ”/Á§n‰øßzà’ðHX×¹¨†h㕳Ôf÷. !)EP…°¹í“¢OJ/ä„k~ŒÓöH—µÖq5Ó/QíX°žÚ=´i8däâˆ;V^ãt_÷˜Ý»f8|¿¶DÞÅ÷BUÉ–{l{z‘‚Kµ—¸¥±ÇÕ±9^Ä#«í+ªdºQEPâIYzH»eÑ“%"$Nè“¡GS¡$¶]/µH{t±²Å#PäKrxí RÄè÷âÍq=È£]ð(7»(Š­ÆÌÎHÇHºVÿj†Wsè­™!N˜ñ#JTGýTp#Ógþ²·F•§ê#$hF„hc‹F—ö[{wpJf,®±Î6™¦‹HrÒ‰/Wšè•ÎW#–ª Çê%—éŸK,¶k5 ‰’•¯¢Ëì±—Ý!²ÍF£Y}}¸-ÙŽRÓ"X´˜còsö2àþ%ÙÇMEÑð@c[ö_D<6{ þ«">’ù%ƒK«=¯¦ÈcÚÙ4ã9I¿¨ñ"õ},ãa}Œ¸éÀ·-ô[ƒ,²Ée0hmiØk¦=ú1«l„v¦Kaå¿‘ÃÜäQÚ„™Ÿ‘nsؼŒ®Ñ£ÛtEPÉÇ{ä¤rczKc'6rÅ='-(\XõP÷2aiÚ1EòÉA1ªéºyûŒOíu³Y¬”‰Kð%l„h¢9+a»>H»{ôÊõ}%[-×l£gX¶]eù}/ºSPVÏëÝ!.²1ÏáôEþIbOô±bkv6_UѤŠë}—â— ÍA[2å–y:!ߥ–Z>HËr˜¢™IZºëe—×QÉ¿™ôg­y¯ƒÓgÑôÌT÷]%%lÍ™çt¸0â¢1¯fФ™t-ø3Æö1mÓã¥ö!±xeÇJ'5LžIâ{ ÕCF£.YgÀņˆCåø+¤]ÒúeZ ÄY}ȾØóá|v=̘Oe28THCóäÂö®’ý/¾»xÈóá—Íid»òá{–NÜ6ò×Hóá|vWJ³ŽÛðFZ]¢?‘½¼ñçÃ.qÙ·Ø.HôÏÏ™sá—µç\‰þgç¥ù#φ\}¤¶1å”ê)j{ô^Hóá—h݈ã¹t®øóá—ÕÙt_[ín„ºY~d÷ðË QTQ]Y]h®Ë×2+k”OzÍoÍ|2ã¶û¯²ºËuDqé7ª%©±sM±VæÅQEÖŠê¹ðËw.—ÓnÚéEt£Û\ù×>qá]h¢¼à¾õφ\w_re—Ñ¡vÓe–Y¨Ôj,²ûcφ\öqKäI|ôÒF‹4#B*Š-v2<øeÇKû=Å:5)gólyð˸àRO¯–ZwÙ|2ã¾úß™ô¶…6Ze&8ÉpT… 1bü—’vúGŸ ¸ìOíßU&…4<¿É¾Èóá{š"h‰¡"hF„hFˆš¢&ˆžÜM4DÑDOn&ˆš"h‰¡"h‰¢&„hF„hF„hF„iF”iF”iF”¿uÁ޹O„fǵÁRý¶7hËšYWÿO”PÒ\‹K1a„·“ØÍŠ1ޏqûRàõ ٹ鲴·3åy_µ.QVºaI+Døý«6icq®?üE+Ö&yêÕ¨y]5ûTñë›t<†{SüÔ›ÝñGëþ´ÆŸéVigµ>44é8ºµC$4¬÷±»ä×&ÛÞ¿Y ƒ×"Y ×#Ìœ·Ÿö=Ø/åþì÷qÓkøíþ?ì,¸ã+çýÆY&–ûÿÐ+ûL~›6_Ó?Ñ’ÿÜ’Gü? þøý/þd°c÷þX°úgý÷ÿaúLOôe_æOÑfŽõcµÉe–_“Rã¥ý…Yƒú;&Mç²1áôÞŸnY?Sñ&}#Í©n<—´GùcZG+!›&7pt/[¯lñ¿õ?«bÏÿ¡-ÿ ÉŠxMWUãÈêF¦j1¿7§ôy3î¸1zl^™ZÝ™2É¡¸Ç“ÞŽ›2f·¹«z{!Oã£eгcn“éf?ZëFeª$ý,r-~™Úü|ô^<¿¨o¦/¥þQZòÿØÔ‘96dË{-Ì—{ž…ÿVôËÔÁ}sÿOƒÜ—®ÂðúœvÍÆÜ%ÊØP²Xµ~’QÒèP÷fÓá“Ð'Jm8/ÕÈɇ4ðÅÚ\‡XNXÞ¨±O­ÚLÿ?’xåŠNçÇ“õpC2„´Ëº»£É&6fú¾“&OmhD°üÙ¡žƒÔGD}4¹úæ?Ký¾Wþääç)N\·d2~Id–§f9GK— žhb†«"Üäò?žÝN[¿OÕÑnÆ£-™¢7ßÁÉKåý>=NèJÌñQƒcÏÉÑ—fª.Êe1µù5Å|žî?ÉïApc̦êº.­lYEì.™[º'±“yÑép¬Q£U"+ó!³è^9ò[39Oé²PI_IéäÚ§ÒĬáœ"Vø'Š+vi® Å¥hÓ:»%vb¹H†ë±2T.,œ”±ËLuš²gáÐÜðÏL’J¬Ã…Fo+ø1f¤NS¾Lr”¶bØÃú_ÿÄM !1"2AQaq‘ #03’²4BRr‚¡±ÁÒábƒ¢³$@P“ÑCstSðD`cÂñâÿÚ?†ï¼;ÖðïBãÜVÉï+xw­áÞ¶q['ïYÿRÏú”8žõ¼Vñ[Åo¼Vñ[Åo¼Vñ[Åo¼Vñ[Åo¼Vñ[Åo¼Vñ[Åo¼V÷Þ·‡zÎ}«x­â·ŠÞ+{W­û—±iÔô WÑCj›H©†e8Óð®‘Uüjb¼總ÅïÅÆ~{“»z4š3\út"kÂy&ö§}d\ã ”*¹ årg·Ð5ÚM[.Á£‰^5Mî©JñLÚÜA9`¼\Þ¬®°Ûqá>Ô)0¼9×[sƒ {Ù^›˜Íçˆojc¼b•¯Ý7®ÅODkÅJ¯»pƒlsïôlúÁ=Á­’¬ÕUmër6 ¤‚'/½ ƒÄt³XðËÝkg‰Nu'‡´8´‘ϧشæÔ¤ÊÍuRmiU-Ñ›JDOàï­Sãrwj:SF¤á.®È÷S4}–RbojwÖF™1Ƹñ†ÎE3Ûè4-2‹5¢ˆ ´OliPmJ”žEsm¡‘ Çd'i3M´õâ°uîº--ˆiæ¥FSf]Mî$Þy£©iꪺ›)7W6ÓÇ+EuM[Åyâw܉Tj¸Ñ4鸶ouæqôlúÁ>Ö\"è˜T¼æÑn…MÍv´»C°Dˆ ­™Óâî©e'¸‘Ë©7GÖkÌb réº%:©ðuKÜn'\ÿÔ¥Þ¨Oýçþ¥KCÐèš:=2Hi3×ù£3Én9n9 Žh‚ Æp[®[®BG?ó òÅn9n9n;Òºç }Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ë}Ý4醚ºM\)ÒU:^c)ëŒS«HËgèžš½%ì}:t¢æù§T%³Æ¯s5uâÇ·‘<Š÷V'Œ ž†43M•ºN^Iw _ÿO´4kë€ó9@ã*ž•ªuâài»6Èü¼–Ôm6T%ÖÃêŠyU©S§oЮڟ‡“†iÚ-Zu[¦‡Y©mDöÜ©š­ ª[´Ðf“[Eе4õ õ+dž¬{5uçS{A‘sL+G4Êu(ÕÖ cddGæ´*oÒ)>†Šç8ZÂd¾¾š½.¦Æ±Ôˆ°ia ™ÀƦÁO<|—hÚK.aÈñiæˆóÚK³ªGCÉpæˆ/ðuGè´Å:u£:ছF«ÛR¥ï{œÑiÅߟ’Úm¨Êd:é}!Sî*¡uZu.úOðò´o ÛNÚT‹O2îÊ×¶µm´ZçÐt\9Rƒ^¡oS»}§ˆ<üö¯XÕ¾Õë·Ú½cS¤Œy¤ÖÑëi¥µ©<±íÔ¿0<hèúY©QÙ SÇä§òZºÕ×ò°”ê4çTkn °Œ=¿é^3¦UøÊ¢+è€éfή8­c|Å>Ž%7ÇM*aÔˉûÓéhä9”ôG‘nCm‰Í¹Ì‘70ªT]ZÕõ6Ðn°ÀZØÃŸÖji¿Jñ—èåÌi-Ù1?šÐ«Ñÿ ¥Ö«Lç˜Z^—F ~Š4[éSxÂCš üUJT|^Á¦3Fkß1•Üù­ßeG²¹sêM„ÓpcÇ£ áô)×{HÇk„μ6Öº…FèÀjhœ Øs–(U¨Zj\Zë[p‰(T¸A \ú»4ñ¯}—[ ÿ…£¾³/s¨±Ò É0?5BFª”>ðáÖÐ=ª»jðs‹x@µ†?©S«ª®4Ä]ŽÐòšçRÞ§¬m¦xzF0v‘tFSÚ‰"×¼¿Èéù–Ó†ëŒã÷( Êcªèôªºžá{·±=ïÑé9ïmŽq`—7‘êBm¥LdÖ ö¶ÓžÇr˜’ Õ¶ÐÛb8rVêiÛ±hÈæ¶©1ØÝ‹xóEͦÙá³»„aܬm6îÚLov ÝM;F Z´ØÖ7“D‘uÌk®é…¬±·åtbšE6‚Ü eäx_ÿ2¯ÆV“Mí¼jæ:Ö²®…MöZê[QÚ´Šu¤òֳ腥á»ãb¨úlÖ=­ÞiÕj›Ú§V܈iç"ƶ«¢ž°€Þváž{Aâõõ¡å†–Íà ç´*Ú5'»ÆKb@–‚'˜FiVÊæ˜pHcÖ-Ñj>›Ÿ‰nÄsÇðNujU(¹¶l¾Ñ7eǨæ¼sDa«q´ 3ºÞ|ú׋Ôp™ÝÌ6â"y{¿Ã×mAlS K®˜ŒzЏV¸1Â[²â&3žäê”é¾›GþäOtáíOc)Ô}F9­±…Ž;S;¨ª•¨èõ]PS{­±iŒqçÉY©«XêÍC«Œí*«F­-[Zø©hrãÕÅP >¼E*x\çî1„'èÔ̽·qÚ`ñžõ'E­¬º´Sm¤–Ó}¥Ùöw¦³Gcª m:n«"è1Ï"é¾X@$¸bëg†*¦”í[\æí@˜18œ•/Ga¨×ÕíÀëOSâõ˶†æsE­Ñ´Š€TÕ\ÐØ.‰æ˜ú¡Ôæék‹Am¦Á6–ª¥BXjK"Þ´®¥GÈk@ùÓz•¡…‚Òaù‚÷zO>¾’tvSÓj –É;NTÙ¢Sc(ÛwÚ†«þ‰Ä7’a¨E=$b+ÓÏÛÍi:5G ”Ý ¹Ô꘽ˆ´ÈžF)ßçEm§´2)Îm(.%ÇhæH?þ¡9Ô€âòo#c‚Ñ(¿k@w`…6kŽÎÔ5€á#¢âÓq Fî_ŠÕº\Üñ=rª¯976ãÉZnÙFî_ŠuFÜ×;m¬#UÍi.ª^ç8’@ËñAÍi‘hâr˜üJs×ZààaäHq’‹œ$–~Å.iœgèåø 5y¸È7]ŸiNs.I‹ŒcÔˆÕ–âó²ò7ñwz¼S´Ü×@q@€a>ŽŒÛ&‘¢/q!­T´r<Ý;mÇ–I´ƒM~°KŽôÏâÍ8Ý0â7³X66õ™ñˆ@‹šEصÄfd­alºÒÏb¼lâç“»1ø•±#3Ÿ3>’¾‘GFk+Wuõ'hÌÊÛ§>Ôl§o´¡m8Ž´Úö sZXÔbGÜG·0%zÁ¬lDÀÄfªÕk†¥Ö AÃŒIFÝVÃ^â`ã1sLU¶ßœv%8› &†¼˜“íN{ÌPu&–SŽ.ºßÀ"^[QÔï.»7푹8¸±ñ¬0ð9òF£,ùÎn-ÕZ£­ª\Zƈ82yªt¥Î}Ku‚cp»òM{€ÈÇúJ-·G%u¢yÂ4÷wƒ~wRÈ!²0ËM¢O@ 2ÁM¢y¬³ýJÓ¨5«JظÍ|›A÷ú—É´qÿ©|›A÷ú—É´qÿ©|›A÷ú—É´qÿ©ðú¸ÿÔ§Åô?qß©F£C÷ú‘- ûýKäÚ¸ÿÔ¾M ûýKäÚ¸ÿÔ¾M ûýKäÚ¸ÿÔ¾M ûýKäÚ¸ÿÔ¾O ÇÔê_'Ð}Çþ¥òmÜê_&Чê?õ/“hSõú—É´qÿ©|›A÷ú—É´qÿ©|›A÷ú”B÷ú—Éôqÿ©aCB'ê?õ,tm Üê_&Ð}Çþ¥òm Üê_&Ð}Çþ¤âÚ¸ÿÔ¶´} Üê_&нÇþ¥¢hUèh¥UÄM®œ¾·¡ð‡òÿ¸ß,tIGÐõôe ´ù£O¤j•IÙw“\xd±èðo×? ô>þ_÷å5“lñP % Níòæ<¼ŒssEÏ2zAÉ5µ ,n]â¥DÂ0d(Yôø7럄úÿ/ûò°Cif®Ä¢ç6'ÑB2¿üžCDBe:[Eßrž(™…š-¹£5æÄ7§ýsðŸCáåÿq½#Èj ÔCŒÇ¢šÃ}¾L®¥4@èwG§ ðo×? ô>þ_÷å‚PŒ0‹œ¤d¼Ø…ühß¼ˆ~ïNÊ:£qD?§ ¢YtÏp|«¯ T%-„iආÔ;ÛLJ2êÌ,Vé "†›áE™Oéu¢Ö謠à6\Ì ÐiçmG öCáåÿq«/( šÑŸ’ \A !J‰Á`¯œU¼: IPœÿšÕ#ç&ÝóTJwZ n˜-óÏ©@­hÉ=Äû‡°5¯¨Lû=„?—ýÆúDŸ'ÛQ=0°ö,pXb‰åÑ´”Ì)&OM=ˆ’ìÏÑÓ4jÖ´wõ§ÁÀ#ŽÈ^ Æ~è|#ü¿î7ÐÔï. hvFµì|NJ×Á¨Ì€;ÁC›cy ”X\…·œÀGqþ_÷å3Y hB3ŸD·'6ŽÒµµ*K…y¼ôœV³ZjTäÅ‹.ê%[âáŸQL•9u#TS¼œçy8ô7š]=³êZxu¢ˆiÚ(™Çš…àì~yøO¡ðòÿ¸Þˆ:0葚ÞRqX桬‚8ô`§ÈhvÔcs)ÑŽô)²^âpN{kRyfhvHk*´0ª<‡QsLcÅ8šsM$íd2püº=‡É××oø*'kø%A;i¸ôø7럄úÿ/ûGGÉ ¨š3Ÿ–èã‚,9¡UáT}– ޏÉY«Zò:–Ó‰SÐÑwGR…KE¢%Ï=Ú§£Q±ƒ>}k’8ì“‚ðw×? ô>þ_÷ aåšš‘5Š h—¸ñDçÚ„³Yy£É;3D/®ØÒ«‰3ó[ÉS©µÝ¾Oƒ~¹øO¡ð‡òÿ¸ß,&¡¬|Œ”`bxòV°aÆ8¢Ð=žKhÉèñÊÍÿ @ûÎGC]/*O“àß®~è|#ü¿î7ÉëX 8!bˆCÔæ® ¢bP<˜`Àfy' ¹£Š€ˆœú)h´¹ç>Cš§£RÁ´Çzv2y"÷f|¯ýsðŸCáåÿq¾XBPµÍf¢TNjøBÑI0ÍX`Wœ3©sâpÙÉ h=äsâŒy¦šÕ À!‚ÊG4I<ŒWÆê‰?Â9'm`‹åø7럄ú/ûòÇ$Ôß/Z?ÂÔk]ó‹ShK*jÆûÌÂ8¬OGj ¬6hq%!Ä?R%­<Ó™v)×eÁ|Þ(–j2ø$ÜF*0wbÀJ‚ƒe¤mŸ„úÿ/ûò…Ù, ,2PÕ$ùmªwóW5Ò¾ÓÅ]p+dË>‰àœæ Cd’¤ˆAÎq¸pà¥À¼õ¹[HC9+©`Uלö‡DN ¯â­D-0±8¯°ðyŽãè|!ü¿î7§$b†ÒÎBÃÊ’=qèÄ¢rè†Ë”ëTŠ·½DtaäcÑàß®~ètæýOªØÅOFK%† Ôèž‹ZÒãÈ)£ WpæYiz;èô‚qXâ¼^›…3iqs”5ìyâ¼íRü)”¤H—aº/8¸Øké´qW1¬Wæ™’¹¸! J$çåãÑàß®~ètÈÏcãjsê;ig ®aˆ8™%j0ʽ‹)€Ì§ .‡;7ñU©é õnÁÎUtgZgtýªRª-{ Ô´­9ãÆÆv@ö£M™ñ<—„jSÄR§h+FÕ›\Öã×*Àè}²¨¸T{ô–ãqv~Å®¥¯ä¥,îXçäB!OGƒ~¹øO¡ÓÝÊÏî5X\§ƒ•õ_qäƒZ`uû\ÑÖdÞ ö·e‚´ŒS‹Â€èp—m,äòCL§ëƒúÂÅS¡ Õi}6F¦-r<])ín÷ÏwäÞ¿I9v¡„†19û¸ÈPçÚôÊ7\fgÈ™‚££ôx7럄ú/ûéâ'‚çÐ1€¤;e½8¦²v_²­?ÿ©¼K\ §Ý¤2¸´ŸÕ‚¯YõiÖ¶Ó{~õÊ­3òZ0ãyT´#f?VÞµR¥!psN=_’JÀt`‚'šÅx7럄ú8àøÛäítÊ/¢9tÇvP›S `Á㢥mÛÀ*Çhµ¹‘k„6#XÜP ¤1Çd¦êêÙ†uïq8ŽH•`ˆ]}±Xtc‡Læ¼õÏÂ}˜>§Æ.ˆœJÙÍmŽÕ€R}b€¤…€X©Op§SçrR2àBcæ!; o3">aEÁpd« hãÅu)aµÑŠÃ'§Š=â¼ïã? ô:Yú¿X౤6Óô¡×IXôÉßü*8yÃÃ’…’ÝXd‚†`9/9OÚÔyoj¾àç(Öˆ9©§HÔw7d©U1c›€Íb„ êY¬2@ bz0UÆ Áÿ\þÐé-ú¿é äœìK>[bUùOÎX!^¸ÚàÒ°èÉeÑ’Ë¥¯áæƒ8‘9-’ÌbÜ–K,!aÓu,F©„Q€¼?LþÐé;8ý „âPÙ•Î\‘«Kiœ¹#ÁZÐ\OµÕ¡õx3’Çœ–8tÝVê’É9ƒÞ a[Ô²[¡`зTÄ)!Lb¤ƒ(Ȉè2° #5ŠÐ]Éÿ‘ô5‡gât*úmóOÄu'i5D`Ч£i`±Q—F(¬°éÁ4ÎïF+­eÑ²èæ²Ã§,”­üèk{?ч‘´ÐîÕ8¨è楗 oby9¬T‘äãäONý~¯Ä8¡äs@b Y,=ÇD®C£.Š\‚ ÈN(á­OLG‘#5Šêé¨ô½‘ìø‚ÄÊ4u©Äv¬³]kAXf¤»Ø‡çf†àƒ OhP°Ë£¯¥—ž(5Ï·”pD ‚¡DðèÀ,”Êà`£tÐúÞ†¿³ñ`‰…”.¥0¢ÁA§íY¨M´ö•Á\Ô ”jö~’ŸGWZ ç:$+´èÅÅÆX.¿ ôutbºüо†·³ñ qtõt; k5”ôœ,Z%Ú³X¨Xä€iE‘Œfœî%Þ‰'1ÓŠ 8Ï¡¯ö~ †%~倄x¬±@`°áÒae‡_Fk48ö¬GräºÖH=ÃâP©Mâ« ÜŽ0euôä²\<އFJ‡;½ gâ"PYby£%ÖœH)Ç– ‚<¹"'­Ï¯£‡bB•Š q[KV>à¯ÑêG6œŠªRÕ“È«¦VÔ{ì³R1öôsSÑeÒåÔ‰R:(cÇÐ×0Nî_X(É8. ¡[nÏ9XBÄ…™žµ2³u¬GqSµDcœŠºq]½Ç’ræœéìL¦>qM¦Ñ²" ÅOY\JqqÁ@æ†Ì‚ƒ²Áv¬ Ç$z—5¶]§Xçöª¯Ð×ö~#£”ñèÝ…1Ä"A€tGäI±5´ŸsOÎ-Í[k‡2Pú!njF.’™ªeoÌ­ÎÅQÆ&#‡Ôú-â¢èY VíÝRˆ¶`QÁ pXâV8BË5€„€®ö,‘Š£Zrô:A?ÃñuÒ†¨‡;’ÚˆµÔã,3BÜeAuÎÊDdóÔž"ꇃL*BfÓhÞâëG5y#Ú¤;‚ðÑÁÃ4vìy)4çŒ+Kmo4)Œ‚Âj9ÒVÓ\[‹1D». ͲãÆJŠ´C?‰¦QçÔûÖ \`, ¡Šž*•ŠÑþ·CZz¿±dðÊGÕá¾CÚAà° ·«óOŠm‚p  Ù+x~ôeÂGÑ@4†þiξþÙ)e˜õ«jŒx·Á@•¿-ä™–¡kËz–ŒÑvc—F8س+7‚É %Píô5£«ñPqP‡(¼Ó¨~¦hT¥Hör[· Ö86y ^Ó-âÇBX:Ó¯qƒÉKÇ5€îÅ €Þ$ŒÖD“Ü¡­õ#¬Ã5iã“sPÖûÊ.´shÉ4Ëžy,¦'ø‘ ~¬Ÿ¢±‡õ•»Ü±jÔÙãÍbß#8< 7g+;é=‹5‚Ññãèk{?'ņkOA„£i²!ÊÙ„mš-y8džÑ1ÇȨZÞY¨&á óX…Iê^®Þ·`¦£îê <[ÛrÁö›Ç±oGY }¿’̺½Aö+œÇ4uôZv¬V]KGíô5}ŸŠËÈãä`¸. ‚È. h¶QpãÍ ±å6&Õ³º†¼:—˜,<º# ÀÍ\°X„i¼Gju0{Êâ *ý _gãÑ’Èy9.+5šÏî[ÑìX›º7QëFݦ¢f#šêX+ÖkYÉÈkhÃ~“J¹•º¥ °•#Êà¯ÅÒ2V?dÌpYÀT{} _gâ·–úÞG¼:7V-Yž9,–k5Š´<8¢ç{P#šÇw­K ¨ÒÁ©LäüÐÕ»Ü+9è乬píD‡Gb‰k€ä!:i‰¹ppÀ¦•Fyáèk‘‰Ùø‚þíSµìX`zÐÄõ!N²%…E™=‰ÛÑÉÁuv, ÈŸbÜ ²EÅÀÉ©•œ®´ÀPàŽJ¹§kŠ·#Ô-qdB‡ÇX^u–³žjæÔ´öºK‚À¤Öµ\ï8y¹:ݾ¦-aÂqµsä´`àsîô5þÏÄÄÂÇij+9S hB´eÍ ž>LtrD…š.ÍäZ×¶p„LJ„g¥!¼”Y-“bÅÎq[xǮ˜RAwZÑÏ_¡4ê aÌ/UýEz¯ê+ÕŸx¯V}â½WõêϼWªþ¢½Wõ꿨¯WýEz³ïêÿ¨¯WýEz¿ê+ÕŸx¯WýEz¿ê*5ÔW«þ¢ ÓÃëê¼W«þ¢½_õêÿ¨¯WýEz³ï•꿨¯U÷•i¥‡Ö*#X¯UýEzŸê+Õw8¯UýEcDŸ¶å…#ï”Êpá–'§q½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½Ëq½ÞFU(óÕ®7r?åIª+Õ³Yp[ˆÃÔç¸ÚÖ‰$¢T¶¬ó,–ó˜SpvîŽ&U˜ÊÌs¨˜¨'uÌx©L š{_‚¥¤ ím*¢æ›=¨hºæxÁmÚ¹ÆÈ­L†`ãpÙíLóÔöðnÐÚìNa¬Àö‹‹nÄEÔêµôª´¼TOskSsY¼C† êUQ¼Øeçõt:>nñé1ÀÇù0¼\ÖÜG!åéu]YÔ<¢¸Óµ“5ˆÌ˜áÔ«éZVáP†ÕÑéO§ÔyûPñ&…CMÏ¥„®¥Ç‚ðÅê© 2 ªÊ­Þù²Ó‡ðýê»m׸¾Ýd‰ª#¹hTô–PyÑôèÑ2/6ÁË©hõˆ¢ïðLÑžùÚk„â0Ç5¥Ñ%šÚ”5!úÌŒÉÿʦê6·G…-[_e¸Ì“ù-'H ¢Ú˜Zù7˜–ëDÑãG¦ê:}Úã.k@v]X­(±´©Ò:=ÚÖ“s˜òL€28§T4ŽÊÚQ{Ð@‹Z8Äí€ð\â*;‰?ó÷ `TšŒ‡üà üÓZç5ÐËmà €OÜžëÒÚ¾³ŒZœÒnq¶dˆtÄíX¤Ù¸[˜‚áù'±ßJDpÿ!^¥2Ð÷05³í{Ñ. ‚qw¶-LuG´Ë®©ìÁ¿—‘VÜí+IÐê1ï«N«ªµ´Ûsž×Àã‰^ñ­JÑéT¬úºÊÔKZO±TÒjÓ¦VuvƒôNïÜ“-!Øò¢qé$˜‰éÏÐ<5íqaµÀ’$8 EÏpkF$œ‚cƒØrsLƒÑ-!ØSQí`þ#c*5ÏfóA޾\€3‘À´ób:EzºE£bZ…ÍN¡ZM7ÁnvË>u. ™RÒÛ„ÚìÂÒyu2-æ´¼+0Ô`³mÁ£gî+EezC5bž%î3µµ8òçìUØ.[«³ƒ/ÚZ"ÚšMrÂâ)í2ìDéœ:Ñ+jÞ)¸±ÔÜwøÿª½F:¾Ëë\K¶,´Ä{a4_¥2ƒµd—¿ja×ÿúþHü¦Ûªk6ð4ãbμ¾ô[YÕF•kƒœÇa“b>ÿl§º]$†Ö§`5IáoÕIfÚNk‹š*;0öÄcÊrU¶ëY c¤‡2Ì|ý«Å®w‹Òç8»˜€Ïˆ÷-#T5]T=Î-·`´çœôjx›™ ùð°3ÒcªYîªTŸ¬×[Lí6™¨HÆÂ8éžµ¢‘¿dQ³§Ó×’ÑÅz•[|2«šwu;íãÞ¼'£7Zk:››Mî{¥æÞ³†(ã½s5ríU›WOž¼•WÒ«]®ñšA¡€)ìß÷\ŸA­ÒËÞYTT7 £”ÔÒÙ¦â$×qN³T©èAÞtoÛ-§n8öä‹ì¯Mµu Ù^ Ò):kéµÏÏV'öö­2…íÓN½*TêÕÃéG%§UóU©Ñ¡EÕYy´m>lÿç¨ÕSñoñ]ï96]wb¯F)š"˜«J£'—ù'–D0{NþJ‰L¸¾Ö² }éÍm1\Ç—oa<“‹¨·f2—PÚún±ÀüÅ —8]¬ÈDk#ðZ;5N¨\Ö\ðbhu Àö—4Ý9ò;$Ïð‰*—œ¦Ê®hs¯ÃÛtvËš ©Èö&Tˆ¸Lzÿgâv'Wv‹EÕÝBÁqÂ3Ng‹R±à5ͰA%OW£Óf®C-l[9Âñ}K<^-Õ[³ˆèÃF¤4s+½ÉÔüZ•Žham‚ã:Š~1ÿ»n×zs¨hôè¹Ù–6%\öís!`Ð0„"›D"ÁM¶œÄ )Ôuʘoæªns—ÓN­²Ü! 0äaííVZ,åC@Œ@= ³ñ;4ªTêxµº˜vöŒ}k{•-U>.Ý]?Õîì’èû†<Ö Šî¢¯7¹ð×{'µiÏ«¤TuÕ\ÓV½«F¡ç›¤iÕª÷ P<ÏTäˆñŠ/aqt8jˆ€qŽ)”õΦ“f²ÀZit·ÁM£XÊ–Ò5B÷ÁžP?V ¨iÔ×Z¢ÑÛt]—;1^ÑjV7é@{˜-¸æ?…2…Z¡Zƒt°Û=^Ó‹›ð…A§HÕ©´æ·q¶ÉáÎ3ZsÉ~ú3nȵØHŒ¬ß¾{)kÅÓisãú@VTÒªS}mì ²'Xøi8pj4P1º× uœÈÙ@ËéO°'¿Zl×ÚÖ6ŸÍh—~×­‰¦¨¦!„ÍÑ÷wªu‹J•jl›cbÿÒ™ãWCôfÖ-,€Ââa½É¯Õº­G:¶”$ÈÆÖö÷&‚÷V t†·^iFÍ…ÎûÄ{V‹Qî{V™~¥¬4âátŽP;VŽÏ}Wù¹`Ûó1ÃÓU÷Q¨Ç¾Û"Ý¡gÝ)õz®4zÀ ˜Ù`qCl²žµ´ËXÝ£ƒqÄe$Ê{hÓséÓêdÈ.--h6«ÛæÛ.`o¬1&0ö{7Ãtª† ©`¡­¸ðc½hTECZÙ¬öÀó¡£ #é£N{ßL6¹¥Ú_‡(üQ£OI¨ÙÑÍ8¶÷“‚Ô6 cEW5•œÐ6CD ¹ÏrŸ¢¯ö~ ‡bÚ¬§?IК.íÜsNv¾­À›Æ9®iâ{Ó“šd†)µÛh –Ö"j4@3’Òª²µ3T ¬Cîpðã‡AiÈò0‹)s“.“=}Î!­’¯¥QµYô˜d+t ‹Ö1¡¬h€šm}F±Õ 08ÅÇ©=¬¨×º™µá¦m<Š“€è4.š¡·X×K€=‡È5£ÎE²OM_ìüAÅQÖ^ ’LFð{Õ*¤NI¿GŽÿÁScYTSeIh—µ±†xf…sµƒÞá«À $[Æ#½XæÖ02î{ÚqX‚/¨ÚwäbßĪMuΰ³ÎßóZ?2‹*1Ð)Ø[~ñ.—~ʱÕ³æ‡=ŠÏ/Ó_h©$ZÀ^R>ôàím"êZ§»]‰.~ÛÇ`v¢Ùªh>³Ýc‹·3ÔOµV{4gTº¡w¬í˜o•îsªi4éÒ§A†§¬sq>ÒV‰yÌ®+W§>³<õ‘ÂÊne;<äjHy/&31j¯¤ ‚b4—ëW“…!Ü®`u= ƇýYšm§ù¿ðZMí®^NΩ͋¤qêʃE¶ŽÓowðwœù*5h4ЂˎpŸºUJté¹úÃSdm˜£½‚V›MÔ\úUYl¹âã6ã”hmp‚/øÏ¡¯ö~ ‡bð—ýãÑɨéÇY½Nv¡Hd®P¬\-Sp'ئé+%néùßšÕ3.'€O#,•µ®Ñ¶gÿŒTC2®Pñ/nCš¦i†k>sZƒ[“'Bœwþ3èkýŸˆ!ؼ%ÿxô8ýȽ°Ì>niœý#þhXây"ûc’.vH20œaN-”´ØG‚ÅåÜ¥b5‘[½ëwk¦£ˆå*`Ûäèoã>†¿Ùø‚‹Â_÷OCÇR¶O·²Vˆ‘Q‡ÚŒC»Cœ{VÍFŽÇ,À>¶hÁÚ¿Žy¯ÝÚZ߆¿Ùø‚‹ÂAªV ´TÕÛ% ¬Î䯴SÖƒ´ûŽ×²&‘=EKÞÛ“˜òëÚ`ã)Ì#°#ü¯5¡ËKNÞÆ} ³ñ;m†ÛMÅ¢ "ïb©IÍ»YŽ>N‘ÿp¢Z…ûÁa´z—›¢ãØÒTš/“ôö/¯Bœåç%\t†ž¶´  ªÔ'+Z¡æó8í¯3¡±ã#þPÍùM‚½U:çé7M]ÀB»SP0ì“NµôîÈ9²£\³4K-%¹¶P©M¥Àð­¦8vŽ‚®[ð ™ëÝ£2«Þq(ù­œGý ®bé47ð ÅÎv8¸ÉÌúÿgâv-"F7ÀSëÑx»˜R+Lp![XYWîé®.ÚÖ}«Ô6gŠÌûušA2sOkç_½„”5=GÆ;M.z:½íœ á‚ƵI¨Ù‚ _P™Tnmç\üÌ|д™š˜N#%³YýëÖOh”AÕÏ=XR1w)E‡§»¥Õª œ6Q¨Ê­¨8ÄæˆiÝ)¿EØ{P'€W|Þ ³â@w±:¦ÛÈ øü³Çñô5þÏÄìZn?õ Ímxk[‰”Ö¹áÁÜ•ÜrUìlEi2gÎÅNmWtÁpÀƲÌy­¦c¼DÓŒ.M«t N]J›€—kÝi<6BÑÉÕ?¦Z·‘i&c5`wz{NøÆB dkĪtjóxQ¦iß„œ²›5c>´h5Íi‹¤¦Ð¸µÇ¨!À)a´»Ãên6Šg01A CB¤{ŸÿÄ+!1AQaq‘0¡Ñðñ ±Áá@P`ÿÚ?!¿Q1}Ùû¯Ú~ëö‡9§”—Ûq¶‚~ëöŸºý¥òÜé°†ëhÚÒv>éØû¥pW¤´ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„ý„þ§pN¯í?uûA®½ƒƒ}Ùû û ûÂc¸‡Ÿù Шe#ûš†\–'»ãé´Jüc¨v¿?F~wbÂÚh'")N³ð¼K—.\¹ráþ¿üÚ€ XÂì÷""5°˜êð÷‚:Þ’a¦`Ëí<Ž"o<øìƒ+ÎSÜ”ô¤u³;¹råË—.\¹p#˜k×@w`4!_ía€7µÃlkÄ‹~eÆMlØ‹¢hF¤Ód¹q}OòQ…µzûÑÙ®%‡ ‹¤&&W"óW99 %·»Õñôgáv"ÂÌà^b zÖõ ·åÇÈCÁL—Vçž‘Ày+§EžY|0ëPŒ[Û«3c*9Á >²«YSX{H p lHe˜ 8ÖþXþG1Jvè ÐÙ(0u´×&Èxn"—–¿h·çáaD¤‘p{«1¶^¢¸Um:óñsÀªB##h™[~ä·‹­ªÊ®ÔÃ%W­>óõ§Þc‚+ƒ>9#õ'Þ~¤ûË”‹—.\¹råË—.\¹råË—.\¹råÅD6ˆýI÷Ÿ­>ñà·½}æ’åË—. ž çö'ŸØžby}‰ùÑ+j yøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ãiøÇÚ~1öŸ˜}§æiù‡Ú~1öŸŒ}§ãiøÇÚ~1öŸŒ}§ö?:Õ÷tLÖËÉ2=õããýÇûñnÒƒa ¡­õ›Ä ²ÙO&?ŠRê.¢—ž¹ŠÁ‚[%kñÀWcRÝk¢Ú—ЭÌÕRí_âI ´·£JA±ÛëÇøÚØt\,á\B›;ãÄàj”dãYõ]¦Ä9úCg­M%ŸÃû€å##ë Ÿ\¦¬õåÃÛñþãýø†kv„™–µÒRýne+i\¶ÿTð¬Jó dáÐþ;|ï¼¹cÎn6ŽŽüÿP¿õV6¨²®Ižíÿlc0ªЯÚjäbæõ¢s4õ]‚ÿùDÕ€`]eæ‹é©ôõSdÔRïÙ©TqŽñOkaùKæÍ¡B¹TÀÇT¹IÄÕ¹º®é`œƒ Ø„ç…îµS·—ÌÍlÕ6 xÇxHöªEt‚·Ó¦m¢¬ÜQ££)Ð@®Çx¶7Ì7ôpQM™=b‚”aVi}~:ˆâmÇ*ÇYJBœ™˜¡ÅÀ ë2Xb ¥jAÁÔöä\,1Z?«*\½#ÞºaªÑòº-»¤Nd n†›¿øp’Ýv ííó}•^–µ;/S¶_¨Oá24Õ²L¥m¿0Á+ig•]£ ‰Æ&ýþ;þÿ`½Þgp1Y{ÅMêÍƒŠˆÙh*ºôisSxuŠÉOégQHß)^³1òUò&%ÙçQV{&‡ºçà"W* WvåWé/yBÍfºJõQ®òí£¤‹á£Ú]e®¡Ò½T¤çLiN‚£ „(Ø ò€åN„=IŸtÓÿ D Pöž¦_x]<:\·\L·]/øä&¶Ë/ D˜—FN‚ûpÇÖ‡ÖgПiÌ.’ÚUU%Ú,FŽÜKÃÌ*}%ßp2jÏ`-½]G0#¬’Ç5wˆ+©éCRul­ÎÏ‚y÷Õt®©Œq3°Ufiquë"ñ¶½QCW"ìânŽz7 †«äéò%VaD_m•6a2;j©·D%”cK`ÑÕ w’¥+yH½3³u,kkJ‰¿€ÙÝ…©¶Äµ ÀQU…­»¨Ýç(¥à_.ñÝ ÙoMÞ¹eú#xåe{U’VL6C)j,]ÓO˜8yë=aÉ…IT(û…rn¡j ¸ ¥dÒ¯Ð/eÓº¨ð¢Â ó§¹x–Ëø,Àªˆ»,p:®w…©Mð~a8º\VôÔM30ÂöNèù•ŸŠV½:e(T‘U®€äË܈¶˜Ç’ûÔ½²ˆ¹ëìËHå…ýŽÑçwØ·/ûŽ®:,R¬Ë ’šø Lq BÁËZqV÷„³S-l£>e&5fÕ¨-ˆº–´ñoy|6)P‹Ë@_b À¢ z[ÞWÏ¢ åãëCÊ'ÅÓÕ–QA·}‘¶hIJûU/IÉE…‹]Ùï3ÍË«^€ V’-HŽ”ë6e…DÈ XèB«’ÉðÄ_H„kkô‹¡DCË»½¦ìòJWš1;ìï=‰P/+'hzÎdš¼ÑJô À3©xú‘Ö€¨Y-îüÆ£…¶Ögv¾òΛ÷ìSû €¸²ž¹Š ½GØ›0ãô³û—{1vÛS-¥JQ€7Í»Åðu”¶ÊHqÙÅú×yZ€vv/½v®.?, ±’öÕ µÎ&‘Ágnm º°Ý%VzhºvJq;ÙHVͺGÅ]f¬{º¼÷Ô QLEk¿ÕBUDt/gw&¢:6ئ=¿øHK Þ¹‹p ªâ  À8é>èÉI8d)ÊÆ¢ô¸é¨»lëè0ŽÍŒER1â_‚Ì(+›*”À4t?øYÇW—o`Ž—0KþË—.…r‡\òßÖöŠõ2£ÃÀkáR‹ ð.P¥lf™G–Œ4£j·}/ÂõFø—.QÈ[NÏRI[ÿFJ¹Ú[u 8ί ”dµ—d2Üâ4Š)Ù¤âÑÇO’êfÏáj¯i3t¥Ç«+âÆÝËã:‹¸â#,W.Iua† ÕZ¥ØE çàÇò¨½°Î~Ö5Wàç90ƒš eu.ù’CèJÉ3Ý7*¡ªPþ ½Kâks€¼ËQ›b3R€»øΙqG˜›¿E&§(âM îs1pa·|‚×2÷Òõ 5qßÊPøj[(eL]È¢¶eaRÀˆìÊdU—ñ7àÉ¿ƒ>%0 i…ˆXi‹¢ÓYø*íVãC7µ,§ÆÀ’¶X†"M‚_1Õ;’â”.$É|¤%¢íalAlÊó0½Æè+Bž³3B¾G‚_ÀßÀñ4¢DΊT+}SS©d!õ†lÜÀ¼C¶y·ÄÜÞP+¦^It]KÊ3ç¦ùH!}35²h÷3©`æp‰eRZL1Cvê$•tÅAÜML,±‡~ᕦ¦˜’(Øñù˜P/DqŠ.Ví1©MƦ©hÛœ°7ù†RÊtfGR;>¸(dà‰®}U0HN„wQâTUOhüþM¤õv› 1‡ÞgÏR>UÀuQà Ä(1ŠàWË‚x2Æð‚õ–¹ÉO3B@H¹zye+³3MÄÌ\Påe v_#ˆ1Ñš¸Û‚ú+´ÕMV ëÜNI„îá2¶bÙ‹pEôˆ•Ø+djËÞeâ ÐÓ/HÇ!/ªN%V‡HiQ 75ýÊÑEgB8‰ÀÝÅC¤Á{…¦_ñòUc¹2i79– LÃ\it%ÆÕ¤"»¯f›N•° u›à ò³)d\.¸¸½Èù«nÁ“—"E(ùv‘±EN¨±DT.-ÄÀÄ%±‚\å¹LºÝ¡Ì_x«ZK$¥ô”…~bU›5}æBÆá…ÍGâ¾McÇx{<ªpÊ:«KúÛ»†as¶)C¹œQZÊXØÇ•¤)sðÌDrõ €äΦc)úÄ&ÔåÌäx”kžäUuF^ùæVˆº,¿ÀQCaˆ!”â8nÍC’zF÷‰tq1= »¥ ¥ -™jŠ_X-…ÊTüO’ÝL"š¾ò”#ÄoLù›3–fXöšŒ%“VLZö" ”\Ó¥L‰G“¬ì£´î7LºÄ"³ƒr\íÖW îAù‹•p«°pJÀñ ]Th¨W+%-é/¹¤}eLÃYÚââ¥4Ú Á o¼µÿ`Ø'ᆾJ «:´ÂÊoŽ‰Ò€ä”äPÞ6Î9öE/Ý.—1Õ€Ã0¹°k C±¸ƒt/y…Pb:­YˆZ5$a6ލ¨Þô´ã¬È`å3CKvG¿ é\”˜a 9Bå' Ëxnª»“5Åö&VaC"ùcÙ˜•ò\¥­”êË1· cËêÞ°^·ƒéòy“ 7 #`ÚeëÀa«o›R>—8¬VZ‰uQÁâ.@K-U ;9Šž\# ©­9†ÈúƵ·Õ©_úGQ˜Eé ”… wˆ+†eu0•#pçª_®|¯1Õʲ¼Â‡¾‹]-â¼C[†73{Ë•ÇdµtJâ:áM“]+™ÖzÎìŠêDiVë20{±ps`Fð\¨­}>O…ˆÛ\Eî¦â ©©]¶ij˜9%¼Še2 ^3kCפªÿ 6ŠÊ TÙÌ+= ­ÁŸîÔöõTé/¤Äqf˨‹ìí™N^’¾±`kU¨p¤º³¢9ÀG%Æžƒ KmP½“M¥bå/ Y¸ÊµŽ%QuÈ–_ÓÏIx…Å3=b¥ܤn¨Ù—´·§À%øj_g=f\|ž¢¦ÑMæ¨bP§[–^Ó®g*Û¬Ëñ‰LV¥/,TrCC]†ö/ú…¢CvÙ}²7 *Ñkkë+9O4ãÖ ×õÝ>ó~7µÄº|¢_A—QÓs{,„1ˆRU™«†`DÚkÖi#ÑI`³¤LA=mLÆ¡‹œu? S8L@©ÔLàwA ±Ä¦"Çi©Ï¬ aÒ Óƒä²@ÖnÀÍÂÀ¦ò¤™å£®Ñ°,¶‹ ML>N♤03”mžXŽykmízUGr=Wš’*Xñ;!Ûã¬da×üÜL6úS+gJU1¦9b\ü¥gŒW%—J»˜Ð)¿î7T鼑=¸ÒƒÍÎÓ]6y‰OF|L«8Œt °5&{…ÀÏ«™„e¬aÕ›˜,ŽQŽU§¡6Uå—o’KLÌg)f®eÈzÌGQ½TÒ3gQOÑ Àº†½ãj8VüL0\oq°< Ï´ÁT˜Wfr 0Ãh~’¼!D1ÐÃ(igæ¡XÎ¥»G4gjœŽçG DFíßiÔÅxåí'FT£Þ–lÐÛœx†ZClÔª ³»6„ŽeE¿^‘vPxmæ Ð:K}Ü«As”d`­9,Ø=e}Jv5Psé¥-ºù)Y¹˜\]™„r«2õªV^‘.xAœ.£-Vô '¶î\MóOÐóªSi}PÆë% Sžj:çq àGéPf› ÃÒZ Ï·Oõ‚2ÔQÚ™4Dv./–´MNM…æXï> ö&,ç ò ÅÇÒBs—­¸@…\î/yûHXÏQ¢ *1jt-ò‹{ªâ¡Qw*Š ^XöN1˜,Ôå_×_$¨íöã©uMnf' ²¢TñÅ›"ÞÌ^ŸÒ r['^™˜ÅØâzÆ’ÏDcäÙ\nž›s³üœˆe¥Û °Dái{ú‘=:Æí9Ђ‚¶ÆÁQ Þe¸$ ËVÓ›@ch0æ%ΰÿÄ‹Ø,"æêrTß)ÂG›Š:4£Y¹TI‚0ÅS,^ZÇ ÒZñnV§VZöô‹¤–„@ ÑâáÖP95üœ|ŸÁí‹[ ¦/’ Ùˆ®á*ŽIg%Ês‚óRÓï1êÎï2‚¶]±ÊÂÁЬG¼2C ‰¹rÆ}Y`Í6_+¿y^¸PŠÔ´m7þ‰L 9}b×ôÅÆ` ; úÌ¢x¢ÄÒãÊEî#­Yhzæ6-°ÒL À±™+·ÈàK\‰€s’Ôû-â"U\À,qÄèS€ñ8Ã[ú|žŽÿá ñë5l_˜¥°úK.)é<ÉÝb\a^ £±â~’[åÚ¥8ÁF±ò˧Ö6•¦æ¬E:GHè·671–^ªø4|ÁÞˆFº~“w…•rBM¬éÎÒþ„aÚbDZÄ ¯3èB:_pÒá†h»é¶W ØaºB  ¿?$Püaîw›Œý¨Yºö”]p¾í%›lñ2±H£˜(áÚ÷ø"˜m¾ˆ¿Kæp×KpP²°ÏDC*âš-pÅwcA®Ð?¤%_¢ÇÚ_®p/R¶Ó4¸2±˜µVƸ·{¦"íq¨˜T\_ñã¼l äÏÚjnÎ~>I´rôD‡|Æ…‚Xâo@'~qÍÐ`lJáá²%•l³¸ ‹1‹Ò<ˆj)®ê¥a81 v ¤CØCbØAÑžMÆß2Äbñ«ŒhÅøbü–SÚ«J˜=Їä“/¤æ[fŒcJ:&J 3«÷ï=PáÛä“Ti ¤ÄÚó2=X¨ià¹UÚ­}|caþ °Ç|®TÁfÄ‚ Òm¨ª µ)ÅrqõËpfÚe¨0[¸û­ëlÄ01{–iì@ÉC¹•em¨† T¯¥zÁ.G˜ Hp0‡¨%r?>ñ2½õ&Œœ?ÕðÛ• <†eäCÆcÖú )Æà¨Øœa4Ç í©Ù­aòk×Ps´K*ÚaªS¤Ù›MãPiËô\‰¦¿X1K ,Yæ<ÀTCDâ¿Fd²Çû ˆ¼Ôpô‡ ´p>©rõÞåˆØg‰yÚݽf·~‘¬°fPØ9Œ¼EU›Y2©Â“šPõš‘À±O©¡æµØåf¢/|³ –@Íä‰ñ(Ê;—SêÄe qú|œ7îJÓrî—y“üË—æ~þÍ#ü»ÃJ¯Ë™r·Ïṿ<þ[•óü»Í3?³k7åÌW—áÞkËðïåøwŠ~Ö[Sü;Ì)oü(?`|¿ò×_ÁÌÎ:¿=Àéjü9˜Bô~”óü;ÌoürÏÉï7/‡3eˬ°÷§þðqƒòÜÛ~0´ü9œ¤ð?Ù²ŸxˆÖ~[‡Pǘª2RÕõøþ±?XŸ¬OÖ'ëõ‰úÄýb~±?XŸ¬OÖ'ëõ‰úÄýb~±?XŸ¬OÖ'ëõ‰úÄýb~±?XŸ¬OÖ'ëõ‰úÄýb~±?XŸ¬OÖ'ëõ‰úÄýb~±?XŸ¬OÖ'ëõáyE:3DÕÜ>°RšP€é£¬4jh€æ cCº±<†&l7·Và˜ûbåFíMºûÇÐŽR·\¼ØÝ+YWTkï)Ô›õW£©Ò©tèˆõG–i:¢Šš‡p%=&»oOLsSPQæñ4„]_HùÚå«jÞ=~Í])HÑýoà¸kbÇ'þ³Îùêý6_ñp@ŽP×s„8ë™_È…Q! ¦ez”³kžTzÂ7…0ÒLÐz‚þ² ¹Qƒ6[oq§åx¦uFÇõÊŠ ³nøŒzx¯Ð+b-ј±½)ø¸ÒħóÊ0DZ¯Y 4ã®Ì'è$ÆýOÇ2Ì…Õb4ôéÈzApõmÞŽòåš´d Ø W‰´ä5l®€ˆ&˜P ÕÞˆãŒrÑ.´ýÇÕÆ9¸Ú<1ù e%ÕÍwk¹Ñß{š:Á`BµÐöºˆÁK:`†0ïGþzÞ·ô†žÑ¢Ñ—7ì.iøŠæ¨–{ŸÀ£hÑ™PË>¶hƒÒ£5‰¡S”ª’£Ü_‚n#¦þt¶$‚Ñ{øƒcZ”#c¤”ÂËé6)]n]üuü(,•-COF‘õ?ŽÍ¬Š·¤5gAÕ`{› Ÿg%ŒÆŽ[< ïá/ø'3ÈQ=A„ðÄ|îµÜXãé;µÔOÐèkÄÔ«Vc¬옄0€(P]qLä¢LZmJË XçFèá××ÏDªIb4ÍÈ› w*4é<è‰ ]Õžl6Á.ºþ˜¶€­ÜiúŸ<­Ö=Ú`öþ.|¥]ñ]Rß}JðVýæeãj©Þœ@ ÕZ'Ü<èBþ²ÿ‚Mšš Ào1qª ¤ðe‰o¨Ì¥Ñ6õQ<Í£±Ò »TX(³Å¢É ¢Ç7qЩQN;A 3 1´_¨bÑS‹wk–càõØŠÙW¸’;©F $ulïO”sÄÑ[7ÌJóÅÎÊ®úûÌ™ˆî+èÌ Ã#š£4éj®2-•‡Aô‹M¦4, â‡íƒœ+¼3a·–j/ W„w¬ŸX+¶<gÃóïJTeWnž,Ͱºß» ;lñïÛüxôcѽâzœTôƒ×‰O,·ËüPE6tø -£øPE6tƒ ˆÙÒw‹€2•~"wð2ÀjÇâ°°d:âZÅÀWÕ®pÊ+B¡g]¯Õs L‡Ö7½„©§M°Þ•R6ôùߨз&Æ okw±—n°à–HšÇ e™©•ù“ÔV/²…çE©°á‘^רæ¸8¬÷¨Ow¥Cí—B—XÐÒs¦üh¸%EI^–PóbäHÕo,á/3/WÕ·¨•u–'!ï’€V9Á!ãVM6)Jt¥£<ä²Ô ªÂÞ¬7.ÿ@fÀ4W†Cµmo.3²FB,ôDU ³pãýgrYàMú9+©Œa§./E0›»²r"-vÂŒmåOüTIöíS/…ô£ëä$•B}Á=ûªþ"åMi¯þfյǘ4Oÿ;¡A’2ŒÓµ® žóa-ÎÚÊÆj0*†èjÃAY%¥%âð ä@mIâ;‡ 6G©¹â^W¢2̶ìtLC³jŽÇ×+¦ØÒòã£+ÄÉÓU*«®E÷Ãr v¡Húßil8+¦‡¨·ŽñEœä/Y"ÌÀˆ³Ä“¼S»ia겊ßþn@Ú ˆ•Q,-X]v̵-:w÷×yÄÏšëåqô(Â%/XñtLP”¡ÄÕ­Q¢”èº/ÄAjBƒG ŽF,,H 2B°è[GyØ5aÃêšøE¼¯5Þe1i|Ò\zЦ'"ƒ®ht3k‰Œ«%Æ5y¦ „Žãš¹TÞ®.ƒ\1ØåJ´K¶Áˆs1Åj Í^—ÃB«êa ð#ÀÚÑTÉAVbš.6ÏÙYÉ 3è$NÛîÅc›sM6ܾÈ'’ó•1a¶ÔJ*Â…vdZp˜‡s‹oz+¨ µ¬Ž¼ô]$¯‰XΙwÅn‰ÓæU뼄a93"k l«9>c¤mZ¶§.uª–· ç4-éQ®TÚ+¦µºí–úãÇt¯`Zt—/²Ý"®±ó¸ú~¹èp^Û{G‘ «›½Ÿiš˜²õï5d= /÷YŽ.“­Ó Ã%†2LJX¾7ÁPÎ~´m°éhÖÃÒ\T\ž ¼Y˜1ÐÕ}àðä?¹’èíLǸ3²fBå³9.wÛŠÛÒ}Rtw‹ètĽ0IQ>]Ü} ÒËCæiÈ Tr©WðŠ¿Åa-žhu)øO‚ z»•²ÔS°¹u؉T¼ü®(o“/«Œ½b+rše°-tŒ ¶ºJ¡×Á®ø±™@Ž=$­ÙyzMÕùOB”$¹iÁõŸé± xøÀ?ÅJšHÞûÔ*k3F4 ®Æ‹BÁÄRÄó0+Š¥êçñpeY¢ƒÁ‰5±R·g¼¹Ù±uB«5æ>0b°Âr]ÏöXŠ+=ïí.•ÝiÊßøzL£)ð"%aoÊxú0—¡«Ìé(å RÝ)ø²£ð*$¨Mm:ëeÞh!#é·¬ï†{ÀAŽÝ®’º¢Ê1›úLwïîj-œí)·ÇIo;tGe[sÌ ßFv„HÃð0ùIãèS&s&xÎöiŒØâJfl'¬³ ÿ~!‚?¼aJîÝ3æo¦SŒ§•Ðn'î…¨ n7] Cz½‘p€8›Ã&XùN/1B×Nwà¼:LØ]¸ìî¾ÄÀwòB—pSd²®<èÉmßÚÔœ¨ƒ\¥Zœtk̵ìÿHX•ð$'ƒQñ¦”õ†b|4Áæë×àüÀ¢x ©_b"A¿G1Äiæið‰vì‡åqô(ˆ_Îd¾f  ˆ‚ú1ø.ü*+‰~û±\?¸’Ѿj^èIYk0©îF„5u˜'ÚásF/­]Nˆ ²ÀTz$I•_{Aj–Ä£îûPàîüqÇÖ(~VÜÇT r½Æ¢¹MA/8bhè­ìAHFÂö{'Ô•#Hภ‚ɳ¡šX½JT0ÊÿΪ$­Þbîg‘|s+8Úƒ¬çØ÷€=ãnï|®>…3ƒ„­— v¿ê@t¾Åö¨Ÿ*.!n¶Õ{Ä¥”1Ãë½x(ÌGq9Tv—Jµ•Ä’V¸¬@Ú¨ÆÌ¹=#õ%JG²¦`Y”UE¡_™™"õ6út²®p[j¸—³, ,wÉŒBÁëœ×„¦«aÚø…@oZ`0(Q÷•¯Ú÷”\lS}zúÀT(`>ﱘë?Tr¨µbwJøL…„_S6•˜†—¬6ÔT¾Ï,*§=yü®>…B¼f×¼Ùmßi¤b½0ÉLÿÜIòX½áJ73òúÁ6ÖwQCˆ‘8ñ0´Ð£¬/k¨_ªx‰#Eê %ŠxºÏy[¬ø¸³jÔ$f—|¥}#îeŒ,—¸Gv9/BðF0è"¹³‹c)H-úx—,íNõÜ¥˜È-Ô*Ñ€â"°s¸pù‡ì !( t'û­¿1•òzÎ(€ŠÒ/«ãÿÚ ÀvrvÛm¶Ûm¶Ûm¿í·ÜV6# ÛmªäÚ¶Ûm¶½í¦Ó±N%m¶ØŒm¶Ûp¦ÀošNÄí[m¶Ûm¶Ûm¶ÞÛ6ÛäÒrI$’I$’I$’I$’I$¤NÞ ¶«m¾lÛ`6Ù±‘¶ƒmÄÛ{Û¶ßm·Kme–rI$’I$’I$’I$’I$™¤’I$’I$’I$’I$’I €x’yeÀ ’I$’I$ITò`’$’I$’I’&O€ A€AH$’I$Ÿ…ä@ ’$A $’I%ñÔë‰ ÃrI$’I$’I$’I$“x+j´’I$’I$’I$’I$(hØõw]¡á$ ²¼Ó¤’I<ʬÐ/$-ãÜã…i$’AÀ‰Ö³9½K%´»t5 I$“·•Õ¡ñænw3§”ÜJ’I$i ‚$`äp òúçtä’I0Eð~Þ÷q<©Œoåi$’J …r†YÌFf.%éKI$‘–ö‰¹W ïn%ý^³’I$?*jK4›ˆ¡‰âÝld’I<¥äë2©Ù¯ÇrN $’B»Âøix ‰yʲwVI$‘ýIœù’:.åŠÄ’I$X‰·Í¶!ùߪ8â®`¤’I%µØ)Ab[ûT¨òi$’HTØÝ¶H7žtÃ,YóôÎI$‘D?Ì „HAbHŒÅ<’I$LŸœ$É~ÏqOÓ$’I •åæh.s@(GÊГi$’F;½;MGNáMï9†I$ lh[W¨;ëé\Øâj’I$M8úò›m¿´¦ÑºÛ¤’I æ5oô%cHºÕÚv؉$’A/{‡k%E^ï³µÚöI$’¼b´]j¤hÕhSÆÈÄ’I$ùÄù½þxç—¦Žä’Ib³¦à«,šGÎ5 $’A%¹d¨,Ý5 &]â!îI$“ju“GÙÙQñPÆ(’I$¯½›d‡‘´öÉ17¼$’I}ñ²^û¡Ë{$¾L)$’IØkFýXDù—´ü²RI$…¤ ǘ+³QAI$Ű’I$!…§¼°‚½>@ðàád’IÜ“lhÚËRßÔÈoº‰9$’G›Ñ=$W/| ½„ˆ¦I$“Ÿï@TD2o€–™4¿~’I$û¶F=Œã‰×GkKߤ’IQÎKð-¤œ.3I$’KáFƒµÚ ÷¦ŒdÆI$“iîy¥ÑcºJ¨+âE¾’I$bö˜5Dì&µ=1«d’I5‹L7‹®’x¬wŒ É$’@x¨Á"ñLS•Séb•œjI$“ ø$Ôà»´u€ÙV4’I$Âþ-Ü˳µòid’IG‰ †´¶pž·Zû¼.)$’H8´ªy1ô T²nÖ=:I$“м/VBK/eønŸê©’I$oXáh&C%ÎH-ýÿä$Ad”’I$’I$HdåºÇ`°$’I$’I ’H$I$’  $’H‚I$‚ $‚H‚H’ ’I$‚I$$’I$’I I$’I$’  $’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I&÷Aà‚$’A’I$’I$I)bì¥Ø¨ ’I$’I ’µ’A’I’I$’I$o`$H$’I$’I$’I$’I$’I$’I$’I$I$’I$’I$’I$’I$’I$“u¶\HI$’I$’$’I$"Â` ’I$’I$€ $’I@ ’I I$’I$’AqÎN$‚A€’I$’I$“ÖÞp¡€$‚ I$’I$’@$’I$’I$’I$’H ‚H€I$’I$’I$’I$’Aç?éþûö9lºzK’I$‘–xŸy-Ù+†EBËd’I$ó2 HŽŸC7½ ´…I$’I/Wìpb6¶?¯h“úI$’N¸ÌŒÕãâ†CÖt¢’I$’˪‰@(Ìí˜Pä’I$Ë9‘´,¾Û®µœÙ$’IŸÛî®÷ZøÊ£ÿÆOÎI$’Er‰]Ð@;Iš’I$‘u‰X éߨ«m"Ê­ä’I$„¹7°üTj˜vÍ‘é?ÿÄ-!1Aa 0Qq¡P‘ð±ÁÑ@`páñ€ÿÚ?Áƒ ã åàÁL# øNˆ|¯a´¶D¥Z2¢¢«?ð–òý5p%1ª!{êhÓÔMHÄ'îǯCUA(§¨J$ °5HAåBÝx)JR”¥)JR”¥)JR”¥)JR”¥)JR”¼¸+^°õnzÉÕéx¾¯‡ª»;êjèbœ¾XXôµHz£‹Òò%=S7ÔДö€tR”¥)JR”¥)JR”¥)JR”¥)JR”¥)xÿ¸!Ü,3ãA™•/Ò%þøöÐè¡L}ÿÂúðD¸è¬o)YJÅŸÐ%| ü ´Èñˆ[ â…ê„ý ø#àÈ(VÈ÷àNþ”¥¡FÙ¦K¹FߥkWCmºÈdR EƧBùE]|*ÖÞÞHyIüËýð]90ŸáÏÏãÈÂ*’³ÏoêýÅ™¹wÏïüàA’Qyv1/²ÒWíÆXþü U¸ÿÑ?¿ë¶¾—›ßõ™qô¯Ïú1HÛrþçýÈåìý dDDD[´O~Hˆø#à‡Ñ°H´~„ÜTHù¥(ðQ\º¹n£m Ò•Ò²—Š6è® »+¡>x«/´ÕPJs9câñ¾YE‘²A¤Æ ê š¢ã ˆyÃÔ†”ê‰0ÚX!Ÿv—‰AΆÊ^Xðð-¼¢H$’‹#ZVUÉMÛBPt鮲ô_EÒÙ‚!róÃ(ÜâÄ[ƒ4\¤ÖK5z¬ CbÒ¬´hnlëÚz' ׯM9TuxœAR“ľ•Ù2 d1ž‡Ä‚gí’±.Ø„4lK“è¢f)¡oÙ~„ãE·ëÒ/Ȱ›f Œ7ü†Ø!xò:bL¬ª¶%ýšGBCk‚´„èÄ«À“À% ´*9ì¾,e/ n”ß Jñys$šB¡•3%ÚMþMòÒ R½zz¥+¬Óí¼¯¡§r?‚ ^7-†‰ózÈ7²Ê$ÖYGw¸d›s­Ø8 W![õï…“E/´ÐÔá N6¼X ‘ h"I¼ÑŒ ì[Ɉö1¿†]5„$„ Nãb´òX5üp³¨á²› —µxØÅÃÈ×À‘Ó†:°™MúXéÊ„ƒMUoàrb´Rn¤e›*'CÃxªC"ö×OÑÑÑL˜ß¥:ÎÇÄ„Ì)6*å7¦è¦°õ'Ø„Ö&„¬Ê*>Æ9Ój•FãÐЗ2žëBå¬24k‹Â6àškä^‰T!è™M‰–,†o±Þ4½á!³ª²\ˆÍ², |å –ÆLyžÊT„##!‘k&ÍB¯Óåf-Àœ%v$+C­¶$h ©‚Û*ù2:ªì¤SúY¯j§ 27È}ȶA":$‚D#e%OæXš!hûHsÙBð7„ÅÝ´#Pìt‹!+ûÚuü ÎÁçb‚i1µx|oÚh‚KltÐ…;"k (JŒ8¼‡Õ Á|“G‘‹©ÆÉ £¬™¨‡¡N6'ûÜi )0C\ 4˜Å®Æ¥aí#4B Á ‹ÚNd{Ùñ ØmpadW±­¡ªÅÛ'± N!.}¹‘ö †ÇúP¸Ì¼‘¢b€¸ÙHZýô^˜½¡ìÞ…Ã0áh^̸0(V„Ì`«áb¤žˆAN„Ms:ØÄ§Ø¬&UÉñ)`9#>Ût,|}‘*xìLì2—!Þ@ųiÆÐ˜·ì­T4 ‘!£M_bò>Øæ4Ahü’lZ4`þ ‘¡¨ÛÈ¢ ›¿Àði0aJùá®Ì±cc”k½Ô+Z#&GŒ*±èN `K¿g$ˆ«‰EÂÒ6-&û%›¢±'ä±aYrž&š+A6Æ5vùSc4C´ÆëxÞÈeNpÙ"$og_÷¡2ð‚Ï „dˆE£Eã2²!ÌeÙCDˆOŽiKóÂû&ž²»Ñš¢±|†ÛÑPؼqE[eŸ•E•æPÍ2< M÷Å(˜Ùx£bcû&‘ –¹²1cb˜ãL‚qRvà²B!(mm»„òv0ûA· ÊUØáDD`ÀÄ‘“³¬O•Z1«‘¯‘$A,‹#Àªh£ž\Ñ>†ú¤"Å{'¢²Ñ ÐÙ{:!¬Òü n’ñ Ò²»ÂÒRÉ¡kЂáhÙ9ÑD}›=hY#¢Y"0`¨¨½ñ0EÂP¼>eŒ‰ùéV‰žÍ´=É &DœTìM=1Ð/g$ýeäÅ0uÃBy„â  Â-ÑE ÜŒÐyr…$[†$nöu "GÈEæ›1HuÃpÎÆÙ\\bÊà’Èx h‡†_L6jŸ\®^Î’‰äÅ0N/žÇ Љñ;†•'hi5°Ó#B4$6F §”%yI–&’8ßì­_ïFYBT¢„HˆðÀh¼>-~†’F3¡¥`À šÀJɦF8²Ô¸_d×ýèLNšŠ™/øLRѽ²ªÖÍXþË?Q_äShŽ J±ñ£gÃÆ‡?$>'óòöïÀã*I/à¢ç|›æÈ¹ °Æ•áàb›|B>ýËüûnc“¡µ´èHü&[蜬›þ…@r„„+ÞÅ‚Kwž‡Ke”l‡à%`ÅÜÊËbiØÐÛg‚rÕ6öÙOýEÃè»®ÈÑêK„m³Nÿþ £"óG•×ôÅmý™Ü/ÓÇàIÿŒG|}3ÿ o6›ðÈË+üônOò95ò=Ž)å<§˜óaë«ÛFÉm$ÄÛÔK‚ p¸B|$BR‚. „DDp‚ 2Ø­:~Þbf‰ØäXѨ¨¨¦¨{¤†–"â*H©¦ID´‡V…dd]¢"!ª²…-\¼ ¼/&ÌÁ»Û²´6`-ÄÙJ9¬2’Ïh06ò£º6eªxÊÃâ¶ëK‹TÁ›gqÀÈYCUEߢPÂïädèÊÜ{zˆ†¼`} °vCc©7D‘Tq!Üs²6!®ÐøX87>G~•:2ÆL„ÎÑM ¢=‡ØÍ«7ÿzçÿÄ-!1AQa 0qP‘¡ð±Ñ@p`Á€áñÿÚ?´ŒŒÊddfQ–dÉ’”¥)JR”¥)JR”¥)JR‘‘‘”¥)x-| A‹—ô$Þˆì#ØÓDddÅÿÂk•²òBÀÝûC(ÓR$0ѯ­qVþ Ì×yNr·Dç#tN@Ýþ$%Š•"zåsñJŽÌÔÿÃ?’scG~KòN ß•Cû.'Êß…QE&D!B„!B„!B„!B„!Bšeâ¡iq¿÷ÿ¿â£oä6T0 šŸ%Ä+ü$äÇ6÷paÙ¦‹X¿Å"gI¢rAWˆ×ø¤x´Š"nð½)¿÷§ÿ í¹\OKIÞïbæÇFÜU%Ö½v÷¶»-b.q™ú¾‡„o%ë§úï¨F’ÕRÜ~õÌÈÊ/;ÖOQ6ßàžV”Õ-;ùõ¦$J•¬¶?Wy+ÑçMÔ^<ö=²²m¦³JOɬ TÅ×i¬øÃ]Œzÿï?Á'2Šòd´lËf,°+ìÖ­Ãò¿Ø{ \ì²J‰m´öb—Ücmïø9Ì!² ØéŒ„'ÃZ!¦1$ļDvO7’dˆJIS-ˆ–¾¤?…(¶0hÙ!¼_ t6Ln¡yfZxNÂbXBæ‚‹#G - &V†¼ +&>¸NTF7†.ÁG“(×4U]7Ç1tr`¦öY3#b{ “hh@±‚X±Á"Þ……¢~•ñ†BFìÆíˆëlœ^(ÙJ<¡;0ÿ(YálÅ]#*Ф«cIå P¨ ÔNИÖÀ×CN}+•L\A&…bðcqð•øBM§F%‘+³Àk4¥¬Âࣈ˜Þ\Fšpnƒdï‘·>”(7ÑÄàÑÑ=†ÕA!šÆ;„v2dǦA< 'cJ•d˜ÊRf5ªfd ©#X‹¦„•„khmG„)m‚sú˜Œó#TYRBÐ’fV†Ò µ¡7eõ1¯žµ¥–E‘1(Ê Ã2”sUÒ0NÈÁXºŠb ÄÉ/Tʬý,C)J5‘ †¬Y8ت³Ò0²6Èh†]™KÑZc)–›-èkL‰Òd‹0M剷ÛfÅK뿈6 ÇɲÞKFŠÙ‚‘2gµ%ëbLØ@F!›8„Ì#U¡güŠ2;kÌ–A¸, y¡t }+†ò-q®±IA¼ä¦¾ð¨öÃ!‰MfŠ!£Ð]tÌ£²^Ô²S¡cŒvWg¤:x´% ðªB½ýM² Á:5`š”t¬oØ£ìd^cc¬Ä´¦2Q‘ÏR/cP‘p©mð64RÞl±•<:·"´¢ *TJ2b/ „©iH­á42´=qŽ(z5¹l'{Q´¿#.ÛjÇÉŽÄüxpeÞÏË1·ä/6ØÒe$,èóô¡£\ÂpøhسÂʉ=‘·‚ÙCÑ;a „GlJP+BxÃ4=a6ÆÁ’èEP²¨™³xúäy'ŒÍy|*†ð á™Ñq«ÂÑØÑ YBË"I0†ÑM?²˜ÑµÒék‰Ä¾¤8‰Æ™!$%ìpYCa Ø;D$j!*4¢9èFlÐlfò*Ò[Ù[cá„[-À¸¸ÇÔˆCBðX'D1 MÚWcZ!bf lLU3?#ԣʉR£S ÊßøG[Û·±øn¨Ëнðõô®]ã —Ç>‘sW fô\ …!¨Aà“Q!÷„5 D˜Â–_zƇÿV¹ccf‹v=Ø”Tïí‚Iä¾#!£×,zì>´3!v<¼`:U£pCšØÄ0¿u³Lú*L±?\U!mA© Eô·3Á¢Ø«E)M ^ÆÍF%çã«h«±*Ë)äNÖFÞÑ›‘yOb®‰WØ´X61)®(ßÒј’¦"ky!àavfKƒ22ùk¢¦¢-1MS(âcé"­‘(A » k´i%Yð«?Fb¿BIÅ~/éJÆFžH½†‡±Fm“ àÜhœbE±K¤45›/[­¤SdYEiÓЄ„ÒçB-(eäJfèJ5²*Y¡­™2áýITYd´*Y§„X,rÃ1ØaÒ„ž×=‰A‰$æÀ¨–1Úo! Ö„†¸`¶½ŒÁ&‚Ñ®= ~â¼?©”К¦-BÐÊCET ›fK³’A6ƒ¨6Št$²lÙ0(J!ªÉ ùe š¼eúpBb± ^)K@’Q·y¶%‚¤.j(Š=B[ð&J6)ä3<1„êƒ\0²iE=cW#j „éI“:þ‚M‘Å[™E¥)в ë" ”Hأت¤9#cbÀ„á’àâ ‘„A(u¡11ÀÛÐÀ¢w"Ê£ÞE¼}:‰¢‘~Üò:iŽ:çÃDU›Ï„ˆÉ8~FûLÆ7Å%ì5`1JbQ³`ؘq#R³k$¨ÐPîý;:\F-`†z¼ÑÂ?^³b½pl´Yw‡ÂAíz)¤bàüx­1åƒÁ ½2>‰äjTòàÙ¦\ älWÓ¸£(‰# ìÁx̆u pÛ)/nÆŸˆ„›0ø”pF$Ä—D£A«2/Ó°¼2¦²"3 Њ7x¼ZU9œE¡ÂÀFƸœÎ®ˆ&?Náh|šl!DÆÊ!x×ÂŒ@d„o‹ðKå`é£éÁŠ… 4ˆ4!!¾!2¢ÒpØ!WìiL¾ ^l)a~Á«éÙÄ$ᔼ2Œ†9¼aÁ LC#®Ùgø_)«éÚ6 6Qñ¡4'É1/1|´^.À©$┼1èEæÑôìãZø¶ËÅ(Š&èø¸bc#ñx¥+)KDáMš¾âΊVJ4D_o…®0" "U1M\†BáÉäϼ"RB6¿NóZå8Ë‘´UÄ3¢q8Ns£ tqŒ£ ²>ÁÁ šÀÉÂDáª&?NñpÏ|µÄèŒJàH†™ –*Z+ˆiÂÖg£#^Äà”ƒ~ ­>•®$á> 2 ŠCÉL ß°ZC0¢Á±è6è´O"FJQ‘ e1Ó°e¦Ilˆ4K•fFºN1!D‹"WE ˆ–Ùf…œ±»—H} Õ±LBX–rTÊZBRBúMhH¿NÑd˜™ËÁúᮋ47D¸±\Ðm!€AMë†"åB3#¯5};ø5G„gÇìðF]¡ÒÀ¢BŒÅ?zò:x)t$ícTê) !ø2&oZ2V&Æø|>çà‚ð,lm £HOÈ×C‘ °ÇK¥š+|Xe’§Â-c+‚òa’ö'P†X7äÕôì4Ša—¢ä‚óUB;<‘%Lí#HÓ*cWBp‘ è´SÊ^®*ÈÑ` ØØØý/TŠ_мwM¯"|!W²qFBY°56Y …ˆ5j%hJtk ÖR¢?")¯éÞ-p¢c)8ƒe2Q6_†ÄÓCvBØ2TÆìM<®®6=™*[6kc4ý;Åa“¡A±àLª”cÀ šáQ`¦x¢­™z}Œ+=¡:X²‘MŽ’M ’màu`UlÓôí†Ëƒe0âü‰”«Œ :2*FA64FÀÖňtœ6ÁøMú•\m‰Ù§éÞ& áÑGÃ! Q;Ëá1¶S'_Šä!lüQç‘Ö «bo±šþ”Iê=¤õ“ÒzOAé=G¨ô ]£ÔzAê=G¨ôž£ÔzIé='¤ô•èôž³×Áè=bz¥ü«–t‘fÁÅ{÷üoš;OOò3]K_ Åâ—ŠQÿøbÒLOXMLzâIþt"S)žý®'ñ *á"¤tcIWi“$K®o7ŠRÿ “šèÃx+JÞÿ‹IþlŒ¬ùýëz~?;cB`˜–SóüV°X¯¤üNQ§úÿ3­~ïÿ!Ÿú#d~ ‰©´ž7묙U+ÖH¢1áq®˜ÓN>RmÅü3¡ oVç9kÊüÉ-´“-ùĸêmìk¶]ŒòñoYôWèÊK·×˜–óâë,>í‰:äkGoäMƒ­›¿eZXsü Z\&Ú1ø^Z,Uhí·|-3†„§+• .ÞgŽÄz¤Ü³ó”³»×þƒ_„IÄ!JR”¥/Ù¯=þâ MßéØŽ¿ÀÔX«“õ Â?Oû;Qá Ž%ådFÄøºu}mŠBO\!ºIð4Íû­ÿC$‹Øò=øHÕI±6xU9쫌1«Ê£û X×ο²2Ÿõ‡úze ntúèA³³ jr‘ŸƒùaÉäÿܓױÿô;+†-¡¡•èß7¥ßém‰\ ¼4ÛôšCÒp7 1ô•±)ß±12ЪþaµøcT–ÛOûiÆiö x\&xH\>[ø%\_úÈЉDj3}/èfz¬l„…y+U¦ÑxÆ_–ÅVU‰ü§Ó[G`Vß§“N‹h}â¿}µ¿ÒÐÀü‘È^;ÏEé<¼¶¿¬®RbkÀ£è/Ëà O­kðj°!¢^ú! ÆÆÑ9Ö$ÃI ¯À¾ÕŽ€˜âB8¬Yí4לoÚ3¤‹ ¶ð—bú7ªìý J4†‘äÈ®·ôןOüOd)R™È'OÂÕöòÿ|R‰ø__ýcëÞ&*JBþGª$458¤åš (ËȦ¸ç¤dr†›© 18[S>{þËd†Ë¡§ài˜ÔÃDõOðR˜/Ûm 6j˜‡£4 p—„<†4i—U'ö„ÛK,Kn?"¸¥ûBÛªOʦhw\þŒúÿ(MTOü£?ò‰ãþ„îø'Œñ-Ñê€^§¡"ëf|#*[ì.¢Oc«Ë‹chÐÆáXÙXØÙK R²Š(¢„Âcê=_¯¢ ÙmñJ"FL‰GEÑõ jU ð”)Ø­d'NÑèÏah÷BðáJ%¬'93f?M»þÄ«ƒòLO­Òµ¯÷û¢-å‚ò¸m­ ƒg= F6eàAe³Y©´ (®˜Q¤We†Sf5Ù‰Põ´lbÑ6Ø®‹= mŒYîÏ™èEjØ2pÿsõàâWcN¢à\Mž )á ì Z°Õh\hÒ+ôPH}m2•л4-AÒŠ)æg‰®.LŽZú/o·ø(&²Y\PMÿ;þ_?ÿÄ+!1AQaq‘ð0¡Áñ± Ñá@P`ÿÚ? D6®yOÑþS&²«ƒEñ¸â>jŽÕÿ)“ ãÕT|tŽ=ubbó;ÏË?Œü³øÄ’L>sûþÏïùýÿ?¿ç÷üþÿŸßóûþÏïùýÿ?¿ç÷üþÿŸßóûþÏïùýÿ?¿ç÷üþÿŸßñ¨Õt0®zn}¿Á2À»gâûàÔûÜþÿŸßóûþY´“ÃìÏêÙÚn M.„âra5d!¾Ó5Ÿ§ˆŠ•`Ó'ø¸b U`e¡šA¥ô ›ùù¿<3ʸ¤ ™Û¢t<—sø0ùçÏ>yóÏž|óç‚ÂP'’ÃhR¼¼˜˜Èt ÐLJÝ+É3xE/L2D°µ¦cŒ·ÌmˆîÌ"®0‡CW«°ìİ€,Å(„éã>yóÏž|óçŸ<ùçÏ>yóÎÿúø™0|îTÐ{q½]Èa¢´ lèà "r'yåvÿyóÊ*„@-x2òÔʒPðžꔹóÏž@oû±X^'Ólí5½ f·";?#^üÊlFç¡¢˜SsË=¿cµyËîÁûþvß¡sªNéÓ‰@x„ÜxÇ>,/¼¾òûËï/¼¾òûÄ¡|/ #Š"ÞEÍÍ,D*4­tÚÒ1ZÆÎB 6J,´{ÂÉ·Î5–Éê`P©ˆ*Š·Ù™.Îk fV{šìV·z/¼¾òûËï/¼¾òûËï/¼¾ò[¿§Œب%‘2SQi*¦+††`øðgîï/¼VèBu­ÙE‚i][o5 ­NÉjßy}à‘ä#8¿1!%WF³YbT?bqZÙﮨ۫ gÈeFNÓÇù*;ì²+¥ó“ð$Y ›Oå“(}Yá|ô€ !æ#üaÛ¹þ¥¤‚a&ÄVÿÈ^!e¿«›¿ï䜟‡Þ1ÿOýsÜÿo¿ËŽ„äÛµ£ã?­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿­ËúÜ¿§Ëú|¿§ËúÜ¿­ËúÜ¿­ËúÜ¿­ÉÕÇ«çü75¬U].Uñ¡pàÑE€€âj_òëÃÕ<ÿ˜¥e±S0Ç6› aÌ\šB 4”?âëHÌqЦ§Æ[%5œä@GÇüM(nRÂÌ!šÅA4;k, ”d7^µÿè ìfm:¶¯N Ýa¬šzyyúÆ1(aAz¹H”<$ÍIÈ68ËnæÎ—þ3)5n,aµ– :Wⱌãþ íõ>ÄDßÉJ?–‡Höþ+n‡ÿÊV¯»çüÇîN¶ž!Ç\ŠíBA£`’EU»ÿˆéғгÃF`–Xe] Ä–rúÿ+Õ¢VlP `d°¶IÀ0TëŸø§Aa3IÑ‘;p^ë eÛÛÃÇ×þJ±@kQÅ8‚·ìÊ&¬:*ÊÈ`Ä_ÕbøÈëK5ßð- [—_÷ûáÿ ûüü`?ªúüüçØ<¾3Î×x¯ŸëŸ`òøÉ)ô¦èóÿäÄDDDDDDDDDDDDDDDDDD@§SSô!E§“Gaå}\½®jD½-õõ,]ÓMM̈Ø?Õ¶CKÿåAr÷j'ûÂK ”Ô BqïjÊHkC~yÞ9H¦H ¥ùç)Ø¡‚…A[U{¸OÞ dK„%œ96|u£J¢¹uóÀ&€)¬[ã {ý¨Ô“ϼ½:Ú´Œ| )84¼ „‡EÐW+”BÌ ªÑ‹ö_tË mØCÑ·*ìhr\¢Œ…"Œyༀòª]è ¥†À$iGoåï-o&¶M¤i@¡VôdÔQ¶‡|˃k…Bnå[þŠþ-?â5´Òu1e¬Äò±©¡tÓwêUÒ¶ lÐÚ訵oCH „DQÿá¶œ ]¨í;/EÁ¤ØŠ-¸–Þæ7š‹ÂПÊ_ø)Í÷sL™<¶¨¢š{vI‰…g)·Ãw†’¤Ê/{|ÞsdÀ»mzé?›xļŠò¹ …Ѽq°Wë‚>‰'Xüb޵oj® ´¸4éëãšu´Ò™°]¦ ÖtuÔ ¬DPxœN OˆFÄTUjhÖQ•E¬@oÁŒ¤c°í*®Õ\PRÒ+•>6ëÞ&℞1Ijì1’ˆœ1xÅRÔ/JÔò`81~  w#G—¬Ž¥<, —Ÿ.M!¶" j™¼ôÛ—–¿ü!¦@`YsÐ:ÛÎtògØ‹=\ rY ¥&ÙËÏüˆ\ýlß:‡ïôÄ3%|”ÉÎ!– Ù0ÐÀkä `„PÒ&SAwè_ŽÉ%Ì{å±T…´ÑÀ„V$î"EÆL«v-Q´’Ó,D]§´H(ÞfÔŠaqŠBƒ€iÙ£IV”¼EIÑJb‰ O¦Ú†_C4†á4„Í9ŠŽ ”TWe·"{†ƒXÐ)• ÌKPi;A‰G ô£2¥ ã:IJ[ м¯&Ñl $$¦òÛA)pÖÞUŒÄØ<+Nõš‰&™W{ä A- (Ì €‘EÊMæÃmwƒ Ô`žÞî"O(XIm1&—¸£j…ÂB=˜­ruFIzZ˜Ë6ƒ††ŒgúJ’!€mNr¤çøWL›2zy9ª°T.±‘á¥à ¸ ®à`Ù g4 ´™ú”èl/ éªßŒQPH£•6µ=]du³AH@£Ñ€a 2JZø6:qãïF˜+Dö<8j ?'Ó Ó¶ÉS숸„îëCïÑœSTÇV!VNñvÓ¼Icò›t¯#¢¼áb;b‡ib ‡.Ø”TJ:Ñã! ·œÓ !n<¶°õ8ÆušXˆ/ÎùÞ@¬ÄTÀEm9!¦å"‡hÔ*ÁŠ"—„X#òóúâ @ŽùXe)„vX}T!Žü2*ãí óŽ^|GJ»» .-±LBƲH“‘&*]¯œ"àª!W“P2†'©*zlÇ&3µ®@8ä­»€ ¢s”„ „Ò[³se»c¡Av4ckµ —'ãUö‰B‹±ã©‡fìQTk»cI6f€[_UÊÃUrIJ¨ú³Ô§Ç†T›K5Sœ¬Y íN,KžÔ“µƒ@èY÷s˜ë:›á'ÙÂk3¡k°}Ä@:õèÇ´ª=tcC3Ez')Í»éÅ CZ¸ $¶Bti=[:ÅîàAøa9Êј žX¦@â^b™=ÈfÃ7r¥»I‘RFÖŒp5BvÀ¥:GUÇ®ÖB¾Hp •Ö“¿; 9‰Ê-7ë[‹Eóÿá`D(˜ Á¥Ëó·xµ*«1ðž=a“Yàõ‹) Aâó›Mó›Úi«¹fò ïdö¿cÇŒIBÌÍSZWw ¡^kßÛ4ÀæGÅ5ôÅ! j߯°ô,EAœ1Kœ•¦òÿø]ÄŃ¢6ñ‚ßøS·^˜eJyò©-5¸ 6G—%)¥îè ¶ÿ­ÈåZW(ùÝ’ÿâ‡ï¦Y&O;³nn]"œ¢"×…ƒk™èÈŠLÿ4©VŒ¬RÀûâÀE— ÔL@'Lµ¸ÊáPìÀ›„·I»²ñPÝŽùp‘ÃÑ ¯Ñ³Ìxÿ®lV°÷‚SO‡>2ôÊïr ï!9Ã\d˜Þ±šWMæ›.: …Ö Sn%W=d¾¦QhPÖ±· ŠL‚‰‹Ѩ?~ 0)œ+€GúÆäÅ!\€‡ íÀ·ñ‡HvC³¹UYìý0Ú×éƒ7Î!ñ#­E4b¬Š ÎMB‡7 P…˜QdDÿ[ˆy8ÀbaSÀÁv/ELÚ6ñ”îcy`ÑxÆà›˜Ýsü‡ÛUÖ8(Î1i£fN*„ÂÖ‘¬a ðåþ³f9#]aÑuUÖofâv{y8[Á|VòòÂuÜ8‹ôspÐ[NXñÔÉÈ Øá¸Nƒ”6œ\*$L5fÃÃ%΂q†•¶àÛw†@ý°¦m§ËC<="õ–ÍPM\”èÅ]픋!cf¢ÂO¾ +|^6àƒ?‡·$ø.AŠëj(’äº?7Nñ# ¬Ç³³꟣n.mA¤Íº9ÀôジH ã©hxÆÅÙŽ ¹S ¦DцÂ;ä.áF5ç ·iŒÏP·Î…‹®°Ã'´Þ8.7ß—¬¢!Þ ëQ=`…ÂhD­âb®^®±§Û*dGn¤© AD¸©Ê2œcª «’Èmï~Š¹Ù˜Û›Ž¶'Íš-_ŒŽJrå¿ ¢ü UWLÐ…àonh0yóœa­÷œàÞq\ •ÅT·WXMjÏÆní cç0p{¡S‘òh0‚õ׎iÊ¥°s˜æÓˆ$<ðëA¼oáèÏ·]zÄV#£–´™§YRØ„ñ%+I´ª}ñ8(ŸÍ&ÏãJ+LíÚ|—Nhþ‰¹õô-ç!>0߸Æ$G´ÇsΑ3£ðdDÄkEë4¡ŠH<0 «ó…в02^[ææÊ‚Œ*á"æÔÏµË”W¤LNÃÜ̓g¶Y²7¼ßü"1‹vÂ÷fÏœ y¼ý0oÀï ÅsIº† š.°Mù`ENU·¬˜£ëU6js:îÃ?x±×;W"GqO¿ý1uè-¥ç¡¾?ÑHÙ‡lÚï4?œmM!–á]®ÎñFr8óq®ðAÖ80Åupi\Ü!¬]yÅÚp9aiš‚ß`ð÷16 œë¸þØ¥A²õãã¯$ ôœäŽ ¹‘ÄäE:‡F/†Aª<Ç_ûŽà #µd?Þ"D/Û/ù1sÉÂàƒ ©¼ R †|àQäöéQ÷<·6#V0þ1µ5¹×ŒyÑ9ÄÄûý%gcî€oSƒ4´˜ÅŸ!¥˜&²Å.ÚÃ\}V3IQ‰§}½`GRpãíšÁOÁfýÜp:±|yøÅК•N7^·—@6%NÖaëV[ðaAó—ÕÂ}'1+ÚrÓy¹:JOßÃE2²\g,,ð¨_8‹֭œΖ§¦ûÀ¢o6šµùø1#IÚâ¤òùÃ<ûô•·t6à:š3¼â’Æe§œpºâeXBò# ³qtÑóš– s…ˆ¯ÆCôÃÞjðV#|àH™h`ä ©U½|cSPù'€Ñ1¹ºeyçnNúu×£ŒZ}Ë«“s‚ Ål"ïðä<`4JE_œê+Z=QÜÈÅ ‚„9™Á“Þ$WqÉu¼(0j-‡(y;ô×x,“4툽RçX×±áã6r¼O9jÛ“B¿¥¬LFâ-/œr¬™u/xÎCšGx ›\(æÆ¥:ijù0£Ã ó­Pìʹ¼­T.ï²}æ.p NŒÕ7§‘¸ëz‚Yh<~øKõÞ&Lªb@>ÌukVÍ8€èiœ3̬JxgxºÒv1%¥Ï¬QlqXužg_±ÞOy½†ÎÕ®Ë%:ÿÖ»<ü±P~™ÒãÕ¬=dÅúD̦ß$rÅWË®!¼èª`¤)߸-Æã~oAžq$Ã-Ö¹¿ —F'Ê.òû;ðkx¨Dò5x5ûá2•´b†s4¸ÛsìºÊGg¼Þ8{1E&²»ÍžÖóýglj//ï›»Dvî—fÔä`ý£ÊãÔK‘>——èužéyÄ\‹sAøÞ®ñ=5ã£Ù_<Ç~~Ø+2b Œ¼æÉ7†·B/,b ¦)=˜c Êpï¡’´ç.Cœ滸GQáïç‹…“$@œÌEw<ç©]iéÁó‚ʳN9]ãtúàìJ?$g6µ†Œdiû1icàÆü:/¡ƒtI3dD³åŠUõ-WÜå‡Î P.*äÖ~é†ESOœC”±ÇíC¼/%u“‹ÎJDräÓ [¾.Á0Þ‚§Æ&´˜pØ:¥Æu«ìcÃîᙵª•vùÀ2›ú$£ï ª4fgvóL½¸)g0)â眓¼èHGOýe8ô?\Z#Š5>ÐoJnOÝ¥ÞQ ªrâÞôbäA«äÆhïdV®M]ž1`CômM7:/ã̓xû8å0©º¸[„yb(U;ÅߣÈáÞh¸°Lâáç'‡µBo¬oS¡Ä80?F×½¤äòþrF%FîxqÀ•ÈåÛÝ93óœæïTcº»ßúÅM“¤ ¾wN7ƒ/‹iûKöÁ.“>Uª9(“´v¾psB¥€”´mÔQð÷í®²x©ñ1X凃Æ»àqá©ç*©ë›ŒâÀ9Â5}q~‰n>gÃ6ú`Ô÷ž0PŽðÍ'ƒX. =¸W¨âà`UÖÎ;€CŒ@/y§œà=årk+C¼*qðÌ8‰»Ô÷„'ÓÆôÑ7¼WŠž È씹&n¿x=ý1'+ȰÍÊð„óÛn'‡¼Þ±*håÎb¤m—%n‹¶¤¸HÛƒ¦@1»©ÁçåÏ ;ùÆ…F«UŠG ×X0#³–>·Ð÷fÌBzD ¼æÄ’¥ÍŠfšë 0GgÎ)‡±ÅBŸÐ`> ÞYñÏ}¼7¨ÑÞKk•˜I†×ßÉ¢Þ2ôf‡èóÆy›µ--&ÂÒ¯"Óì€By}ÆD†¨+óŒŠø_Œ*à€EÃèÐçææØ<¹HÁöèõMöòýrÈò]ž2ëˆVÝ+ãÖ(µw;9N°Ä`çR€Áf„X•=–í1mÖ$Ž¼Ë“Vh\×âþŒ<«Ã0Qˆ!©ˆÔ2÷™‘q\^ð½jèÁEC¬ª*íÁ怢Üh! £ŠøÄðk fÛ myÆÄ 'Æì}xÁ* å.Að„¸0Wíe Gnš4;a.ÆpT?Î`Uú ¸_w×'HO:uŠd\a…E ¼RwëgŽp{ìãÊ®%¿àÍߣ|±ôoã!3¬Ni”‹0-‰†Úd:J$ÁÃuŒBkM .$û’râ7Ù‹±.áÒ[èo¸ØÅñÁ…Vàùàúv 0ò³vÛ‹Ÿ|œL‚7¹½¨}pƒ5¡éÞ\˜ Í•çöÆ?™-Xî«öɹ›ÚÍ`ÜDïÖ“fk±ðûå6*½=æÐ[··Ï§XF¿ë6â= Ĭ+Õ®[Ùç1p½ãâˆâ¹K&°¢[¹ÅCù͵ÄqšwâpüŒ¶ÞhG‘¼f¸cJÖ¼eAˆ‰€EÛ†U³ áÆðƒþ2¬„K®MœûÃî Ö—1_|t.†C±zÙ³²àï*§'§“Ó€ûÖà¸Úž Ê‚%×ïƒÍ צà¤O1eƒˆmSÝáĸaà#è`•;ÍÐòvÉxh€SàáK„Zp'~ð"ƒ;Ö®3`6Ò½eD§‰Æ D¹0vÌR™w§H0M,ÄuãÇéTå"|8”÷ÉŒà/._ñÙ£„cx0Öà«ç ÂÄ-`a!›®ÅÙ€bZÎp…U£ß¬tsÀë*Çlœ{8Ù/ïš5õAõW_C¹EôùÿGÖ”Ä ¨wdôà;ª‹Ûˆ.¾^+{|öÃé#´‚OMûâBç¿lem ¶Ò|dÇ 0÷9$H¸××Q Sœƒ¬6ŒŽ³È@Ž î)¬Ü Üq*8Ãzï¼^ 2üÓ³ä0dîߣ’òÆ¢°íÄЗ*÷xÈŒ¾\啾C¼OXuµ˜dKyÍŽ™\»(Çý|äÐõÎTWF9m#ƒEH¾úú¹·Eäùñì0À»/|𼓼 mÕ%-C³ÞHÂjý1J1*1Øéªo½LHŠ®Çd`³eñ¼±Ç )Ë2Ë(ñÆ0k èÇŠí笠X˜«O×Ú½÷‹òúsß»ò0ÑËÞ5™ä0äR‡¼aÂèÃôwËÄ™$ âÐQç'☖Rô$סÂ4uÔÆI\Û¡sEpxª¦½¸œ»pÔ³„Ÿ¾Š Èœ9# „Û=òaŽG•’ÿ’gSØï»‡“ëôÃê^"žö¸ •÷wÎðî‰;V“x‡+6ƒ?»Œ@ñÏxPà7™FØ'IONRpd‹´çÇO.nÐ:Ät—S!*\$¦—¼o'nD'Y)ç¼a6¿Eø5¾¾ žqAŒÈÊV äGÔ¦…¤1á_Âô<àʦnªÿ¡1hYÝqê$ÝÁ]f=p¹|âz~Îw®hT×ÆÃƒP«Õ|8uÚÓg™›3S%9Kï"ðÙ ¥xy2Úb ÑÇçœÕÊ$ýÓƒ ù”ÙS®8çb6èÆ„œl¯ƒÒkÖ7{äÖF˜|ï€ãÉ›¨.±ªr|ãgÈŠã Váˆ"£n)¨8òǘaÏÑPŽ&`|`¦[ã¯ÈHNâOÜ/Ïýç…t†%¡é2ZÀ_SJD~ñ6 »/{÷Ž/0&-b¹ ÕÃb¿Ї¿ êtj¿¦- åÌú?÷Œ›@ŠöÈbÈRÁÚúÍÄÝ&<3ŽJ(÷²¹³TÂÏvòï(HÎŒ©Rã¦* zÇCrò`! c¶€ñ‚E­s¬ë\Lö-Üé¯Aãr[Íd‡ œR¤ðe]GèÆÕÛ>øÎj&*,ÙR‘óŸt%æÍfžX4|lÖ±B>šÍ9SQ¯xþwœ EÁ¢ÖçS¶™6m¬FX4‡œ@:Føpc„À¹ÂNÑ!üþ‰²S‡á¿Œuy™¸–rbõ"mqò&kg_)ýWÿS禛I{ú e‡9Á}å…Õ^¨a@›Íâ½/Èsñ•´ÞTÁ½˜rȘý8‘›‚ŸL ‚s!A¬ f»žß÷ôÀÏz=uЏ2§ñ1jääzÈ 8 á´Á@UFaQA &LG*Lq:чê}˜q‡ç\à ÐÜ6cá0àQ+¬-¤ìÎ.xu•Á>[ý½8`ˆáØ9¦`í½®I±=\©Ûë#Aô Bh=Ç ® › ±ÜxfÊmSÎW}.l:0ªÜBƒi ëH‹É‚„Ñ:|cA3ÈGDáÍ<ƒHr`´,p_¦Œœ¿¹÷¹Øø½ƒGÞfC f¬ö\A:MÁ†™Ã+dnüe]‡8Ð0ÔÊÞDç‰}a"ùÿ^Ë^ò\pM!òýîWœê‘&o å9ؤ¹)mÓFsCãv|“¼xÙ,f¡áV· NT•Õëp]'¬*C±XH ­®¸á” ˜ŽR¾ß¢ :<`åRA=åQ–ˆ×È÷œÄ"‹¯©Œ¸ ÿÑ2XÃ@øÇp&Ýï‚ûàÉ%îb=Š'/Ó„›}ñPЖkÞÙ)­üd àðâDGŠ«ÎP4S+S¨*g%—¤˜k ¨5ºãç"P&®ŒyÀ•úhmý³…X$>ópZŸV/AY£,¡Ã“ˆáCy`6› Ô­GG·6—ˆlŸLdÊÒ:~˜² 7ÞU¥K„¸Kó.X*cÿY¸AªÎ ¥êÀŒ½ óž:wµhFw¡ÜÊ*¨Xã‚û èd€ILéíºÈ„@[NÎ7ôÖ ”_ØÖ=E~\ãO B|r^ég”DPøoEDØÎ%“€ûÅõ€jAÀñ:¢My×¥Ó?òÊ€:»~¤Þ*;•ÜϦ#ôÏ(€XÕ¾üÜ0Hkd اš½p`3h~š§û7ˆR‚C I<Ž:õðW\ý1:­¾[‹g§{ õ¾2°´ªô±D«e«ÛæZå<Ü›,ÞD½Ï†È'?õ‡è»Ñï¨Íi·ÁE§Æ0×€\sªlM\f€àx0ÄåçoÑã®ó†¾=rú,Ø)Ç· ƒäœãŒøÂrù¸“ :ãÈ=íÞXd*ɧƭ}ëYZÞ”Q^þÝ`)Eå4cL_jûÞTi+gÇ¼Ž…¥l–EõˆœŸgN𺖠“å½à8Q}8ƒž xãKæ3C26w¶-…Éòqó–þÁHðïëŠñõQvý Ẁa@™Ä©ÙÀNoŒ@-¹Z¾æÞ5IÂ{…Y£Öi‡IN—]åœ6Œ Ç×9¬ÍLa*1Ð/öc¤Óµçx¡°i¥1P!ÖZàjÅ ›eËeóìØÝý³w!Ý´ þæz r›œ;Î%PˆƒïÅÇ¡¨B¿Î±T\)áyLa Õ<˜Èu†oE>!õļWŠ×a[ŠIª ~G£¼f‘át#¯üÂu0Ûºxã7Ž,êWKÝŠ c6ñžoL•˵èÚW5­„—»/“zï|fÊ·Dú¦ïNQ‡’‹¤ç771(®œTËdcηÎ7ž>µè þ°""£a.¥ç~\c‘…šì¥³ÓwæÔ™­¹É€UKglãdÕàmçl¨*êS-ˆD¯–s>PÑízž°U§ý¯Î˜éާçœb’‘PÎ¥?œ´CSJ³Æs)×ß8IÜLø÷,ïœ` C°MÇÇéª&”T¡Ö¥][iÝç ¥.õÏyH¤Bb»UÿYX~uÌëœnå¼ðS$/‡Æðí³ œ¾×œc·G;=âˆuðzï ÒÊy·pH¶¡„îùC.Ûˆ ðÏÓ¼xiRN9×X ŽÖõAÇÆ«GjûP¥Ç¯¦Òð“çùÄ@!ìs‰ <…߯¾pa"ô ×èí)zçµ"av®ÊÀ(}·‡Û 'U¼J›þßëíJ!ïçˆytÒàO´a$IãipDô‘ÁµçÚ?ƒØ>ƒ‚×Û@EÉéŠí¯¾T·µ?`ðkáæÇì76EÈžSƒÎ”56ÏNUµiµýüàÚ¤VBB'Œ´âsUå°Tä4—æ°)gÛÁ÷ÀàÁÅÐÃHw“ÌozÈÞ'NCÁ¬ˆ—¼Š,ÑAððâÈ NƒÙë[Dfž ÆÆ0ðÓϼ’‡±8WL¹*­wú*a·fIh\.ÝySyQcÙ¼f€8ÔbDDˆ.2?{lÕø?;À¨#UeoRþøؼ«…ûLÓê䤿Ö[¬“+š™ZÞ!A9 ¡MŽóL ˆLÐÝŸÍÄ »8Æ;”T•>â”§Y†b8)S³ë‘L¶ö¿b‡—ªäëÆZtûÿÖ4¼‡ìe@ç|³•‚ì'û1m²% ¼}pœ@Ø烓Û@_õŒ±¨T9¾u„ &Ó¹Ûq g¤˜˜[¥Ûý~ˆ/@9öq©¿møMwó•ƒV….眇j›/ž±y”bÖ/˜‚wz{mÖ NÃFÃÆjLl~åý³q¡Ù×ï2’ÕÑ~a«A(G(øÞ^÷H0g–ÇýfõÑ:¿\H w:V§txý±ÄS¢ö=aëã³|raT*&zÁ ËIõÁ?!*d$6'ÆMà <ëRØIóÖ$“ìãéåùrÈŸÊnçŒ2¦ï\£¯Ì‡¹8Åš¬Zy8ÉŸ9ÓÎBm9~ƒ‰ô Lv$Q‘μ¾²á†€&†rã „9½Årj!ñ*/oÑBSª…á'í0’%®ÄQ¡†Ìç<€:pá rÕ|óÎ"ÞÒÁ{f€ Œsp¼æÜ!,»ú ýpÏŠE¤=b&†ØKôç‚ N.* y«}êâJk·N ÅŠ·4'Ô^²a#{±ÀƒÙqs¢´qþ¹Ê¤Ühx¼f˜˜íôÅXµ5êu‹Ýz99ÈIÃä§ÎhDÂf¨Ä¡ª—b ÅÍR.˜O~óQà^ç׎Lð§Ó%Œ£¾æÎ(!䳜–¿Q1n±¥¸9 "ÙŸMc Bȇًòà!òšõ…9)6ú?¢§5è ˆò¸U„. « ®­µ„8VÖM8¯æ® A-Â& ¹ƒœB(-ºÅ]Ç^7·$¸†Y0uŽqëÆäl]\AÆ”WnûŽQ ƒÖ]3pÆùÈ­C¨` Á5b­;±ÑÒc„X6k6ûáEwš˜\jsŽE1ù!¯¦3@W_\**¦ :ÚÅ÷Ž¿é°lµB_©„Ó*_Þïš!S,¹öQð£üþügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_Æ~ügà_ÇùXg$ddÇ9´¡å‚ê¢CGÊ`HVúóˆÊ ƒU=c!E\‘Ž•˜FÇhëvEJÆÖ€ ¡ÓÄ‚&è¾--LN–HPiZìWRæça‘ÀÌJE7LYL¯äzvä×Bñ„~(ÔJ]®£0 =ãRòˆ]ë– œ­´êL”¼ïÊ ¬Z=i:ÕÔÍ7¼™UB%‰<#ÞmÛM}’ºr2êäs"Õ "sû©Î ËçüWSz"_¯ÿ êS¶ ñÙëƒD£ÿ"°\1v™b{x•] Ú%éõÈÆ¦bpØà(B6fYWÒ ½ ÝÆ~Pª$¾6Êï\©Á|[Å?ùè[€‘Ìç-d5áHΊlådDt鈚UÚã ÐojÁR8¿w@Lõ—UNÎ2M]¬Ü“WÕ{Æè¯‘þägT¿Ý»a‘‚@€ÑW/ å Ú;X‰”<‹¤ƒ¡„ ÞÕ\uBÚmDÁÊ8²D°Â ðëM¨ªA盕©K‹ôäKïØ<Äh”•¤yrHyšâáJ)1vÓEm ©È3b©o<ˆàMcÕëëÂh ÿàmo "$8¤–!šJûªÆ‰Ùeš²à7HBT“ÀtÛÿ }ª|ªgR½x~ŸžÓTñ7’äí´ov˜Ý@0Ç#³LÿH*à<Ž )Æ8^AZ1‰­"}?ä/ \9‡rŸá@Þ±;HÀEUx¼ÆQ<™ëmÛl:a®ˆìNÿÊV+ÿF î=Æ7 ÿˇ&¾åc¯X›><‚¨ÐnEC„(žÌPç8¶»ìM&œãÍBªׯðÃ$FÉ@“{ŸòGcˆœ(ÞPE 1'YƒÅ©uÇ*N²1‚’ô¤ø&Ä•Fd1À @%ŽÝå`3š í1ª†pÏe €]<â4A¯{öp2Ø­¤Òeì ”2èðD%,Aša»¥»¹Ý닺-2V0uu3ž&¢P,•, »1KúI$Ñ,Mh¨¡¢•—Z™Ýn*…ô2…C^ Œ °bPË¥Ó(aiu#odòI’@ ˉ Æ­YˆZŽæ«VÔV‰¥j¥Ì)B()À ¨©¬e*m`Û BîîP€7§ð#ÿø{p0¤R É”AÞ0†>ÀÚøaTZ»UóßbÖ…íLµ‹Ì+Äi 8 ¡ÂóÅ“ö7þóWÃ[cqÐ4-…i+A±Í*¶VÀmŒ^¢ ¶âظ !p©æeŠñ@&’—à,Ò”»¡ºÜœ)E§*­ö'•*J# ”h¯#«!8‰A¸Ø„MçC%™@»nˆÊïkFH°¤€QŸÖ3Ò‚š`¨¡ÄÈÔÁãU!¨‡Wü’à·ª&¥óà˜dTÀ1AQjr ˜<­‡‘wôÄ\ °é¶ñaJ«áZ»Ó‘ô.äg á ù\Ô{­ª¸]ß6äà ZСÀ­¿²ËëˆSk,*ðü&ívþ°A#ÞL€[x€WÛÿÅ$ÏPä.Û)K›‡UZÈ¥t¹]Ôûš”E°K‡’hc#*RŽmåNÁåFIhQ -Þi6r 0BC]4rŒ³ëTÐíK0ÏÊ"ÙÅ™0Ã:§d’ÏR)D¤¼€u(–zêm]¹÷2\&ö²îüg«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~OyêüžóÕù=ç«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~OyêüžóÕù=ç«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~OyêüžóÕù=ç«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~OyêüžóÕù=ç«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~OyêüžóÕù=ç«ò{ÏWä÷ž¯Éï=_“Þz¿'¼õ~Oà¤ïÔ^M`®hÑXpbÊ!Þ¬8ÀÚ XùeKØeÍÚÀÖ¯‹Ø¼µ^ŒÝš+DŠLŠrÙ“nŽÅE¡Ò"”{ïÀ.ä‚ nòöÀŒDxǦЀ'_¼ãå“Ë Z¸ÛÕ¸-Qµ‡*`²p/|\R4UÑ”M”憌©.Í{ÿÊl“/³¯8Qi\æÊb`wÊ£´HkÝqÒƒÖþüaã‘í -Iá)Àdç`/«ö°ó16‹~ú~¸62¢åÇ›qtæ*gü©2BÆàF§cÆ·¦[ÛK1Ç,º05 xçÐ@µvª¾W'Og©~†°æÎØç"Ü ´„Æ1s ¹¥k®S™ê ¸ƒ¶9•yá¶KÄ¡ãþ™€@oQNMÜ$:k@B.m+HŽï„›@˜Q‡P…*Ò:Ä«r°´€¨ ´•Îù h=ŸåŽ’3+<ëȈ»®)Õÿ¢ÁiìÃÎ’kœÓó{½ÆaH†“…qÈO\ïÈzÒöHqyyÏ™ƒQ‡Îý½(Q ¼á©-YYnÅ š2¯¬7Øà¤PÙc‚ýÕe¨3vŠmTÒBʰ@”M’ÃÝ(‘.@Mâ²·äh"EIµ'Á nÍÊÌN(4X¦Í@$0„wš@£Ù¬ÜD”J„O>Ù4öùjîÕ\釣p€¡U§ŒG†¥¡S"AJ3®¶¥@-Õ.з™ !8¢4t+1úQhðQtA&lPª9¡ÞgN€‹Gq¤4%°  cmí8Id N)zr¬­a®•Þ_Y¦X,Œ ¾hbˆõF†+ª]×Wõ\Îé”@"]¿ü3È[ÕR{b—èë“μmu>[›ù‹f,w PçöÎ"õ©Û°Wü’ÕJ{•Ÿþg¬4 v j¨QõÿçQùO° ‘#é e8 °óºPV¥[¼ƒúÁ^DaJoM7š ÎT€Mဿ½•5»O”;çM-š„Ž£6“iécëv6p^ÂìŽ11J6˜,9·k P&¨@6qLQU·ѱ(‚á +–2ƒ bm­0íjÀ§^Ý:Ë4ÂÍÆÉ+åHÌPÅk¶C’3™~ÉŠæ’‘ulÿág­ ÉÐÀlkáÁnÄØQXbÈ[€ÛCÑK(’ºNðD{—’Y{Ÿ¥Gå—iu2.é¡S—?¿¤ÓI®r¡#k P-K¾Ü[q#`¤–ÊùÎlä›>uíÅGôsÖ8OÈó3’'zË÷é´­W—ñ9Á5MDø¼—œx€èëŽQ‹„< ‰èÿán-±%ýÜé0•#‚q0³­¶R¬ð¥_5ÃŒ`úT~SÆS¾Óã,iCAÅ«Ž$껑}ÐYÚTŽÉ*&àzV8„AÚ(D µ0üû>0Tƒ¼H²Øá¡PbŸT;Ü…X‰coi7˜ÂÆô…À0F´ÐjÔP[EÍcKã=À‚ÓÐÀ´‚/’P72ÉP~ˆA@šb5edØl¢2bDât$ÀcÉG5CþØqBªX¦=e/Ј¶Õ–4i¨e¡¯&.¼còl2,‘‡[GµÁ6På£@µNmD@§”£P³Äd>8°¦¡ÌB²@€ëÝÊP\ÎÈ 2† 0¥¨Ð€Â ‘n° ¶ÍpÊ@K!È+©Dé€ÀÀRA"0L-ªã3´¿6º‡7'ß)-IØ¡Šæ¢F`RÜUЧùîÔÅ@ÔA&-¤dÚ'ö[ë|R¡š¤%e­w(D H1´:?NÊxÃZ?¥¨¸Öõ (Joºè\ö4‘X.‰HB†o Òdh³@4Ÿy§„£õÁ"à®ê!Æù¹×·üc(@UË\XPQ«5‚cm»ƒt0*ÔJ€Öl~uð‚{«D;1[ KÇøVt"w*h=¸9Ù„ä¢Séû÷¬EǼ€,(Àårìéb%˜ž hr€ÝÜŒNø“€9W£ €(&(´‹€3/ !i0üŠj„oS׳þ0É{ ¥ª ‹aúô~SÆ™€ú‡°)LŒ¬H&Ξ–‘Çúy Ñ-m­Þ `ª.`u”Òs6š´ˆCtÛW'+ctl«ŒËDXD;&Р 8j D^st‘²._d$èiŒž•u¢¨˜œD©ÀbÛÛJqäŠL DÏa.& ¶‰¨ b ÒÃÂÖW}›ˆqiݪZª$”)ía`ˆb ˆ·ÕÌ=¡Óe¢'O± ³U(ÙÉmpÆÎuáæ²àU­€š¿Œ>J³U‘QÞΜ´«&gG¥à¡"qû¯ÏIÞ%È/ )TZ)nŠ2Dîv  öÔ@žÖ¢!Võ‰Ä©W¶ƒrŽ…Yݨ•tA/‡rÙ¢¥q€ûCÈF¶ˆ Å‹Jà[RƒeÀCÒL±% B“¤ ÌöbP-VÊxÍT%ÃßJ!ÝÆNÌ™o`ãÂ)ÚópÛ.¡ÏXÇR2µ±w¢ë¡qÆ(ªu-Ð œƒ8ÎB…Æž‰·IiD± QuBù^0¸ú­$+^`Óò2ÈíI`I€4âEp¨B$©ÞìCÌ|cˆöŽ™¢ùóŒ—8»£¡Ðð í1 >ç¥ÀëKõRÿø4iøšÅJ ½ÅÓ5Ú l¡ßBå  WZï õ¶_»ã] æÃ±ëšY›Æ“ÈÏäºjÁ»ððStŽöV=S´H±ÉlÞíS}G8§!»G–Ñ­1“ŸNA+±¢P `(…èKŠVÅq8é•[ÁË2Òâúä@ƒ j|ùÚ…ª£ÿà…”ñ€vÓçdûÿ†XÙ´ä3t1bͅŧŸG¿8Üzð»fÜv# ZX½˜}ßy˜}YÅ>Eô2°Z"yáqh-ÇEôå ªh‡ªÊ`‹—y‡Rí#%¹Ýgïˆ#¿íl]?âƒC6žm,~Øah ´®;ý*?)ã6h]óÆ&2…C«ÅGOø]b\Bë9Ƴ¬EâcÌ0ÑL-2OéÆ)K@Ù °ë–@À%xÀ~o{Ò‡¡‡_¾ kÏl&oSåÞ—4ËèÀžVØ;ãÖkËu9ûã“¶«=Œ>Ž6dµ;ÃPð°wRkÞ¡óñü†6ºI½`aácþ(Ÿ¤Š?)ã&ã®1bë\ØËÅ~¸W£¤ž2‰¿ðèÿ+GŒC$g<)uõÅ>0çÏþ짨ÔôΑ ø<žÏ®vÑØ>ƒùÂE¨ •5wypUccßs4ÜP‚-xŠ_WŽä6›Öç\óS ¡Ëç/Êüà`Ífckâ`Ï1L»ÆQ 6Yu§ÖcÂÿ7þ˜MÎŽžÌqBDÁ?H~SÆJBÒ>Æmm(K1éöËÁƒGÆhâb¹sŒ|ôaƒ~2¸ß)ï4yÅE¡vßÇX<„›Ǽ?­²æ wòÀÎ7,C^ÞJ= ¼}Óˆr m°z4]B6hœs­o) ï”?`e)²¥„Ðïp.GÝ7,@᪛ïq! ð‹pG8õEzxâU:¡ }fàuá<Ôe‘¨õ›œdSW"h~ ÉòbRYOœý¿JÊxÊ”+Íðg|„ç¬*§1¶áovÒyŠsøÈ¯f)Íw€™QÏ^7Ö9†c”`»!{zs[ØØ =÷=wU:þ’òŸÆ5 æEùÈ㔥 ú %·­ch7àÚM9ܳàƒB⿜eŠm>€~Á‘ ,ç0¼l6ûw„ ó–lðžñüÙ’ o¯'ŒcØbí#Ê«ì^1Ð PÂtÚ}ýd®- "£ÐvywˆsÏ9¦EÃl‰3B°ú~•”ñˆ-AçÐÂ,…yrMµÃ:Ëå3²hãxáW³X ˜1¿ñ²ç;þ73ÈM 8m/lã½~ø´-µƒIôÂt8ÂÛ[lÞþ˜2£NÆ ýŒ( `àó‹ÈáÄqÓÖ(÷>e´S8ÎïJPyôºÂ«^ÊÅ>K+Öðí¨ç“ôï9¡Ž‹uöÿy¹E’ŒOÏ:Å@F?áQ}\ûe=©ÚnYö߯iÑ›fÜ# ?¥èü§Œ__°M|œóÎ,\z&1ª•}1ùËûñ‰Å|,Ñ|\|Î1ƼsÅŽtc/]cOÄ?[…þrÔ‚“Û­Þ8Æ‹·ˆÊäLÓ@ƒ‚þ|â(ݑڼ÷¯ãË ,A8UzÔ×}«ö÷ˆFò•oç 5cãl±;÷«œ4ç¯ß†!HVÃè°¿qu䚉׌'ë´sH <¼r,3œSå:¹È“$»!w5¯nàtsPº‰“ÎÉ!P)± xòbp ÊJ±ÇV¡|íø½¦€Þ¦=\„ÛbÐê‹2ÔZ‡ Ôa³sxš€Ü¤t Žé¦k «4à,- ³÷>3Œ~Ày¡SíÙ犜ÿ0fõ'Î;Ò|IK|®„]’ýq5AòäD¨¢Bׯæ±x@Ð;¾BO0%¡)8S’7“Exau:Ñ¿JÊxÀ©&Žw’uŠÏž_wé0™G¹V1€ÓÑ `ö­ª ~'ƒ…"¹ Ú]u„Ü!õŽ .†§íÕR–öê©‹Ÿ4qU (kOà ‹È„üèÈÉŠô! $äEwxààO“‘Ô.êØ¸ÉY# ýKƒrËI¦ž{2¯6égoD¤PAðÿ6€¥Xî9Õ~™«òM•{Åaá”YN‘ßÎð·%²ÌÝ6?s8ºt;SYÃ|b"rÉ ®ø9âòásLn¢?Vÿ¼P Xº9@ë‚ y•ìú­iA·¢x(œ…u(«‡N±² ìsŠ]Åg«Œ"qèxœýѨ?P2ùÏ Bü =bQB€«ÔžkâŒâ– -â–  (âŒâ– _â– )'); INSERT INTO `snippets` (key, value) VALUES ('tableflip', '(╯°□°)╯︵ â”»â”â”»'); cawbird-1.4.2/sql/init/Create.3.sql000066400000000000000000000002471416632607600167760ustar00rootroot00000000000000PRAGMA user_version = 3; ALTER TABLE `accounts` ADD migrated INT DEFAULT 0; CREATE TABLE IF NOT EXISTS `info`( key VARCHAR(40) PRIMARY KEY, value VARCHAR(255) );cawbird-1.4.2/sql/init/Create.4.sql000066400000000000000000000003601416632607600167730ustar00rootroot00000000000000PRAGMA user_version = 4; ALTER TABLE `accounts` ADD token VARCHAR(100); ALTER TABLE `accounts` ADD token_secret VARCHAR(100); ALTER TABLE `accounts` ADD consumer_key VARCHAR(100); ALTER TABLE `accounts` ADD consumer_secret VARCHAR(100);cawbird-1.4.2/src/000077500000000000000000000000001416632607600137335ustar00rootroot00000000000000cawbird-1.4.2/src/Account.vala000066400000000000000000000624061416632607600162040ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class Account : GLib.Object { public int64 id; public int64 migration_date; public bool suppress_dm_notifications; public bool suppress_mention_notifications; public Sql.Database db; public string screen_name; public string name; public string avatar_url; public string? banner_url; public string? website; public string? description; public Cairo.Surface avatar_small {public get; public set;} public Cairo.Surface avatar {public get; public set;} public Rest.OAuthProxy proxy; public Cb.UserStream user_stream; public Cb.UserCounter user_counter; private UserEventReceiver event_receiver; public NotificationManager notifications; public int64[] friends; public int64[] blocked; public int64[] muted; public int64[] disabled_rts; public GLib.GenericArray filters; private bool proxy_load_fallback = false; public signal void info_changed (string screen_name, string name, Cairo.Surface avatar_small, Cairo.Surface avatar); public Account (int64 id, string screen_name, string name) { this.id = id; this.screen_name = screen_name; this.name = name; this.migration_date = -1; this.filters = new GLib.GenericArray (); this.event_receiver = new UserEventReceiver (this); this.notifications = new NotificationManager (this); } /** * Initializes the database. All account databases are VersionedDatabases * and are stored in accounts/ID.db. * */ public void init_database () { if (db != null) return; this.db = new Sql.Database (Dirs.config (@"accounts/$id.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); user_counter = new Cb.UserCounter (); this.load_filters (); if (this.migration_date < 0 && Cawbird.db != null) { this.migration_date = Cawbird.db.select ("accounts") .cols ("migrated") .where_eqi ("id", this.id) .once_i64 (); } if (this.migration_date == 0) { this.migrate_from_corebird (); } } /** * Initializes the RestProxy object with loaded secrets. */ public void init_proxy () { if (proxy != null) return; var proxy_values = get_proxy_values(); if (proxy_values.length != 4) { critical ("Could not load token{_secret} for user %s", this.screen_name); } create_proxy(proxy_values[0], proxy_values[1], proxy_values[2], proxy_values[3]); this.user_stream = new Cb.UserStream (this.screen_name, proxy); this.user_stream.register (this.event_receiver); } public string[] get_proxy_values() { var proxy_values = new string[0]; init_database (); Cawbird.db.select ("accounts").cols ("consumer_key", "consumer_secret", "token", "token_secret") .where_eqi ("id", id) .run ((vals) => { proxy_values = vals; return false; //stop }); if (proxy_values[0] == null) { this.proxy_load_fallback = true; db.select ("common").cols ("token", "token_secret") .run((vals) => { proxy_values = { Settings.get_consumer_key (Cawbird.old_consumer_k), Settings.get_consumer_secret (Cawbird.old_consumer_s), vals[0], vals[1] }; return false; // stop }); } return proxy_values; } private void create_proxy(string consumer_key, string consumer_secret, string token, string token_secret){ proxy = new Rest.OAuthProxy (consumer_key, consumer_secret, "https://api.twitter.com/", false); proxy.token = token; proxy.token_secret = token_secret; } public void uninit () { this.proxy = null; this.user_counter.save (this.db.get_sqlite_db ()); this.user_stream.unregister (this.event_receiver); this.user_stream.stop (); this.user_stream = null; } /** * Loads the small and normally sized avatars from disk. * Normal: accounts/ID.png * Small: accounts/ID_small.png */ public void load_avatar () { string path = Dirs.config (@"accounts/$(id).png"); string small_path = Dirs.config (@"accounts/$(id)_small.png"); if (!GLib.FileUtils.test (path, GLib.FileTest.EXISTS) || !GLib.FileUtils.test (small_path, GLib.FileTest.EXISTS)) { update_avatar.begin(this.avatar_url, () => { this.avatar = load_surface (path); this.avatar_small = load_surface (small_path); info_changed (screen_name, name, avatar, avatar_small); }); } else { this.avatar = load_surface (path); this.avatar_small = load_surface (small_path); info_changed (screen_name, name, avatar, avatar_small); } } public void set_new_avatar (Cairo.Surface new_avatar) { string path = Dirs.config (@"accounts/$(id).png"); string small_path = Dirs.config (@"accounts/$(id)_small.png"); Cairo.Surface avatar = scale_surface ((Cairo.ImageSurface)new_avatar, 48, 48); Cairo.Surface avatar_small = scale_surface ((Cairo.ImageSurface)new_avatar, 24, 24); write_surface (avatar, path); write_surface (avatar_small, small_path); this.avatar = avatar; this.avatar_small = avatar_small; } /** * Download the appropriate user info from the Twitter server, * updating the local information stored in this class' local variables * and the information stored in the account's database file. * * @param screen_name The screen name to use for the API call or null in * which case the ID will be used. */ public async void query_user_info () { UserInfo user_info; try { user_info = yield Twitter.get().get_own_user_info(proxy); } catch (GLib.Error e) { warning (e.message); return; } bool values_changed = false; this.id = user_info.id; if (this.name != user_info.name) { this.name = user_info.name; values_changed = true; } if (this.screen_name != user_info.screen_name) { string old_screen_name = this.screen_name; this.screen_name = user_info.screen_name; Utils.update_startup_account (old_screen_name, this.screen_name); values_changed = true; } this.description = user_info.description; this.banner_url = user_info.banner_url; /* Website URL */ this.website = user_info.website; string avatar_url = user_info.avatar_url; values_changed |= yield update_avatar (avatar_url); if (values_changed || proxy_load_fallback) { proxy_load_fallback = false; if (this.db != null) this.save_info (); info_changed (this.screen_name, this.name, this.avatar_small, this.avatar); } } public async void init_information () { var collect_obj = new Collect (4); collect_obj.finished.connect (() => { init_information.callback (); }); query_user_info.begin (() => { collect_obj.emit (); }); load_id_array.begin (collect_obj, "1.1/friendships/no_retweets/ids.json", true, (obj, res) => { Json.Array? arr = load_id_array.end (res); if (arr != null) { this.set_disabled_rts (arr); collect_obj.emit (); } }); load_id_array.begin (collect_obj, "1.1/blocks/ids.json", false, (obj, res) => { Json.Array? arr = load_id_array.end (res); if (arr != null) { this.set_blocked (arr); collect_obj.emit (); } }); load_id_array.begin (collect_obj, "1.1/mutes/users/ids.json", false, (obj, res) => { Json.Array? arr = load_id_array.end (res); if (arr != null) { this.set_muted (arr); collect_obj.emit (); } }); yield; } private async Json.Array? load_id_array (Collect collect_obj, string function, bool direct) { var call = this.proxy.new_call (); call.set_function (function); call.set_method ("GET"); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); collect_obj.emit (); return null; } if (direct) return root.get_array (); else return root.get_object ().get_array_member ("ids"); } /** * Updates the account's avatar picture. * This means that the new avatar will be downloaded if necessary and * scaled appropriately. * * @param url The url of the (possibly) new avatar(optional). */ private async bool update_avatar (string url = "") { string dest_path = Dirs.config (@"accounts/$(id)_small.png"); string big_dest = Dirs.config (@"accounts/$(id).png"); if (url.length > 0 && url == this.avatar_url) { if (GLib.FileUtils.test (dest_path, GLib.FileTest.EXISTS) && GLib.FileUtils.test (big_dest, GLib.FileTest.EXISTS)) return false; } debug ("Using %s to update the avatar(old: %s)", url, this.avatar_url); if (url.length > 0) { var msg = new Soup.Message ("GET", url); SOUP_SESSION.queue_message (msg, (_s, _msg) => { var data_stream = new MemoryInputStream.from_data (msg.response_body.data, GLib.g_free); string type = Cb.Utils.get_file_type (url); Gdk.Pixbuf pixbuf; try { pixbuf = new Gdk.Pixbuf.from_stream(data_stream); pixbuf.save(big_dest, type); data_stream.close (); double scale_x = 24.0 / pixbuf.get_width(); double scale_y = 24.0 / pixbuf.get_height(); var scaled_pixbuf = new Gdk.Pixbuf(Gdk.Colorspace.RGB, pixbuf.has_alpha, 8, 24, 24); pixbuf.scale(scaled_pixbuf, 0, 0, 24, 24, 0, 0, scale_x, scale_y, Gdk.InterpType.HYPER); scaled_pixbuf.save(dest_path, type); debug ("saving to %s", dest_path); this.avatar_small = Gdk.cairo_surface_create_from_pixbuf (scaled_pixbuf, 1, null); this.avatar = Gdk.cairo_surface_create_from_pixbuf (pixbuf, 1, null); } catch (GLib.Error e) { critical (e.message); } this.avatar_url = url; Cawbird.db.update ("accounts").val ("avatar_url", url).where_eqi ("id", id).run (); update_avatar.callback (); }); yield; return true; } else { critical ("Not implemented yet"); } return false; } /** * Saves the account info both in the account's database and in the * global one. */ public void save_info () { db.replace ("info").vali64 ("id", id) .val ("screen_name", screen_name) .val ("name", name) .run (); Cawbird.db.replace ("accounts").vali64 ("id", id) .val ("screen_name", screen_name) .val ("name", name) .val ("avatar_url", avatar_url) .val ("consumer_key", proxy.consumer_key) .val ("consumer_secret", proxy.consumer_secret) .val ("token", proxy.token) .val ("token_secret", proxy.token_secret) .run (); } /** * Load all the filters from the database. */ private void load_filters () { this.db.select ("filters").cols ("content", "id") .order ("id").run ((cols) => { Cb.Filter f = new Cb.Filter (cols[0]); f.set_id (int.parse (cols[1])); filters.add (f); return true; }); } public void add_filter (owned Cb.Filter f) { this.filters.add (f); } public bool disabled_rts_for (int64 user_id) { foreach (int64 id in disabled_rts) { if (id == user_id) { return true; } } return false; } /** * Checks if any of the filters associated to this acount match * the given tweet. * * @param t The tweet to check for * * @return true iff at least one of the filters match, false otherwise. */ public bool filter_matches (Cb.Tweet t) { if (t.source_tweet.author.id == this.id) return false; string text = t.get_filter_text (); for (int i = 0; i < filters.length; i ++) { var f = this.filters.get (i); if (f.matches (text)) { return true; } } return false; } public void set_friends (Json.Array friends_array) { this.friends = new int64[friends_array.get_length ()]; debug ("Adding %d friends...", friends.length); for (int i = 0; i < friends_array.get_length (); i ++) { this.friends[i] = friends_array.get_int_element (i); } } public bool follows_id (int64 user_id) { foreach (int64 id in this.friends) if (id == user_id) return true; return false; } public void follow_id (int64 user_id) { this.friends.resize (this.friends.length + 1); this.friends[this.friends.length - 1] = user_id; } public void unfollow_id (int64 user_id) { if (this.friends == null || this.friends.length == 0) { warning ("friends == null"); return; } int64[] new_friends = new int64[this.friends.length]; int o = 0; bool found = false; for (int i = 0; i < this.friends.length; i++) { if (this.friends[i] == user_id) { found = true; continue; } new_friends[o] = this.friends[i]; o ++; } if (found) new_friends.resize (new_friends.length - 1); this.friends = new_friends; } public void set_muted (Json.Array muted_array) { this.muted = new int64[muted_array.get_length ()]; debug ("Add %d muted ids", this.muted.length); for (int i = 0; i < this.muted.length; i ++) { this.muted[i] = muted_array.get_int_element (i); } } public void mute_id (int64 id) { this.muted.resize (this.muted.length + 1); this.muted[this.muted.length - 1] = id; } public void unmute_id (int64 id) { if (this.muted == null || this.muted.length == 0) { warning ("muted == null"); return; } int64[] new_muted = new int64[this.muted.length - 1]; int o = 0; for (int i = 0; i < this.muted.length; i++) { if (this.muted[i] == id) { continue; } new_muted[o] = this.muted[i]; o ++; } this.muted = new_muted; } public void set_blocked (Json.Array blocked_array) { this.blocked = new int64[blocked_array.get_length ()]; debug ("Add %d blocked ids", this.blocked.length); for (int i = 0; i < this.blocked.length; i ++) { this.blocked[i] = blocked_array.get_int_element (i); } } public void block_id (int64 id) { this.blocked.resize (this.blocked.length + 1); this.blocked[this.blocked.length - 1] = id; } public void unblock_id (int64 id) { if (this.blocked == null || this.blocked.length == 0) { warning ("blocked == null"); return; } int64[] new_blocked = new int64[this.blocked.length - 1]; int o = 0; for (int i = 0; i < this.blocked.length; i++) { if (this.blocked[i] == id) { continue; } new_blocked[o] = this.blocked[i]; o ++; } this.blocked = new_blocked; } public void set_disabled_rts (Json.Array disabled_rts_array) { this.disabled_rts = new int64[disabled_rts_array.get_length ()]; debug ("Add %d disabled_rts ids", this.disabled_rts.length); for (int i = 0; i < this.disabled_rts.length; i ++) { this.disabled_rts[i] = disabled_rts_array.get_int_element (i); } } public void add_disabled_rts_id (int64 user_id) { this.disabled_rts.resize (this.disabled_rts.length + 1); this.disabled_rts[this.disabled_rts.length - 1] = user_id; } public void remove_disabled_rts_id (int64 user_id) { if (this.disabled_rts == null || this.disabled_rts.length == 0) { warning ("disabled_rts == null"); return; } int64[] new_disabled_rts = new int64[this.disabled_rts.length - 1]; int o = 0; for (int i = 0; i < this.disabled_rts.length; i++) { if (this.disabled_rts[i] == user_id) { continue; } new_disabled_rts[o] = this.disabled_rts[i]; o ++; } this.disabled_rts = new_disabled_rts; } public bool blocked_or_muted (int64 user_id) { foreach (int64 id in this.muted) if (id == user_id) return true; foreach (int64 id in this.blocked) if (id == user_id) return true; return false; } public bool is_blocked (int64 user_id) { foreach (int64 id in this.blocked) if (id == user_id) return true; return false; } public bool is_muted (int64 user_id) { foreach (int64 id in this.muted) if (id == user_id) return true; return false; } private void migrate_from_corebird () { var corebird_db_path = Dirs.corebird_config (@"accounts/$id.db"); if (GLib.FileUtils.test (corebird_db_path, GLib.FileTest.EXISTS)) { var corebird_db = new Sql.Database (corebird_db_path, "", 1); // Use version 1 to prevent updating // Migrate DM history - they're unique by ID // But avatar URLs can be (always are?) null so we need to null-coallesce them corebird_db.select ("dm_threads").cols ("user_id", "name", "screen_name", "last_message", "last_message_id", "avatar_url").run ((vals) => { this.db.insert_ignore ("dm_threads").vali64 ("user_id", int64.parse(vals[0])) .val ("name", vals[1]) .val ("screen_name", vals[2]) .val ("last_message", vals[3]) .vali64 ("last_message_id", int64.parse(vals[4])) .val ("avatar_url", vals[5] ?? "") .run (); return true; }); corebird_db.select ("dms").cols ("from_id", "to_id", "from_screen_name", "to_screen_name", "from_name", "to_name", "timestamp", "avatar_url", "id", "text").run ((vals) => { this.db.insert_ignore ("dms").vali64 ("from_id", int64.parse(vals[0])) .vali64 ("to_id", int64.parse(vals[1])) .val ("from_screen_name", vals[2]) .val ("to_screen_name", vals[3]) .val ("from_name", vals[4]) .val ("to_name", vals[5]) .vali ("timestamp", int.parse(vals[6])) .val ("avatar_url", vals[7] ?? "") .vali64 ("id", int64.parse(vals[8])) .val ("text", vals[9]) .run (); return true; }); // Filter IDs could change if people made new ones, so we just work with content corebird_db.select ("filters").cols ("content").run ((vals) => { var filter_match_count = this.db.select ("filters") .count ("id") .where_eq ("content", vals[0]).once_i64 (); if (filter_match_count == 0) { Utils.create_persistent_filter (vals[0], this); } //Else the user put the filter back already return true; }); // Common is common and pre-populated // Info is account info, which is pre-populated // User_cache can be rebuilt } // Else no Corebird account to migrate // Set the migrated value so that we don't try again Cawbird.db.update ("accounts").vali64 ("migrated", GLib.get_real_time ()).where_eqi ("id", this.id).run (); } // Notification suppression is used for first creation so that users don't get swamped // with notifications. Unsurpressing is done in two parts because we fetch notifications // and DMs separately and so one can be finished before the other. public void suppress_notifications() { this.suppress_dm_notifications = true; this.suppress_mention_notifications = true; } public void unsuppress_dm_notifications() { this.suppress_dm_notifications = false; } public void unsuppress_mention_notifications() { this.suppress_mention_notifications = false; } /** Static stuff ********************************************************************/ private static GLib.GenericArray? accounts = null; public static Account get_nth (uint index) { if (GLib.unlikely (accounts == null)) lookup_accounts (); return accounts.get (index); } public static uint get_n () { if (GLib.unlikely (accounts == null)) lookup_accounts (); return accounts.length; } public static void update_api_details(int64 id, Rest.OAuthProxy proxy) { Cawbird.db.update ("accounts").val ("consumer_key", proxy.consumer_key) .val ("consumer_secret", proxy.consumer_secret) .val ("token", proxy.token) .val ("token_secret", proxy.token_secret) .where_eqi ("id", id) .run (); } private static Account construct_from_details(int64 id, string screen_name, string name, string avatar_url, int64 migrated) { Account acc = new Account(id, screen_name, name); acc.avatar_url = avatar_url; acc.load_avatar(); acc.migration_date = migrated; return acc; } public static Account create_account (UserInfo user_info, Rest.OAuthProxy proxy) { var migrated = GLib.get_real_time (); Cawbird.db.insert ("accounts").vali64 ("id", user_info.id) .val ("screen_name", user_info.screen_name) .val ("name", user_info.name) .val ("avatar_url", user_info.avatar_url) .val ("consumer_key", proxy.consumer_key) .val ("consumer_secret", proxy.consumer_secret) .val ("token", proxy.token) .val ("token_secret", proxy.token_secret) .vali64 ("migrated", migrated) .run (); Account acc = construct_from_details (user_info.id, user_info.screen_name, user_info.name, user_info.avatar_url, migrated); Account.add_account(acc); return acc; } /** * Look up the accounts. Each account has a .db in ~/.config/cawbird/accounts/ * The accounts are initialized with only their screen_name and their ID. */ private static void lookup_accounts () { assert (accounts == null); accounts = new GLib.GenericArray (); Cawbird.db.select ("accounts").cols ("id", "screen_name", "name", "avatar_url", "migrated").run ((vals) => { Account acc = construct_from_details (int64.parse(vals[0]), vals[1], vals[2], vals[3], int64.parse(vals[4])); accounts.add (acc); return true; }); } /** * Adds the given account to the end of the current account list. * * @param acc The account to add. */ public static void add_account (Account acc) { accounts.add (acc); } /** * Removes the acccunt with th given screen name from the account list. * * @param screen_name The screen name of the account to remove. */ public static void remove_account (string screen_name) { if (GLib.unlikely (accounts == null)) lookup_accounts (); for (uint i = 0; i < accounts.length; i ++) { var a = accounts.get (i); if (a.screen_name == screen_name) { accounts.remove (a); return; } } } /** * Returns an unowned reference to the account with the given screen name. * * @param screen_name The screen name of the account to return * @return An unowned reference to the account object with the given screen name or * null of no such instance could be found. */ public static unowned Account? query_account (string screen_name) { if (GLib.unlikely (accounts == null)) lookup_accounts (); var lower_screen_name = screen_name.down(); for (uint i = 0; i < accounts.length; i ++) { unowned Account a = accounts.get (i); var acct_screen_name = a.screen_name.down(); if (lower_screen_name == acct_screen_name || lower_screen_name == "@" + acct_screen_name) return a; } return null; } public static unowned Account? query_account_by_id (int64 id) { if (GLib.unlikely (accounts == null)) lookup_accounts (); for (uint i = 0; i < accounts.length; i ++) { unowned Account a = accounts.get (i); if (id == a.id) return a; } return null; } } cawbird-1.4.2/src/Cawbird.vala000066400000000000000000000560641416632607600161660ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class Cawbird : Gtk.Application { public static int RESPONSIVE_LIMIT = 440; public static Sql.Database db; public static Cb.SnippetManager snippet_manager; public signal void account_added (Account acc); public signal void account_removed (Account acc); public signal void account_window_changed (int64? old_id, int64 new_id); private SettingsDialog? settings_dialog = null; private GLib.GenericArray active_accounts; private bool started_as_service = false; public static string old_consumer_k; public static string old_consumer_s; public static string consumer_k; public static string consumer_s; const GLib.ActionEntry[] app_entries = { {"show-settings", show_settings_activated }, {"show-shortcuts", show_shortcuts_activated }, {"quit", quit_application }, {"show-about-dialog", about_activated }, {"show-dm-thread", show_dm_thread, "(xx)" }, {"show-window", show_window, "x" }, {"mark-read", mark_read_activated, "(xx)" }, {"reply-to-tweet", reply_to_tweet_activated, "(xx)" }, }; static construct { try { // Base64-encoding our tokens to make them less obvious to searches // Original Cawbird tokens old_consumer_k = decode("VmY5dG9yRFcyWk93MzJEZmhVdEk5Y3NMOA=="); old_consumer_s = decode("MThCRXIxbWRESDQ2Y0podzVtVU13SGUyVGlCRXhPb3BFRHhGYlB6ZkpybG5GdXZaSjI="); // Tokens for this build consumer_k = decode(Config.CONSUMER_KEY); consumer_s = decode(Config.CONSUMER_SECRET); } catch (GLib.Error e) { critical("Invalid consumer tokens: %s", e.message); } } private static string decode(string base64_string) throws GLib.Error { var decoded = GLib.Base64.decode(base64_string); foreach (var c in decoded) { if (!(c == '+' || c == '/' || c == '=' || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') )) { throw new GLib.Error(Quark.from_string("cawbird"), 64, "Bad character in decoded output of key/secret %s: %c", base64_string, c); } } return (string)decoded; } public Cawbird () { GLib.Object(application_id: "uk.co.ibboard.cawbird"); active_accounts = new GLib.GenericArray (); /* Create the directories here already since the database below needs it */ Dirs.create_dirs (); db = new Sql.Database (Dirs.config ("Cawbird.db"), Sql.CAWBIRD_INIT_FILE, Sql.CAWBIRD_SQL_VERSION); var migrations = db.select ("info") .count ("key") .where_eq ("key", "migration").once_i64 (); if (migrations == 0) { var corebird_db_path = Dirs.corebird_config (@"Corebird.db"); if (GLib.FileUtils.test (corebird_db_path, GLib.FileTest.EXISTS)) { var corebird_db = new Sql.Database (corebird_db_path, "", 1); // Use version 1 to prevent updating // Snippet IDs could change if people made new ones, so we just work with content corebird_db.select ("snippets").cols ("key", "value").run ((vals) => { var snippet_match_count = db.select ("snippets").count ("id").where_eq ("key", vals[0]).once_i64 (); if (snippet_match_count == 0) { db.insert ("snippets") .val ("key", vals[0]).val ("value", vals[1]). run(); } //Else the user recreated the snippet already return true; }); } db.insert_ignore ("info").val ("key", "migration").val ("value", GLib.get_real_time ().to_string ()).run (); } try { var favourites_path = Dirs.config ("image-favorites/"); var favourites_dir = GLib.Dir.open (favourites_path); var has_favs = false; string? name = null; while ((name = favourites_dir.read_name ()) != null) { has_favs = true; break; } if (!has_favs) { // No current favourites - transfer any old ones var corebird_favourites_path = Dirs.corebird_config ("image-favorites/"); if (GLib.FileUtils.test (corebird_favourites_path, GLib.FileTest.EXISTS)) { var corebird_favourites_dir = GLib.Dir.open (corebird_favourites_path); while ((name = corebird_favourites_dir.read_name ()) != null) { GLib.File old_file = File.new_for_path (Path.build_filename (corebird_favourites_path, name)); GLib.File new_file = File.new_for_path (Path.build_filename (favourites_path, name)); try { debug ("Transferring favourite image %s to %s", old_file.get_path(), new_file.get_path()); old_file.copy(new_file, 0, null, null); } catch (Error e) { warning ("Error: %s\n", e.message); } } } } } catch (GLib.FileError e) { error ("Error: %s", e.message); } snippet_manager = new Cb.SnippetManager (db.get_sqlite_db ()); OptionEntry[] options = new OptionEntry[6]; // TRANSLATORS: Description of the `--tweet` option for the command-line options[0] = {"tweet", 't', 0, OptionArg.STRING, null, _("Shows only the 'compose tweet' window for the given account, nothing else."), // TRANSLATORS: Used as the placeholder for the account name in the `--help` output _("account-name") }; // TRANSLATORS: Description of the `--start-service` option for the command-line options[1] = {"start-service", 's', 0, OptionArg.NONE, null, _("Start service"), null}; // TRANSLATORS: Description of the `--stop-service` option for the command-line options[2] = {"stop-service", 'p', 0, OptionArg.NONE, null, _("Stop service, if it has been started as a service"), null}; // TRANSLATORS: Description of the `--print-startup-accounts` option for the command-line options[3] = {"print-startup-accounts", 'a', 0, OptionArg.NONE, null, _("Print configured startup accounts"), null}; // TRANSLATORS: Description of the `--account` option for the command-line options[4] = {"account", 'c', 0, OptionArg.STRING, null, _("Open the window for the given account"), _("account-name")}; options[5] = {null}; this.add_main_option_entries(options); #if VIDEO this.add_option_group (Gst.init_get_option_group ()); #endif this.handle_local_options.connect(do_handle_local_options); this.command_line.connect(handle_global_options); this.activate.connect(handle_activate); this.startup.connect(handle_startup); this.shutdown.connect(handle_shutdown); } private int do_handle_local_options(GLib.VariantDict options) { if (options.contains("print-startup-accounts")) { string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); foreach (unowned string acc in startup_accounts) { stdout.printf ("%s\n", acc); } return 0; } return -1; } private int handle_global_options (GLib.ApplicationCommandLine cmd_line) { var name = null; var options = cmd_line.get_options_dict(); if (options.contains("stop-service")) { if (this.started_as_service) { debug ("Stopping service"); /* Starting as a service adds an extra hold() */ this.release (); } else { warning ("--stop-service passed, but cawbird has not been started as a service"); } return 0; } else if (options.contains("start-service")) { if (!this.started_as_service) { this.started_as_service = true; } return 0; } else if (options.lookup("tweet", "s", ref name)) { open_startup_windows (name, null); return 0; } else if (options.lookup("account", "s", ref name)) { open_startup_windows(null, name); return 0; } return -1; } private void handle_activate () { if (started_as_service) { this.hold (); string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); if (startup_accounts.length == 1 && startup_accounts[0] == "") startup_accounts.resize (0); debug ("Configured startup accounts: %d", startup_accounts.length); uint n_accounts = Account.get_n (); debug ("Configured accounts: %u", n_accounts); foreach (unowned string screen_name in startup_accounts) { Account? acc = Account.query_account (screen_name); if (acc != null) { debug ("Service: Starting account %s...", screen_name); this.start_account (acc); } else { warning ("Invalid startup account: '%s'", screen_name); } } } else { open_startup_windows (null, null); } } private void show_settings_activated () { /* We don't set the settings dialog transient to any window because we already save its size */ if (this.settings_dialog != null) return; var dialog = new SettingsDialog (this); var action = (GLib.SimpleAction)this.lookup_action ("show-settings"); action.set_enabled (false); dialog.delete_event.connect (() => { action.set_enabled (true); this.settings_dialog = null; return Gdk.EVENT_PROPAGATE; }); dialog.show (); } private void about_activated () { var active_window = get_active_window (); var ad = new AboutDialog (); ad.modal = true; ad.set_transient_for (active_window); ad.show_all (); } private void show_shortcuts_activated () { var builder = new Gtk.Builder.from_resource ("/uk/co/ibboard/cawbird/ui/shortcuts-window.ui"); var shortcuts_window = (Gtk.Window) builder.get_object ("shortcuts_window"); shortcuts_window.show (); } private void handle_startup () { this.set_resource_base_path ("/uk/co/ibboard/cawbird"); typeof (LazyMenuButton).ensure (); typeof (FavImageView).ensure (); typeof (Cb.EmojiChooser).ensure (); typeof (ChildSizedScroller).ensure (); debug ("startup"); Utils.load_custom_css (); Utils.load_custom_icons (); Utils.init_soup_session (); Twitter.get ().init (); this.set_accels_for_action ("win.compose-tweet", {Settings.get_accel ("compose-tweet")}); this.set_accels_for_action ("win.toggle-topbar", {Settings.get_accel ("toggle-sidebar")}); this.set_accels_for_action ("app.show-settings", {Settings.get_accel ("show-settings")}); this.set_accels_for_action ("app.quit", {"Q"}); this.set_accels_for_action ("app.show-shortcuts", {"question", "F1"}); this.set_accels_for_action ("win.show-account-dialog", {Settings.get_accel ("show-account-dialog")}); this.set_accels_for_action ("win.show-account-list", {Settings.get_accel ("show-account-list")}); this.set_window_switching_accels(); // TweetInfoPage this.set_accels_for_action ("tweet.reply", {"r"}); this.set_accels_for_action ("tweet.favorite", {"l", "f"}); this.add_action_entries (app_entries, this); // If the user wants the dark theme, apply it var gtk_s = Gtk.Settings.get_default (); if (Settings.use_dark_theme ()) { gtk_s.gtk_application_prefer_dark_theme = true; } if (gtk_s.gtk_decoration_layout.contains ("menu")) { gtk_s.gtk_decoration_layout = gtk_s.gtk_decoration_layout.replace ("menu", ""); } } public void set_window_switching_accels() { var shortcut_key = Settings.get_shortcut_key_string(); this.set_accels_for_action ("win.switch-page(0)", {shortcut_key + "1"}); this.set_accels_for_action ("win.switch-page(1)", {shortcut_key + "2"}); this.set_accels_for_action ("win.switch-page(2)", {shortcut_key + "3"}); this.set_accels_for_action ("win.switch-page(3)", {shortcut_key + "4"}); this.set_accels_for_action ("win.switch-page(4)", {shortcut_key + "5"}); this.set_accels_for_action ("win.switch-page(5)", {shortcut_key + "6"}); this.set_accels_for_action ("win.switch-page(6)", {shortcut_key + "7"}); this.set_accels_for_action ("win.previous", {shortcut_key + "Left", "Back"}); this.set_accels_for_action ("win.next", {shortcut_key + "Right", "Forward"}); } private void handle_shutdown () { Cb.MediaDownloader.get_default ().shutdown (); } /** * Open startup windows. * Semantics: Open a window for every account in the startup-accounts array. * If that array is empty, look at all the account and if there is one, open that one. * If there is none, open a MainWindow with a null account. */ private void open_startup_windows (string? compose_screen_name, string? account_name) { /* Explicitly prefer compose-name over account-name */ if (compose_screen_name != null && account_name != null) { account_name = null; } if (compose_screen_name != null) { Account? acc = Account.query_account (compose_screen_name); if (acc == null) { critical ("No account named `%s` is configured. Exiting.", account_name); return; } acc.init_proxy (); acc.query_user_info.begin (); var cw = new ComposeTweetWindow (null, acc, null, ComposeTweetWindow.Mode.NORMAL); cw.show (); this.add_window (cw); return; } if (account_name != null) { Account? acc = Account.query_account (account_name); if (acc == null) { critical ("No account named `%s` is configured. Exiting.", account_name); return; } acc.init_proxy (); acc.query_user_info.begin (); add_window_for_account (acc); return; } string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); /* Handle the stupid case where only one item is in the array but it's empty */ if (startup_accounts.length == 1 && startup_accounts[0] == "") startup_accounts.resize (0); uint n_accounts = Account.get_n (); if (startup_accounts.length == 0) { if (n_accounts == 1) { add_window_for_screen_name (Account.get_nth (0).screen_name); } else if (n_accounts == 0) { var window = new MainWindow (this, null); add_window (window); window.show_all (); } else { /* We have multiple configured accounts but still none in autostart. This should never happen but we handle the case anyway by just opening the first one. */ add_window_for_screen_name (Account.get_nth (0).screen_name); } } else { bool opened_window = false; foreach (unowned string account in startup_accounts) { if (!is_window_open_for_screen_name (account, null)) { if (add_window_for_screen_name (account)) { opened_window = true; } } } /* If we did not open any window at all since all windows for every account in the startups-account array were already open, just open a new window with a null account (unless the null account window already exists) */ if (!opened_window) { if (n_accounts > 0) { /* Check if *any* of the configured accounts (not just startup-accounts) is not opened in a window */ for (uint i = 0; i < Account.get_n (); i ++) { var account = Account.get_nth (i); if (!is_window_open_for_user_id (account.id, null)) { add_window_for_account (account); return; } } } foreach (Gtk.Window w in this.get_windows ()) { MainWindow main_win = (MainWindow)w; if (main_win.account == null) { return; } } var m = new MainWindow (this, null); add_window (m); m.show_all (); } } } /** * Adds a new MainWindow instance with the account that * has the given screen name. * Note that this only works if the account is already properly * set up and won't warn or fail if if isn't. * * @param screen_name The screen name of the account to add a * MainWindow for. * * @return true if a window has been opened, false otherwise */ public bool add_window_for_screen_name (string screen_name) { Account? acc = Account.query_account (screen_name); if (acc != null) { add_window_for_account (acc); return true; } warning ("Could not add window for account '%s'", screen_name); return false; } public void add_window_for_account (Account account) { var window = new MainWindow (this, account); this.add_window (window); window.show_all (); } /** * Checks if there's currently a MainWindow instance open that has a * reference to the account with the given screen name. * (This makes a linear search over all open windows, with a text comparison * in each iteration) * * @param screen_name The screen name to search for * @return TRUE if a window with the account associated to the given * screen name is open, FALSE otherwise. */ public bool is_window_open_for_screen_name (string screen_name, out MainWindow? window = null) { unowned GLib.List windows = this.get_windows (); foreach (Gtk.Window win in windows) { if (win is MainWindow) { MainWindow main_win = (MainWindow)win; if (main_win.account != null && main_win.account.screen_name == screen_name) { window = main_win; return true; } } } window = null; return false; } public bool is_window_open_for_user_id (int64 user_id, out MainWindow? window = null) { unowned GLib.List windows = this.get_windows (); foreach (Gtk.Window win in windows) { if (win is MainWindow) { MainWindow main_win = (MainWindow)win; if (main_win.account != null && main_win.account.id == user_id) { window = main_win; return true; } } } window = null; return false; } /** * Quits the application, saving all open windows and their geometries. */ private void quit_application () { unowned GLib.List windows = this.get_windows (); string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); if (startup_accounts.length == 1 && startup_accounts[0] == "") startup_accounts.resize (0); if (startup_accounts.length != 0) { base.quit (); return; } string[] account_names = new string[windows.length ()]; int index = 0; foreach (var win in windows) { if (!(win is MainWindow)) continue; var mw = (MainWindow)win; string screen_name = mw.account.screen_name; mw.save_geometry (); account_names[index] = screen_name; index ++; } account_names.resize (index + 1); Settings.get ().set_strv ("startup-accounts", account_names); base.quit (); } public void start_account (Account acc) { for (int i = 0; i < this.active_accounts.length; i ++) { var account = this.active_accounts.get (i); if (acc == account) { /* This can very well happen when we've been started as a service */ debug ("Account %s is already active", acc.screen_name); return; } } acc.init_proxy (); acc.init_information.begin (() => { acc.user_stream.start (); }); this.active_accounts.add (acc); } public void stop_account (Account acc) { bool found = false; for (int i = 0; i < this.active_accounts.length; i ++) { var account = this.active_accounts.get (i); if (account == acc) { found = true; break; } } if (!found) { warning ("Can't stop account %s since it's not in the list of active accounts", acc.screen_name); return; } /* If we got started as a service and the given account is in the * startup accounts, don't stop it here */ string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); if (this.started_as_service && acc.screen_name in startup_accounts) { // Don't stop account } else { acc.uninit (); this.active_accounts.remove (acc); } } /********************************************************/ private void show_dm_thread (GLib.SimpleAction a, GLib.Variant? value) { // Values: Account id, sender_id int64 account_id = value.get_child_value (0).get_int64 (); int64 sender_id = value.get_child_value (1).get_int64 (); MainWindow main_window; if (is_window_open_for_user_id (account_id, out main_window)) { var bundle = new Cb.Bundle (); bundle.put_int64 (DMPage.KEY_SENDER_ID, sender_id); main_window.main_widget.switch_page (Page.DM, bundle); main_window.present (); } else { var account = Account.query_account_by_id (account_id); if (account == null) { /* Security measure, should never happen. */ critical ("No account with id %s found", account_id.to_string ()); return; } main_window = new MainWindow (this, account); this.add_window (main_window); var bundle = new Cb.Bundle (); bundle.put_int64 (DMPage.KEY_SENDER_ID, sender_id); main_window.main_widget.switch_page (Page.DM, bundle); main_window.show_all (); } } private void show_window (GLib.SimpleAction a, GLib.Variant? value) { int64 user_id = value.get_int64 (); MainWindow main_window; if (is_window_open_for_user_id (user_id, out main_window)) { main_window.present (); } else { var account = Account.query_account_by_id (user_id); if (account == null) { /* Security measure, should never happen. */ critical ("No account with id %s found", user_id.to_string ()); return; } main_window = new MainWindow (this, account); this.add_window (main_window); main_window.show_all (); } } private void mark_read_activated (GLib.SimpleAction a, GLib.Variant? v) { int64 account_id = v.get_child_value (0).get_int64 (); int64 tweet_id = v.get_child_value (1).get_int64 (); MainWindow main_window; if (is_window_open_for_user_id (account_id, out main_window)) { main_window.mark_tweet_as_read (tweet_id); } } private void reply_to_tweet_activated (GLib.SimpleAction a, GLib.Variant? v) { int64 account_id = v.get_child_value (0).get_int64 (); int64 tweet_id = v.get_child_value (1).get_int64 (); MainWindow main_window; if (is_window_open_for_user_id (account_id, out main_window)) { main_window.reply_to_tweet (tweet_id); main_window.present (); } } } cawbird-1.4.2/src/CbAvatarCache.c000066400000000000000000000152331416632607600165120ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbAvatarCache.h" G_DEFINE_TYPE (CbAvatarCache, cb_avatar_cache, G_TYPE_OBJECT); typedef struct _CacheEntry CacheEntry; struct _CacheEntry { gint64 user_id; int refcount; char *url; cairo_surface_t *surface; }; static void cache_entry_destroy (CacheEntry *entry) { g_free (entry->url); if (entry->surface) cairo_surface_destroy (entry->surface); } static inline CacheEntry * get_entry_for_user_id (CbAvatarCache *self, gint64 user_id) { guint i; for (i = 0; i < self->entries->len; i ++) { CacheEntry *e = &g_array_index (self->entries, CacheEntry, i); if (e->user_id == user_id) return e; } return NULL; } CbAvatarCache * cb_avatar_cache_new (void) { return CB_AVATAR_CACHE (g_object_new (CB_TYPE_AVATAR_CACHE, NULL)); } void cb_avatar_cache_add (CbAvatarCache *self, gint64 user_id, cairo_surface_t *surface, const char *url) { CacheEntry *entry = NULL; g_return_if_fail (CB_IS_AVATAR_CACHE (self)); entry = get_entry_for_user_id (self, user_id); if (entry == NULL) { /* Actually a new entry */ g_array_set_size (self->entries, self->entries->len + 1); entry = &g_array_index (self->entries, CacheEntry, self->entries->len - 1); entry->user_id = user_id; if (surface) entry->surface = cairo_surface_reference (surface); entry->url = g_strdup (url); /* The subsequent incrase_refcount will push this up to 1. Entries with * refcount 0 won't be removed from the cache until a decrease_refcount call */ entry->refcount = 0; } else { if (entry->surface != NULL) cairo_surface_destroy (entry->surface); if (surface) entry->surface = cairo_surface_reference (surface); if (entry->url != NULL) g_free (entry->url); entry->url = g_strdup (url); } } void cb_avatar_cache_set_avatar (CbAvatarCache *self, gint64 user_id, cairo_surface_t *surface, const char *url) { CacheEntry *entry = NULL; g_return_if_fail (CB_IS_AVATAR_CACHE (self)); g_return_if_fail (surface != NULL); entry = get_entry_for_user_id (self, user_id); g_assert (entry != NULL); if (entry->surface != NULL) cairo_surface_destroy (entry->surface); entry->surface = cairo_surface_reference (surface); if (entry->url != NULL) g_free (entry->url); entry->url = g_strdup (url); } cairo_surface_t * cb_avatar_cache_get_surface_for_id (CbAvatarCache *self, gint64 user_id, gboolean *out_found) { const CacheEntry *entry = NULL; g_return_val_if_fail (CB_IS_AVATAR_CACHE (self), NULL); g_return_val_if_fail (user_id > 0, NULL); g_return_val_if_fail (out_found != NULL, NULL); entry = get_entry_for_user_id (self, user_id); if (entry != NULL) { *out_found = TRUE; return entry->surface; /* Can still be NULL... */ } else { *out_found = FALSE; return NULL; } } void cb_avatar_cache_set_url (CbAvatarCache *self, gint64 user_id, const char *url) { CacheEntry *entry = NULL; g_return_if_fail (CB_IS_AVATAR_CACHE (self)); entry = get_entry_for_user_id (self, user_id); g_assert (entry != NULL); if (entry->url != NULL) g_free (entry->url); entry->url = g_strdup (url); } void cb_avatar_cache_decrease_refcount_for_surface (CbAvatarCache *self, cairo_surface_t *surface) { guint i; guint index = (guint) -1; CacheEntry *entry = NULL; g_return_if_fail (CB_IS_AVATAR_CACHE (self)); g_return_if_fail (surface != NULL); for (i = 0; i < self->entries->len; i ++) { CacheEntry *e = &g_array_index (self->entries, CacheEntry, i); if (e->surface == surface) { entry = e; index = i; break; } } if (entry == NULL) { /* Surface not even in cache */ return; } entry->refcount --; if (entry->refcount <= 0) { g_debug ("Removing avatar with id %" G_GINT64_FORMAT " from cache", entry->user_id); g_array_remove_index_fast (self->entries, index); } } void cb_avatar_cache_increase_refcount_for_surface (CbAvatarCache *self, cairo_surface_t *surface) { guint i; g_return_if_fail (CB_IS_AVATAR_CACHE (self)); g_return_if_fail (surface != NULL); for (i = 0; i < self->entries->len; i ++) { CacheEntry *e = &g_array_index (self->entries, CacheEntry, i); if (e->surface == surface) { e->refcount ++; break; } } } const char * cb_avatar_cache_get_url_for_id (CbAvatarCache *self, gint64 user_id) { const CacheEntry *entry; g_return_val_if_fail (CB_IS_AVATAR_CACHE (self), NULL); g_return_val_if_fail (user_id > 0, NULL); entry = get_entry_for_user_id (self, user_id); if (entry == NULL) return NULL; return entry->url; } guint cb_avatar_cache_get_n_entries (CbAvatarCache *self) { g_return_val_if_fail (CB_IS_AVATAR_CACHE (self), 0); return self->entries->len; } static void cb_avatar_cache_finalize (GObject *obj) { CbAvatarCache *self = CB_AVATAR_CACHE (obj); g_array_free (self->entries, TRUE); G_OBJECT_CLASS (cb_avatar_cache_parent_class)->finalize (obj); } static void cb_avatar_cache_init (CbAvatarCache *self) { self->entries = g_array_new (FALSE, TRUE, sizeof (CacheEntry)); g_array_set_clear_func (self->entries, (GDestroyNotify) cache_entry_destroy); } static void cb_avatar_cache_class_init (CbAvatarCacheClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_avatar_cache_finalize; } cawbird-1.4.2/src/CbAvatarCache.h000066400000000000000000000057651416632607600165300ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef AVATAR_CACHE_H #define AVATAR_CACHE_H #include #include G_BEGIN_DECLS typedef struct _CbAvatarCache CbAvatarCache; struct _CbAvatarCache { GObject parent_instance; GArray *entries; }; #define CB_TYPE_AVATAR_CACHE cb_avatar_cache_get_type () G_DECLARE_FINAL_TYPE (CbAvatarCache, cb_avatar_cache, CB, AVATAR_CACHE, GObject); CbAvatarCache *cb_avatar_cache_new (void); void cb_avatar_cache_decrease_refcount_for_surface (CbAvatarCache *self, cairo_surface_t *surface); void cb_avatar_cache_increase_refcount_for_surface (CbAvatarCache *self, cairo_surface_t *surface); void cb_avatar_cache_add (CbAvatarCache *self, gint64 user_id, cairo_surface_t *surface, const char *url); void cb_avatar_cache_set_avatar (CbAvatarCache *self, gint64 user_id, cairo_surface_t *surface, const char *url); cairo_surface_t * cb_avatar_cache_get_surface_for_id (CbAvatarCache *self, gint64 user_id, gboolean *out_found); const char * cb_avatar_cache_get_url_for_id (CbAvatarCache *self, gint64 user_id); guint cb_avatar_cache_get_n_entries (CbAvatarCache *self); void cb_avatar_cache_set_url (CbAvatarCache *self, gint64 user_id, const char *url); G_END_DECLS #endif cawbird-1.4.2/src/CbBundle.c000066400000000000000000000162051416632607600155610ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbBundle.h" #include G_DEFINE_TYPE (CbBundle, cb_bundle, G_TYPE_OBJECT); CbBundle * cb_bundle_new (void) { return CB_BUNDLE (g_object_new (CB_TYPE_BUNDLE, NULL)); } static GValue * find_value (CbBundle *self, int key) { guint i; for (i = 0; i < self->keys->len; i ++) { int k = g_array_index (self->keys, int, i); if (k == key) { return &g_array_index (self->values, GValue, i); } } return NULL; } guint cb_bundle_get_size (CbBundle *self) { g_return_val_if_fail (CB_IS_BUNDLE (self), 0); g_assert (self->keys->len == self->values->len); return self->keys->len; } void cb_bundle_put_string (CbBundle *self, int key, const char *val) { GValue *gvalue; g_return_if_fail (CB_IS_BUNDLE (self)); g_return_if_fail (val != NULL); g_assert (find_value (self, key) == NULL); g_array_append_val (self->keys, key); g_array_set_size (self->values, self->values->len + 1); gvalue = &g_array_index (self->values, GValue, self->values->len - 1); g_assert (self->keys->len == self->values->len); g_value_init (gvalue, G_TYPE_STRING); g_value_set_string (gvalue, val); } const char * cb_bundle_get_string (CbBundle *self, int key) { GValue *gvalue; g_return_val_if_fail (CB_IS_BUNDLE (self), NULL); gvalue = find_value (self, key); if (gvalue != NULL) return g_value_get_string (gvalue); return NULL; } void cb_bundle_put_int (CbBundle *self, int key, int val) { GValue *gvalue; g_return_if_fail (CB_IS_BUNDLE (self)); g_assert (find_value (self, key) == NULL); g_array_append_val (self->keys, key); g_array_set_size (self->values, self->values->len + 1); gvalue = &g_array_index (self->values, GValue, self->values->len - 1); g_assert (self->keys->len == self->values->len); g_value_init (gvalue, G_TYPE_INT); g_value_set_int (gvalue, val); } int cb_bundle_get_int (CbBundle *self, int key) { GValue *gvalue; g_return_val_if_fail (CB_IS_BUNDLE (self), 0); gvalue = find_value (self, key); if (gvalue != NULL) return g_value_get_int (gvalue); return -1; } void cb_bundle_put_int64 (CbBundle *self, int key, gint64 val) { GValue *gvalue; g_return_if_fail (CB_IS_BUNDLE (self)); g_assert (find_value (self, key) == NULL); g_array_append_val (self->keys, key); g_array_set_size (self->values, self->values->len + 1); gvalue = &g_array_index (self->values, GValue, self->values->len - 1); g_assert (self->keys->len == self->values->len); g_value_init (gvalue, G_TYPE_INT64); g_value_set_int64 (gvalue, val); } gint64 cb_bundle_get_int64 (CbBundle *self, int key) { GValue *gvalue; g_return_val_if_fail (CB_IS_BUNDLE (self), 0); gvalue = find_value (self, key); if (gvalue != NULL) return g_value_get_int64 (gvalue); return -1; } void cb_bundle_put_bool (CbBundle *self, int key, gboolean val) { GValue *gvalue; g_return_if_fail (CB_IS_BUNDLE (self)); g_assert (find_value (self, key) == NULL); g_array_append_val (self->keys, key); g_array_set_size (self->values, self->values->len + 1); gvalue = &g_array_index (self->values, GValue, self->values->len - 1); g_assert (self->keys->len == self->values->len); g_value_init (gvalue, G_TYPE_BOOLEAN); g_value_set_boolean (gvalue, val); } gboolean cb_bundle_get_bool (CbBundle *self, int key) { GValue *gvalue; g_return_val_if_fail (CB_IS_BUNDLE (self), 0); gvalue = find_value (self, key); if (gvalue != NULL) return g_value_get_boolean (gvalue); return FALSE; } void cb_bundle_put_object (CbBundle *self, int key, GObject *val) { GValue *gvalue; g_return_if_fail (CB_IS_BUNDLE (self)); g_assert (find_value (self, key) == NULL); g_array_append_val (self->keys, key); g_array_set_size (self->values, self->values->len + 1); gvalue = &g_array_index (self->values, GValue, self->values->len - 1); g_assert (self->keys->len == self->values->len); g_value_init (gvalue, G_TYPE_POINTER); g_value_set_pointer (gvalue, g_object_ref (val)); } GObject * cb_bundle_get_object (CbBundle *self, int key) { GValue *gvalue; g_return_val_if_fail (CB_IS_BUNDLE (self), 0); gvalue = find_value (self, key); if (gvalue != NULL) return g_value_get_pointer (gvalue); return NULL; } gboolean cb_bundle_equals (CbBundle *self, CbBundle *other) { guint i; g_return_val_if_fail (CB_IS_BUNDLE (self), FALSE); if (other == NULL) return FALSE; g_return_val_if_fail (CB_IS_BUNDLE (other), FALSE); for (i = 0; i < self->values->len; i ++) { GValue *v1; GValue *v2; char *contents1; char *contents2; int key = g_array_index (self->keys, int, i); v1 = find_value (self, key); v2 = find_value (other, key); /* They must contains the same keys */ if (v1 == NULL) return FALSE; contents1 = g_strdup_value_contents (v1); contents2 = g_strdup_value_contents (v2); if (strcmp (contents1, contents2) != 0) return FALSE; g_free (contents1); g_free (contents2); } return self->keys->len == other->keys->len; } static void cb_bundle_finalize (GObject *object) { guint i; CbBundle *self = CB_BUNDLE (object); /* _put_object ref's the object, so unref it here */ for (i = 0; i < self->values->len; i ++) { GValue *v = &g_array_index (self->values, GValue, i); /* We only insert pointers for _put_object */ if (G_VALUE_HOLDS_POINTER (v)) g_object_unref (G_OBJECT (g_value_get_pointer (v))); } g_array_unref (self->keys); g_array_unref (self->values); G_OBJECT_CLASS (cb_bundle_parent_class)->finalize (object); } static void cb_bundle_class_init (CbBundleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_bundle_finalize; } static void cb_bundle_init (CbBundle *self) { self->keys = g_array_new (FALSE, FALSE, sizeof (int)); self->values = g_array_new (FALSE, TRUE, sizeof (GValue)); g_array_set_clear_func (self->values, (GDestroyNotify)g_value_reset); } cawbird-1.4.2/src/CbBundle.h000066400000000000000000000041571416632607600155710ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_BUNDLE_H_ #define _CB_BUNDLE_H_ #include /* * TODO: * Make every CbBundle instance take a fixed amount of data instead * of keeping two GArrays around. The keys are already required to be * integers, so just also require them to be subsequent integers starting * from 0 and just them as indeces into an array. */ struct _CbBundle { GObject parent_instance; GArray *values; GArray *keys; }; typedef struct _CbBundle CbBundle; #define CB_TYPE_BUNDLE cb_bundle_get_type () G_DECLARE_FINAL_TYPE (CbBundle, cb_bundle, CB, BUNDLE, GObject); CbBundle * cb_bundle_new (void); gboolean cb_bundle_equals (CbBundle *self, CbBundle *other); void cb_bundle_put_string (CbBundle *self, int key, const char *val); const char * cb_bundle_get_string (CbBundle *self, int key); void cb_bundle_put_int (CbBundle *self, int key, int val); int cb_bundle_get_int (CbBundle *self, int key); void cb_bundle_put_int64 (CbBundle *self, int key, gint64 val); gint64 cb_bundle_get_int64 (CbBundle *self, int key); void cb_bundle_put_bool (CbBundle *self, int key, gboolean val); gboolean cb_bundle_get_bool (CbBundle *self, int key); void cb_bundle_put_object (CbBundle *self, int key, GObject *object); GObject * cb_bundle_get_object (CbBundle *self, int key); #endif cawbird-1.4.2/src/CbBundleHistory.c000066400000000000000000000076751416632607600171560ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbBundleHistory.h" #include G_DEFINE_TYPE (CbBundleHistory, cb_bundle_history, G_TYPE_OBJECT); static void cb_bundle_history_finalize (GObject *object) { CbBundleHistory *self = CB_BUNDLE_HISTORY (object); int i; for (i = 0; i < HISTORY_SIZE; i ++) { if (self->bundles[i] != NULL) g_object_unref (self->bundles[i]); } G_OBJECT_CLASS (cb_bundle_history_parent_class)->finalize (object); } static void cb_bundle_history_init (CbBundleHistory *self) { int i; for (i = 0; i < HISTORY_SIZE; i ++) self->elements[i] = -1; self->pos = -1; } static void cb_bundle_history_class_init (CbBundleHistoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_bundle_history_finalize; } CbBundleHistory * cb_bundle_history_new () { return CB_BUNDLE_HISTORY (g_object_new (CB_TYPE_BUNDLE_HISTORY, NULL)); } void cb_bundle_history_push (CbBundleHistory *self, int v, CbBundle *bundle) { if (self->pos < HISTORY_SIZE - 1) { /* More space available. Just put it at the end and increase pos */ self->pos ++; } else { /* No space left. Move everything one place to the start and then add @bundle */ memmove (self->elements, self->elements + 1, sizeof (int) * (HISTORY_SIZE - 1)); memmove (self->bundles, self->bundles + 1, sizeof (CbBundle *) * (HISTORY_SIZE - 1)); } self->elements[self->pos] = v; self->bundles[self->pos] = bundle ? g_object_ref (bundle) : NULL; } int cb_bundle_history_back (CbBundleHistory *self) { if (self->pos > 0) { self->pos --; return self->elements[self->pos]; } return -1; } int cb_bundle_history_forward (CbBundleHistory *self) { if (self->pos < HISTORY_SIZE - 1) { self->pos ++; return self->elements[self->pos]; } return -1; } gboolean cb_bundle_history_at_start (CbBundleHistory *self) { return self->pos == 0; } gboolean cb_bundle_history_at_end (CbBundleHistory *self) { if (self->pos == HISTORY_SIZE - 1) return TRUE; if (self->pos == -1 || self->elements[self->pos] == -1 || self->elements[self->pos + 1] == -1) return TRUE; return FALSE; } void cb_bundle_history_remove_current (CbBundleHistory *self) { self->elements[self->pos] = -1; if (self->bundles[self->pos] != NULL) g_object_unref (self->bundles[self->pos]); self->bundles[self->pos] = NULL; /* Fill an eventual gap */ if (self->pos + 1 < HISTORY_SIZE - 1 && self->elements[self->pos + 1] != -1) { memmove (self->elements + self->pos, self->elements + self->pos + 1, (HISTORY_SIZE - self->pos - 1) * sizeof (int)); memmove (self->bundles + self->pos, self->bundles + self->pos + 1, (HISTORY_SIZE - self->pos - 1) * sizeof (CbBundle*)); } } int cb_bundle_history_get_current (CbBundleHistory *self) { if (self->pos == -1) return -1; return self->elements[self->pos]; } CbBundle * cb_bundle_history_get_current_bundle (CbBundleHistory *self) { if (self->pos == -1) return NULL; return self->bundles[self->pos]; } cawbird-1.4.2/src/CbBundleHistory.h000066400000000000000000000041001416632607600171370ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef CB_BUNDLE_HISTORY_H #define CB_BUNDLE_HISTORY_H #include #include "CbBundle.h" #define HISTORY_SIZE 10 struct _CbBundleHistory { GObject parent_instance; int pos; int elements[HISTORY_SIZE]; CbBundle *bundles[HISTORY_SIZE]; }; typedef struct _CbBundleHistory CbBundleHistory; #define CB_TYPE_BUNDLE_HISTORY cb_bundle_history_get_type () G_DECLARE_FINAL_TYPE (CbBundleHistory, cb_bundle_history, CB, BUNDLE_HISTORY, GObject); CbBundleHistory * cb_bundle_history_new (void); void cb_bundle_history_push (CbBundleHistory *self, int v, CbBundle *bundle); int cb_bundle_history_back (CbBundleHistory *self); int cb_bundle_history_forward (CbBundleHistory *self); gboolean cb_bundle_history_at_start (CbBundleHistory *self); gboolean cb_bundle_history_at_end (CbBundleHistory *self); void cb_bundle_history_remove_current (CbBundleHistory *self); int cb_bundle_history_get_current (CbBundleHistory *self); CbBundle * cb_bundle_history_get_current_bundle (CbBundleHistory *self); #endif cawbird-1.4.2/src/CbDeltaUpdater.c000066400000000000000000000061311416632607600167230ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbDeltaUpdater.h" G_DEFINE_TYPE(CbDeltaUpdater, cb_delta_updater, G_TYPE_OBJECT) static gboolean minutely_cb (gpointer user_data) { CbDeltaUpdater *self = user_data; GDateTime *now; GList *widgets; GList *l; if (!GTK_IS_WIDGET (self->listbox)) return G_SOURCE_REMOVE; widgets = gtk_container_get_children (GTK_CONTAINER (self->listbox)); now = g_date_time_new_now_local (); for (l = widgets; l != NULL; l = l->next) { GtkWidget *row = GTK_WIDGET (l->data); if (CB_IS_TWITTER_ITEM (row)) { CbTwitterItem *item = CB_TWITTER_ITEM (row); gint64 timestamp = cb_twitter_item_get_timestamp (item); gint64 last_set_timediff = cb_twitter_item_get_last_set_timediff (item); GDateTime *item_time = g_date_time_new_from_unix_local (timestamp); GTimeSpan time_diff = g_date_time_difference (now, item_time); int seconds = time_diff / 1000 / 1000; if (last_set_timediff < 60) { /* New minute */ cb_twitter_item_update_time_delta (item, now); cb_twitter_item_set_last_set_timediff (item, seconds / 60); } else if (seconds > last_set_timediff + 60) { /* New hour */ cb_twitter_item_update_time_delta (item, now); cb_twitter_item_set_last_set_timediff (item, seconds / 60); } g_date_time_unref (item_time); } } g_date_time_unref (now); g_list_free (widgets); return G_SOURCE_CONTINUE; } static void cb_delta_updater_finalize (GObject *object) { CbDeltaUpdater *self = CB_DELTA_UPDATER (object); if (self->minutely_id != 0) g_source_remove (self->minutely_id); G_OBJECT_CLASS (cb_delta_updater_parent_class)->finalize (object); } static void cb_delta_updater_init (CbDeltaUpdater *self) { self->minutely_id = g_timeout_add (1000 * 60, minutely_cb, self); } static void cb_delta_updater_class_init (CbDeltaUpdaterClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_delta_updater_finalize; } CbDeltaUpdater * cb_delta_updater_new (GtkWidget *listbox) { CbDeltaUpdater *self = g_object_new (CB_TYPE_DELTA_UPDATER, NULL); g_return_val_if_fail (GTK_IS_LIST_BOX (listbox), self); self->listbox = listbox; return self; } cawbird-1.4.2/src/CbDeltaUpdater.h000066400000000000000000000035751416632607600167410ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef DELTA_UPDATER_H #define DELTA_UPDATER_H #include #include #include "CbTwitterItem.h" typedef struct _CbDeltaUpdater CbDeltaUpdater; typedef struct _CbDeltaUpdaterClass CbDeltaUpdaterClass; #define CB_TYPE_DELTA_UPDATER (cb_delta_updater_get_type ()) #define CB_DELTA_UPDATER(obj) (G_TYPE_CHECK_INSTANCE_CAST(obj, CB_TYPE_DELTA_UPDATER, CbDeltaUpdater)) #define CB_DELTA_UPDATER_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST(cls, CB_TYPE_DELTA_UPDATER, CbDeltaUpdaterClass)) #define CB_IS_DELTA_UPDATER(obj) (G_TYPE_CHECK_INSTANCE_TYPE(obj, CB_TYPE_DELTA_UPDATER)) #define CB_IS_DELTA_UPDATER_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE(cls, CB_TYPE_DELTA_UPDATER)) #define CB_DELTA_UPDATER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS(obj, CB_TYPE_DELTA_UPDATER, CbDeltaUpdaterClass)) struct _CbDeltaUpdater { GObject parent_instance; GtkWidget *listbox; guint minutely_id; }; struct _CbDeltaUpdaterClass { GObjectClass parent_class; }; GType cb_delta_updater_get_type (void) G_GNUC_CONST; CbDeltaUpdater * cb_delta_updater_new (GtkWidget *listbox); #endif cawbird-1.4.2/src/CbEmojiChooser.c000066400000000000000000000525521416632607600167430ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ /* * This is basically GtkEmojiChooser from GTK+ but with less features. */ #include "CbEmojiChooser.h" #include #define EMOJI_PER_ROW 7 enum { EMOJI_PICKED, LAST_SIGNAL }; static int signals[LAST_SIGNAL]; G_DEFINE_TYPE (CbEmojiChooser, cb_emoji_chooser, GTK_TYPE_BOX); static void scroll_to_section (GtkButton *button, gpointer data) { EmojiSection *section = data; CbEmojiChooser *chooser; GtkAdjustment *adj; GtkAllocation alloc = { 0, 0, 0, 0 }; double page_increment, value; gboolean dummy; chooser = CB_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (button), CB_TYPE_EMOJI_CHOOSER)); adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); if (section->heading) gtk_widget_get_allocation (section->heading, &alloc); page_increment = gtk_adjustment_get_page_increment (adj); value = gtk_adjustment_get_value (adj); gtk_adjustment_set_page_increment (adj, alloc.y - value); g_signal_emit_by_name (chooser->scrolled_window, "scroll-child", GTK_SCROLL_PAGE_FORWARD, FALSE, &dummy); gtk_adjustment_set_page_increment (adj, page_increment); } static void add_emoji (GtkWidget *box, gboolean prepend, GVariant *item, gunichar modifier); #define MAX_RECENT (EMOJI_PER_ROW * 3) static void populate_recent_section (CbEmojiChooser *chooser) { GVariant *variant; GVariant *item; GVariantIter iter; variant = g_settings_get_value (chooser->settings, "recent-emoji"); g_variant_iter_init (&iter, variant); while ((item = g_variant_iter_next_value (&iter))) { GVariant *emoji_data; gunichar modifier; emoji_data = g_variant_get_child_value (item, 0); g_variant_get_child (item, 1, "u", &modifier); add_emoji (chooser->recent.box, FALSE, emoji_data, modifier); g_variant_unref (emoji_data); g_variant_unref (item); } g_variant_unref (variant); } static void add_recent_item (CbEmojiChooser *chooser, GVariant *item, gunichar modifier) { GList *children, *l; int i; GVariantBuilder builder; g_variant_ref (item); g_variant_builder_init (&builder, G_VARIANT_TYPE ("a((auss)u)")); g_variant_builder_add (&builder, "(@(auss)u)", item, modifier); children = gtk_container_get_children (GTK_CONTAINER (chooser->recent.box)); for (l = children, i = 1; l; l = l->next, i++) { GVariant *item2 = g_object_get_data (G_OBJECT (l->data), "emoji-data"); gunichar modifier2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (l->data), "modifier")); if (modifier == modifier2 && g_variant_equal (item, item2)) { gtk_widget_destroy (GTK_WIDGET (l->data)); i--; continue; } if (i >= MAX_RECENT) { gtk_widget_destroy (GTK_WIDGET (l->data)); continue; } g_variant_builder_add (&builder, "(@(auss)u)", item2, modifier2); } g_list_free (children); add_emoji (chooser->recent.box, TRUE, item, modifier); g_settings_set_value (chooser->settings, "recent-emoji", g_variant_builder_end (&builder)); g_variant_unref (item); } static void emoji_activated (GtkFlowBox *box, GtkFlowBoxChild *child, gpointer data) { CbEmojiChooser *chooser = data; char *text; GtkWidget *label; GVariant *item; gunichar modifier; label = gtk_bin_get_child (GTK_BIN (child)); text = g_strdup (gtk_label_get_label (GTK_LABEL (label))); item = (GVariant*) g_object_get_data (G_OBJECT (child), "emoji-data"); modifier = (gunichar) GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (child), "modifier")); add_recent_item (chooser, item, modifier); g_signal_emit (data, signals[EMOJI_PICKED], 0, text); g_free (text); } static void add_emoji (GtkWidget *box, gboolean prepend, GVariant *item, gunichar modifier) { GtkWidget *child; GtkWidget *label; PangoAttrList *attrs; GVariant *codes; char text[64]; char *p = text; int i; codes = g_variant_get_child_value (item, 0); for (i = 0; i < g_variant_n_children (codes); i++) { gunichar code; g_variant_get_child (codes, i, "u", &code); if (code == 0) code = modifier; if (code != 0) p += g_unichar_to_utf8 (code, p); } g_variant_unref (codes); p[0] = 0; label = gtk_label_new (text); attrs = pango_attr_list_new (); pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_X_LARGE)); gtk_label_set_attributes (GTK_LABEL (label), attrs); pango_attr_list_unref (attrs); child = gtk_flow_box_child_new (); gtk_style_context_add_class (gtk_widget_get_style_context (child), "emoji"); g_object_set_data_full (G_OBJECT (child), "emoji-data", g_variant_ref (item), (GDestroyNotify)g_variant_unref); if (modifier != 0) g_object_set_data (G_OBJECT (child), "modifier", GUINT_TO_POINTER (modifier)); gtk_container_add (GTK_CONTAINER (child), label); gtk_widget_show_all (child); gtk_flow_box_insert (GTK_FLOW_BOX (box), child, prepend ? 0 : -1); } typedef struct { CbEmojiChooser *chooser; GVariantIter iter; GtkWidget *box; /* We need to keep this around so subsequent emojis get added to the rigth section */ } PopulateData; static gboolean populate_one_emoji (gpointer user_data) { const int N = 4; /* Kinda-sorta sweetspot on my system... */ PopulateData *data = user_data; GVariant *item; const char *name; guint i = 0; while (i < N) { item = g_variant_iter_next_value (&data->iter); if (item == NULL) { data->chooser->populate_idle_id = 0; return G_SOURCE_REMOVE; } g_variant_get_child (item, 1, "&s", &name); if (strcmp (name, data->chooser->body.first) == 0) data->box = data->chooser->body.box; else if (strcmp (name, data->chooser->nature.first) == 0) data->box = data->chooser->nature.box; else if (strcmp (name, data->chooser->food.first) == 0) data->box = data->chooser->food.box; else if (strcmp (name, data->chooser->travel.first) == 0) data->box = data->chooser->travel.box; else if (strcmp (name, data->chooser->activities.first) == 0) data->box = data->chooser->activities.box; else if (strcmp (name, data->chooser->objects.first) == 0) data->box = data->chooser->objects.box; else if (strcmp (name, data->chooser->symbols.first) == 0) data->box = data->chooser->symbols.box; else if (strcmp (name, data->chooser->flags.first) == 0) data->box = data->chooser->flags.box; add_emoji (data->box, FALSE, item, 0); g_variant_unref (item); i ++; } return G_SOURCE_CONTINUE; } static void populate_emoji_chooser (CbEmojiChooser *self) { PopulateData *data = g_malloc0 (sizeof (PopulateData)); data->chooser = self; g_variant_iter_init (&data->iter, self->data); data->box = self->people.box; self->populate_idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, populate_one_emoji, data, g_free); } static void adj_value_changed (GtkAdjustment *adj, gpointer data) { CbEmojiChooser *chooser = data; double value = gtk_adjustment_get_value (adj); EmojiSection const *sections[] = { &chooser->recent, &chooser->people, &chooser->body, &chooser->nature, &chooser->food, &chooser->travel, &chooser->activities, &chooser->objects, &chooser->symbols, &chooser->flags, }; EmojiSection const *select_section = sections[0]; gsize i; /* Figure out which section the current scroll position is within */ for (i = 0; i < G_N_ELEMENTS (sections); ++i) { EmojiSection const *section = sections[i]; GtkAllocation alloc; if (section->heading) gtk_widget_get_allocation (section->heading, &alloc); else gtk_widget_get_allocation (section->box, &alloc); if (value < alloc.y) break; select_section = section; } /* Un/Check the section buttons accordingly */ for (i = 0; i < G_N_ELEMENTS (sections); ++i) { EmojiSection const *section = sections[i]; if (section == select_section) gtk_widget_set_state_flags (section->button, GTK_STATE_FLAG_CHECKED, FALSE); else gtk_widget_unset_state_flags (section->button, GTK_STATE_FLAG_CHECKED); } } static gboolean filter_func (GtkFlowBoxChild *child, gpointer data) { EmojiSection *section = data; CbEmojiChooser *chooser; GVariant *emoji_data; const char *text; const char *name; gboolean res; res = TRUE; chooser = CB_EMOJI_CHOOSER (gtk_widget_get_ancestor (GTK_WIDGET (child), CB_TYPE_EMOJI_CHOOSER)); text = gtk_entry_get_text (GTK_ENTRY (chooser->search_entry)); emoji_data = (GVariant *) g_object_get_data (G_OBJECT (child), "emoji-data"); if (text[0] == 0) goto out; if (!emoji_data) goto out; g_variant_get_child (emoji_data, 1, "&s", &name); res = strstr (name, text) != NULL; out: if (res) section->empty = FALSE; return res; } static void invalidate_section (EmojiSection *section) { section->empty = TRUE; gtk_flow_box_invalidate_filter (GTK_FLOW_BOX (section->box)); } static void update_headings (CbEmojiChooser *chooser) { gtk_widget_set_visible (chooser->people.heading, !chooser->people.empty); gtk_widget_set_visible (chooser->people.box, !chooser->people.empty); gtk_widget_set_visible (chooser->body.heading, !chooser->body.empty); gtk_widget_set_visible (chooser->body.box, !chooser->body.empty); gtk_widget_set_visible (chooser->nature.heading, !chooser->nature.empty); gtk_widget_set_visible (chooser->nature.box, !chooser->nature.empty); gtk_widget_set_visible (chooser->food.heading, !chooser->food.empty); gtk_widget_set_visible (chooser->food.box, !chooser->food.empty); gtk_widget_set_visible (chooser->travel.heading, !chooser->travel.empty); gtk_widget_set_visible (chooser->travel.box, !chooser->travel.empty); gtk_widget_set_visible (chooser->activities.heading, !chooser->activities.empty); gtk_widget_set_visible (chooser->activities.box, !chooser->activities.empty); gtk_widget_set_visible (chooser->objects.heading, !chooser->objects.empty); gtk_widget_set_visible (chooser->objects.box, !chooser->objects.empty); gtk_widget_set_visible (chooser->symbols.heading, !chooser->symbols.empty); gtk_widget_set_visible (chooser->symbols.box, !chooser->symbols.empty); gtk_widget_set_visible (chooser->flags.heading, !chooser->flags.empty); gtk_widget_set_visible (chooser->flags.box, !chooser->flags.empty); if (chooser->recent.empty && chooser->people.empty && chooser->body.empty && chooser->nature.empty && chooser->food.empty && chooser->travel.empty && chooser->activities.empty && chooser->objects.empty && chooser->symbols.empty && chooser->flags.empty) gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "empty"); else gtk_stack_set_visible_child_name (GTK_STACK (chooser->stack), "list"); } static void search_changed (GtkEntry *entry, gpointer data) { CbEmojiChooser *chooser = data; invalidate_section (&chooser->recent); invalidate_section (&chooser->people); invalidate_section (&chooser->body); invalidate_section (&chooser->nature); invalidate_section (&chooser->food); invalidate_section (&chooser->travel); invalidate_section (&chooser->activities); invalidate_section (&chooser->objects); invalidate_section (&chooser->symbols); invalidate_section (&chooser->flags); update_headings (chooser); } static void setup_section (CbEmojiChooser *chooser, EmojiSection *section, const char *first, gunichar label, const char *icon_name) { GtkAdjustment *adj; section->first = first; #if GTK_CHECK_VERSION(3, 23, 2) GtkWidget *icon; icon = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR); gtk_button_set_image (GTK_BUTTON (section->button), icon); #else char text[14]; char *p; p = text; p += g_unichar_to_utf8 (label, p); p += g_unichar_to_utf8 (0xfe0e, p); p[0] = 0; gtk_button_set_label (GTK_BUTTON (section->button), text); #endif adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (chooser->scrolled_window)); gtk_container_set_focus_vadjustment (GTK_CONTAINER (section->box), adj); gtk_flow_box_set_filter_func (GTK_FLOW_BOX (section->box), filter_func, section, NULL); g_signal_connect (section->button, "clicked", G_CALLBACK (scroll_to_section), section); } static void cb_emoji_chooser_finalize (GObject *object) { CbEmojiChooser *self = CB_EMOJI_CHOOSER (object); if (self->data != NULL) g_variant_unref (self->data); g_clear_object (&self->settings); if (self->populate_idle_id != 0) g_source_remove (self->populate_idle_id); G_OBJECT_CLASS (cb_emoji_chooser_parent_class)->finalize (object); } static void cb_emoji_chooser_init (CbEmojiChooser *self) { GtkAdjustment *adj; gtk_widget_init_template (GTK_WIDGET (self)); adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window)); g_signal_connect (adj, "value-changed", G_CALLBACK (adj_value_changed), self); setup_section (self, &self->recent, NULL, 0x1f557, "emoji-recent-symbolic"); setup_section (self, &self->people, "grinning face", 0x1f642, "emoji-people-symbolic"); setup_section (self, &self->body, "selfie", 0x1f44d, "emoji-body-symbolic"); setup_section (self, &self->nature, "monkey face", 0x1f337, "emoji-nature-symbolic"); setup_section (self, &self->food, "grapes", 0x1f374, "emoji-food-symbolic"); setup_section (self, &self->travel, "globe showing Europe-Africa", 0x2708, "emoji-travel-symbolic"); setup_section (self, &self->activities, "jack-o-lantern", 0x1f3c3, "emoji-activities-symbolic"); setup_section (self, &self->objects, "muted speaker", 0x1f514, "emoji-objects-symbolic"); setup_section (self, &self->symbols, "ATM sign", 0x2764, "emoji-symbols-symbolic"); setup_section (self, &self->flags, "chequered flag", 0x1f3f4, "emoji-flags-symbolic"); /* We scroll to the top on show, so check the right button for the 1st time */ gtk_widget_set_state_flags (self->recent.button, GTK_STATE_FLAG_CHECKED, FALSE); gtk_widget_show_all (GTK_WIDGET (self)); } static void cb_emoji_chooser_class_init (CbEmojiChooserClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = cb_emoji_chooser_finalize; signals[EMOJI_PICKED] = g_signal_new ("emoji-picked", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING|G_SIGNAL_TYPE_STATIC_SCOPE); gtk_widget_class_set_template_from_resource (widget_class, "/uk/co/ibboard/cawbird/ui/cb-emoji-chooser.ui"); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, search_entry); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, stack); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, scrolled_window); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, recent.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, recent.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, people.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, people.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, people.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, body.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, body.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, body.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, nature.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, nature.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, nature.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, food.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, food.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, food.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, travel.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, travel.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, travel.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, activities.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, activities.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, activities.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, objects.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, objects.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, objects.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, symbols.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, symbols.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, symbols.button); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, flags.box); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, flags.heading); gtk_widget_class_bind_template_child (widget_class, CbEmojiChooser, flags.button); gtk_widget_class_bind_template_callback (widget_class, emoji_activated); gtk_widget_class_bind_template_callback (widget_class, search_changed); } GtkWidget * cb_emoji_chooser_new (void) { return GTK_WIDGET (g_object_new (CB_TYPE_EMOJI_CHOOSER, NULL)); } void cb_emoji_chooser_populate (CbEmojiChooser *self) { if (self->populated) return; self->populated = TRUE; populate_emoji_chooser (self); } gboolean cb_emoji_chooser_try_init (CbEmojiChooser *self) { GBytes *bytes; GVariant *settings_test; gboolean recent_in_correct_format = FALSE; GSettingsSchemaSource *schema_source; GSettingsSchema *schema; schema_source = g_settings_schema_source_get_default (); schema = g_settings_schema_source_lookup (schema_source, "org.gtk.Settings.EmojiChooser", TRUE); if (schema == NULL) { g_warning ("Emoji chooser: Schema not found"); return FALSE; } g_settings_schema_unref (schema); schema = NULL; self->settings = g_settings_new ("org.gtk.Settings.EmojiChooser"); settings_test = g_settings_get_value (self->settings, "recent-emoji"); recent_in_correct_format = g_variant_is_of_type (settings_test, G_VARIANT_TYPE ("a((auss)u)")); g_variant_unref (settings_test); if (!recent_in_correct_format) { g_warning ("Emoji chooser: Recent variant in wrong format"); return FALSE; } bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/emoji.data", 0, NULL); if (bytes == NULL) { g_debug("Trying newer emoji data"); const char *lang; char q[10]; char* path; // Newer GTK has language-specific emoji data lang = pango_language_to_string (gtk_get_default_language ()); if (strchr (lang, '-')) { int i; for (i = 0; lang[i] != '-' && i < 9; i++) { q[i] = lang[i]; } q[i] = '\0'; lang = q; } path = g_strconcat ("/org/gtk/libgtk/emoji/", lang, ".data", NULL); bytes = g_resources_lookup_data (path, 0, NULL); if (bytes == NULL && !g_strcmp0(lang, "en")) { g_debug("Could not find emoji/%s.data. Falling back to emoji/en.data", lang); bytes = g_resources_lookup_data ("/org/gtk/libgtk/emoji/en.data", 0, NULL); } else { g_debug("Using emoji/%s.data resource", lang); } } else { g_debug("Using old emoji.data resource"); } if (bytes == NULL) { g_warning ("Emoji chooser: resources not available"); return FALSE; } // We used to checksum values, but that's not viable with multi-lingual resources because // there are too many valid values! self->data = g_variant_ref_sink (g_variant_new_from_bytes (G_VARIANT_TYPE ("a(auss)"), bytes, TRUE)); g_bytes_unref (bytes); populate_recent_section (self); return TRUE; } cawbird-1.4.2/src/CbEmojiChooser.h000066400000000000000000000034631416632607600167450ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef __CB_EMOJI_CHOOSER_H__ #define __CB_EMOJI_CHOOSER_H__ #include typedef struct { GtkWidget *box; GtkWidget *heading; GtkWidget *button; const char *first; gunichar label; gboolean empty; } EmojiSection; struct _CbEmojiChooser { GtkBox parent_instance; guint populated : 1; guint populate_idle_id; GtkWidget *search_entry; GtkWidget *stack; GtkWidget *scrolled_window; EmojiSection recent; EmojiSection people; EmojiSection body; EmojiSection nature; EmojiSection food; EmojiSection travel; EmojiSection activities; EmojiSection objects; EmojiSection symbols; EmojiSection flags; GVariant *data; GSettings *settings; }; typedef struct _CbEmojiChooser CbEmojiChooser; #define CB_TYPE_EMOJI_CHOOSER cb_emoji_chooser_get_type () G_DECLARE_FINAL_TYPE (CbEmojiChooser, cb_emoji_chooser, CB, EMOJI_CHOOSER, GtkBox); GtkWidget * cb_emoji_chooser_new (void); gboolean cb_emoji_chooser_try_init (CbEmojiChooser *self); void cb_emoji_chooser_populate (CbEmojiChooser *self); #endif cawbird-1.4.2/src/CbFilter.c000066400000000000000000000051311416632607600155710ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbFilter.h" G_DEFINE_TYPE (CbFilter, cb_filter, G_TYPE_OBJECT); static void cb_filter_finalize (GObject *obj) { CbFilter *filter = CB_FILTER (obj); g_free (filter->contents); if (filter->regex != NULL) g_regex_unref (filter->regex); G_OBJECT_CLASS (cb_filter_parent_class)->finalize (obj); } static void cb_filter_init (CbFilter *filter) { filter->contents = NULL; filter->regex = NULL; } static void cb_filter_class_init (CbFilterClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_filter_finalize; } CbFilter * cb_filter_new (const char *expr) { CbFilter *filter = CB_FILTER (g_object_new (CB_TYPE_FILTER, NULL)); cb_filter_reset (filter, expr); return filter; } void cb_filter_reset (CbFilter *filter, const char *expr) { g_return_if_fail (CB_IS_FILTER (filter)); g_return_if_fail (expr != NULL); filter->regex = g_regex_new (expr, G_REGEX_CASELESS, 0, /* No match flags */ NULL); filter->contents = g_strdup (expr); } gboolean cb_filter_matches (CbFilter *filter, const char *text) { g_return_val_if_fail (CB_IS_FILTER (filter), FALSE); g_return_val_if_fail (text != NULL, FALSE); if (filter->regex == NULL) return FALSE; return g_regex_match (filter->regex, text, 0, NULL); } const char * cb_filter_get_contents (CbFilter *filter) { g_return_val_if_fail (CB_IS_FILTER (filter), ""); return filter->contents; } int cb_filter_get_id (CbFilter *filter) { g_return_val_if_fail (CB_IS_FILTER (filter), 0); return filter->id; } void cb_filter_set_id (CbFilter *filter, int id) { g_return_if_fail (CB_IS_FILTER (filter)); g_return_if_fail (id >= 0); filter->id = id; } cawbird-1.4.2/src/CbFilter.h000066400000000000000000000030301416632607600155720ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef FILTER_H #define FILTER_H #include G_BEGIN_DECLS typedef struct _CbFilter CbFilter; struct _CbFilter { GObject parent_instance; int id; char *contents; GRegex *regex; }; #define CB_TYPE_FILTER cb_filter_get_type () G_DECLARE_FINAL_TYPE (CbFilter, cb_filter, CB, FILTER, GObject); GType cb_filter_get_type (void) G_GNUC_CONST; CbFilter *cb_filter_new (const char *expr); void cb_filter_reset (CbFilter *filter, const char *expr); gboolean cb_filter_matches (CbFilter *filter, const char *text); const char *cb_filter_get_contents (CbFilter *filter); int cb_filter_get_id (CbFilter *filter); void cb_filter_set_id (CbFilter *filter, int id); G_END_DECLS #endif cawbird-1.4.2/src/CbGtkCompat.h000066400000000000000000000040511416632607600162420ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef CB_GTK_COMPAT_H #define CB_GTK_COMPAT_H #include #if GTK_CHECK_VERSION(3, 9, 1) void gtk_widget_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline) { if (orientation == GTK_ORIENTATION_HORIZONTAL) { g_assert (minimum_baseline == NULL); g_assert (natural_baseline == NULL); if (for_size == -1) gtk_widget_get_preferred_width (widget, minimum, natural); else gtk_widget_get_preferred_width_for_height (widget, for_size, minimum, natural); } else /* VERTICAL */ { gtk_widget_get_preferred_height_and_baseline_for_width (widget, for_size, minimum, natural, minimum_baseline, natural_baseline); } } #endif #endif cawbird-1.4.2/src/CbMedia.c000066400000000000000000000150561416632607600153720ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbMedia.h" G_DEFINE_TYPE (CbMedia, cb_media, G_TYPE_OBJECT); enum { PROGRESS, HIRES_PROGRESS, LAST_SIGNAL }; static guint media_signals[LAST_SIGNAL] = { 0 }; static void cb_media_finalize (GObject *object) { CbMedia *media = CB_MEDIA (object); if (media->surface != NULL) { cairo_surface_destroy (media->surface); } if (media->surface_hires != NULL) { cairo_surface_destroy (media->surface_hires); } g_free (media->thumb_url); g_free (media->target_url); g_free (media->url); g_free (media->alt_text); if (media->consumer_key) { g_free (media->consumer_key); } if (media->consumer_secret) { g_free (media->consumer_secret); } if (media->token) { g_free (media->token); } if (media->token_secret) { g_free (media->token_secret); } if (media->animation) g_object_unref (media->animation); G_OBJECT_CLASS (cb_media_parent_class)->finalize (object); } static void cb_media_class_init (CbMediaClass *class) { GObjectClass *gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = cb_media_finalize; media_signals[PROGRESS] = g_signal_new ("progress", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); media_signals[HIRES_PROGRESS] = g_signal_new ("hires-progress", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void cb_media_init (CbMedia *media) { media->surface = NULL; media->animation = NULL; media->loading = FALSE; media->loaded = FALSE; media->loading_hires = FALSE; media->loaded_hires = FALSE; media->invalid = FALSE; media->surface = NULL; media->surface_hires = NULL; media->consumer_key = NULL; media->consumer_secret = NULL; media->token = NULL; media->token_secret = NULL; media->url = NULL; media->alt_text = NULL; media->percent_loaded = 0; media->percent_loaded_hires = 0; media->width = -1; media->height = -1; media->thumb_width = -1; media->thumb_height = -1; } CbMedia * cb_media_new (void) { return CB_MEDIA (g_object_new (CB_TYPE_MEDIA, NULL)); } gboolean cb_media_is_video (CbMedia *media) { switch (media->type) { case CB_MEDIA_TYPE_ANIMATED_GIF: case CB_MEDIA_TYPE_TWITTER_VIDEO: case CB_MEDIA_TYPE_INSTAGRAM_VIDEO: return TRUE; default: return FALSE; } return FALSE; } gboolean cb_media_requires_authentication (CbMedia *media) { return media->consumer_key && media->consumer_secret && media->token && media->token_secret; } static gboolean emit_media_progress (gpointer data) { CbMedia *media = data; g_return_val_if_fail (CB_IS_MEDIA (media), G_SOURCE_REMOVE); g_signal_emit (data, media_signals[PROGRESS], 0); return G_SOURCE_REMOVE; } void cb_media_update_progress (CbMedia *media, double progress) { g_return_if_fail (CB_IS_MEDIA (media)); g_return_if_fail (progress >= 0); media->percent_loaded = progress; g_main_context_invoke (NULL, emit_media_progress, media); } void cb_media_loading_finished (CbMedia *media) { g_return_if_fail (CB_IS_MEDIA (media)); if (media->invalid) { // Short-circuit for invalid media cb_media_update_progress (media, 1.0); return; } media->thumb_width = cairo_image_surface_get_width(media->surface); media->thumb_height = cairo_image_surface_get_height(media->surface); // Take these sizes as full size if full size isn't set. // This happens when loading third-party images which don't have // Twitter's scaling variants. if (media->width == -1) { media->width = media->thumb_width; } if (media->height == -1) { media->height = media->thumb_height; } media->invalid = FALSE; media->loaded = TRUE; media->loading = FALSE; if (media->height == media->thumb_height && media->width == media->thumb_width) { // There is no higher res to load so pretend we did. // The get_highest_res_surface() function then deals with what is available media->loaded_hires = TRUE; } else if (cb_media_is_video (media)) { // Video doesn't have a hires, it runs the URL through GStreamer, so pretend we loaded the hires image media->loaded_hires = TRUE; } cb_media_update_progress (media, 1.0); } CbMediaType cb_media_type_from_url (const char *url) { if (g_str_has_suffix (url, "/photo/1")) return CB_MEDIA_TYPE_ANIMATED_GIF; if (g_str_has_suffix (url, ".gif")) return CB_MEDIA_TYPE_GIF; return CB_MEDIA_TYPE_IMAGE; } static gboolean emit_media_hires_progress (gpointer data) { CbMedia *media = data; g_return_val_if_fail (CB_IS_MEDIA (media), G_SOURCE_REMOVE); g_signal_emit (data, media_signals[HIRES_PROGRESS], 0); return G_SOURCE_REMOVE; } void cb_media_update_hires_progress (CbMedia *media, double progress) { g_return_if_fail (CB_IS_MEDIA (media)); g_return_if_fail (progress >= 0); media->percent_loaded_hires = progress; g_main_context_invoke (NULL, emit_media_hires_progress, media); } void cb_media_loading_hires_finished (CbMedia *media) { g_return_if_fail (CB_IS_MEDIA (media)); media->loaded_hires = TRUE; media->loading_hires = FALSE; cb_media_update_hires_progress (media, 1.0); } cairo_surface_t * cb_media_get_highest_res_surface (CbMedia *media) { return media->surface_hires == NULL ? media->surface : media->surface_hires; }cawbird-1.4.2/src/CbMedia.h000066400000000000000000000046601416632607600153760ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef MEDIA_H #define MEDIA_H #include #include #include G_BEGIN_DECLS typedef enum { CB_MEDIA_TYPE_IMAGE, CB_MEDIA_TYPE_GIF, CB_MEDIA_TYPE_ANIMATED_GIF, CB_MEDIA_TYPE_TWITTER_VIDEO, CB_MEDIA_TYPE_INSTAGRAM_VIDEO, CB_MEDIA_TYPE_UNKNOWN } CbMediaType; struct _CbMedia { GObject parent_instance; char *url; char *thumb_url; char *target_url; char *alt_text; int width; int height; int thumb_width; int thumb_height; gchar *consumer_key; gchar *consumer_secret; gchar *token; gchar *token_secret; CbMediaType type; guint loading: 1; guint loaded : 1; guint loading_hires: 1; guint loaded_hires : 1; guint invalid : 1; double percent_loaded; double percent_loaded_hires; cairo_surface_t *surface; cairo_surface_t *surface_hires; GdkPixbufAnimation *animation; }; typedef struct _CbMedia CbMedia; #define CB_TYPE_MEDIA cb_media_get_type () G_DECLARE_FINAL_TYPE (CbMedia, cb_media, CB, MEDIA, GObject); CbMedia * cb_media_new (void); gboolean cb_media_is_video (CbMedia *media); gboolean cb_media_requires_authentication (CbMedia *media); void cb_media_loading_finished (CbMedia *media); void cb_media_update_progress (CbMedia *media, double progress); void cb_media_loading_hires_finished (CbMedia *media); void cb_media_update_hires_progress (CbMedia *media, double progress); CbMediaType cb_media_type_from_url (const char *url); cairo_surface_t * cb_media_get_highest_res_surface (CbMedia *media); G_END_DECLS #endif cawbird-1.4.2/src/CbMediaDownloader.c000066400000000000000000000471101416632607600174050ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbMediaDownloader.h" #include #include #include #include #include G_DEFINE_TYPE (CbMediaDownloader, cb_media_downloader, G_TYPE_OBJECT); typedef struct { CbMedia *media; SoupSession *soup_session; } LoadingData; static void loading_data_free (LoadingData *data) { g_object_unref (data->media); g_object_unref (data->soup_session); g_free (data); } CbMediaDownloader * cb_media_downloader_get_default (void) { static CbMediaDownloader *d = NULL; if (G_UNLIKELY (d == NULL)) { d = CB_MEDIA_DOWNLOADER (g_object_new (CB_TYPE_MEDIA_DOWNLOADER, NULL)); } return d; } static void mark_invalid (CbMedia *media) { media->invalid = TRUE; media->loaded = TRUE; media->loading = FALSE; media->loaded_hires = TRUE; media->loading_hires = FALSE; cb_media_loading_finished (media); cb_media_loading_hires_finished (media); } static const char * canonicalize_url (const char *url) { int ret = 0; if (g_str_has_prefix (url,"http://")) ret += 7; else if (g_str_has_prefix (url, "https://")) ret += 8; if (g_str_has_prefix(url + ret, "www.")) ret += 4; return url + ret; } static void load_animation (GInputStream *input_stream, cairo_surface_t **target_surface, GdkPixbufAnimation **target_animation, GCancellable *cancellable, GError **error) { GdkPixbufAnimation *animation; GdkPixbuf *frame; cairo_surface_t *surface; cairo_t *ct; gboolean has_alpha; animation = gdk_pixbuf_animation_new_from_stream (input_stream, NULL, error); if (*error) { return; } frame = gdk_pixbuf_animation_get_static_image (animation); if (g_cancellable_is_cancelled (cancellable)) { g_object_unref (animation); return; } if (!gdk_pixbuf_animation_is_static_image (animation)) *target_animation = animation; /* Takes ref */ else *target_animation = NULL; has_alpha = gdk_pixbuf_get_has_alpha (frame); surface = cairo_image_surface_create (has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, gdk_pixbuf_get_width (frame), gdk_pixbuf_get_height (frame)); ct = cairo_create (surface); gdk_cairo_set_source_pixbuf (ct, frame, 0.0, 0.0); cairo_paint (ct); cairo_destroy (ct); *target_surface = surface; if (*target_animation == NULL) { g_object_unref (animation); } } static void load_media_url (const char *url, LoadingData *task_data, cairo_surface_t **surface, GdkPixbufAnimation **animation, gchar *consumer_key, gchar *consumer_secret, gchar *token, gchar *token_secret, GCallback progress_callback, GCancellable *cancellable, GError **error, gpointer data) { SoupMessage *msg; GInputStream *input_stream; msg = soup_message_new ("GET", url); if (msg == NULL) { g_set_error (error, CB_MEDIA_DOWNLOADER_ERROR, CB_MEDIA_DOWNLOADER_ERROR_SOUP_MESSAGE_NEW, "soup_message_new failed for URI '%s'", url); return; } if (consumer_key && consumer_secret && token && token_secret) { gchar *oauth_authorization_parameters; gchar **url_parameters = NULL; int url_parameters_length; gchar *authorization_text; url_parameters_length = oauth_split_url_parameters (url, &url_parameters); oauth_sign_array2_process (&url_parameters_length, &url_parameters, NULL, OA_HMAC, msg->method, consumer_key, consumer_secret, token, token_secret); oauth_authorization_parameters = oauth_serialize_url_sep (url_parameters_length, 1, url_parameters, ", ", 6); authorization_text = g_strdup_printf ("OAuth realm=\"\", %s", oauth_authorization_parameters); soup_message_headers_append (msg->request_headers, "Authorization", authorization_text); oauth_free_array (&url_parameters_length, &url_parameters); free (oauth_authorization_parameters); } if (progress_callback != NULL) { g_signal_connect (msg, "got-chunk", progress_callback, data); } soup_session_send_message (task_data->soup_session, msg); if (msg->status_code != SOUP_STATUS_OK) { g_set_error (error, CB_MEDIA_DOWNLOADER_ERROR, msg->status_code, "Request on '%s' returned status '%s'", url, soup_status_get_phrase (msg->status_code)); g_object_unref (msg); return; } if (g_cancellable_is_cancelled (cancellable)) return; input_stream = g_memory_input_stream_new_from_data (msg->response_body->data, msg->response_body->length, NULL); load_animation (input_stream, surface, animation, cancellable, error); g_input_stream_close (input_stream, NULL, NULL); g_object_unref (input_stream); g_object_unref (msg); } static void cb_media_downloader_get_instagram_url (CbMediaDownloader *downloader, LoadingData *task_data) { CbMedia *media = task_data->media; SoupMessage *msg = soup_message_new ("GET", media->url); GRegex *medium_regex; GRegex *url_regex; GMatchInfo *match_info; soup_session_send_message (task_data->soup_session, msg); if (msg->status_code != SOUP_STATUS_OK) { g_object_unref (msg); media->url = NULL; return; } medium_regex = g_regex_new ("", 0, 0, NULL); g_regex_match (medium_regex, (const char *)msg->response_body->data, 0, &match_info); if (g_match_info_get_match_count (match_info) > 0) { g_match_info_free (match_info); /* Video! */ url_regex = g_regex_new ("response_body->data, 0, &match_info); media->url = g_match_info_fetch (match_info, 1); g_regex_unref (url_regex); media->type = CB_MEDIA_TYPE_INSTAGRAM_VIDEO; } g_match_info_free (match_info); url_regex = g_regex_new ("response_body->data, 0, &match_info); media->thumb_url = g_match_info_fetch (match_info, 1); g_free (media->target_url); media->target_url = g_strdup (media->thumb_url); g_regex_unref (url_regex); g_regex_unref (medium_regex); g_match_info_free (match_info); g_object_unref (msg); } static void cb_media_downloader_load_twitter_video (CbMediaDownloader *downloader, LoadingData *task_data) { CbMedia *media = task_data->media; SoupMessage *msg = soup_message_new ("GET", media->url); GRegex *regex; GMatchInfo *match_info; soup_session_send_message (task_data->soup_session, msg); if (msg->status_code != SOUP_STATUS_OK) { mark_invalid (media); g_object_unref (msg); return; } regex = g_regex_new ("response_body->data, 0, &match_info); if (g_match_info_get_match_count (match_info) > 0) { g_assert (media->type == CB_MEDIA_TYPE_ANIMATED_GIF); media->url = g_match_info_fetch (match_info, 1); g_regex_unref (regex); g_match_info_free (match_info); g_object_unref (msg); return; } else { g_regex_unref (regex); g_match_info_free (match_info); regex = g_regex_new ("response_body->data, 0, &match_info); media->url = g_match_info_fetch (match_info, 1); media->type = CB_MEDIA_TYPE_TWITTER_VIDEO; } g_regex_unref (regex); g_match_info_free (match_info); regex = g_regex_new ("poster=\"(.*?)\"", 0, 0, NULL); g_regex_match (regex, (const char *)msg->response_body->data, 0, &match_info); media->thumb_url = g_match_info_fetch (match_info, 1); g_regex_unref (regex); g_match_info_free (match_info); g_object_unref (msg); } static void cb_media_downloader_load_real_url (CbMediaDownloader *downloader, LoadingData *task_data, const char *regex_str1, int match_index1) { CbMedia *media = task_data->media; SoupMessage *msg = soup_message_new ("GET", media->url); GRegex *regex; GMatchInfo *match_info; soup_session_send_message (task_data->soup_session, msg); if (msg->status_code != SOUP_STATUS_OK) { /* Will mark it invalid later */ media->url = NULL; g_object_unref (msg); return; } regex = g_regex_new (regex_str1, 0, 0, NULL); g_regex_match (regex, (const char *)msg->response_body->data, 0, &match_info); media->thumb_url = g_match_info_fetch (match_info, match_index1); g_regex_unref (regex); g_match_info_free (match_info); g_object_unref (msg); } static void update_media_progress (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data) { CbMedia *media = user_data; if (msg->response_headers == NULL) return; double chunk_percent = chunk->length / (double)soup_message_headers_get_content_length (msg->response_headers); cb_media_update_progress (media, media->percent_loaded + chunk_percent); } static void cb_media_downloader_load_threaded (CbMediaDownloader *downloader, LoadingData *task_data, GCancellable *cancellable) { const char *url; CbMedia *media; g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); media = task_data->media; url = canonicalize_url (media->url); if (g_cancellable_is_cancelled (cancellable)) return; /* For these, we first need to download some html and get the real URL of the image we want to display */ if (g_str_has_prefix (url, "instagr.am") || g_str_has_prefix (url, "instagram.com/p/")) { cb_media_downloader_get_instagram_url (downloader, task_data); } else if (g_str_has_prefix (url, "ow.ly/i/") || g_str_has_prefix (url, "flickr.com/photos/") || g_str_has_prefix (url, "flic.kr/p/") || g_str_has_prefix (url, "flic.kr/s/")) { cb_media_downloader_load_real_url (downloader, task_data, "url == NULL) { g_warning ("Media is invalid. (url %s)", url); mark_invalid (media); return; } if (g_cancellable_is_cancelled (cancellable)) return; char *download_url = media->thumb_url ? media->thumb_url : media->url; GError *error = NULL; load_media_url (download_url, task_data, &media->surface, &media->animation, media->consumer_key, media->consumer_secret, media->token, media->token_secret, G_CALLBACK(update_media_progress), cancellable, &error, media); if (error) { g_warning ("Couldn't load pixbuf: %s (%s)", error->message, download_url); mark_invalid (media); g_error_free (error); return; } cb_media_loading_finished (media); } void load_in_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { CbMediaDownloader *downloader = source_object; LoadingData *data = task_data; cb_media_downloader_load_threaded (downloader, data, cancellable); g_task_return_boolean (task, TRUE); g_object_unref (task); } void cb_media_downloader_load_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; LoadingData *data; g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); g_return_if_fail (CB_IS_MEDIA (media)); g_return_if_fail (!media->loaded); g_return_if_fail (!media->loading); g_return_if_fail (media->surface == NULL); media->loading = TRUE; task = g_task_new (downloader, downloader->cancellable, callback, user_data); data = g_new0 (LoadingData, 1); data->media = g_object_ref (media); data->soup_session = soup_session_new (); g_task_set_task_data (task, data, (GDestroyNotify)loading_data_free); g_task_run_in_thread (task, load_in_thread); } void cb_media_downloader_reload_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (media->invalid); media->loaded = FALSE; media->invalid = FALSE; media->percent_loaded = 0; if (media->surface != NULL) { cairo_surface_destroy (media->surface); media->surface = NULL; } cb_media_downloader_load_async(downloader, media, callback, user_data); } static void update_media_hires_progress (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data) { CbMedia *media = user_data; if (msg->response_headers == NULL) return; double chunk_percent = chunk->length / (double)soup_message_headers_get_content_length (msg->response_headers); cb_media_update_hires_progress (media, media->percent_loaded_hires + chunk_percent); } static void cb_media_downloader_load_hires_threaded (CbMediaDownloader *downloader, LoadingData *task_data, GCancellable *cancellable) { CbMedia *media; g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); media = task_data->media; GError *error = NULL; load_media_url (media->url, task_data, &media->surface_hires, &media->animation, media->consumer_key, media->consumer_secret, media->token, media->token_secret, G_CALLBACK(update_media_hires_progress), cancellable, &error, media); if (error) { g_warning ("Couldn't load hires pixbuf: %s (%s)", error->message, media->url); g_error_free (error); return; } media->width = cairo_image_surface_get_width(media->surface_hires); media->height = cairo_image_surface_get_height(media->surface_hires); cb_media_loading_hires_finished (media); } void load_hires_in_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { CbMediaDownloader *downloader = source_object; LoadingData *data = task_data; cb_media_downloader_load_hires_threaded (downloader, data, cancellable); g_task_return_boolean (task, TRUE); g_object_unref (task); } void cb_media_downloader_load_hires_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; LoadingData *data; g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); g_return_if_fail (CB_IS_MEDIA (media)); g_return_if_fail (media->surface_hires == NULL); if (media->loading_hires || media->loaded_hires) { return; } media->loading_hires = TRUE; task = g_task_new (downloader, downloader->cancellable, callback, user_data); data = g_new0 (LoadingData, 1); data->media = g_object_ref (media); data->soup_session = soup_session_new (); g_task_set_task_data (task, data, (GDestroyNotify)loading_data_free); g_task_run_in_thread (task, load_hires_in_thread); } gboolean cb_media_downloader_load_finish (CbMediaDownloader *downloader, GAsyncResult *result, GError **error) { g_return_val_if_fail (g_task_is_valid (result, downloader), FALSE); return g_task_propagate_boolean (G_TASK (result), error); } void cb_media_downloader_load_all (CbMediaDownloader *downloader, CbMiniTweet *t) { guint i; g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); if (downloader->disabled) return; for (i = 0; i < t->n_medias; i ++) cb_media_downloader_load_async (downloader, t->medias[i], NULL, NULL); } void cb_media_downloader_disable (CbMediaDownloader *downloader) { g_return_if_fail (CB_IS_MEDIA_DOWNLOADER (downloader)); downloader->disabled = TRUE; } void cb_media_downloader_shutdown (CbMediaDownloader *downloader) { g_debug ("MediaDownloader shutdown"); g_cancellable_cancel (downloader->cancellable); g_object_unref (downloader->cancellable); // XXX OK? g_object_unref (downloader); } gboolean is_media_candidate (const char *url) { url = canonicalize_url (url); return g_str_has_prefix (url, "instagr.am") || g_str_has_prefix (url, "instagram.com/p/") || (g_str_has_prefix (url, "i.imgur.com") && !g_str_has_suffix (url, "gifv")) || g_str_has_prefix (url, "d.pr/i/") || g_str_has_prefix (url, "ow.ly/i/") || g_str_has_prefix (url, "flickr.com/photos/") || g_str_has_prefix (url, "flic.kr/p/") || g_str_has_prefix (url, "flic.kr/s/") || #ifdef VIDEO g_str_has_suffix (url, "/photo/1/") || g_str_has_prefix (url, "video.twimg.com/ext_tw_video") || #endif g_str_has_prefix (url, "pbs.twimg.com/media/") || g_str_has_prefix (url, "twitpic.com") ; } gboolean is_twitter_media_candidate (const char *url) { url = canonicalize_url (url); return #ifdef VIDEO g_str_has_prefix (url, "/photo/1/") || g_str_has_prefix (url, "video.twimg.com/ext_tw_video") || g_str_has_prefix (url, "video.twimg.com/amplify_video") || #endif g_str_has_prefix (url, "pbs.twimg.com/media/") ; } static void cb_media_downloader_init (CbMediaDownloader *downloader) { downloader->disabled = FALSE; downloader->cancellable = g_cancellable_new (); } static void cb_media_downloader_class_init (CbMediaDownloaderClass *class) { } GQuark cb_media_downloader_error_quark (void) { static GQuark error; if (!error) error = g_quark_from_static_string ("cb_media_downloader_error_quark"); return error; }cawbird-1.4.2/src/CbMediaDownloader.h000066400000000000000000000065311416632607600174140ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_INLINE_MEDIA_DOWNLOADER_H_ #define _CB_INLINE_MEDIA_DOWNLOADER_H_ #include #include #include "CbMedia.h" #include "CbTypes.h" G_BEGIN_DECLS struct _CbMediaDownloader { GObject parent_instance; GCancellable *cancellable; guint disabled : 1; }; typedef struct _CbMediaDownloader CbMediaDownloader; #define CB_TYPE_MEDIA_DOWNLOADER (cb_media_downloader_get_type ()) G_DECLARE_FINAL_TYPE (CbMediaDownloader, cb_media_downloader, CB, MEDIA_DOWNLOADER, GObject); CbMediaDownloader * cb_media_downloader_get_default (void); void cb_media_downloader_load_all (CbMediaDownloader *downloader, CbMiniTweet *t); void cb_media_downloader_load_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data); void cb_media_downloader_reload_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data); void cb_media_downloader_load_hires_async (CbMediaDownloader *downloader, CbMedia *media, GAsyncReadyCallback callback, gpointer user_data); gboolean cb_media_downloader_load_finish (CbMediaDownloader *downloader, GAsyncResult *result, GError **error); void cb_media_downloader_disable (CbMediaDownloader *downloader); void cb_media_downloader_shutdown (CbMediaDownloader *downloader); gboolean is_media_candidate (const char *url); gboolean is_twitter_media_candidate (const char *url); G_END_DECLS typedef enum { CB_MEDIA_DOWNLOADER_ERROR_UNKNOWN, CB_MEDIA_DOWNLOADER_ERROR_SOUP_MESSAGE_NEW // We also use all SOUP_STATUS_... codes } CbMediaDownloaderErrorCode; #define CB_MEDIA_DOWNLOADER_ERROR cb_media_downloader_error_quark() GQuark cb_media_downloader_error_quark (void); #endif cawbird-1.4.2/src/CbMediaImageWidget.c000066400000000000000000000133441416632607600174770ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbMediaImageWidget.h" G_DEFINE_TYPE (CbMediaImageWidget, cb_media_image_widget, GTK_TYPE_SCROLLED_WINDOW) static void cb_media_image_widget_finalize (GObject *object) { CbMediaImageWidget *self = CB_MEDIA_IMAGE_WIDGET (object); g_clear_object (&self->drag_gesture); if (self->image_surface != NULL) { cairo_surface_destroy(self->image_surface); } if (self->media && self->hires_progress_id) { g_signal_handler_disconnect (self->media, self->hires_progress_id); } G_OBJECT_CLASS (cb_media_image_widget_parent_class)->finalize (object); } static void drag_begin_cb (GtkGestureDrag *gesture, double start_x, double start_y, gpointer user_data) { CbMediaImageWidget *self = user_data; GtkAdjustment *adjustment; adjustment = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (self)); self->drag_start_hvalue = gtk_adjustment_get_value (adjustment); adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self)); self->drag_start_vvalue = gtk_adjustment_get_value (adjustment); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } static void drag_update_cb (GtkGestureDrag *gesture, double offset_x, double offset_y, gpointer user_data) { CbMediaImageWidget *self = user_data; GtkAdjustment *adjustment; adjustment = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (self)); gtk_adjustment_set_value (adjustment, self->drag_start_hvalue - offset_x); adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self)); gtk_adjustment_set_value (adjustment, self->drag_start_vvalue - offset_y); gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED); } static void cb_media_image_widget_class_init (CbMediaImageWidgetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_media_image_widget_finalize; } static void cb_media_image_widget_init (CbMediaImageWidget *self) { self->image = gtk_image_new (); gtk_container_add (GTK_CONTAINER (self), self->image); self->drag_gesture = gtk_gesture_drag_new (GTK_WIDGET (self)); gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->drag_gesture), GDK_BUTTON_MIDDLE); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->drag_gesture), GTK_PHASE_CAPTURE); g_signal_connect (self->drag_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self); g_signal_connect (self->drag_gesture, "drag-update", G_CALLBACK (drag_update_cb), self); } void hires_progress (CbMedia *media, gpointer user_data) { if (!media->loaded_hires) { return; } CbMediaImageWidget *self = CB_MEDIA_IMAGE_WIDGET(user_data); gtk_image_set_from_surface (GTK_IMAGE (self->image), media->surface_hires); cairo_surface_destroy(self->image_surface); self->image_surface = NULL; } GtkWidget * cb_media_image_widget_new (CbMedia *media, GdkRectangle *max_dimensions) { CbMediaImageWidget *self; int win_width; int win_height; g_return_val_if_fail (CB_IS_MEDIA (media), NULL); g_return_val_if_fail (!media->invalid, NULL); g_return_val_if_fail (media->surface != NULL, NULL); self = CB_MEDIA_IMAGE_WIDGET (g_object_new (CB_TYPE_MEDIA_IMAGE_WIDGET, NULL)); if (media->type == CB_MEDIA_TYPE_GIF) { gtk_image_set_from_animation (GTK_IMAGE (self->image), media->animation); } else if (media->loaded_hires) { gtk_image_set_from_surface (GTK_IMAGE (self->image), cb_media_get_highest_res_surface(media)); } else { double scale_width = media->width * 1.0 / media->thumb_width; double scale_height = media->height * 1.0 / media->thumb_height; self->media = media; self->hires_progress_id = g_signal_connect(media, "hires-progress", G_CALLBACK(hires_progress), self); self->image_surface = cairo_image_surface_create(cairo_image_surface_get_format(media->surface), media->width, media->height); cairo_t *ct = cairo_create(self->image_surface); cairo_scale(ct, scale_width, scale_height); cairo_set_source_surface (ct, media->surface, 0, 0); cairo_paint(ct); cairo_destroy(ct); gtk_image_set_from_surface (GTK_IMAGE (self->image), self->image_surface); if (!media->loading) { // NULL callback because we should pick it up from the earlier g_signal_connect cb_media_downloader_load_hires_async (cb_media_downloader_get_default(), media, NULL, NULL); } } win_width = media->width; win_height = media->height; if (win_width > max_dimensions->width) { win_width = max_dimensions->width; } if (win_height > max_dimensions->height) { win_height = max_dimensions->height; } gtk_widget_set_size_request (GTK_WIDGET (self), win_width, win_height); gtk_widget_set_tooltip_text (GTK_WIDGET (self), media->alt_text); atk_object_set_description(gtk_widget_get_accessible(GTK_WIDGET(self)), media->alt_text == NULL ? "" : media->alt_text); return GTK_WIDGET (self); }cawbird-1.4.2/src/CbMediaImageWidget.h000066400000000000000000000027521416632607600175050ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_MEDIA_IMAGE_WIDGET_H_ #define _CB_MEDIA_IMAGE_WIDGET_H_ #include #include "CbMedia.h" #include "CbMediaDownloader.h" #define CB_TYPE_MEDIA_IMAGE_WIDGET cb_media_image_widget_get_type () G_DECLARE_FINAL_TYPE (CbMediaImageWidget, cb_media_image_widget, CB, MEDIA_IMAGE_WIDGET, GtkScrolledWindow); struct _CbMediaImageWidget { GtkScrolledWindow parent_instance; GtkWidget *image; GtkGesture *drag_gesture; cairo_surface_t *image_surface; double drag_start_hvalue; double drag_start_vvalue; CbMedia *media; gulong hires_progress_id; }; typedef struct _CbMediaImageWidget CbMediaImageWidget; GtkWidget * cb_media_image_widget_new (CbMedia *media, GdkRectangle *max_dimensions); #endif cawbird-1.4.2/src/CbMediaVideoWidget.c000066400000000000000000000265521416632607600175300ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbMediaVideoWidget.h" #include "CbGtkCompat.h" G_DEFINE_TYPE(CbMediaVideoWidget, cb_media_video_widget, GTK_TYPE_STACK) static void cb_media_video_widget_show_error (CbMediaVideoWidget *self, const char *error_message) { gtk_stack_set_visible_child (GTK_STACK (self), self->error_label); gtk_label_set_label (GTK_LABEL (self->error_label), error_message); } static void cb_media_video_widget_start_video (CbMediaVideoWidget *self) { #ifdef VIDEO g_assert (self->media_url != NULL); g_object_set (self->src, "uri", self->media_url, NULL); /* Will set to PLAYING once we get the ASYNC_DONE message */ gst_element_set_state (self->src, GST_STATE_PAUSED); #endif } static void cb_media_video_widget_stop_video (CbMediaVideoWidget *self) { #ifdef VIDEO gst_element_set_state (self->src, GST_STATE_NULL); #endif if (self->video_progress_id != 0) { g_source_remove (self->video_progress_id); self->video_progress_id = 0; } g_cancellable_cancel (self->cancellable); } static void soup_message_received_cb (SoupSession *session, SoupMessage *message, gpointer user_data) { CbMediaVideoWidget *self = user_data; GRegex *regex; GMatchInfo *match_info; char *match = NULL; if (message->status_code != SOUP_STATUS_OK) { if (message->status_code != SOUP_STATUS_CANCELLED) { char *msg = g_strdup_printf ("%u %s", message->status_code, soup_status_get_phrase (message->status_code)); cb_media_video_widget_show_error (self, msg); g_free (msg); } return; } regex = g_regex_new ("response_body->data, 0, &match_info); match = g_match_info_fetch (match_info, 1); g_debug ("Real url: %s", match); if (match == NULL) { cb_media_video_widget_show_error (self, "Error: Could not get real URL"); goto out; } else { self->media_url = match; cb_media_video_widget_start_video (self); } out: g_regex_unref (regex); } static void cancelled_cb (GCancellable *cancellable, gpointer user_data) { CbMediaVideoWidget *self = user_data; if (self->session != NULL && self->message != NULL) { soup_session_cancel_message (self->session, self->message, SOUP_STATUS_CANCELLED); } } static void fetch_real_url (CbMediaVideoWidget *self, const char *first_url) { self->session = soup_session_new (); self->message = soup_message_new ("GET", first_url); g_signal_connect (self->cancellable, "cancelled", G_CALLBACK (cancelled_cb), self); soup_session_queue_message (self->session, self->message, soup_message_received_cb, self); } static gboolean video_progress_timeout_cb (gpointer user_data) { CbMediaVideoWidget *self = user_data; gint64 duration_ns; gint64 position_ns; #ifdef VIDEO gst_element_query_duration (self->src, GST_FORMAT_TIME, &duration_ns); if (duration_ns > 0) { double fraction; gst_element_query_position (self->src, GST_FORMAT_TIME, &position_ns); fraction = (double)position_ns / (double)duration_ns; gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->video_progress), fraction); } #endif return G_SOURCE_CONTINUE; } #ifdef VIDEO static gboolean watch_cb (GstBus *bus, GstMessage *message, gpointer user_data) { CbMediaVideoWidget *self = user_data; switch (message->type) { case GST_MESSAGE_BUFFERING: { int percent; CbSurfaceProgress *sp = CB_SURFACE_PROGRESS (self->surface_progress); gst_message_parse_buffering (message, &percent); cb_surface_progress_set_progress (sp, MAX (cb_surface_progress_get_progress (sp), percent / 100.0)); g_debug ("Buffering: %d", percent); } break; case GST_MESSAGE_EOS: { /* Loop */ gst_element_seek (self->src, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE); } break; case GST_MESSAGE_ASYNC_DONE: { g_debug ("ASYNC DONE"); gtk_stack_set_visible_child_name (GTK_STACK (self), "video"); gst_element_set_state (self->src, GST_STATE_PLAYING); if (self->video_progress_id == 0) self->video_progress_id = g_timeout_add (50, video_progress_timeout_cb, self); } break; case GST_MESSAGE_ERROR: { GError *error; char *msg; gst_message_parse_error (message, &error, &msg); g_critical ("%s", msg); cb_media_video_widget_show_error (self, msg); g_free (msg); g_error_free (error); } break; default: {} } return TRUE; } #endif static void cb_media_video_widget_destroy (GtkWidget *widget) { CbMediaVideoWidget *self = CB_MEDIA_VIDEO_WIDGET (widget); cb_media_video_widget_stop_video (self); GTK_WIDGET_CLASS (cb_media_video_widget_parent_class)->destroy (widget); } static gboolean cb_media_video_widget_key_press_event (GtkWidget *widget, GdkEventKey *event) { CbMediaVideoWidget *self = CB_MEDIA_VIDEO_WIDGET (widget); cb_media_video_widget_stop_video (self); return GDK_EVENT_PROPAGATE; } static void cb_media_video_widget_finalize (GObject *object) { CbMediaVideoWidget *self = CB_MEDIA_VIDEO_WIDGET (object); g_object_unref (self->cancellable); if (self->session) g_object_unref (self->session); if (self->message) g_object_unref (self->message); g_free (self->media_url); G_OBJECT_CLASS (cb_media_video_widget_parent_class)->finalize (object); } static void cb_media_video_widget_init (CbMediaVideoWidget *self) { #ifdef VIDEO GstBus *bus; #endif GtkWidget *box; guint flags; self->error_label = gtk_label_new (""); gtk_label_set_line_wrap (GTK_LABEL (self->error_label), TRUE); gtk_label_set_selectable (GTK_LABEL (self->error_label), TRUE); gtk_widget_set_halign (self->error_label, GTK_ALIGN_CENTER); gtk_widget_set_valign (self->error_label, GTK_ALIGN_CENTER); gtk_widget_show (self->error_label); self->video_progress = gtk_progress_bar_new (); gtk_widget_show (self->video_progress); self->surface_progress = cb_surface_progress_new (); gtk_widget_show (self->surface_progress); gtk_container_add (GTK_CONTAINER (self), self->surface_progress); gtk_container_add (GTK_CONTAINER (self), self->error_label); self->cancellable = g_cancellable_new (); gtk_stack_set_visible_child (GTK_STACK (self), self->surface_progress); /* Init gstreamer stuff */ #ifdef VIDEO self->src = gst_element_factory_make ("playbin", "video"); self->sink = gst_element_factory_make ("gtksink", "gtksink"); if (self->sink == NULL) { cb_media_video_widget_show_error (self, "Could not create gtksink. Need gst-plugins-bad >= 1.6"); return; } g_object_get (self->sink, "widget", &self->area, NULL); gtk_widget_set_hexpand (self->area, TRUE); gtk_widget_set_vexpand (self->area, TRUE); /* We will switch to the "video" child later after getting an ASYNC_DONE message from gstreamer */ bus = gst_element_get_bus (self->src); gst_bus_add_watch (bus, watch_cb, self); g_object_set (self->src, "video-sink", self->sink, "ring-buffer-max-size", (10 * 1024 * 1024), NULL); g_object_get (self->src, "flags", &flags, NULL); g_object_set (self->src, "flags", flags | (1 << 7) /* DOWNLOAD */, NULL); #endif box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_container_add (GTK_CONTAINER (box), self->area); gtk_style_context_add_class (gtk_widget_get_style_context (self->video_progress), "embedded-progress"); gtk_container_add (GTK_CONTAINER (box), self->video_progress); gtk_stack_add_named (GTK_STACK (self), box, "video"); } static void cb_media_video_widget_class_init (CbMediaVideoWidgetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = cb_media_video_widget_finalize; widget_class->destroy = cb_media_video_widget_destroy; widget_class->key_press_event = cb_media_video_widget_key_press_event; } CbMediaVideoWidget * cb_media_video_widget_new (CbMedia *media, GdkRectangle *max_dimensions) { int h; int width; int height; double scale, scale_x = 1.0, scale_y = 1.0; CbMediaVideoWidget *self = CB_MEDIA_VIDEO_WIDGET (g_object_new (CB_TYPE_MEDIA_VIDEO_WIDGET, NULL)); g_return_val_if_fail (CB_IS_MEDIA (media), self); g_return_val_if_fail (media->surface != NULL, self); g_return_val_if_fail (media->url != NULL, self); cb_surface_progress_set_surface (CB_SURFACE_PROGRESS (self->surface_progress), media->surface); gtk_widget_measure (self->video_progress, GTK_ORIENTATION_VERTICAL, -1, &h, NULL, NULL, NULL); width = media->width; height = media->height + h; if (width > max_dimensions->width) scale_x = max_dimensions->width * 1.0 / width; if (height > max_dimensions->height) scale_y = max_dimensions->height * 1.0 / height; scale = MIN (scale_x, scale_y); gtk_widget_set_size_request (GTK_WIDGET (self), (int)(width * scale), (int)(height * scale)); switch (media->type) { case CB_MEDIA_TYPE_TWITTER_VIDEO: case CB_MEDIA_TYPE_INSTAGRAM_VIDEO: self->media_url = g_strdup (media->url); break; case CB_MEDIA_TYPE_ANIMATED_GIF: fetch_real_url (self, media->url); break; default: g_warn_if_reached (); } gtk_widget_set_tooltip_text (GTK_WIDGET (self), media->alt_text); atk_object_set_description(gtk_widget_get_accessible(GTK_WIDGET(self)), media->alt_text == NULL ? "" : media->alt_text); return self; } void cb_media_video_widget_start (CbMediaVideoWidget *self) { g_assert (gtk_widget_get_parent (GTK_WIDGET (self)) != NULL); /* We can only do this now, since the GtkDrawingArea from gstreamer needs to have a * parent set when the video starts, otherwise it will create its own GtkWindow. */ if (self->media_url != NULL) cb_media_video_widget_start_video (self); else g_debug ("Retrieving real URL first..."); } cawbird-1.4.2/src/CbMediaVideoWidget.h000066400000000000000000000033231416632607600175240ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_MEDIA_VIDEO_WIDGET_H_ #define _CB_MEDIA_VIDEO_WIDGET_H_ #ifdef VIDEO #include #endif #include #include #include "CbMedia.h" #include "CbSurfaceProgress.h" #define CB_TYPE_MEDIA_VIDEO_WIDGET cb_media_video_widget_get_type () G_DECLARE_FINAL_TYPE (CbMediaVideoWidget, cb_media_video_widget, CB, MEDIA_VIDEO_WIDGET, GtkStack); struct _CbMediaVideoWidget { GtkStack parent_instance; #ifdef VIDEO GstElement *src; GstElement *sink; #endif SoupSession *session; SoupMessage *message; GtkWidget *area; GtkWidget *surface_progress; GtkWidget *video_progress; GtkWidget *error_label; GCancellable *cancellable; guint video_progress_id; char *media_url; }; typedef struct _CbMediaVideoWidget CbMediaVideoWidget; CbMediaVideoWidget * cb_media_video_widget_new (CbMedia *media, GdkRectangle *max_dimensions); void cb_media_video_widget_start (CbMediaVideoWidget *self); #endif cawbird-1.4.2/src/CbMessageReceiver.c000066400000000000000000000026361416632607600174240ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbMessageReceiver.h" G_DEFINE_INTERFACE (CbMessageReceiver, cb_message_receiver, G_TYPE_OBJECT); static void cb_message_receiver_default_init (CbMessageReceiverInterface *self) { self->stream_message_received = NULL; } void cb_message_receiver_stream_message_received (CbMessageReceiver *self, CbStreamMessageType type, JsonNode *node) { CbMessageReceiverInterface *iface; g_return_if_fail (CB_IS_MESSAGE_RECEIVER (self)); iface = CB_MESSAGE_RECEIVER_GET_IFACE (self); return iface->stream_message_received (self, type, node); } cawbird-1.4.2/src/CbMessageReceiver.h000066400000000000000000000030631416632607600174240ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef __CB_MESSAGE_RECEIVER_H__ #define __CB_MESSAGE_RECEIVER_H__ #include #include #include "CbTypes.h" #define CB_TYPE_MESSAGE_RECEIVER (cb_message_receiver_get_type()) G_DECLARE_INTERFACE (CbMessageReceiver, cb_message_receiver, CB, MESSAGE_RECEIVER, GObject) struct _CbMessageReceiverInterface { GTypeInterface base_iface; void (*stream_message_received) (CbMessageReceiver *self, CbStreamMessageType type, JsonNode *node); }; void cb_message_receiver_stream_message_received (CbMessageReceiver *self, CbStreamMessageType type, JsonNode *node); #endif cawbird-1.4.2/src/CbSnippetManager.c000066400000000000000000000131701416632607600172630ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbSnippetManager.h" #include G_DEFINE_TYPE (CbSnippetManager, cb_snippet_manager, G_TYPE_OBJECT); static int load_snippet_cb (void *data, int n_cols, char **col_text, char **col_name) { CbSnippetManager *self = data; g_hash_table_insert (self->snippets, g_strdup (col_text[1]), g_strdup (col_text[2])); return 0; } static void cb_snippet_manager_load_snippets (CbSnippetManager *self) { char *err = NULL; g_assert (!self->inited); sqlite3_exec (self->db, "SELECT `id`, `key`, `value` FROM `snippets` ORDER BY `id`;", load_snippet_cb, self, &err); if (err != NULL) { g_warning ("Couldn't load snippets: %s", err); g_free (err); return; } } static void cb_snippet_manager_finalize (GObject *object) { CbSnippetManager *self = CB_SNIPPET_MANAGER (object); g_hash_table_destroy (self->snippets); G_OBJECT_CLASS (cb_snippet_manager_parent_class)->finalize (object); } static void cb_snippet_manager_class_init (CbSnippetManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_snippet_manager_finalize; } CbSnippetManager * cb_snippet_manager_new (sqlite3 *db) { CbSnippetManager *sm = CB_SNIPPET_MANAGER (g_object_new (CB_TYPE_SNIPPET_MANAGER, NULL)); sm->db = db; return sm; } static void cb_snippet_manager_init (CbSnippetManager *self) { self->inited = FALSE; self->snippets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); } guint cb_snippet_manager_n_snippets (CbSnippetManager *self) { if (!self->inited) cb_snippet_manager_load_snippets (self); return g_hash_table_size (self->snippets); } void cb_snippet_manager_remove_snippet (CbSnippetManager *self, const char *snippet_key) { sqlite3_stmt *stmt; if (!self->inited) cb_snippet_manager_load_snippets (self); g_hash_table_remove (self->snippets, snippet_key); sqlite3_prepare_v2 (self->db, "DELETE FROM `snippets` WHERE `key`=?;", -1, &stmt, NULL); sqlite3_bind_text (stmt, 1, snippet_key, -1, NULL); if (sqlite3_step (stmt) != SQLITE_DONE) { g_warning ("Couldn't remove snippet %s", snippet_key); } sqlite3_finalize (stmt); } void cb_snippet_manager_insert_snippet (CbSnippetManager *self, const char *key, const char *value) { sqlite3_stmt *stmt; if (!self->inited) cb_snippet_manager_load_snippets (self); g_hash_table_insert (self->snippets, g_strdup (key), g_strdup (value)); sqlite3_prepare_v2 (self->db, "INSERT INTO `snippets`(`key`, `value`) VALUES (?, ?);", -1, &stmt, NULL); sqlite3_bind_text (stmt, 1, key, -1, NULL); sqlite3_bind_text (stmt, 2, value, -1, NULL); if (sqlite3_step (stmt) != SQLITE_DONE) g_warning ("Couldn't insert snippet %s", key); sqlite3_finalize (stmt); } static gboolean has_snippet_n_predicate (gpointer key, gpointer value, gpointer user_data) { const struct { const char *k; gsize k_len; } *data = user_data; return strlen (key) == data->k_len && strncmp (data->k, key, data->k_len) == 0; } gboolean cb_snippet_manager_has_snippet_n (CbSnippetManager *self, const char *key, gsize key_length_in_bytes) { static struct { const char *k; gsize k_len; } data; data.k = key; data.k_len = key_length_in_bytes; if (!self->inited) cb_snippet_manager_load_snippets (self); return g_hash_table_find (self->snippets, has_snippet_n_predicate, &data) != NULL; } const char * cb_snippet_manager_get_snippet (CbSnippetManager *self, const char *key) { if (!self->inited) cb_snippet_manager_load_snippets (self); return g_hash_table_lookup (self->snippets, key); } void cb_snippet_manager_query_snippets (CbSnippetManager *self, GHFunc func, gpointer user_data) { if (!self->inited) cb_snippet_manager_load_snippets (self); g_hash_table_foreach (self->snippets, func, user_data); } void cb_snippet_manager_set_snippet (CbSnippetManager *self, const char *old_key, const char *key, const char *value) { if (!self->inited) cb_snippet_manager_load_snippets (self); if (!g_hash_table_contains (self->snippets, old_key)) { g_warning ("No snippet '%s' found in database", old_key); return; } cb_snippet_manager_remove_snippet (self, old_key); cb_snippet_manager_insert_snippet (self, key, value); } cawbird-1.4.2/src/CbSnippetManager.h000066400000000000000000000056031416632607600172720ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef CB_SNIPPET_MANAGER_H #define CB_SNIPPET_MANAGER_H #include #include #include struct _CbSnippetManager { GObject parent_instance; GHashTable *snippets; sqlite3 *db; guint inited : 1; }; typedef struct _CbSnippetManager CbSnippetManager; #define CB_TYPE_SNIPPET_MANAGER cb_snippet_manager_get_type() G_DECLARE_FINAL_TYPE (CbSnippetManager, cb_snippet_manager, CB, SNIPPET_MANAGER, GObject); /* * TODO: This is only a GObject because we can bind that properly in the vapi, * but a SnippetManager exists only once and for the entire lifetime of the GtkApplication * object we have... */ CbSnippetManager * cb_snippet_manager_new (sqlite3 *db); guint cb_snippet_manager_n_snippets (CbSnippetManager *self); void cb_snippet_manager_remove_snippet (CbSnippetManager *self, const char *snippet_key); void cb_snippet_manager_insert_snippet (CbSnippetManager *self, const char *key, const char *value); gboolean cb_snippet_manager_has_snippet_n (CbSnippetManager *self, const char *key, gsize key_length_in_bytes); const char * cb_snippet_manager_get_snippet (CbSnippetManager *self, const char *key); void cb_snippet_manager_query_snippets (CbSnippetManager *self, GHFunc func, gpointer user_data); void cb_snippet_manager_set_snippet (CbSnippetManager *self, const char *old_key, const char *key, const char *value); #endif cawbird-1.4.2/src/CbSurfaceProgress.c000066400000000000000000000120021416632607600174540ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbSurfaceProgress.h" G_DEFINE_TYPE(CbSurfaceProgress, cb_surface_progress, GTK_TYPE_WIDGET) static gboolean cb_surface_progress_draw (GtkWidget *widget, cairo_t *ct) { CbSurfaceProgress *self = CB_SURFACE_PROGRESS (widget); int width, height; cairo_surface_t *tmp_surface; cairo_t *ctx; double arc_size, cx, cy, radius; double scale; if (self->surface == NULL) return GDK_EVENT_PROPAGATE; width = gtk_widget_get_allocated_width (widget); height = gtk_widget_get_allocated_height (widget); scale = MIN ((double)width / (double)cairo_image_surface_get_width (self->surface), (double)height / (double)cairo_image_surface_get_height (self->surface)); tmp_surface = cairo_surface_create_similar (self->surface, CAIRO_CONTENT_COLOR_ALPHA, width, height); ctx = cairo_create (tmp_surface); /* Draw the surface slightly translucent on the widget's surface */ cairo_save (ct); cairo_rectangle (ct, 0, 0, width, height); cairo_scale (ct, scale, scale); cairo_set_source_surface (ct, self->surface, 0, 0); cairo_paint_with_alpha (ct, 0.5); cairo_restore (ct); /* Draw self->surface on tmp surface */ cairo_save (ctx); cairo_rectangle (ctx, 0, 0, width, height); cairo_scale (ctx, scale, scale); cairo_set_source_surface (ctx, self->surface, 0, 0); cairo_fill (ctx); cairo_restore (ctx); arc_size = MIN (width, height) * 2.0; cx = width / 2.0; cy = height / 2.0; radius = (arc_size / 2.0) - 0.5; cairo_set_operator (ctx, CAIRO_OPERATOR_DEST_IN); cairo_set_source_rgba (ctx, 1.0, 0.0, 0.0, 1.0); cairo_move_to (ctx, width / 2.0, height / 2.0); cairo_arc (ctx, cx, cy, radius, 0, 0); cairo_arc (ctx, cx, cy, radius, 0, 2 * G_PI * self->progress); cairo_move_to (ctx, cx, cy); cairo_fill (ctx); /* Draw the tmp surface */ cairo_rectangle (ct, 0, 0, width, height); cairo_set_source_surface (ct, tmp_surface, 0, 0); cairo_fill (ct); cairo_surface_destroy (tmp_surface); return GDK_EVENT_PROPAGATE; } static void cb_surface_progress_get_preferred_width (GtkWidget *widget, int *minimum, int *natural) { *minimum = *natural = 1; } static void cb_surface_progress_get_preferred_height (GtkWidget *widget, int *minimum, int *natural) { *minimum = *natural = 1; } static void cb_surface_progress_finalize (GObject *object) { CbSurfaceProgress *self = CB_SURFACE_PROGRESS (object); if (self->surface) cairo_surface_destroy (self->surface); G_OBJECT_CLASS (cb_surface_progress_parent_class)->finalize (object); } static void cb_surface_progress_init (CbSurfaceProgress *self) { gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); } static void cb_surface_progress_class_init (CbSurfaceProgressClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->finalize = cb_surface_progress_finalize; widget_class->draw = cb_surface_progress_draw; widget_class->get_preferred_width = cb_surface_progress_get_preferred_width; widget_class->get_preferred_height = cb_surface_progress_get_preferred_height; } GtkWidget * cb_surface_progress_new (void) { return GTK_WIDGET (g_object_new (CB_TYPE_SURFACE_PROGRESS, NULL)); } double cb_surface_progress_get_progress (CbSurfaceProgress *self) { return self->progress; } void cb_surface_progress_set_progress (CbSurfaceProgress *self, double progress) { if (progress >= 1.0) self->progress = 1.0; else if (progress < 0.0) self->progress = 0.0; else self->progress = progress; gtk_widget_queue_draw (GTK_WIDGET (self)); } cairo_surface_t * cb_surface_progress_get_surface (CbSurfaceProgress *self) { return self->surface; } void cb_surface_progress_set_surface (CbSurfaceProgress *self, cairo_surface_t *surface) { if (self->surface) cairo_surface_destroy (self->surface); self->surface = cairo_surface_reference (surface); gtk_widget_queue_resize (GTK_WIDGET (self)); } cawbird-1.4.2/src/CbSurfaceProgress.h000066400000000000000000000032701416632607600174700ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_SURFACE_PROGRESS_H_ #define _CB_SURFACE_PROGRESS_H_ #include #define CB_TYPE_SURFACE_PROGRESS cb_surface_progress_get_type () G_DECLARE_FINAL_TYPE (CbSurfaceProgress, cb_surface_progress, CB, SURFACE_PROGRESS, GtkWidget); struct _CbSurfaceProgress { GtkWidget parent_instance; cairo_surface_t *surface; double progress; }; typedef struct _CbSurfaceProgress CbSurfaceProgress; GtkWidget * cb_surface_progress_new (void); double cb_surface_progress_get_progress (CbSurfaceProgress *self); void cb_surface_progress_set_progress (CbSurfaceProgress *self, double progress); cairo_surface_t * cb_surface_progress_get_surface (CbSurfaceProgress *self); void cb_surface_progress_set_surface (CbSurfaceProgress *self, cairo_surface_t *surface); #endif cawbird-1.4.2/src/CbTextTransform.c000066400000000000000000000245361416632607600171760ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbTextTransform.h" #include "CbMediaDownloader.h" #include "CbTypes.h" #include "CbUtils.h" #include #include char * cb_text_transform_tweet (const CbMiniTweet *tweet, guint flags, guint64 quote_id) { return cb_text_transform_text (tweet->text, tweet->entities, tweet->n_entities, flags, tweet->n_medias, quote_id, tweet->display_range_start); } const int TRAILING = 1 << 0; static inline gboolean is_hashtag (const char *s) { return s[0] == '#'; } static inline gboolean is_link (const char *s) { return s != NULL && (g_str_has_prefix (s, "http://") || g_str_has_prefix (s, "https://")); } static inline gboolean is_quote_link (const CbTextEntity *e, gint64 quote_id) { char *suffix = g_strdup_printf ("/status/%" G_GINT64_FORMAT, quote_id); gboolean ql; ql = (e->target != NULL) && (g_str_has_prefix (e->target, "https://twitter.com/") && g_str_has_suffix (e->target, suffix)); g_free (suffix); return ql; } static inline gboolean is_media_url (const char *url, const char *display_text, gsize media_count) { return (is_twitter_media_candidate (url != NULL ? url : display_text) && media_count == 1) || g_str_has_prefix (display_text, "pic.twitter.com/"); } static inline gboolean is_whitespace (const char *s) { while (*s != '\0') { if (!isspace (*s)) return FALSE; s++; } return TRUE; } char * cb_text_transform_fix_encoding (const char *text) { GString *fixed_string; gunichar cur_char; const gchar *str; gchar *valid_string; guint valid_start = 0; guint cur_pos = 0; guint entity_pos = 0; gboolean in_entity = FALSE; str = text; fixed_string = g_string_new (NULL); // Apparently this is a C-ish pattern for walking the string while(*str) { cur_char = g_utf8_get_char (str); if (in_entity) { if ( (cur_char >= '0' && cur_char <= '9') || (cur_char >= 'A' && cur_char <= 'Z') || (cur_char >= 'a' && cur_char <= 'z') || (cur_char == '#' && entity_pos == cur_pos - 1)) { // Continue - but don't "continue;" because we need to do the stuff at the end of the loop } else if (cur_char == ';') { // Assume the entity was valid. // Or we had REALLY bad luck and found an old tweet where someone used an ampersand. no space, some alphanumeric text AND a semicolon! in_entity = FALSE; } else { // Entity was invalid if (valid_start < entity_pos) { // There's valid substring text to add valid_string = g_utf8_substring(text, valid_start, entity_pos); g_string_append(fixed_string, valid_string); g_free(valid_string); } g_string_append(fixed_string, "&"); valid_start = entity_pos + 1; if (cur_char == '&') { // If it's a consecutive & then we're in a new entity already! in_entity = TRUE; entity_pos = cur_pos; } else { in_entity = FALSE; } } } else if (cur_char == '&') { entity_pos = cur_pos; in_entity = TRUE; } // Else all is good, just keep going str = g_utf8_next_char(str); cur_pos += 1; } if (in_entity) { // Handle a trailing entity // TODO: Avoid code copy-and-paste from the WHILE loop if (valid_start < entity_pos) { valid_string = g_utf8_substring(text, valid_start, entity_pos); g_string_append(fixed_string, valid_string); g_free(valid_string); } g_string_append(fixed_string, "&"); valid_start = entity_pos + 1; } // Add any trailing text if (valid_start < cur_pos) { valid_string = g_utf8_substring(text, valid_start, cur_pos); g_string_append(fixed_string, valid_string); g_free(valid_string); } return g_string_free(fixed_string, FALSE); } char * cb_text_transform_text (const char *text, CbTextEntity *entities, gsize n_entities, guint flags, gsize n_medias, gint64 quote_id, guint display_range_start) { GString *str; const guint text_len = g_utf8_strlen (text, -1); int i; char *end_str; char *encoded_before; gboolean last_entity_was_trailing = FALSE; guint last_end = 0; guint cur_end = text_len; if (text_len == 0) return g_strdup (text); str = g_string_new (NULL); for (i = (int)n_entities - 1; i >= 0; i --) { char *btw; guint entity_to; gsize btw_length = cur_end - entities[i].to; if (entities[i].to <= display_range_start) continue; entity_to = entities[i].to - display_range_start; btw = g_utf8_substring (text, entity_to, cur_end); if (!is_whitespace (btw) && btw_length > 0) { g_free (btw); break; } else cur_end = entity_to; if (entities[i].to == cur_end && (is_hashtag (entities[i].display_text) || is_link (entities[i].target))) { entities[i].info |= TRAILING; cur_end = entities[i].from - display_range_start; } else { g_free (btw); break; } g_free (btw); } for (i = 0; i < (int)n_entities; i ++) { CbTextEntity *entity = &entities[i]; char *before; char *entity_text; guint entity_to; guint entity_from; gboolean entity_exists = FALSE; if (entity->to <= display_range_start) continue; entity_to = entity->to - display_range_start; entity_from = entity->from - display_range_start; entity_text = g_utf8_substring(text, entity_from, entity_to); // If the entity text doesn't match the text between the indices (ignoring case, because sometimes Twitter has @ibboard // in the text and @IBBoard in the entity) then something went wrong with our data! // We ignore the case of ASCII characters because a) g_strcasecmp is deprecated and recommends the ASCII functions // and b) case changing has mainly been seen with usernames, which are only ASCII anyway if (is_hashtag (entity->display_text)) { // We build hashtags with "#" but the text might use the FULLWIDTH NUMBER SIGN // so we have to ignore it in the comparison char *original_text_no_hash = g_utf8_offset_to_pointer (entity->original_text, 1); char *entity_text_no_hash = g_utf8_offset_to_pointer (entity_text, 1); entity_exists = g_ascii_strcasecmp(original_text_no_hash, entity_text_no_hash) == 0; } else { entity_exists = g_ascii_strcasecmp(entity->original_text, entity_text) == 0; } if (!entity_exists) { g_info("Skipping entity - expected %s but found %s. Likely bad indices (%u to %u)", entity->original_text, entity_text, entity->from, entity->to); g_free(entity_text); continue; } g_free(entity_text); before = g_utf8_substring (text, last_end, entity_from); if (!(last_entity_was_trailing && is_whitespace (before))) { encoded_before = cb_text_transform_fix_encoding (before); g_string_append (str, encoded_before); g_free (encoded_before); } g_free (before); if ((flags & CB_TEXT_TRANSFORM_REMOVE_TRAILING_HASHTAGS) > 0 && (entity->info & TRAILING) > 0 && is_hashtag (entity->display_text)) { last_end = entity_to; last_entity_was_trailing = TRUE; continue; } last_entity_was_trailing = FALSE; if (((flags & CB_TEXT_TRANSFORM_REMOVE_MEDIA_LINKS) > 0 && is_media_url (entity->target, entity->display_text, n_medias)) || (quote_id != 0 && is_quote_link (entity, quote_id))) { last_end = entity_to; continue; } if ((flags & CB_TEXT_TRANSFORM_EXPAND_LINKS) > 0) { if (entity->display_text[0] == '@') g_string_append (str, entity->display_text); else g_string_append (str, entity->target ? entity->target : entity->display_text); } else { g_string_append (str, "target ? entity->target : entity->display_text); g_string_append (str, "\""); if (entity->tooltip_text != NULL) { char *title_tmp = cb_utils_escape_ampersands (entity->tooltip_text); char *title_escaped = cb_utils_escape_quotes (title_tmp); g_string_append (str, " title=\""); g_string_append (str, title_escaped); g_string_append (str, "\""); g_free (title_escaped); g_free (title_tmp); } g_string_append (str, ">"); g_string_append (str, entity->display_text); g_string_append (str,""); } last_end = entity_to; } end_str = g_utf8_substring (text, last_end, text_len); encoded_before = cb_text_transform_fix_encoding (end_str); g_string_append (str, encoded_before); g_free (encoded_before); g_free (end_str); return g_strchomp(g_string_free (str, FALSE)); } cawbird-1.4.2/src/CbTextTransform.h000066400000000000000000000032661416632607600172000ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef __TEXT_TRANSFORM_H #define __TEXT_TRANSFORM_H #include #include "CbTypes.h" typedef enum { CB_TEXT_TRANSFORM_REMOVE_MEDIA_LINKS = 1 << 0, CB_TEXT_TRANSFORM_REMOVE_TRAILING_HASHTAGS = 1 << 1, CB_TEXT_TRANSFORM_EXPAND_LINKS = 1 << 2 } CbTransformFlags; char *cb_text_transform_tweet (const CbMiniTweet *tweet, guint flags, guint64 quote_id); char *cb_text_transform_text (const char *text, CbTextEntity *entities, gsize n_entities, guint flags, gsize n_medias, gint64 quote_id, guint display_range_start); char *cb_text_transform_fix_encoding (const char *text); #endif cawbird-1.4.2/src/CbTweet.c000066400000000000000000000367251416632607600154510ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbTweet.h" #include "CbTextTransform.h" #include /* TODO: We might want to put this into a utils.c later */ static gboolean usable_json_value (JsonObject *object, const char *name) { if (!json_object_has_member (object, name)) return FALSE; return !json_object_get_null_member (object, name); } G_DEFINE_TYPE (CbTweet, cb_tweet, G_TYPE_OBJECT); enum { STATE_CHANGED, QUOTE_STATE_CHANGED, LAST_SIGNAL }; static guint tweet_signals[LAST_SIGNAL] = { 0 }; gboolean cb_tweet_is_hidden (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), TRUE); return (tweet->state & (CB_TWEET_STATE_HIDDEN_FORCE | CB_TWEET_STATE_HIDDEN_UNFOLLOWED | CB_TWEET_STATE_HIDDEN_FILTERED | CB_TWEET_STATE_HIDDEN_RTS_DISABLED | CB_TWEET_STATE_HIDDEN_RT_BY_USER | CB_TWEET_STATE_HIDDEN_RT_BY_FOLLOWEE | CB_TWEET_STATE_HIDDEN_AUTHOR_BLOCKED | CB_TWEET_STATE_HIDDEN_RETWEETER_BLOCKED | CB_TWEET_STATE_HIDDEN_AUTHOR_MUTED | CB_TWEET_STATE_HIDDEN_RETWEETER_MUTED)) > 0; } gboolean cb_tweet_has_inline_media (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE); if (tweet->retweeted_tweet != NULL) return tweet->retweeted_tweet->n_medias > 0; return tweet->source_tweet.n_medias > 0; } gboolean cb_tweet_has_quoted_inline_media (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE); return tweet->quoted_tweet != NULL && tweet->quoted_tweet->n_medias > 0; } /* TODO: Replace these 3 functinos with one that returns a pointer to a CbUserIdentity? */ gint64 cb_tweet_get_user_id (CbTweet *tweet) { if (tweet->retweeted_tweet != NULL) return tweet->retweeted_tweet->author.id; return tweet->source_tweet.author.id; } const char * cb_tweet_get_screen_name (CbTweet *tweet) { if (tweet->retweeted_tweet != NULL) return tweet->retweeted_tweet->author.screen_name; return tweet->source_tweet.author.screen_name; } const char * cb_tweet_get_user_name (CbTweet *tweet) { if (tweet->retweeted_tweet != NULL) return tweet->retweeted_tweet->author.user_name; return tweet->source_tweet.author.user_name; } const char * cb_tweet_get_language (CbTweet *tweet) { if (tweet->retweeted_tweet != NULL) return tweet->retweeted_tweet->language; return tweet->source_tweet.language; } CbMedia ** cb_tweet_get_medias (CbTweet *tweet, int *n_medias) { g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); g_return_val_if_fail (n_medias != NULL, NULL); if (tweet->retweeted_tweet != NULL) { *n_medias = tweet->retweeted_tweet->n_medias; return tweet->retweeted_tweet->medias; } else { *n_medias = tweet->source_tweet.n_medias; return tweet->source_tweet.medias; } } CbMedia ** cb_tweet_get_quoted_medias (CbTweet *tweet, int *n_medias) { g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); g_return_val_if_fail (tweet->quoted_tweet != NULL, NULL); *n_medias = tweet->quoted_tweet->n_medias; return tweet->quoted_tweet->medias; } char ** cb_tweet_get_mentions (CbTweet *tweet, int *n_mentions) { CbTextEntity *entities; gsize n_entities; gsize i, x; char **mentions; g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); g_return_val_if_fail (n_mentions != NULL, NULL); if (tweet->retweeted_tweet != NULL) { entities = tweet->retweeted_tweet->entities; n_entities = tweet->retweeted_tweet->n_entities; } else { entities = tweet->source_tweet.entities; n_entities = tweet->source_tweet.n_entities; } *n_mentions = 0; for (i = 0; i < n_entities; i ++) if (entities[i].display_text[0] == '@') (*n_mentions) ++; if (*n_mentions == 0) return NULL; mentions = g_malloc (sizeof(char*) * (*n_mentions)); x = 0; for (i = 0; i < n_entities; i ++) if (entities[i].display_text[0] == '@') { mentions[x] = g_strdup (&entities[i].display_text[1]); x ++; } return mentions; } void cb_tweet_load_from_json (CbTweet *tweet, JsonNode *status_node, gint64 account_id, GDateTime *now) { JsonObject *status; JsonObject *user; g_return_if_fail (CB_IS_TWEET (tweet)); g_return_if_fail (status_node != NULL); g_return_if_fail (now != NULL); status = json_node_get_object (status_node); user = json_object_get_object_member (status, "user"); tweet->id = json_object_get_int_member (status, "id"); tweet->retweet_count = (guint) json_object_get_int_member (status, "retweet_count"); tweet->favorite_count = (guint) json_object_get_int_member (status, "favorite_count"); cb_mini_tweet_parse (&tweet->source_tweet, status); if (json_object_has_member (status, "retweeted_status")) { JsonObject *rt = json_object_get_object_member (status, "retweeted_status"); JsonObject *rt_user = json_object_get_object_member (rt, "user"); tweet->retweeted_tweet = g_malloc (sizeof(CbMiniTweet)); cb_mini_tweet_init (tweet->retweeted_tweet); cb_mini_tweet_parse (tweet->retweeted_tweet, rt); cb_mini_tweet_parse_entities (tweet->retweeted_tweet, rt); tweet->avatar_url = g_strdup (json_object_get_string_member (rt_user, "profile_image_url_https")); if (json_object_get_boolean_member (rt_user, "protected")) tweet->state |= CB_TWEET_STATE_PROTECTED; if (json_object_get_boolean_member (rt_user, "verified")) tweet->state |= CB_TWEET_STATE_VERIFIED; if (usable_json_value (rt, "possibly_sensitive") && json_object_get_boolean_member (rt, "possibly_sensitive")) tweet->state |= CB_TWEET_STATE_NSFW; } else { cb_mini_tweet_parse_entities (&tweet->source_tweet, status); tweet->avatar_url = g_strdup (json_object_get_string_member (user, "profile_image_url_https")); if (json_object_get_boolean_member (user, "protected")) tweet->state |= CB_TWEET_STATE_PROTECTED; if (json_object_get_boolean_member (user, "verified")) tweet->state |= CB_TWEET_STATE_VERIFIED; if (usable_json_value (status, "possibly_sensitive") && json_object_get_boolean_member (status, "possibly_sensitive")) tweet->state |= CB_TWEET_STATE_NSFW; } if (json_object_has_member (status, "quoted_status")) { JsonObject *quote = json_object_get_object_member (status, "quoted_status"); tweet->quoted_tweet = g_malloc (sizeof (CbMiniTweet)); cb_mini_tweet_init (tweet->quoted_tweet); cb_mini_tweet_parse (tweet->quoted_tweet, quote); cb_mini_tweet_parse_entities (tweet->quoted_tweet, quote); if (usable_json_value (quote, "possibly_sensitive") && json_object_get_boolean_member (quote, "possibly_sensitive")) tweet->quote_state |= CB_TWEET_STATE_NSFW; } else if (tweet->retweeted_tweet != NULL && json_object_has_member (json_object_get_object_member (status, "retweeted_status"), "quoted_status")) { JsonObject *quote = json_object_get_object_member (json_object_get_object_member (status, "retweeted_status"), "quoted_status"); tweet->quoted_tweet = g_malloc (sizeof (CbMiniTweet)); cb_mini_tweet_init (tweet->quoted_tweet); cb_mini_tweet_parse (tweet->quoted_tweet, quote); cb_mini_tweet_parse_entities (tweet->quoted_tweet, quote); if (usable_json_value (quote, "possibly_sensitive") && json_object_get_boolean_member (quote, "possibly_sensitive")) tweet->quote_state |= CB_TWEET_STATE_NSFW; } if (json_object_get_boolean_member (status, "favorited")) tweet->state |= CB_TWEET_STATE_FAVORITED; if (json_object_has_member (status, "current_user_retweet")) { JsonObject *cur_rt = json_object_get_object_member (status, "current_user_retweet"); tweet->my_retweet = json_object_get_int_member (cur_rt, "id"); tweet->state |= CB_TWEET_STATE_RETWEETED; } else if (json_object_get_boolean_member (status, "retweeted") || (tweet->retweeted_tweet != NULL && tweet->source_tweet.author.id == account_id)) { /* The 'retweeted' flag is not reliable so we additionally check if the tweet is authored by the authenticating user */ tweet->my_retweet = tweet->id; tweet->state |= CB_TWEET_STATE_RETWEETED; } #ifdef DEBUG { JsonGenerator *generator = json_generator_new (); json_generator_set_root (generator, status_node); json_generator_set_pretty (generator, TRUE); tweet->json_data = json_generator_to_data (generator, NULL); g_object_unref (generator); } #endif } gboolean cb_tweet_is_reply (CbTweet *tweet) { return (tweet->retweeted_tweet != NULL && tweet->retweeted_tweet->reply_id != 0) || (tweet->retweeted_tweet == NULL && tweet->source_tweet.reply_id != 0); } gboolean cb_tweet_is_flag_set (CbTweet *tweet, guint flag) { return (tweet->state & flag) > 0; } void cb_tweet_set_flag (CbTweet *tweet, guint flag) { guint prev_state; g_return_if_fail (CB_IS_TWEET (tweet)); prev_state = tweet->state; tweet->state |= flag; if (tweet->state != prev_state) g_signal_emit (tweet, tweet_signals[STATE_CHANGED], 0); } void cb_tweet_unset_flag (CbTweet *tweet, guint flag) { guint prev_state; g_return_if_fail (CB_IS_TWEET (tweet)); prev_state = tweet->state; tweet->state &= ~flag; if (tweet->state != prev_state) g_signal_emit (tweet, tweet_signals[STATE_CHANGED], 0); } gboolean cb_tweet_is_quoted_flag_set (CbTweet *tweet, guint flag) { return (tweet->quote_state & flag) > 0; } void cb_tweet_set_quoted_flag (CbTweet *tweet, guint flag) { guint prev_state; g_return_if_fail (CB_IS_TWEET (tweet)); prev_state = tweet->quote_state; tweet->quote_state |= flag; if (tweet->quote_state != prev_state) g_signal_emit (tweet, tweet_signals[QUOTE_STATE_CHANGED], 0); } void cb_tweet_unset_quoted_flag (CbTweet *tweet, guint flag) { guint prev_state; g_return_if_fail (CB_IS_TWEET (tweet)); prev_state = tweet->quote_state; tweet->quote_state &= ~flag; if (tweet->quote_state != prev_state) g_signal_emit (tweet, tweet_signals[QUOTE_STATE_CHANGED], 0); } char * cb_tweet_get_formatted_text (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); if (tweet->retweeted_tweet != NULL) return cb_text_transform_tweet (tweet->retweeted_tweet, 0, 0); else return cb_text_transform_tweet (&tweet->source_tweet, 0, 0); } char * cb_tweet_get_trimmed_text (CbTweet *tweet, guint transform_flags) { gint64 quote_id; g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); quote_id = tweet->quoted_tweet != NULL ? tweet->quoted_tweet->id : 0; if (tweet->retweeted_tweet != NULL) return cb_text_transform_tweet (tweet->retweeted_tweet, transform_flags, quote_id); else return cb_text_transform_tweet (&tweet->source_tweet, transform_flags, quote_id); } char * cb_tweet_get_real_text (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); if (tweet->retweeted_tweet != NULL) return cb_text_transform_tweet (tweet->retweeted_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0); else return cb_text_transform_tweet (&tweet->source_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0); } char * cb_tweet_get_filter_text (CbTweet *tweet) { GString *string; char *text; g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); string = g_string_new (0); if (tweet->retweeted_tweet != NULL) text = cb_text_transform_tweet (tweet->retweeted_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS, 0); else text = cb_text_transform_tweet (&tweet->source_tweet, CB_TEXT_TRANSFORM_EXPAND_LINKS ,0); g_string_append (string, text); g_free (text); int n_mentions; char ** mentions = cb_tweet_get_mentions(tweet, &n_mentions); for (int i = 0; i < n_mentions; i++) { g_string_append (string, " @"); g_string_append (string, mentions[i]); } g_free (mentions); g_string_append (string, " ["); if (tweet->retweeted_tweet != NULL) g_string_append (string, "rt"); if (tweet->quoted_tweet != NULL) g_string_append (string, ",quote"); g_string_append_c (string, ']'); return g_string_free (string, FALSE); } gboolean cb_tweet_get_seen (CbTweet *tweet) { g_return_val_if_fail (CB_IS_TWEET (tweet), FALSE); return tweet->seen; } void cb_tweet_set_seen (CbTweet *tweet, gboolean value) { g_return_if_fail (CB_IS_TWEET (tweet)); value = !!value; if (value && !tweet->seen && tweet->notification_id != NULL) { GApplication *app = g_application_get_default (); g_application_withdraw_notification (app, tweet->notification_id); tweet->notification_id = NULL; } tweet->seen = value; } CbUserIdentity * cb_tweet_get_reply_users (CbTweet *tweet, guint *n_reply_users) { g_return_val_if_fail (CB_IS_TWEET (tweet), NULL); g_return_val_if_fail (n_reply_users != NULL, NULL); if (tweet->retweeted_tweet != NULL) { *n_reply_users = tweet->retweeted_tweet->n_reply_users; return tweet->retweeted_tweet->reply_users; } else { *n_reply_users = tweet->source_tweet.n_reply_users; return tweet->source_tweet.reply_users; } *n_reply_users = 0; return NULL; /* shrug */ } CbTweet * cb_tweet_new (void) { return (CbTweet *)g_object_new (CB_TYPE_TWEET, NULL); } static void cb_tweet_finalize (GObject *object) { CbTweet *tweet = (CbTweet *)object; g_free (tweet->avatar_url); g_free (tweet->notification_id); cb_mini_tweet_free (&tweet->source_tweet); if (tweet->retweeted_tweet != NULL) { cb_mini_tweet_free (tweet->retweeted_tweet); g_free (tweet->retweeted_tweet); } if (tweet->quoted_tweet != NULL) { cb_mini_tweet_free (tweet->quoted_tweet); g_free (tweet->quoted_tweet); } #ifdef DEBUG g_free (tweet->json_data); #endif G_OBJECT_CLASS (cb_tweet_parent_class)->finalize (object); } static void cb_tweet_init (CbTweet *tweet) { tweet->state = 0; tweet->quoted_tweet = NULL; tweet->retweeted_tweet = NULL; tweet->notification_id = NULL; tweet->seen = TRUE; } static void cb_tweet_class_init (CbTweetClass *class) { GObjectClass *gobject_class = (GObjectClass *)class; gobject_class->finalize = cb_tweet_finalize; tweet_signals[STATE_CHANGED] = g_signal_new ("state-changed", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } cawbird-1.4.2/src/CbTweet.h000066400000000000000000000124151416632607600154440ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_TWEET_H_ #define _CB_TWEET_H_ #include #include #include "CbTypes.h" #include "CbMedia.h" #define CB_TWEET_MAX_LENGTH 280 typedef enum { /** Force hiding (there's no way this flag will ever get flipped...)*/ CB_TWEET_STATE_HIDDEN_FORCE = 1 << 0, /** Hidden because we unfollowed the author */ CB_TWEET_STATE_HIDDEN_UNFOLLOWED = 1 << 1, /** Hidden because one of the filters matched the tweet */ CB_TWEET_STATE_HIDDEN_FILTERED = 1 << 2, CB_TWEET_STATE_HIDDEN_RTS_DISABLED = 1 << 3, /** Hidden because it's a RT by the authenticating user */ CB_TWEET_STATE_HIDDEN_RT_BY_USER = 1 << 4, CB_TWEET_STATE_HIDDEN_RT_BY_FOLLOWEE = 1 << 5, /** Hidden because the author is blocked */ CB_TWEET_STATE_HIDDEN_AUTHOR_BLOCKED = 1 << 6, /** Hidden because the author of a retweet is blocked */ CB_TWEET_STATE_HIDDEN_RETWEETER_BLOCKED = 1 << 7, /** Hidden because the author was muted */ CB_TWEET_STATE_HIDDEN_AUTHOR_MUTED = 1 << 8, /** Hidden because the author of a retweet is muted */ CB_TWEET_STATE_HIDDEN_RETWEETER_MUTED = 1 << 9, /* The authenticating user retweeted this tweet */ CB_TWEET_STATE_RETWEETED = 1 << 10, /* The authenticating user favorited this tweet */ CB_TWEET_STATE_FAVORITED = 1 << 11, /* This tweet has been deleted by its author */ CB_TWEET_STATE_DELETED = 1 << 12, /* The author of this tweet is verified */ CB_TWEET_STATE_VERIFIED = 1 << 13, /* The author of this tweet is protected */ CB_TWEET_STATE_PROTECTED = 1 << 14, /* At least one media attached to this tweet is marked sensitive */ CB_TWEET_STATE_NSFW = 1 << 15 } CbTweetState; struct _CbTweet { GObject parent_instance; guint state : 16; guint quote_state : 16; gint64 id; CbMiniTweet source_tweet; CbMiniTweet *retweeted_tweet; CbMiniTweet *quoted_tweet; char *avatar_url; gint64 my_retweet; char *notification_id; guint seen : 1; guint retweet_count; guint favorite_count; #ifdef DEBUG /* In debug mode, we save the entire json we got from Twitter so we can later look at it */ char *json_data; #endif }; typedef struct _CbTweet CbTweet; #define CB_TYPE_TWEET cb_tweet_get_type () G_DECLARE_FINAL_TYPE (CbTweet, cb_tweet, CB, TWEET, GObject); CbTweet * cb_tweet_new (void); gboolean cb_tweet_is_hidden (CbTweet *tweet); gboolean cb_tweet_is_reply (CbTweet *tweet); CbUserIdentity * cb_tweet_get_reply_users (CbTweet *tweet, guint *n_reply_users); CbMedia ** cb_tweet_get_medias (CbTweet *tweet, int *n_medias); CbMedia ** cb_tweet_get_quoted_medias(CbTweet *tweet, int *n_medias); char ** cb_tweet_get_mentions (CbTweet *tweet, int *n_mentions); gboolean cb_tweet_has_inline_media (CbTweet *tweet); gint64 cb_tweet_get_user_id (CbTweet *tweet); const char * cb_tweet_get_screen_name (CbTweet *tweet); const char * cb_tweet_get_user_name (CbTweet *tweet); const char * cb_tweet_get_language (CbTweet *tweet); void cb_tweet_load_from_json (CbTweet *tweet, JsonNode *status_node, gint64 account_id, GDateTime *now); /* Flag stuff */ gboolean cb_tweet_is_flag_set (CbTweet *tweet, guint flag); void cb_tweet_set_flag (CbTweet *tweet, guint flag); void cb_tweet_unset_flag (CbTweet *tweet, guint flag); gboolean cb_tweet_is_quoted_flag_set (CbTweet *tweet, guint flag); void cb_tweet_set_quoted_flag (CbTweet *tweet, guint flag); void cb_tweet_unset_quoted_flag (CbTweet *tweet, guint flag); char * cb_tweet_get_formatted_text (CbTweet *tweet); char * cb_tweet_get_trimmed_text (CbTweet *tweet, guint transform_flags); char * cb_tweet_get_real_text (CbTweet *tweet); char * cb_tweet_get_filter_text (CbTweet *tweet); gboolean cb_tweet_get_seen (CbTweet *tweet); void cb_tweet_set_seen (CbTweet *tweet, gboolean value); #endif cawbird-1.4.2/src/CbTweetModel.c000066400000000000000000000451431416632607600164240ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbTweetModel.h" static void cb_tweet_model_iface_init (GListModelInterface *iface); G_DEFINE_TYPE_WITH_CODE (CbTweetModel, cb_tweet_model, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, cb_tweet_model_iface_init)); static GType cb_tweet_model_get_item_type (GListModel *model) { return CB_TYPE_TWEET; } static guint cb_tweet_model_get_n_items (GListModel *model) { CbTweetModel *self = CB_TWEET_MODEL (model); return self->tweets->len; } static gpointer cb_tweet_model_get_item (GListModel *model, guint index) { CbTweetModel *self = CB_TWEET_MODEL (model); CbTweet *tweet; g_assert (index < self->tweets->len); tweet = g_ptr_array_index (self->tweets, index); return g_object_ref (tweet); } static void cb_tweet_model_iface_init (GListModelInterface *iface) { iface->get_item_type = cb_tweet_model_get_item_type; iface->get_n_items = cb_tweet_model_get_n_items; iface->get_item = cb_tweet_model_get_item; } static inline void emit_items_changed (CbTweetModel *self, guint position, guint removed, guint added) { g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); } static void cb_tweet_model_init (CbTweetModel *self) { self->tweets = g_ptr_array_new_with_free_func (g_object_unref); self->hidden_tweets = g_ptr_array_new_with_free_func (g_object_unref); self->thread_mode = FALSE; self->min_id = G_MAXINT64; self->max_id = G_MININT64; } static void cb_tweet_model_finalize (GObject *object) { CbTweetModel *self = CB_TWEET_MODEL (object); g_ptr_array_unref (self->tweets); g_ptr_array_unref (self->hidden_tweets); G_OBJECT_CLASS (cb_tweet_model_parent_class)->finalize (object); } static void cb_tweet_model_class_init (CbTweetModelClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_tweet_model_finalize; } CbTweetModel * cb_tweet_model_new (void) { return CB_TWEET_MODEL (g_object_new (CB_TYPE_TWEET_MODEL, NULL)); } static inline void update_min_max_id (CbTweetModel *self, gint64 old_id) { int i; #ifdef DEBUG /* This should be called *after* the * tweet has been removed from self->tweets! */ for (i = 0; i < self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); g_assert (t->id != old_id); } #endif if (old_id == self->max_id) { if (self->tweets->len > 0) { CbTweet *t = g_ptr_array_index (self->tweets, self->thread_mode ? self->tweets->len - 1 : 0); self->max_id = t->id; // Don't remove newer (higher ID) hidden tweets in case we unhide them later } else { self->max_id = G_MININT64; g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len); } } if (old_id == self->min_id) { if (self->tweets->len > 0) { CbTweet *t = g_ptr_array_index (self->tweets, self->thread_mode ? 0 : self->tweets->len - 1); self->min_id = t->id; /* We just removed the tweet with the min_id, so now remove all hidden tweets * with an id lower than the new min_id */ for (i = 0; i < self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t->id < self->min_id) { g_ptr_array_remove_index (self->hidden_tweets, i); i --; } } } else { self->min_id = G_MAXINT64; g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len); } } } int cb_tweet_model_index_of (CbTweetModel *self, gint64 id) { int i; g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE); for (i = 0; i < self->tweets->len; i ++) { CbTweet *tweet = g_ptr_array_index (self->tweets, i); if (tweet->id == id) return i; } return -1; } int cb_tweet_model_index_of_retweet (CbTweetModel *self, gint64 id) { int i; g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE); for (i = 0; i < self->tweets->len; i ++) { CbTweet *tweet = g_ptr_array_index (self->tweets, i); if (tweet->retweeted_tweet != NULL && tweet->retweeted_tweet->id == id) return i; } return -1; } static void remove_tweet_at_pos (CbTweetModel *self, guint index) { g_assert (index < self->tweets->len); CbTweet *tweet = g_ptr_array_index (self->tweets, index); gint64 id = tweet->id; g_ptr_array_remove_index (self->tweets, index); tweet = NULL; /* We just unreffed it, so potentially freed */ update_min_max_id (self, id); emit_items_changed (self, index, 1, 0); } static inline void insert_sorted (CbTweetModel *self, CbTweet *tweet) { int insert_pos = -1; gint64 id = self->thread_mode && tweet->retweeted_tweet != NULL ? tweet->retweeted_tweet->id : tweet->id; if (id > self->max_id) { insert_pos = self->thread_mode ? self->tweets->len : 0; } else if (id < self->min_id) { insert_pos = self->thread_mode ? 0 : self->tweets->len; } else { /* This case should be relatively rare in real life since * we only ever add tweets at the top or bottom of a list */ int i; CbTweet *next = g_ptr_array_index (self->tweets, 0); for (i = 1; i < self->tweets->len; i ++) { CbTweet *cur = next; next = g_ptr_array_index (self->tweets, i); gint64 older_id, newer_id, cur_id; if (self->thread_mode) { cur_id = cur->retweeted_tweet != NULL ? cur->retweeted_tweet->id : cur->id; older_id = cur_id; newer_id = next->retweeted_tweet != NULL ? next->retweeted_tweet->id : next->id; } else { cur_id = cur->id; older_id = next->id; newer_id = cur_id; } if (newer_id > id && older_id < id) { insert_pos = i; break; } else if (cur_id == id) { // We found a duplicate! Could be caused by injecting the user's own tweet, // so ignore it break; } } } if (insert_pos == -1) { /* This can happen if the same tweet gets inserted into an empty model twice. * Generally, we'd like to ignore double insertions, at least right now I can't * think of a good use case for it. (2017-06-13) */ return; } g_object_ref (tweet); g_ptr_array_insert (self->tweets, insert_pos, tweet); emit_items_changed (self, insert_pos, 0, 1); if (id > self->max_id) self->max_id = id; if (id < self->min_id) self->min_id = id; } static void hide_tweet_internal (CbTweetModel *self, guint index) { CbTweet *tweet = g_ptr_array_index (self->tweets, index); gint64 id = tweet->id; g_object_ref (tweet); g_ptr_array_remove_index (self->tweets, index); g_object_ref (tweet); /* Have to ref manually */ g_ptr_array_add (self->hidden_tweets, tweet); g_object_unref (tweet); update_min_max_id (self, id); } static void show_tweet_internal (CbTweetModel *self, guint index) { CbTweet *tweet = g_ptr_array_index (self->hidden_tweets, index); g_object_ref (tweet); g_ptr_array_remove_index (self->hidden_tweets, index); insert_sorted (self, tweet); g_object_unref (tweet); } gboolean cb_tweet_model_contains_id (CbTweetModel *self, gint64 id) { return cb_tweet_model_index_of (self, id) != -1; } void cb_tweet_model_clear (CbTweetModel *self) { int l; g_return_if_fail (CB_IS_TWEET_MODEL (self)); l = self->tweets->len; g_ptr_array_remove_range (self->tweets, 0, l); g_ptr_array_remove_range (self->hidden_tweets, 0, self->hidden_tweets->len); self->min_id = G_MAXINT64; self->max_id = G_MININT64; emit_items_changed (self, 0, l, 0); } void cb_tweet_model_set_thread_mode (CbTweetModel *self, gboolean thread_mode) { g_return_if_fail (self->min_id == G_MAXINT64); g_return_if_fail (self->max_id == G_MININT64); self->thread_mode = thread_mode; } CbTweet * cb_tweet_model_get_for_id (CbTweetModel *self, gint64 id, int diff) { int i; g_return_val_if_fail (CB_IS_TWEET_MODEL (self), NULL); for (i = 0; i < self->tweets->len; i ++) { CbTweet *tweet = g_ptr_array_index (self->tweets, i); if (tweet->id == id) { if (i + diff < self->tweets->len && i + diff >= 0) return g_ptr_array_index (self->tweets, i + diff); return NULL; } } return NULL; } gboolean cb_tweet_model_delete_id (CbTweetModel *self, gint64 id, gboolean *seen) { int i; g_return_val_if_fail (CB_IS_TWEET_MODEL (self), FALSE); for (i = 0; i < self->tweets->len; i ++) { CbTweet *tweet = g_ptr_array_index (self->tweets, i); if (tweet->id == id) { *seen = tweet->seen; g_assert (!cb_tweet_is_hidden (tweet)); cb_tweet_set_flag (tweet, CB_TWEET_STATE_DELETED); return TRUE; } else if (cb_tweet_is_flag_set (tweet, CB_TWEET_STATE_RETWEETED) && tweet->my_retweet == id) { cb_tweet_unset_flag (tweet, CB_TWEET_STATE_RETWEETED); } } *seen = FALSE; return FALSE; } void cb_tweet_model_remove_tweet (CbTweetModel *self, CbTweet *tweet) { int i; g_return_if_fail (CB_IS_TWEET_MODEL (self)); g_return_if_fail (CB_IS_TWEET (tweet)); #ifdef DEBUG if (!cb_tweet_is_hidden (tweet)) g_assert (cb_tweet_model_contains_id (self, tweet->id)); #endif if (cb_tweet_is_hidden (tweet)) { for (i = 0; i < self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t == tweet) { g_ptr_array_remove_index (self->hidden_tweets, i); break; } } } else { int pos = -1; for (i = 0; i < self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (t == tweet) { pos = i; break; } } g_assert (pos != -1); remove_tweet_at_pos (self, pos); } } void cb_tweet_model_toggle_flag_on_user_tweets (CbTweetModel *self, gint64 user_id, CbTweetState flag, gboolean active) { int i; g_return_if_fail (CB_IS_TWEET_MODEL (self)); for (i = 0; i < self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (cb_tweet_get_user_id (t) == user_id) { if (active) { if (cb_tweet_model_set_tweet_flag (self, t, flag)) i --; } else { if (cb_tweet_model_unset_tweet_flag (self, t, flag)) i --; } } } /* Aaaand now the same thing for hidden tweets */ for (i = 0; i < self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (cb_tweet_get_user_id (t) == user_id) { if (active) { if (cb_tweet_model_set_tweet_flag (self, t, flag)) i --; } else { if (cb_tweet_model_unset_tweet_flag (self, t, flag)) i --; } } } } void cb_tweet_model_toggle_flag_on_user_retweets (CbTweetModel *self, gint64 user_id, CbTweetState flag, gboolean active) { int i; g_return_if_fail (CB_IS_TWEET_MODEL (self)); for (i = 0; i < self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (t->retweeted_tweet != NULL && t->source_tweet.author.id == user_id) { if (active) { if (cb_tweet_model_set_tweet_flag (self, t, flag)) i --; } else { if (cb_tweet_model_unset_tweet_flag (self, t, flag)) i --; } } } /* Aaaand now the same thing for hidden tweets */ for (i = 0; i < self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t->retweeted_tweet != NULL && t->source_tweet.author.id == user_id) { if (active) { if (cb_tweet_model_set_tweet_flag (self, t, flag)) i --; } else { if (cb_tweet_model_unset_tweet_flag (self, t, flag)) i --; } } } } gboolean cb_tweet_model_set_tweet_flag (CbTweetModel *self, CbTweet *tweet, CbTweetState flag) { int i; if (cb_tweet_is_hidden (tweet)) { #ifdef DEBUG gboolean found = FALSE; /* Should be in hidden_tweets now, hu? */ for (i = 0; self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t == tweet) { found = TRUE; break; } } g_assert (found); #endif cb_tweet_set_flag (tweet, flag); } else { #ifdef DEBUG /* Now it should be is self->tweets */ gboolean found = FALSE; /* Should be in hidden_tweets now, hu? */ for (i = 0; self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (t == tweet) { found = TRUE; break; } } g_assert (found); #endif cb_tweet_set_flag (tweet, flag); if (cb_tweet_is_hidden (tweet)) { /* Could be hidden now. */ for (i = 0; i < self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (t == tweet) { hide_tweet_internal (self, i); emit_items_changed (self, i, 1, 0); break; } } return TRUE; } } return FALSE; } gboolean cb_tweet_model_unset_tweet_flag (CbTweetModel *self, CbTweet *tweet, CbTweetState flag) { int i; if (cb_tweet_is_hidden (tweet)) { #ifdef DEBUG gboolean found = FALSE; /* Should be in hidden_tweets now, hu? */ for (i = 0; self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t == tweet) { found = TRUE; break; } } g_assert (found); #endif cb_tweet_unset_flag (tweet, flag); if (!cb_tweet_is_hidden (tweet)) { /* Tweet not hidden anymore. Move to self->tweets * and emit items-changed. */ for (i = 0; i < self->hidden_tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->hidden_tweets, i); if (t == tweet) { show_tweet_internal (self, i); return TRUE; } } } } else { #ifdef DEBUG /* Now it should be is self->tweets */ gboolean found = FALSE; /* Should be in hidden_tweets now, hu? */ for (i = 0; self->tweets->len; i ++) { CbTweet *t = g_ptr_array_index (self->tweets, i); if (t == tweet) { found = TRUE; break; } } g_assert (found); #endif cb_tweet_unset_flag (tweet, flag); } return FALSE; } void cb_tweet_model_add (CbTweetModel *self, CbTweet *tweet) { g_return_if_fail (CB_IS_TWEET_MODEL (self)); g_return_if_fail (CB_IS_TWEET (tweet)); if (cb_tweet_is_hidden (tweet)) { g_object_ref (tweet); g_ptr_array_add (self->hidden_tweets, tweet); } else { insert_sorted (self, tweet); } } void cb_tweet_model_remove_oldest_n_visible (CbTweetModel *self, guint amount) { int size_before; int start; if (amount < 1) { return; } g_return_if_fail (CB_IS_TWEET_MODEL (self)); size_before = self->tweets->len; if (amount > size_before) { amount = size_before; } if (self->thread_mode) { start = 0; } else { start = size_before - amount; } g_ptr_array_remove_range (self->tweets, start, amount); update_min_max_id (self, self->min_id); emit_items_changed (self, start, amount, 0); } void cb_tweet_model_remove_tweets_later_than (CbTweetModel *self, gint64 id) { g_return_if_fail (CB_IS_TWEET_MODEL (self)); if (self->tweets->len == 0) return; if (self->thread_mode) { for (guint i = self->tweets->len; i > 0; i--) { CbTweet *cur = g_ptr_array_index (self->tweets, i - 1); if (cur->id < id) break; remove_tweet_at_pos (self, i - 1); } } else { while (self->tweets->len > 0) { CbTweet *first = g_ptr_array_index (self->tweets, 0); if (first->id < id) break; remove_tweet_at_pos (self, 0); } } } cawbird-1.4.2/src/CbTweetModel.h000066400000000000000000000077301416632607600164310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef CB_TWEET_MODEL_H #define CB_TWEET_MODEL_H #include #include #include "CbTweet.h" typedef struct _CbTweetModel CbTweetModel; typedef struct _CbTweetModelClass CbTweetModelClass; #define CB_TYPE_TWEET_MODEL (cb_tweet_model_get_type ()) #define CB_TWEET_MODEL(obj) (G_TYPE_CHECK_INSTANCE_CAST(obj, CB_TYPE_TWEET_MODEL, CbTweetModel)) #define CB_TWEET_MODEL_CLASS(cls) (G_TYPE_CHECK_CLASS_CAST(cls, CB_TYPE_TWEET_MODEL, CbTweetModelClass)) #define CB_IS_TWEET_MODEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE(obj, CB_TYPE_TWEET_MODEL)) #define CB_IS_TWEET_MODEL_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE(cls, CB_TYPE_TWEET_MODEL)) #define CB_TWEET_MODEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS(obj, CB_TYPE_TWEET_MODEL, CbTweetModelClass)) struct _CbTweetModel { GObject parent_instance; gboolean thread_mode; GPtrArray *tweets; GPtrArray *hidden_tweets; gint64 min_id; gint64 max_id; }; struct _CbTweetModelClass { GObjectClass parent_class; }; GType cb_tweet_model_get_type (void) G_GNUC_CONST; CbTweetModel *cb_tweet_model_new (void); int cb_tweet_model_index_of (CbTweetModel *self, gint64 id); gboolean cb_tweet_model_contains_id (CbTweetModel *self, gint64 id); void cb_tweet_model_clear (CbTweetModel *self); void cb_tweet_model_set_thread_mode (CbTweetModel *self, gboolean thread_mode); CbTweet *cb_tweet_model_get_for_id (CbTweetModel *self, gint64 id, int diff); gboolean cb_tweet_model_delete_id (CbTweetModel *self, gint64 id, gboolean *seen); void cb_tweet_model_remove_tweet (CbTweetModel *self, CbTweet *tweet); void cb_tweet_model_toggle_flag_on_user_tweets (CbTweetModel *self, gint64 user_id, CbTweetState flag, gboolean active); void cb_tweet_model_toggle_flag_on_user_retweets (CbTweetModel *self, gint64 user_id, CbTweetState flag, gboolean active); gboolean cb_tweet_model_set_tweet_flag (CbTweetModel *self, CbTweet *tweet, CbTweetState flag); gboolean cb_tweet_model_unset_tweet_flag (CbTweetModel *self, CbTweet *tweet, CbTweetState flag); void cb_tweet_model_add (CbTweetModel *self, CbTweet *tweet); void cb_tweet_model_remove_oldest_n_visible (CbTweetModel *self, guint amount); void cb_tweet_model_remove_tweets_later_than (CbTweetModel *self, gint64 id); #endif cawbird-1.4.2/src/CbTwitterItem.c000066400000000000000000000051311416632607600166250ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbTwitterItem.h" G_DEFINE_INTERFACE (CbTwitterItem, cb_twitter_item, G_TYPE_OBJECT); static int default_update_time_delta (CbTwitterItem *self, GDateTime *now) { /* NOPE */ return 0; } static void cb_twitter_item_default_init (CbTwitterItemInterface *self) { self->get_sort_factor = NULL; self->update_time_delta = default_update_time_delta; self->get_timestamp = NULL; self->set_last_set_timediff = NULL; self->get_last_set_timediff = NULL; } gint64 cb_twitter_item_get_sort_factor (CbTwitterItem *self) { CbTwitterItemInterface *iface; g_return_val_if_fail (CB_IS_TWITTER_ITEM (self), 0); iface = CB_TWITTER_ITEM_GET_IFACE (self); return iface->get_sort_factor (self); } int cb_twitter_item_update_time_delta (CbTwitterItem *self, GDateTime *now) { CbTwitterItemInterface *iface; g_return_val_if_fail (CB_IS_TWITTER_ITEM (self), 0); iface = CB_TWITTER_ITEM_GET_IFACE (self); return iface->update_time_delta (self, now); } gint64 cb_twitter_item_get_timestamp (CbTwitterItem *self) { CbTwitterItemInterface *iface; g_return_val_if_fail (CB_IS_TWITTER_ITEM (self), 0); iface = CB_TWITTER_ITEM_GET_IFACE (self); return iface->get_timestamp (self); } void cb_twitter_item_set_last_set_timediff (CbTwitterItem *self, GTimeSpan span) { CbTwitterItemInterface *iface; g_return_if_fail (CB_IS_TWITTER_ITEM (self)); iface = CB_TWITTER_ITEM_GET_IFACE (self); iface->set_last_set_timediff (self, span); } GTimeSpan cb_twitter_item_get_last_set_timediff (CbTwitterItem *self) { CbTwitterItemInterface *iface; g_return_val_if_fail (CB_IS_TWITTER_ITEM (self), 0); iface = CB_TWITTER_ITEM_GET_IFACE (self); return iface->get_last_set_timediff (self); } cawbird-1.4.2/src/CbTwitterItem.h000066400000000000000000000036571416632607600166450ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_TWITTER_ITEM_H_ #define _CB_TWITTER_ITEM_H_ #include #define CB_TYPE_TWITTER_ITEM (cb_twitter_item_get_type()) G_DECLARE_INTERFACE (CbTwitterItem, cb_twitter_item, CB, TWITTER_ITEM, GObject) struct _CbTwitterItemInterface { GTypeInterface base_iface; gint64 (*get_sort_factor) (CbTwitterItem *self); int (*update_time_delta) (CbTwitterItem *self, GDateTime *now); gint64 (*get_timestamp) (CbTwitterItem *self); void (*set_last_set_timediff) (CbTwitterItem *self, GTimeSpan span); GTimeSpan (*get_last_set_timediff) (CbTwitterItem *self); }; gint64 cb_twitter_item_get_sort_factor (CbTwitterItem *self); int cb_twitter_item_update_time_delta (CbTwitterItem *self, GDateTime *now); gint64 cb_twitter_item_get_timestamp (CbTwitterItem *self); /* Basically just for CbDeltaUpdater */ void cb_twitter_item_set_last_set_timediff (CbTwitterItem *self, GTimeSpan span); GTimeSpan cb_twitter_item_get_last_set_timediff (CbTwitterItem *self); #endif cawbird-1.4.2/src/CbTypes.c000066400000000000000000000535441416632607600154630ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbTypes.h" #include "CbMediaDownloader.h" #include "CbUtils.h" #include #include void cb_user_identity_free (CbUserIdentity *id) { g_free (id->screen_name); g_free (id->user_name); } void cb_user_identity_copy (const CbUserIdentity *id, CbUserIdentity *id2) { g_free (id2->screen_name); id2->screen_name = g_strdup (id->screen_name); g_free (id2->user_name); id2->user_name = g_strdup (id->user_name); id2->id = id->id; id2->verified = id->verified; } void cb_user_identity_parse (CbUserIdentity *id, JsonObject *user_obj) { id->id = json_object_get_int_member (user_obj, "id"); id->screen_name = g_strdup (json_object_get_string_member (user_obj, "screen_name")); id->user_name = g_strdup (json_object_get_string_member (user_obj, "name")); id->verified = json_object_get_boolean_member (user_obj, "verified"); } void cb_text_entity_free (CbTextEntity *e) { g_free (e->original_text); g_free (e->display_text); g_free (e->tooltip_text); g_free (e->target); } void cb_text_entity_copy (const CbTextEntity *e1, CbTextEntity *e2) { e2->from = e1->from; e2->to = e1->to; g_free(e2->original_text); e2->original_text = g_strdup (e1->original_text); g_free (e2->display_text); e2->display_text = g_strdup (e1->display_text); g_free (e2->tooltip_text); e2->tooltip_text = g_strdup (e1->tooltip_text); g_free (e2->target); e2->target = g_strdup (e1->target); e2->info = e1->info; } void cb_mini_tweet_free (CbMiniTweet *t) { guint i; g_free (t->text); g_free (t->language); for (i = 0; i < t->n_medias; i ++) g_object_unref (t->medias[i]); g_free (t->medias); for (i = 0; i < t->n_entities; i ++) cb_text_entity_free (&t->entities[i]); g_free (t->entities); for (i = 0; i < t->n_reply_users; i ++) cb_user_identity_free (&t->reply_users[i]); g_free (t->reply_users); cb_user_identity_free (&t->author); } void cb_mini_tweet_copy (const CbMiniTweet *t1, CbMiniTweet *t2) { guint i; t2->id = t1->id; t2->created_at = t1->created_at; cb_user_identity_copy (&t1->author, &t2->author); g_free (t2->text); t2->text = g_strdup (t1->text); g_free (t2->language); t2->language = g_strdup (t1->language); t2->n_entities = t1->n_entities; t2->entities = g_new0 (CbTextEntity, t2->n_entities); for (i = 0; i < t2->n_entities; i ++) cb_text_entity_copy (&t1->entities[i], &t2->entities[i]); t2->n_medias = t1->n_medias; t2->medias = g_new0 (CbMedia*, t2->n_medias); for (i = 0; i < t2->n_medias; i ++) t2->medias[i] = g_object_ref (t1->medias[i]); } void cb_mini_tweet_init (CbMiniTweet *t) { t->reply_id = 0; t->medias = NULL; t->n_medias = 0; t->entities = NULL; t->n_entities = 0; t->reply_users = NULL; t->n_reply_users = 0; } void cb_mini_tweet_parse (CbMiniTweet *t, JsonObject *obj) { GDateTime *time; JsonObject *extended_object; const char *tweet_text; if (json_object_has_member (obj, "extended_tweet")) extended_object = json_object_get_object_member (obj, "extended_tweet"); else extended_object = obj; time = cb_utils_parse_date (json_object_get_string_member (obj, "created_at")); t->id = json_object_get_int_member (obj, "id"); if (json_object_has_member (extended_object, "full_text")) tweet_text = json_object_get_string_member (extended_object, "full_text"); else tweet_text = json_object_get_string_member (extended_object, "text"); if (json_object_has_member (extended_object, "display_text_range")) { /* We only remove the prefix */ guint start = (guint)json_array_get_int_element ( json_object_get_array_member (extended_object, "display_text_range"), 0); guint i; const char *p = tweet_text; /* Skip ahead */ for (i = 0; i < start; i ++) p = g_utf8_next_char (p); t->text = g_strdup (p); t->display_range_start = start; } else { t->text = g_strdup (tweet_text); t->display_range_start= 0; } if (json_object_has_member (extended_object, "lang")) { t->language = g_strdup(json_object_get_string_member (extended_object, "lang")); } else { t->language = g_strdup("und"); } t->created_at = g_date_time_to_unix (time); cb_user_identity_parse (&t->author, json_object_get_object_member (obj, "user")); g_date_time_unref (time); } static int json_object_get_member_size (JsonObject *obj, const char *member_name) { if (!obj || !json_object_has_member (obj, member_name)) return 0; return (int)json_array_get_length (json_object_get_array_member (obj, member_name)); } void cb_mini_tweet_parse_entities (CbMiniTweet *t, JsonObject *status) { JsonObject *extended_obj = status; JsonObject *entities; JsonArray *urls; JsonArray *hashtags; JsonArray *user_mentions; int media_count; guint i, p; int url_index = 0; guint n_reply_users = 0; int max_entities; gboolean direct_duplicate = FALSE; if (json_object_has_member (status, "extended_tweet")) extended_obj = json_object_get_object_member (status, "extended_tweet"); entities = json_object_get_object_member (extended_obj, "entities"); urls = json_object_get_array_member (entities, "urls"); hashtags = json_object_get_array_member (entities, "hashtags"); user_mentions = json_object_get_array_member (entities, "user_mentions"); media_count = json_object_get_member_size (entities, "media"); if (json_object_has_member (status, "extended_entities")) media_count += json_object_get_member_size (json_object_get_object_member (status, "extended_entities"), "media"); if (json_object_has_member (status, "in_reply_to_status_id") && !json_object_get_null_member (status, "in_reply_to_status_id")) { guint reply_index = 0; gint64 reply_to_user_id = 0; reply_to_user_id = json_object_get_int_member (status, "in_reply_to_user_id"); /* Check how many of the user mentions are reply mentions */ t->reply_id = json_object_get_int_member (status, "in_reply_to_status_id"); for (i = 0, p = json_array_get_length (user_mentions); i < p; i ++) { JsonObject *mention = json_node_get_object (json_array_get_element (user_mentions, i)); JsonArray *indices = json_object_get_array_member (mention, "indices"); gint64 user_id = json_object_get_int_member (mention, "id"); if (json_array_get_int_element (indices, 1) <= t->display_range_start) n_reply_users ++; else break; if (i == 0 && user_id == reply_to_user_id) direct_duplicate = TRUE; } if (!direct_duplicate) n_reply_users ++; t->reply_users = g_new0 (CbUserIdentity, n_reply_users); t->n_reply_users = n_reply_users; if (!direct_duplicate) { t->reply_users[0].id = reply_to_user_id; t->reply_users[0].screen_name = g_strdup (json_object_get_string_member (status, "in_reply_to_screen_name")); t->reply_users[0].user_name = (reply_to_user_id == t->author.id) ? g_strdup(t->author.user_name) : g_strdup (""); reply_index = 1; } /* Now fill ->reply_users. The very first entry is always the user this tweet * *actually* replies to. */ for (i = 0; i < n_reply_users - (direct_duplicate ? 0 : 1); i ++) { JsonObject *mention = json_node_get_object (json_array_get_element (user_mentions, i)); t->reply_users[reply_index].id = json_object_get_int_member (mention, "id"); t->reply_users[reply_index].screen_name = g_strdup (json_object_get_string_member (mention, "screen_name")); t->reply_users[reply_index].user_name = cb_utils_escape_ampersands (json_object_get_string_member (mention, "name")); reply_index ++; } } max_entities = json_array_get_length (urls) + json_array_get_length (hashtags) + json_array_get_length (user_mentions) + media_count; media_count += (int)json_array_get_length (urls); t->medias = g_new0 (CbMedia*, media_count); t->entities = g_new0 (CbTextEntity, max_entities); /* URLS */ for (i = 0, p = json_array_get_length (urls); i < p; i ++) { JsonObject *url = json_node_get_object (json_array_get_element (urls, i)); const char *expanded_url = json_object_get_string_member (url, "expanded_url"); JsonArray *indices; indices = json_object_get_array_member (url, "indices"); t->entities[url_index].from = json_array_get_int_element (indices, 0); t->entities[url_index].to = json_array_get_int_element (indices, 1); t->entities[url_index].original_text = cb_utils_escape_ampersands (json_object_get_string_member (url, "url")); t->entities[url_index].display_text = cb_utils_escape_ampersands (json_object_get_string_member (url, "display_url")); t->entities[url_index].tooltip_text = cb_utils_escape_ampersands (expanded_url); t->entities[url_index].target = cb_utils_escape_ampersands (expanded_url); url_index ++; } /* HASHTAGS */ for (i = 0, p = json_array_get_length (hashtags); i < p; i ++) { JsonObject *hashtag = json_node_get_object (json_array_get_element (hashtags, i)); JsonArray *indices = json_object_get_array_member (hashtag, "indices"); const char *text = json_object_get_string_member (hashtag, "text"); t->entities[url_index].from = json_array_get_int_element (indices, 0); t->entities[url_index].to = json_array_get_int_element (indices, 1); t->entities[url_index].original_text = g_strdup_printf ("#%s", text); t->entities[url_index].display_text = g_strdup_printf ("#%s", text); t->entities[url_index].tooltip_text = g_strdup_printf ("#%s", text); t->entities[url_index].target = NULL; url_index ++; } /* USER MENTIONS */ for (i = 0, p = json_array_get_length (user_mentions); i < p; i ++) { JsonObject *mention = json_node_get_object (json_array_get_element (user_mentions, i)); JsonArray *indices = json_object_get_array_member (mention, "indices"); const char *screen_name = json_object_get_string_member (mention, "screen_name"); const char *id_str = json_object_get_string_member (mention, "id_str"); t->entities[url_index].from = json_array_get_int_element (indices, 0); t->entities[url_index].to = json_array_get_int_element (indices, 1); t->entities[url_index].original_text = g_strdup_printf ("@%s", screen_name); t->entities[url_index].display_text = g_strdup_printf ("@%s", screen_name); t->entities[url_index].tooltip_text = cb_utils_escape_ampersands (json_object_get_string_member (mention, "name")); t->entities[url_index].target = g_strdup_printf ("@%s/@%s", id_str, screen_name); url_index ++; } /* MEDIA */ if (json_object_has_member (entities, "media")) { JsonArray *medias = json_object_get_array_member (entities, "media"); for (i = 0, p = json_array_get_length (medias); i < p; i ++) { JsonObject *url = json_node_get_object (json_array_get_element (medias, i)); JsonArray *indices = json_object_get_array_member (url, "indices"); char *url_str = cb_utils_escape_ampersands (json_object_get_string_member (url, "url")); int k; gboolean duplicate = FALSE; /* Check for duplicates */ for (k = 0; k < url_index; k ++) { const char *target = t->entities[k].target; if (target != NULL && strcmp (target, url_str) == 0) { duplicate = TRUE; break; } } if (duplicate) { g_free (url_str); continue; } t->entities[url_index].from = json_array_get_int_element (indices, 0); t->entities[url_index].to = json_array_get_int_element (indices, 1); t->entities[url_index].original_text = cb_utils_escape_ampersands (json_object_get_string_member (url, "url")); t->entities[url_index].display_text = cb_utils_escape_ampersands (json_object_get_string_member (url, "display_url")); t->entities[url_index].target = url_str; url_index ++; } } // The docs say "media" is legacy and go with "extended entities" - https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/extended-entities-object if (json_object_has_member (status, "extended_entities")) { JsonArray *media_array = json_object_get_array_member (json_object_get_object_member (status, "extended_entities"), "media"); guint array_len = json_array_get_length (media_array); for (int i = 0; i < array_len; i ++) { JsonObject *media_obj = json_node_get_object (json_array_get_element (media_array, i)); const char *media_type = json_object_get_string_member (media_obj, "type"); if (strcmp (media_type, "photo") == 0) { const char *url = json_object_has_member (media_obj, "media_url_https") ? json_object_get_string_member (media_obj, "media_url_https") : json_object_get_string_member (media_obj, "media_url"); if (is_media_candidate (url)) { t->medias[t->n_medias] = cb_media_new (); t->medias[t->n_medias]->type = CB_MEDIA_TYPE_IMAGE; t->medias[t->n_medias]->url = g_strdup_printf ("%s:large", url); t->medias[t->n_medias]->target_url = g_strdup_printf ("%s:orig", url); if (json_object_has_member (media_obj, "ext_alt_text")) { // Only "extended media" has alt text t->medias[t->n_medias]->alt_text = g_strdup(json_object_get_string_member (media_obj, "ext_alt_text")); } if (json_object_has_member (media_obj, "sizes")) { JsonObject *sizes = json_object_get_object_member (media_obj, "sizes"); JsonObject *small = json_object_get_object_member (sizes, "small"); t->medias[t->n_medias]->thumb_width = json_object_get_int_member (small, "w"); t->medias[t->n_medias]->thumb_height = json_object_get_int_member (small, "h"); t->medias[t->n_medias]->thumb_url = g_strdup_printf ("%s:small", url); // We'll show images at "large" size, and original size is available via the target URL JsonObject *large = json_object_get_object_member (sizes, "large"); t->medias[t->n_medias]->width = json_object_get_int_member (large, "w"); t->medias[t->n_medias]->height = json_object_get_int_member (large, "h"); } t->n_medias ++; } else { const char *url = json_object_has_member (media_obj, "media_url_https") ? json_object_get_string_member (media_obj, "media_url_https") : json_object_get_string_member (media_obj, "media_url"); g_warning("URL %s was marked as type photo but not a media candidate", url); } } else if (strcmp (media_type, "video") == 0 || strcmp (media_type, "animated_gif") == 0) { JsonObject *video_info = json_object_get_object_member (media_obj, "video_info"); JsonArray *variants = json_object_get_array_member (video_info, "variants"); JsonObject *variant = NULL; gchar *thumb_url = NULL; const char *target_url = NULL; int thumb_width = -1; int thumb_height = -1; int width = -1; int height = -1; guint q, k; if (json_object_has_member (media_obj, "sizes")) { JsonObject *sizes = json_object_get_object_member (media_obj, "sizes"); JsonObject *small = json_object_get_object_member (sizes, "small"); thumb_width = json_object_get_int_member (small, "w"); thumb_height = json_object_get_int_member (small, "h"); const char *url = json_object_has_member (media_obj, "media_url_https") ? json_object_get_string_member (media_obj, "media_url_https") : json_object_get_string_member (media_obj, "media_url"); thumb_url = g_strdup_printf ("%s:small", url); // User "large" as an approximation of full size (although it's still capped at 2048px) // Videos are unlikely to be above 2048px, though JsonObject *large = json_object_get_object_member (sizes, "large"); width = json_object_get_int_member (large, "w"); height = json_object_get_int_member (large, "h"); } for (k = 0, q = json_array_get_length (variants); k < q; k ++) { JsonObject *v = json_node_get_object (json_array_get_element (variants, k)); if (strcmp (json_object_get_string_member (v, "content_type"), "application/x-mpegURL") == 0) { variant = v; target_url = json_object_get_string_member(media_obj, "expanded_url"); break; } } if (variant == NULL && json_array_get_length (variants) > 0) variant = json_node_get_object (json_array_get_element (variants, 0)); if (variant != NULL) { t->medias[t->n_medias] = cb_media_new (); t->medias[t->n_medias]->url = g_strdup(json_object_get_string_member (variant, "url")); t->medias[t->n_medias]->thumb_url = g_strdup (thumb_url); t->medias[t->n_medias]->target_url = target_url ? g_strdup (target_url) : NULL; t->medias[t->n_medias]->type = CB_MEDIA_TYPE_TWITTER_VIDEO; t->medias[t->n_medias]->width = width; t->medias[t->n_medias]->height = height; t->medias[t->n_medias]->thumb_width = thumb_width; t->medias[t->n_medias]->thumb_height = thumb_height; if (json_object_has_member (media_obj, "ext_alt_text")) { // Only "extended media" for GIFs has alt text. Videos never do. t->medias[t->n_medias]->alt_text = g_strdup(json_object_get_string_member (media_obj, "ext_alt_text")); } if (t->medias[t->n_medias]->alt_text == NULL && json_object_has_member (media_obj, "additional_media_info")) { JsonObject *additional_media_info = json_object_get_object_member (media_obj, "additional_media_info"); if (json_object_has_member (additional_media_info, "title") && json_object_has_member (additional_media_info, "description")) { t->medias[t->n_medias]->alt_text = g_strstrip (g_strdup_printf ("%s\n\n%s", json_object_get_string_member (additional_media_info, "title"), json_object_get_string_member (additional_media_info, "description"))); } } t->n_medias ++; } if (thumb_url != NULL) { g_free(thumb_url); } } else { g_debug ("Unhandled media type: %s", media_type); } } } if (t->n_medias == 0) { for (i = 0, p = json_array_get_length (urls); i < p; i ++) { JsonObject *url = json_node_get_object (json_array_get_element (urls, i)); const char *expanded_url = json_object_get_string_member (url, "expanded_url"); if (is_media_candidate (expanded_url)) { t->medias[t->n_medias] = cb_media_new (); t->medias[t->n_medias]->url = g_strdup (expanded_url); t->medias[t->n_medias]->type = cb_media_type_from_url (expanded_url); t->medias[t->n_medias]->target_url = g_strdup (expanded_url); t->n_medias ++; } } } t->n_entities = url_index; #if 0 g_debug ("Wasted entities: %d", max_entities - t->n_entities); g_debug ("Wasted media : %d", media_count - t->n_medias); #endif if (t->n_entities > 0) { guint i, k; /* Sort entities. */ for (i = 0; i < t->n_entities; i ++) for (k = 0; k < t->n_entities; k++) if (t->entities[i].from < t->entities[k].from) { CbTextEntity tmp = { 0 }; cb_text_entity_copy (&t->entities[i], &tmp); cb_text_entity_copy (&t->entities[k], &t->entities[i]); cb_text_entity_copy (&tmp, &t->entities[k]); cb_text_entity_free (&tmp); } } } cawbird-1.4.2/src/CbTypes.h000066400000000000000000000067441416632607600154700ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef __TYPES_H #define __TYPES_H #include #include #include "CbMedia.h" typedef enum { CB_STREAM_MESSAGE_UNSUPPORTED, CB_STREAM_MESSAGE_DELETE, CB_STREAM_MESSAGE_DM_DELETE, CB_STREAM_MESSAGE_RT_DELETE, CB_STREAM_MESSAGE_SCRUB_GEO, CB_STREAM_MESSAGE_LIMIT, CB_STREAM_MESSAGE_DISCONNECT, CB_STREAM_MESSAGE_FRIENDS, CB_STREAM_MESSAGE_EVENT, CB_STREAM_MESSAGE_WARNING, CB_STREAM_MESSAGE_DIRECT_MESSAGE, CB_STREAM_MESSAGE_TWEET, CB_STREAM_MESSAGE_MENTION, CB_STREAM_MESSAGE_EVENT_LIST_CREATED, CB_STREAM_MESSAGE_EVENT_LIST_DESTROYED, CB_STREAM_MESSAGE_EVENT_LIST_UPDATED, CB_STREAM_MESSAGE_EVENT_LIST_UNSUBSCRIBED, CB_STREAM_MESSAGE_EVENT_LIST_SUBSCRIBED, CB_STREAM_MESSAGE_EVENT_LIST_MEMBER_ADDED, CB_STREAM_MESSAGE_EVENT_LIST_MEMBER_REMOVED, CB_STREAM_MESSAGE_EVENT_FAVORITE, CB_STREAM_MESSAGE_EVENT_UNFAVORITE, CB_STREAM_MESSAGE_EVENT_FOLLOW, CB_STREAM_MESSAGE_EVENT_UNFOLLOW, CB_STREAM_MESSAGE_EVENT_BLOCK, CB_STREAM_MESSAGE_EVENT_UNBLOCK, CB_STREAM_MESSAGE_EVENT_MUTE, CB_STREAM_MESSAGE_EVENT_UNMUTE, CB_STREAM_MESSAGE_EVENT_USER_UPDATE, CB_STREAM_MESSAGE_EVENT_QUOTED_TWEET, CB_STREAM_MESSAGE_EVENT_HIDE_RTS, CB_STREAM_MESSAGE_EVENT_SHOW_RTS, CB_STREAM_MESSAGE_TIMELINE_LOADED, CB_STREAM_MESSAGE_MENTIONS_LOADED, CB_STREAM_MESSAGE_FAVORITES_LOADED, CB_STREAM_MESSAGE_DIRECT_MESSAGES_LOADED } CbStreamMessageType; struct _CbUserIdentity { gint64 id; guint verified : 1; guint protected_account : 1; char *screen_name; char *user_name; }; typedef struct _CbUserIdentity CbUserIdentity; void cb_user_identity_free (CbUserIdentity *id); void cb_user_identity_copy (const CbUserIdentity *id, CbUserIdentity *id2); void cb_user_identity_parse (CbUserIdentity *id, JsonObject *user_obj); struct _CbTextEntity { guint from; guint to; guint info : 1; char *original_text; char *display_text; char *tooltip_text; char *target; }; typedef struct _CbTextEntity CbTextEntity; void cb_text_entity_free (CbTextEntity *e); void cb_text_entity_copy (const CbTextEntity *e1, CbTextEntity *e2); struct _CbMiniTweet { gint64 id; gint64 created_at; guint display_range_start; CbUserIdentity author; char *language; char *text; gint64 reply_id; CbTextEntity *entities; guint n_entities; CbMedia **medias; guint n_medias; CbUserIdentity *reply_users; guint n_reply_users; }; typedef struct _CbMiniTweet CbMiniTweet; void cb_mini_tweet_free (CbMiniTweet *tweet); void cb_mini_tweet_copy (const CbMiniTweet *t1, CbMiniTweet *t2); void cb_mini_tweet_init (CbMiniTweet *t); void cb_mini_tweet_parse (CbMiniTweet *t, JsonObject *obj); void cb_mini_tweet_parse_entities (CbMiniTweet *t, JsonObject *obj); #endif cawbird-1.4.2/src/CbUserCompletionModel.c000066400000000000000000000134151416632607600203010ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbUserCompletionModel.h" static void cb_user_completion_model_iface_init (GListModelInterface *iface); G_DEFINE_TYPE_WITH_CODE (CbUserCompletionModel, cb_user_completion_model, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, cb_user_completion_model_iface_init)); static GType cb_user_completion_model_get_item_type (GListModel *model) { return G_TYPE_POINTER; } static guint cb_user_completion_model_get_n_items (GListModel *model) { CbUserCompletionModel *self = CB_USER_COMPLETION_MODEL (model); return self->items->len; } static gpointer cb_user_completion_model_get_item (GListModel *model, guint index) { CbUserCompletionModel *self = CB_USER_COMPLETION_MODEL (model); CbUserIdentity *id = &g_array_index (self->items, CbUserIdentity, index); g_assert (index < self->items->len); return id; } static void cb_user_completion_model_iface_init (GListModelInterface *iface) { iface->get_item_type = cb_user_completion_model_get_item_type; iface->get_n_items = cb_user_completion_model_get_n_items; iface->get_item = cb_user_completion_model_get_item; } static void cb_user_completion_model_init (CbUserCompletionModel *self) { self->items = g_array_new (FALSE, FALSE, sizeof (CbUserIdentity)); g_array_set_clear_func (self->items, (GDestroyNotify)cb_user_identity_free); } static void cb_user_completion_model_finalize (GObject *object) { CbUserCompletionModel *self = CB_USER_COMPLETION_MODEL (object); g_array_free (self->items, TRUE); G_OBJECT_CLASS (cb_user_completion_model_parent_class)->finalize (object); } static void cb_user_completion_model_class_init (CbUserCompletionModelClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_user_completion_model_finalize; } CbUserCompletionModel * cb_user_completion_model_new (void) { return CB_USER_COMPLETION_MODEL (g_object_new (CB_TYPE_USER_COMPLETION_MODEL, NULL)); } static inline gboolean cb_user_completion_model_has_id (CbUserCompletionModel *self, guint64 id) { guint x; /* Look for duplicate */ for (x = 0; x < self->items->len; x ++) { const CbUserIdentity *existing_id = &g_array_index (self->items, CbUserIdentity, x); if (existing_id->id == id) return TRUE; } return FALSE; } static inline void emit_items_changed (CbUserCompletionModel *self, guint position, guint removed, guint added) { g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); } void cb_user_completion_model_insert_items (CbUserCompletionModel *self, CbUserIdentity *ids, guint ids_len) { guint i; guint size_before; guint added = 0; g_return_if_fail (CB_IS_USER_COMPLETION_MODEL (self)); if (ids_len == 0) return; size_before = self->items->len; for (i = 0; i < ids_len; i ++) { CbUserIdentity *id = &ids[i]; CbUserIdentity *new_id; if (cb_user_completion_model_has_id (self, id->id)) continue; g_array_set_size (self->items, self->items->len + 1); new_id = &g_array_index (self->items, CbUserIdentity, self->items->len - 1); new_id->id = id->id; new_id->screen_name = g_steal_pointer (&id->screen_name); new_id->user_name = g_steal_pointer (&id->user_name); new_id->verified = id->verified; new_id->protected_account = id->protected_account; added ++; } emit_items_changed (self, size_before, 0, added); } void cb_user_completion_model_insert_infos (CbUserCompletionModel *self, CbUserInfo *infos, guint infos_len) { guint i; guint size_before; guint added = 0; g_return_if_fail (CB_IS_USER_COMPLETION_MODEL (self)); if (infos_len == 0) return; size_before = self->items->len; for (i = 0; i < infos_len; i ++) { CbUserInfo *info = &infos[i]; CbUserIdentity *new_id; if (cb_user_completion_model_has_id (self, info->user_id)) continue; g_array_set_size (self->items, self->items->len + 1); new_id = &g_array_index (self->items, CbUserIdentity, self->items->len - 1); new_id->id = info->user_id; new_id->screen_name = g_steal_pointer (&info->screen_name); new_id->user_name = g_steal_pointer (&info->user_name); new_id->verified = info->verified; new_id->protected_account = info->protected_account; added ++; } emit_items_changed (self, size_before, 0, added); } void cb_user_completion_model_clear (CbUserCompletionModel *self) { guint old_size; g_return_if_fail (CB_IS_USER_COMPLETION_MODEL (self)); old_size = self->items->len; g_array_remove_range (self->items, 0, self->items->len); emit_items_changed (self, 0, old_size, 0); } cawbird-1.4.2/src/CbUserCompletionModel.h000066400000000000000000000037331416632607600203100ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_USER_COMPLETION_MODEL_H #define _CB_USER_COMPLETION_MODEL_H #include "CbTypes.h" #include "CbUserCounter.h" #include #include struct _CbUserCompletionModel { GObject parent_instance; GArray *items; }; typedef struct _CbUserCompletionModel CbUserCompletionModel; #define CB_TYPE_USER_COMPLETION_MODEL cb_user_completion_model_get_type () G_DECLARE_FINAL_TYPE (CbUserCompletionModel, cb_user_completion_model, CB, USER_COMPLETION_MODEL, GObject); CbUserCompletionModel * cb_user_completion_model_new (void); void cb_user_completion_model_insert_items (CbUserCompletionModel *self, CbUserIdentity *ids, guint ids_len); void cb_user_completion_model_insert_infos (CbUserCompletionModel *self, CbUserInfo *infos, guint infos_len); void cb_user_completion_model_clear (CbUserCompletionModel *self); #endif cawbird-1.4.2/src/CbUserCounter.c000066400000000000000000000255161416632607600166330ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbUserCounter.h" #include #include void cb_user_info_destroy (CbUserInfo *info) { g_free (info->screen_name); g_free (info->user_name); } static int score_sort (gconstpointer p1, gconstpointer p2) { const CbUserInfo *ui1 = p1; const CbUserInfo *ui2 = p2; return ui1->score < ui2->score; } G_DEFINE_TYPE (CbUserCounter, cb_user_counter, G_TYPE_OBJECT); CbUserCounter * cb_user_counter_new (void) { return CB_USER_COUNTER (g_object_new (CB_TYPE_USER_COUNTER, NULL)); } void cb_user_counter_id_seen (CbUserCounter *counter, const CbUserIdentity *id) { g_return_if_fail (CB_IS_USER_COUNTER (counter)); g_return_if_fail (id != NULL); cb_user_counter_user_seen_full (counter, id->id, id->screen_name, id->user_name, id->verified, id->protected_account); } void user_seen (CbUserCounter *counter, gint64 user_id, const char *screen_name, const char *user_name, gboolean verified, gboolean protected_account, gboolean extras_known) { gboolean found = FALSE; guint i; g_return_if_fail (CB_IS_USER_COUNTER (counter)); g_return_if_fail (screen_name != NULL); g_return_if_fail (user_name != NULL); for (i = 0; i < counter->user_infos->len; i ++) { CbUserInfo *ui = &g_array_index (counter->user_infos, CbUserInfo, i); if (ui->user_id == user_id) { ui->score ++; ui->screen_name = g_strdup (screen_name); ui->user_name = g_strdup (user_name); if (extras_known) { ui->verified = verified; ui->protected_account = protected_account; } ui->changed = TRUE; found = TRUE; break; } } if (!found) { CbUserInfo *ui; g_array_set_size (counter->user_infos, counter->user_infos->len + 1); ui = &g_array_index (counter->user_infos, CbUserInfo, counter->user_infos->len - 1); ui->user_id = user_id; ui->screen_name = g_strdup (screen_name); ui->user_name = g_strdup (user_name); ui->changed = TRUE; /* Because we just inserted this, eh */ ui->score = 1; ui->verified = verified; ui->protected_account = protected_account; } } // Lightweight call where we don't have all of the details (e.g. from DMs) void cb_user_counter_user_seen (CbUserCounter *counter, gint64 user_id, const char *screen_name, const char *user_name) { user_seen (counter, user_id, screen_name, user_name, FALSE, FALSE, FALSE); } // Full call where we know whether accounts are verified or protected void cb_user_counter_user_seen_full (CbUserCounter *counter, gint64 user_id, const char *screen_name, const char *user_name, gboolean verified, gboolean protected_account) { user_seen (counter, user_id, screen_name, user_name, verified, protected_account, TRUE); } int cb_user_counter_save (CbUserCounter *counter, sqlite3 *db) { int count = 0; guint i; g_return_val_if_fail (CB_IS_USER_COUNTER (counter), 0); g_return_val_if_fail (db != NULL, 0); sqlite3_exec (db, "BEGIN TRANSACTION;", NULL, NULL, NULL); for (i = 0; i < counter->user_infos->len; i ++) { sqlite3_stmt *stmt; int ok; CbUserInfo *ui = &g_array_index (counter->user_infos, CbUserInfo, i); if (!ui->changed) continue; ui->changed = FALSE; /* Actually save entry in DB */ ok = sqlite3_prepare_v2 (db, "INSERT OR REPLACE INTO `user_cache` (id, screen_name, user_name, score, verified, protected_account) " "VALUES(?, ?, ?, ?, ?, ?)", -1, &stmt, NULL); if (ok != SQLITE_OK) { g_warning ("SQL Error: %s", sqlite3_errmsg (db)); continue; } sqlite3_bind_int64 (stmt, 1, ui->user_id); sqlite3_bind_text (stmt, 2, ui->screen_name, -1, NULL); sqlite3_bind_text (stmt, 3, ui->user_name, -1, NULL); sqlite3_bind_int (stmt, 4, ui->score); sqlite3_bind_int (stmt, 5, ui->verified); sqlite3_bind_int (stmt, 6, ui->protected_account); ok = sqlite3_step (stmt); if (ok != SQLITE_DONE) g_critical ("%s", sqlite3_errstr (ok)); sqlite3_finalize (stmt); count ++; } sqlite3_exec (db, "END TRANSACTION;", NULL, NULL, NULL); counter->changed = FALSE; g_array_remove_range (counter->user_infos, 0, counter->user_infos->len); return count; } void cb_user_counter_query_by_prefix (CbUserCounter *counter, sqlite3 *db, const char *prefix, int max_results, CbUserInfo **results, int *n_results) { int i; struct { GArray *infos; int lowest_score; } query_data; g_return_if_fail (CB_IS_USER_COUNTER (counter)); g_return_if_fail (prefix != NULL); g_return_if_fail (results != NULL); g_return_if_fail (max_results > 0); g_return_if_fail (n_results != NULL); query_data.infos = g_array_new (FALSE, TRUE, sizeof (CbUserInfo)); query_data.lowest_score = G_MAXINT; g_array_set_clear_func (query_data.infos, (GDestroyNotify)cb_user_info_destroy); for (i = 0; i < counter->user_infos->len; i ++) { const CbUserInfo *ui = &g_array_index (counter->user_infos, CbUserInfo, i); char *user_name; char *screen_name; gboolean full = query_data.infos->len >= max_results; /* Will already be in the results from the sql query */ if (!ui->changed) continue; if (full && ui->score < query_data.lowest_score) continue; screen_name = g_utf8_strdown (ui->screen_name, -1); user_name = g_utf8_strdown (ui->user_name, -1); if (g_str_has_prefix (screen_name, prefix) || g_str_has_prefix (user_name, prefix)) { CbUserInfo *new_ui; /* Copy user info into result array */ g_array_set_size (query_data.infos, query_data.infos->len + 1); new_ui = &g_array_index (query_data.infos, CbUserInfo, query_data.infos->len - 1); new_ui->user_id = ui->user_id; new_ui->screen_name = g_strdup (ui->screen_name); new_ui->user_name = g_strdup (ui->user_name); new_ui->score = ui->score; new_ui->verified = ui->verified; new_ui->protected_account = ui->protected_account; query_data.lowest_score = MIN (query_data.lowest_score, ui->score); } g_free (user_name); g_free (screen_name); } if (query_data.infos->len == 0) query_data.lowest_score = -1; sqlite3_stmt *stmt; int status = sqlite3_prepare_v2 (db, "SELECT `id`, `screen_name`, `user_name`, `score`, " "`verified`, `protected_account` " "FROM `user_cache` WHERE `screen_name` LIKE ? " "OR `user_name` LIKE ? ORDER BY `score` DESC LIMIT ? " "COLLATE NOCASE;", -1, &stmt, NULL); if (status != SQLITE_OK) { g_warning ("%s:%d SQL Error: %s", __FUNCTION__, __LINE__, sqlite3_errmsg (db)); } char *sql_prefix = strdup(prefix); strcat(sql_prefix, "%"); sqlite3_bind_text (stmt, 1, sql_prefix, -1, NULL); sqlite3_bind_text (stmt, 2, sql_prefix, -1, NULL); sqlite3_bind_int (stmt, 3, max_results); while ((status = sqlite3_step(stmt)) == SQLITE_ROW) { gboolean found = FALSE; CbUserInfo *ui; guint i; gint64 user_id; user_id = sqlite3_column_int64(stmt, 0); /* Check for duplicates first */ for (i = 0; i < query_data.infos->len; i ++) { const CbUserInfo *_ui = &g_array_index (query_data.infos, CbUserInfo, i); if (_ui->user_id == user_id) { found = TRUE; break; } } if (found) { continue; } g_array_set_size (query_data.infos, query_data.infos->len + 1); ui = &g_array_index (query_data.infos, CbUserInfo, query_data.infos->len - 1); ui->user_id = user_id; // Apparently it's safe to cast unsigned characters to signed ui->screen_name = g_strdup ((char *) sqlite3_column_text(stmt, 1)); ui->user_name = g_strdup ((char *) sqlite3_column_text(stmt, 2)); ui->score = atoi ((char *) sqlite3_column_text(stmt, 3)); ui->verified = *sqlite3_column_text(stmt, 4) == '1'; ui->protected_account = *sqlite3_column_text(stmt, 5) == '1'; query_data.lowest_score = MIN (query_data.lowest_score, ui->score); } if (status != SQLITE_DONE) { g_warning ("%s:%d SQL Error: %s", __FUNCTION__, __LINE__, sqlite3_errmsg (db)); } sqlite3_finalize (stmt); free(sql_prefix); /* Now sort after score */ g_array_sort (query_data.infos, score_sort); /* Remove everything after max_results */ if (query_data.infos->len > max_results) g_array_remove_range (query_data.infos, max_results, query_data.infos->len - max_results); g_assert (query_data.infos->len <= max_results); /* Just use the GArray's data */ *n_results = query_data.infos->len; *results = (CbUserInfo*) g_array_free (query_data.infos, FALSE); } static void cb_user_counter_finalize (GObject *obj) { CbUserCounter *counter = CB_USER_COUNTER (obj); g_array_free (counter->user_infos, TRUE); G_OBJECT_CLASS (cb_user_counter_parent_class)->finalize (obj); } static void cb_user_counter_class_init (CbUserCounterClass *counter_class) { GObjectClass *object_class = G_OBJECT_CLASS (counter_class); object_class->finalize = cb_user_counter_finalize; } static void cb_user_counter_init (CbUserCounter *counter) { counter->changed = FALSE; counter->user_infos = g_array_new (FALSE, TRUE, sizeof (CbUserInfo)); g_array_set_clear_func (counter->user_infos, (GDestroyNotify)cb_user_info_destroy); } cawbird-1.4.2/src/CbUserCounter.h000066400000000000000000000053331416632607600166330ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef USER_COUNTER_H #define USER_COUNTER_H #include #include #include "CbTypes.h" G_BEGIN_DECLS typedef struct _CbUserInfo CbUserInfo; struct _CbUserInfo { gint64 user_id; char *screen_name; char *user_name; guint score; guint changed : 1; guint verified : 1; guint protected_account : 1; }; typedef struct _CbUserCounter CbUserCOunter; struct _CbUserCounter { GObject parent_instance; guint changed : 1; GArray *user_infos; }; #define CB_TYPE_USER_COUNTER cb_user_counter_get_type () G_DECLARE_FINAL_TYPE (CbUserCounter, cb_user_counter, CB, USER_COUNTER, GObject); GType cb_user_counter_get_type (void) G_GNUC_CONST; CbUserCounter * cb_user_counter_new (void); void cb_user_counter_id_seen (CbUserCounter *counter, const CbUserIdentity *id); void cb_user_counter_user_seen (CbUserCounter *counter, gint64 user_id, const char *screen_name, const char *user_name); void cb_user_counter_user_seen_full (CbUserCounter *counter, gint64 user_id, const char *screen_name, const char *user_name, gboolean verified, gboolean protected_account); int cb_user_counter_save (CbUserCounter *counter, sqlite3 *db); void cb_user_counter_query_by_prefix (CbUserCounter *counter, sqlite3 *db, const char *prefix, int max_results, CbUserInfo **results, int *n_results); /* CbUserInfo */ void cb_user_info_destroy (CbUserInfo *info); G_END_DECLS #endif cawbird-1.4.2/src/CbUserStream.c000066400000000000000000000627361416632607600164540ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "cawbird.h" #include "CbUserStream.h" #include "CbUtils.h" #include #include #define short_url_length 23 G_DEFINE_TYPE (CbUserStream, cb_user_stream, G_TYPE_OBJECT); gboolean load_timeline_tweets (gpointer user_data); gboolean load_mentions_tweets (gpointer user_data); gboolean load_favourited_tweets (gpointer user_data); gboolean load_dm_tweets (gpointer user_data); enum { INTERRUPTED, RESUMED, LAST_SIGNAL }; enum { STATE_STOPPED, /* Initial state */ STATE_RUNNING, /* Started and message received */ STATE_STARTED, /* Started, but no message/heartbeat received yet */ STATE_STOPPING, /* Stopping the stream */ }; static guint user_stream_signals[LAST_SIGNAL] = { 0 }; static void cb_user_stream_finalize (GObject *o) { CbUserStream *self = CB_USER_STREAM (o); cb_user_stream_stop (self); g_ptr_array_unref (self->receivers); g_free (self->account_name); if (self->network_changed_id != 0) { g_signal_handler_disconnect (self->network_monitor, self->network_changed_id); } G_OBJECT_CLASS (cb_user_stream_parent_class)->finalize (o); } static void cb_user_stream_restart (CbUserStream *self) { self->restarting = TRUE; cb_user_stream_stop (self); cb_user_stream_start (self); } static gboolean network_cb (gpointer user_data) { CbUserStream *self = user_data; gboolean available; if (self->state == STATE_RUNNING) { self->network_timeout_id = 0; return G_SOURCE_REMOVE; } available = g_network_monitor_get_network_available (self->network_monitor); if (available) { g_debug ("%u Restarting stream (reason: network available (timeout))", self->state); self->network_timeout_id = 0; cb_user_stream_restart (self); return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } static void start_network_timeout (CbUserStream *self) { if (self->network_timeout_id != 0) return; self->network_timeout_id = g_timeout_add (1 * 1000, network_cb, self); } static void network_changed_cb (GNetworkMonitor *monitor, gboolean available, gpointer user_data) { CbUserStream *self = user_data; if (available == self->network_available) return; self->network_available = available; if (available) { g_debug ("%u Restarting stream (reason: Network available (callback))", self->state); cb_user_stream_restart (self); } else { g_debug ("%u Connection lost (%s) Reason: network unavailable", self->state, self->account_name); g_signal_emit (self, user_stream_signals[INTERRUPTED], 0); cb_clear_source (&self->heartbeat_timeout_id); start_network_timeout (self); } } static void cb_user_stream_init (CbUserStream *self) { self->receivers = g_ptr_array_new (); self->restarting = FALSE; self->state = STATE_STOPPED; self->proxy_data_set = FALSE; self->network_monitor = g_network_monitor_get_default (); self->network_available = g_network_monitor_get_network_available (self->network_monitor); self->network_changed_id = g_signal_connect (self->network_monitor, "network-changed", G_CALLBACK (network_changed_cb), self); if (!self->network_available) start_network_timeout (self); } static void cb_user_stream_class_init (CbUserStreamClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = cb_user_stream_finalize; user_stream_signals[INTERRUPTED] = g_signal_new ("interrupted", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); user_stream_signals[RESUMED] = g_signal_new ("resumed", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } CbUserStream * cb_user_stream_new (const char *account_name, OAuthProxy *proxy) { CbUserStream *self = CB_USER_STREAM (g_object_new (CB_TYPE_USER_STREAM, NULL)); self->account_name = g_strdup (account_name); self->proxy = REST_PROXY(proxy); g_debug ("Creating stream for %s", account_name); return self; } void stream_tweet (CbUserStream *self, CbStreamMessageType message_type, JsonNode *node) { guint i; #if DEBUG JsonGenerator *gen = json_generator_new (); json_generator_set_root (gen, node); json_generator_set_pretty (gen, FALSE); gchar *json_dump = json_generator_to_data (gen, NULL); g_debug ("Message with type %d on stream @%s: %s", message_type, self->account_name, json_dump); #else g_debug ("Message with type %d on stream @%s", message_type, self->account_name); #endif if (message_type == CB_STREAM_MESSAGE_UNSUPPORTED) { g_debug ("Skipped unsupported message on stream @%s\n", self->account_name); return; } for (i = 0; i < self->receivers->len; i++) { cb_message_receiver_stream_message_received (g_ptr_array_index (self->receivers, i), message_type, node); } } void cb_user_stream_inject_tweet (CbUserStream *self, CbStreamMessageType message_type, const gchar *content) { JsonParser *parser; JsonNode *root_node; JsonObject *root_obj; GError *error = NULL; parser = json_parser_new (); json_parser_load_from_data (parser, content, -1, &error); if (error) { g_warning("Failed to parse %s", content); return; } root_node = json_parser_get_root (parser); root_obj = json_node_get_object (root_node); if (json_object_has_member (root_obj, "event")) { // We've got a single DM, which is wrapped, so unwrap it root_node = json_object_get_member (root_obj, "event"); } else if (json_object_has_member (root_obj, "quoted_status_permalink")) { // Quote tweets don't include the quoted tweet URL in the returned text, but they do when they come in the timeline // So we need to fudge it here and add the URL entity and the text before we send it to the app JsonObject *entities; JsonArray *urls; JsonObject *permalink = json_object_get_object_member (root_obj, "quoted_status_permalink"); const gchar *quoted_url = json_object_get_string_member (permalink, "url"); entities = json_object_get_object_member (root_obj, "entities"); urls = json_object_get_array_member (entities, "urls"); gboolean url_found = FALSE; for (guint i = 0, p = json_array_get_length (urls); i < p; i ++) { JsonObject *url_obj = json_node_get_object (json_array_get_element (urls, i)); const char *url = json_object_get_string_member (url_obj, "url"); if (!g_strcmp0 (url, quoted_url)) { url_found = TRUE; break; } } if (!url_found) { // Get the old text length JsonArray *display_range = json_object_get_array_member (root_obj, "display_text_range"); guint64 old_length = json_array_get_int_element (display_range, 1); // Create and set the new text gchar *new_full_text = g_strdup_printf ("%s %s", json_object_get_string_member (root_obj, "full_text"), quoted_url); json_object_set_string_member (root_obj, "full_text", new_full_text); guint64 old_length_with_space = old_length + 1; guint64 new_length = old_length_with_space + short_url_length; g_free (new_full_text); // Build the URL entity JsonObject *url_obj = json_object_new (); json_object_set_string_member (url_obj, "url", quoted_url); json_object_set_string_member (url_obj, "expanded_url", json_object_get_string_member (permalink, "expanded")); json_object_set_string_member (url_obj, "display_url", json_object_get_string_member (permalink, "display")); JsonArray *indicies = json_array_sized_new (2); json_array_add_int_element (indicies, old_length_with_space); json_array_add_int_element (indicies, new_length); json_object_set_array_member (url_obj, "indices", indicies); json_array_add_object_element (urls, url_obj); // Update the text length json_array_remove_element (display_range, 1); json_array_add_int_element (display_range, new_length); } } stream_tweet (self, message_type, root_node); } // TODO: Refactor a common "load_tweets_done" that parses, sets the last ID and sends the right message type void load_timeline_tweets_done (GObject *source_object, GAsyncResult *result, gpointer user_data) { CbUserStream *self = user_data; GError *error = NULL; JsonNode *root_node; JsonArray *root_arr; guint len; root_node = cb_utils_load_threaded_finish (result, &error); if (error != NULL) { g_warning ("%s: %s (%s - %d)", __FUNCTION__, error->message, g_quark_to_string (error->domain), error->code); if (error->domain == REST_PROXY_ERROR && error->code == REST_PROXY_ERROR_SSL) { g_debug ("Reloading timeline on SSL failure"); load_timeline_tweets (self); } return; } root_arr = json_node_get_array (root_node); len = json_array_get_length (root_arr); g_debug ("Got %d timeline tweets", len); gboolean first_load = self->last_home_id == 0; for (guint i = len; i > 0; i--) { JsonNode *node = json_array_get_element (root_arr, i - 1); JsonObject *obj = json_node_get_object (node); self->last_home_id = json_object_get_int_member (obj, "id"); stream_tweet(self, CB_STREAM_MESSAGE_TWEET, node); } if (first_load) { stream_tweet (self, CB_STREAM_MESSAGE_TIMELINE_LOADED, json_node_new(JSON_NODE_NULL)); } g_cancellable_cancel(self->home_cancellable); self->home_cancellable = NULL; } gboolean load_timeline_tweets (gpointer user_data) { CbUserStream *self = user_data; if (self->home_cancellable && ! g_cancellable_is_cancelled(self->home_cancellable)) { g_debug ("Cancelling existing timeline cancellable"); g_cancellable_cancel(self->home_cancellable); } gboolean is_first_load = self->last_home_id == 0; char* requested_tweet_count = is_first_load ? "28" : "200"; RestProxyCall *proxy_call = rest_proxy_new_call (self->proxy); g_debug("Loading timeline tweets"); rest_proxy_call_set_function (proxy_call, "1.1/statuses/home_timeline.json"); rest_proxy_call_set_method (proxy_call, "GET"); rest_proxy_call_add_param (proxy_call, "count", requested_tweet_count); rest_proxy_call_add_param (proxy_call, "contributor_details", "true"); rest_proxy_call_add_param (proxy_call, "include_my_retweet", "true"); rest_proxy_call_add_param (proxy_call, "tweet_mode", "extended"); rest_proxy_call_add_param (proxy_call, "include_ext_alt_text", "true"); if (!is_first_load) { char since_id [21]; // We may occasionally miss tweets (bug #147). This appears to be because of eventual consistency (tweets appear at the server // that we query *after* our last query but are timestamped *before* the 'since' ID for that query). So we need to try and overlap a bit. // Tweet IDs are "snowflakes" with 12 bits of sequence (lowest), 5 bits of worker ID, 5 bits of data centre, and then the timestamp. // https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L27-L36 // The timestamp is to millisecond accuracy, so we want to ignore the last three base-10 digits. Plus more digits to give more than // one second of overlap. 10 bits is ~1s and every extra bit is double that. // We mask the ID with that value to see whether we get any missed tweets. // Note: this will result in at least the last tweet being reloaded each time. gint timestamp_shift = 5 + 5 + 12; gint overlap_shift = 13; // 13bits ~= 8 seconds sprintf(since_id, "%" G_GINT64_FORMAT, self->last_home_id & (-1L << (timestamp_shift + overlap_shift))); rest_proxy_call_add_param(proxy_call, "since_id", since_id); } self->home_cancellable = g_cancellable_new(); cb_utils_load_threaded_async (proxy_call, self->home_cancellable, load_timeline_tweets_done, self); return TRUE; } void load_mentions_tweets_done (GObject *source_object, GAsyncResult *result, gpointer user_data) { CbUserStream *self = user_data; GError *error = NULL; JsonNode *root_node; JsonArray *root_arr; guint len; root_node = cb_utils_load_threaded_finish (result, &error); if (error != NULL) { g_warning ("%s: %s (%s - %d)", __FUNCTION__, error->message, g_quark_to_string (error->domain), error->code); if (error->domain == REST_PROXY_ERROR && error->code == REST_PROXY_ERROR_SSL) { g_debug ("Reloading mentions on SSL failure"); load_mentions_tweets (self); } return; } root_arr = json_node_get_array (root_node); len = json_array_get_length (root_arr); g_debug ("Got %d mention tweets", len); gboolean first_load = self->last_mentions_id == 0; for (guint i = len; i > 0; i--) { JsonNode *node = json_array_get_element (root_arr, i - 1); JsonObject *obj = json_node_get_object (node); self->last_mentions_id = json_object_get_int_member (obj, "id"); stream_tweet(self, CB_STREAM_MESSAGE_MENTION, node); } if (first_load) { stream_tweet (self, CB_STREAM_MESSAGE_MENTIONS_LOADED, json_node_new(JSON_NODE_NULL)); } g_cancellable_cancel(self->mentions_cancellable); self->mentions_cancellable = NULL; } gboolean load_mentions_tweets (gpointer user_data) { CbUserStream *self = user_data; if (self->mentions_cancellable && ! g_cancellable_is_cancelled(self->mentions_cancellable)) { g_debug ("Cancelling existing mentions cancellable"); g_cancellable_cancel(self->mentions_cancellable); } gboolean is_first_load = self->last_mentions_id == 0; char* requested_tweet_count = is_first_load ? "28" : "200"; RestProxyCall *proxy_call = rest_proxy_new_call (self->proxy); g_debug("Loading mention tweets"); rest_proxy_call_set_function (proxy_call, "1.1/statuses/mentions_timeline.json"); rest_proxy_call_set_method (proxy_call, "GET"); rest_proxy_call_add_param (proxy_call, "count", requested_tweet_count); rest_proxy_call_add_param (proxy_call, "contributor_details", "true"); rest_proxy_call_add_param (proxy_call, "include_my_retweet", "true"); rest_proxy_call_add_param (proxy_call, "include_entities", "true"); rest_proxy_call_add_param (proxy_call, "tweet_mode", "extended"); rest_proxy_call_add_param (proxy_call, "include_ext_alt_text", "true"); if (!is_first_load) { char since_id [20]; sprintf(since_id, "%ld", self->last_mentions_id); rest_proxy_call_add_param(proxy_call, "since_id", since_id); } self->mentions_cancellable = g_cancellable_new(); cb_utils_load_threaded_async (proxy_call, self->mentions_cancellable, load_mentions_tweets_done, self); return TRUE; } void load_favourited_tweets_done (GObject *source_object, GAsyncResult *result, gpointer user_data) { CbUserStream *self = user_data; GError *error = NULL; JsonNode *root_node; JsonArray *root_arr; guint len; root_node = cb_utils_load_threaded_finish (result, &error); if (error != NULL) { g_warning ("%s: %s (%s - %d)", __FUNCTION__, error->message, g_quark_to_string (error->domain), error->code); if (error->domain == REST_PROXY_ERROR && error->code == REST_PROXY_ERROR_SSL) { g_debug ("Reloading favorited on SSL failure"); load_favourited_tweets (self); } return; } root_arr = json_node_get_array (root_node); len = json_array_get_length (root_arr); g_debug ("Got %d favourited tweets", len); gboolean first_load = self->last_favourited_id == 0; for (guint i = len; i > 0; i--) { JsonNode *node = json_array_get_element (root_arr, i - 1); JsonObject *obj = json_node_get_object (node); self->last_favourited_id = json_object_get_int_member (obj, "id"); stream_tweet(self, CB_STREAM_MESSAGE_EVENT_FAVORITE, node); } if (first_load) { stream_tweet (self, CB_STREAM_MESSAGE_FAVORITES_LOADED, json_node_new(JSON_NODE_NULL)); } g_cancellable_cancel(self->favourited_cancellable); self->favourited_cancellable = NULL; } gboolean load_favourited_tweets (gpointer user_data) { CbUserStream *self = user_data; if (self->favourited_cancellable && ! g_cancellable_is_cancelled(self->favourited_cancellable)) { g_debug ("Cancelling existing favourites cancellable"); g_cancellable_cancel(self->favourited_cancellable); } gboolean is_first_load = self->last_favourited_id == 0; char* requested_tweet_count = is_first_load ? "28" : "200"; RestProxyCall *proxy_call = rest_proxy_new_call (self->proxy); g_debug("Loading favourited tweets"); rest_proxy_call_set_function (proxy_call, "1.1/favorites/list.json"); rest_proxy_call_set_method (proxy_call, "GET"); rest_proxy_call_add_param (proxy_call, "count", requested_tweet_count); rest_proxy_call_add_param (proxy_call, "contributor_details", "true"); rest_proxy_call_add_param (proxy_call, "include_my_retweet", "true"); rest_proxy_call_add_param (proxy_call, "include_entities", "true"); rest_proxy_call_add_param (proxy_call, "tweet_mode", "extended"); rest_proxy_call_add_param (proxy_call, "include_ext_alt_text", "true"); if (!is_first_load) { char since_id [20]; sprintf(since_id, "%ld", self->last_favourited_id); rest_proxy_call_add_param(proxy_call, "since_id", since_id); } self->favourited_cancellable = g_cancellable_new(); cb_utils_load_threaded_async (proxy_call, self->favourited_cancellable, load_favourited_tweets_done, self); return TRUE; } // Fix a cyclic definition void load_dm_tweets_with_cursor (gpointer user_data, const gchar *cursor); void load_dm_tweets_done (GObject *source_object, GAsyncResult *result, gpointer user_data) { CbUserStream *self = user_data; GError *error = NULL; JsonNode *root_node; JsonObject *root_obj; JsonArray *root_arr; guint len; root_node = cb_utils_load_threaded_finish (result, &error); if (error != NULL) { g_warning ("%s: %s (%s - %d)", __FUNCTION__, error->message, g_quark_to_string (error->domain), error->code); if (error->domain == REST_PROXY_ERROR && error->code == REST_PROXY_ERROR_SSL) { g_debug ("Reloading DMs on SSL failure"); load_dm_tweets (self); } return; } root_obj = json_node_get_object (root_node); root_arr = json_object_get_array_member(root_obj, "events"); len = json_array_get_length (root_arr); // TODO: Look for a "next_cursor" and load older DMs // https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/list-events g_debug ("Got %d DMs", len); gboolean all_newer = TRUE; gboolean all_older = TRUE; for (guint i = len; i > 0; i--) { JsonNode *node = json_array_get_element (root_arr, i - 1); JsonObject *obj = json_node_get_object (node); int message_type = CB_STREAM_MESSAGE_UNSUPPORTED; const gchar *type = json_object_get_string_member(obj, "type"); if (strcmp(type, "message_create") == 0) { message_type = CB_STREAM_MESSAGE_DIRECT_MESSAGE; } gint64 id = strtol (json_object_get_string_member (obj, "id"), NULL, 10); if (id < self->first_dm_id) { self->first_dm_id = id; all_newer = FALSE; } else if (id <= self->last_dm_id) { all_older = FALSE; all_newer = FALSE; // DMs behave differently to other "timelines" so we need to ignore messages we've seen // And we assume we've seen it if it has an older ID. But we can't break because later // in the collection is newer and might be unseen. continue; } else { all_older = FALSE; if (id > self->new_last_dm_id) { self->new_last_dm_id = id; } } stream_tweet (self, message_type, node); } g_cancellable_cancel(self->dm_cancellable); self->dm_cancellable = NULL; gboolean first_load = self->last_dm_id == 0; // Limit fetches for throttling. 5 at first load lets us load ~300 old DMs from last 30 days. // 1 recursion when scheduled every 2 minutes runs right to the "15/15min" limit. // This *should* only cause throttling problems just when someone has a big backlog AND has // lots of tweets coming in every 2 minutes. unsigned char max_recursions = first_load ? 5 : 1; if ((all_newer || all_older) && self->dm_recursions < max_recursions && json_object_has_member(root_obj, "next_cursor")) { self->dm_recursions++; const gchar *cursor = json_object_get_string_member(root_obj, "next_cursor"); load_dm_tweets_with_cursor(user_data, cursor); } else { if (first_load) { stream_tweet (self, CB_STREAM_MESSAGE_DIRECT_MESSAGES_LOADED, json_node_new(JSON_NODE_NULL)); } self->last_dm_id = self->new_last_dm_id; self->dm_recursions = 0; } } void load_dm_tweets_with_cursor (gpointer user_data, const gchar *cursor) { CbUserStream *self = user_data; if (self->dm_cancellable && ! g_cancellable_is_cancelled(self->dm_cancellable)) { g_debug ("Cancelling existing cancellable"); g_cancellable_cancel(self->dm_cancellable); } RestProxyCall *proxy_call = rest_proxy_new_call (self->proxy); rest_proxy_call_set_function (proxy_call, "1.1/direct_messages/events/list.json"); rest_proxy_call_set_method (proxy_call, "GET"); rest_proxy_call_add_param (proxy_call, "count", "50"); if (cursor) { rest_proxy_call_add_param(proxy_call, "cursor", cursor); } g_debug("Loading DM tweets for cursor %s", cursor ? cursor : "none"); self->dm_cancellable = g_cancellable_new(); cb_utils_load_threaded_async (proxy_call, self->dm_cancellable, load_dm_tweets_done, self); } gboolean load_dm_tweets (gpointer user_data) { load_dm_tweets_with_cursor(user_data, NULL); return TRUE; } void cb_user_stream_start (CbUserStream *self) { g_debug("Loading timeline tweets on start"); load_timeline_tweets (self); g_debug("Loading mention tweets on start"); load_mentions_tweets (self); g_debug("Loading favourited tweets on start"); load_favourited_tweets (self); g_debug("Loading DMs on start"); load_dm_tweets (self); if (!self->timeline_timeout) { g_debug("Adding timeout for timeline"); self->timeline_timeout = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 60 * 2, load_timeline_tweets, self, NULL); } if (!self->mentions_timeout) { g_debug("Adding timeout for mentions"); self->mentions_timeout = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 60 * 2, load_mentions_tweets, self, NULL); } if (!self->favourited_timeout) { g_debug("Adding timeout for favourites"); self->favourited_timeout = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 60 * 2, load_favourited_tweets, self, NULL); } if (!self->dm_timeout) { g_debug("Adding timeout for DMs"); self->dm_timeout = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT, 60 * 2, load_dm_tweets, self, NULL); } } void cb_user_stream_stop (CbUserStream *self) { if (self->timeline_timeout) { g_source_remove (self->timeline_timeout); self->timeline_timeout = 0; } if (self->mentions_timeout) { g_source_remove (self->mentions_timeout); self->mentions_timeout = 0; } if (self->favourited_timeout) { g_source_remove (self->favourited_timeout); self->favourited_timeout = 0; } if (self->dm_timeout) { g_source_remove (self->dm_timeout); self->dm_timeout = 0; } } void cb_user_stream_set_proxy_data (CbUserStream *self, const char *token, const char *token_secret) { // oauth_proxy_set_token (OAUTH_PROXY (self->proxy), token); // oauth_proxy_set_token_secret (OAUTH_PROXY (self->proxy), token_secret); self->proxy_data_set = TRUE; } void cb_user_stream_register (CbUserStream *self, CbMessageReceiver *receiver) { g_ptr_array_add (self->receivers, receiver); } void cb_user_stream_unregister (CbUserStream *self, CbMessageReceiver *receiver) { guint i; for (i = 0; i < self->receivers->len; i ++) { CbMessageReceiver *r = g_ptr_array_index (self->receivers, i); if (r == receiver) { g_ptr_array_remove_index_fast (self->receivers, i); break; } } }cawbird-1.4.2/src/CbUserStream.h000066400000000000000000000061571416632607600164540ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef __CB_USER_STREAM_H__ #define __CB_USER_STREAM_H__ #include #include #include #include "CbMessageReceiver.h" #include "CbTypes.h" G_BEGIN_DECLS #define CB_TYPE_USER_STREAM (cb_user_stream_get_type ()) G_DECLARE_FINAL_TYPE (CbUserStream, cb_user_stream, CB, USER_STREAM, GObject); struct _CbUserStream { GObject parent_instance; GPtrArray *receivers; RestProxy *proxy; GNetworkMonitor *network_monitor; guint network_timeout_id; guint heartbeat_timeout_id; guint network_changed_id; gint64 last_home_id; guint timeline_timeout; GCancellable *home_cancellable; gint64 last_mentions_id; guint mentions_timeout; GCancellable *mentions_cancellable; gint64 last_favourited_id; guint favourited_timeout; GCancellable *favourited_cancellable; gint64 first_dm_id; gint64 last_dm_id; gint64 new_last_dm_id; // Placeholder for the next value of last_dm_id so that we can page back if lots of tweets came in unsigned char dm_recursions; guint dm_timeout; GCancellable *dm_cancellable; char *account_name; guint state; guint restarting : 1; guint proxy_data_set : 1; guint network_available: 1; }; typedef struct _CbUserStream CbUserStream; CbUserStream * cb_user_stream_new (const char *account_name, OAuthProxy *proxy); void cb_user_stream_set_proxy_data (CbUserStream *self, const char *token, const char *token_secret); void cb_user_stream_register (CbUserStream *self, CbMessageReceiver *receiver); void cb_user_stream_unregister (CbUserStream *self, CbMessageReceiver *receiver); void cb_user_stream_start (CbUserStream *self); void cb_user_stream_stop (CbUserStream *self); void cb_user_stream_push_data (CbUserStream *self, const char *data); void cb_user_stream_inject_tweet (CbUserStream *self, CbStreamMessageType message_type, const gchar *content) G_END_DECLS; #endif cawbird-1.4.2/src/CbUtils.c000066400000000000000000000414541416632607600154540ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #include "CbUtils.h" #include #include #include void cb_utils_bind_model (GtkWidget *listbox, GListModel *model, GtkListBoxCreateWidgetFunc func, void *data) { g_return_if_fail (GTK_IS_LIST_BOX (listbox)); g_return_if_fail (G_IS_LIST_MODEL (model)); /* This entire function is just a hack around valac ref'ing the listbox * in its own constructor when calling gtk_list_box_bind_model there */ gtk_list_box_bind_model (GTK_LIST_BOX (listbox), model, func, data, NULL); } typedef struct { GtkWidget *listbox; CbUtilsCreateWidgetFunc create_widget_func; gpointer create_widget_func_data; } ModelData; static void non_gobject_model_changed (GListModel *model, guint position, guint removed, guint added, gpointer user_data) { ModelData *data = user_data; GtkListBox *box = GTK_LIST_BOX (data->listbox); guint i; while (removed--) { GtkListBoxRow *row; row = gtk_list_box_get_row_at_index (box, position); gtk_container_remove (GTK_CONTAINER (box), GTK_WIDGET (row)); } for (i = 0; i < added; i++) { gpointer item; GtkWidget *widget; item = g_list_model_get_item (model, position + i); widget = data->create_widget_func (item, data->create_widget_func_data); /* We allow the create_widget_func to either return a full * reference or a floating reference. If we got the floating * reference, then turn it into a full reference now. That means * that gtk_list_box_insert() will take another full reference. * Finally, we'll release this full reference below, leaving only * the one held by the box. */ if (g_object_is_floating (widget)) g_object_ref_sink (widget); gtk_widget_show (widget); gtk_list_box_insert (box, widget, position + i); g_object_unref (widget); } } void cb_utils_bind_non_gobject_model (GtkWidget *listbox, GListModel *model, CbUtilsCreateWidgetFunc func, gpointer *user_data) { ModelData *data; g_return_if_fail (GTK_IS_LIST_BOX (listbox)); g_return_if_fail (G_IS_LIST_MODEL (model)); g_return_if_fail (g_list_model_get_item_type (model) == G_TYPE_POINTER); g_return_if_fail (g_object_get_data (G_OBJECT (listbox), "model-hack") == NULL); data = g_malloc (sizeof (*data)); data->listbox = listbox; data->create_widget_func = func; data->create_widget_func_data = user_data; g_signal_connect (model, "items-changed", G_CALLBACK (non_gobject_model_changed), data); g_object_set_data (G_OBJECT (listbox), "model-hack", data); } void cb_utils_unbind_non_gobject_model (GtkWidget *listbox, GListModel *model) { ModelData *data; g_return_if_fail (GTK_IS_LIST_BOX (listbox)); g_return_if_fail (G_IS_LIST_MODEL (model)); g_return_if_fail (g_object_get_data (G_OBJECT (listbox), "model-hack") != NULL); data = g_object_get_data (G_OBJECT (listbox), "model-hack"); g_signal_handlers_disconnect_by_func (model, non_gobject_model_changed, data); g_assert (data != NULL); g_free (data); } void cb_utils_linkify_user (const CbUserIdentity *user, GString *str) { g_string_append (str, "user_name) > 0) { char *s1, *s2, *s3, *s4; /* TODO: Write one function doing all 4 things, since we need that often * and execute it often? */ s1 = cb_utils_escape_quotes (user->user_name); s2 = cb_utils_escape_ampersands (s1); s3 = cb_utils_escape_quotes (s2); s4 = cb_utils_escape_ampersands (s3); g_string_append (str, "title=\""); g_string_append (str, s4); g_string_append_c (str, '"'); g_free (s1); g_free (s2); g_free (s3); g_free (s4); } g_string_append (str, ">@"); g_string_append (str, user->screen_name); g_string_append (str, ""); } char * cb_utils_escape_quotes (const char *in) { if (in == NULL) { return NULL; } gsize bytes = strlen (in); gsize n_quotes = 0; const char *p = in; gunichar c; char *result; const char *last; char *out_pos; c = g_utf8_get_char (p); while (c != '\0') { if (c == '"') n_quotes ++; p = g_utf8_next_char (p); c = g_utf8_get_char (p); } result = g_malloc (bytes + (n_quotes * 5) + 1); result[bytes + (n_quotes * 5)] = '\0'; p = in; c = g_utf8_get_char (p); last = p; out_pos = result; while (c != '\0') { if (c == '"') { int bytes = p - last; memcpy (out_pos, last, bytes); last = p; out_pos[bytes + 0] = '&'; out_pos[bytes + 1] = 'q'; out_pos[bytes + 2] = 'u'; out_pos[bytes + 3] = 'o'; out_pos[bytes + 4] = 't'; out_pos[bytes + 5] = ';'; last += 1; /* Skip " */ out_pos += bytes + 6; } p = g_utf8_next_char (p); c = g_utf8_get_char (p); } memcpy (out_pos, last, p - last); return result; } /* TODO: Code duplication here with escape_quotes */ char * cb_utils_escape_ampersands (const char *in) { if (in == NULL) { return NULL; } gsize bytes = strlen (in); gsize n_ampersands = 0; const char *p = in; gunichar c; char *result; const char *last; char *out_pos; c = g_utf8_get_char (p); while (c != '\0') { if (c == '&') n_ampersands ++; p = g_utf8_next_char (p); c = g_utf8_get_char (p); } /* 'amp;' and not '&' since the input already contains the '&' */ result = g_malloc (bytes + (n_ampersands * strlen ("amp;")) + 1); result[bytes + (n_ampersands * strlen ("amp;"))] = '\0'; p = in; c = g_utf8_get_char (p); last = p; out_pos = result; while (c != '\0') { if (c == '&') { int bytes = p - last; memcpy (out_pos, last, bytes); last = p; out_pos[bytes + 0] = '&'; out_pos[bytes + 1] = 'a'; out_pos[bytes + 2] = 'm'; out_pos[bytes + 3] = 'p'; out_pos[bytes + 4] = ';'; last += 1; /* Skip & */ out_pos += bytes + 5; } p = g_utf8_next_char (p); c = g_utf8_get_char (p); } memcpy (out_pos, last, p - last); return result; } GDateTime * cb_utils_parse_date (const char *_in) { char in[31]; const char *month_str; int year, month, hour, minute, day; GDateTime *result; GDateTime *result_local; GTimeZone *time_zone; GTimeZone *local_time_zone; double seconds; /* The input string is ASCII, in the form 'Wed Jun 20 19:01:28 +0000 2012' */ if (!_in) return g_date_time_new_now_local (); g_assert (strlen (_in) == 30); memcpy (in, _in, 30); in[3] = '\0'; in[7] = '\0'; in[10] = '\0'; in[13] = '\0'; in[16] = '\0'; in[19] = '\0'; in[25] = '\0'; in[30] = '\0'; year = atoi (in + 26); day = atoi (in + 8); hour = atoi (in + 11); minute = atoi (in + 14); seconds = atof (in + 17); month_str = in + 4; switch (month_str[0]) { case 'J': /* January */ if (month_str[1] == 'u' && month_str[2] == 'n') month = 6; else if (month_str[1] == 'u' && month_str[2] == 'l') month = 7; else month = 1; break; case 'F': month = 2; break; case 'M': if (month_str[1] == 'a' && month_str[2] == 'r') month = 3; else month = 5; break; case 'A': if (month_str[1] == 'p') month = 4; else month = 8; break; case 'S': month = 9; break; case 'O': month = 10; break; case 'N': month = 11; break; case 'D': month = 12; break; default: month = 0; g_warn_if_reached (); break; } #if GLIB_CHECK_VERSION(2, 68, 0) time_zone = g_time_zone_new_identifier (in + 20); if (!time_zone) { time_zone = g_time_zone_new_utc (); } #else time_zone = g_time_zone_new (in + 20); #endif result = g_date_time_new (time_zone, year, month, day, hour, minute, seconds); g_assert (result); local_time_zone = g_time_zone_new_local (); result_local = g_date_time_to_timezone (result, local_time_zone); g_time_zone_unref (local_time_zone); g_time_zone_unref (time_zone); g_date_time_unref (result); return result_local; } char * cb_utils_get_file_type (const char *url) { const char *filename; const char *extension; char *type; filename = g_strrstr (url, "/"); if (filename == NULL) filename = url; else filename += 1; extension = g_strrstr (filename, "."); if (extension == NULL) return g_strdup (""); extension += 1; type = g_ascii_strdown (extension, -1); if (strcmp (type, "jpg") == 0) { g_free (type); return g_strdup ("jpeg"); } return type; } char * cb_utils_rest_proxy_call_to_string (RestProxyCall *call) { GString *str = g_string_new (NULL); RestParams *params = rest_proxy_call_get_params (call); GHashTable *params_table = rest_params_as_string_hash_table (params); g_string_append (str, rest_proxy_call_get_method (call)); g_string_append_c (str, ' '); g_string_append (str, rest_proxy_call_get_function (call)); if (g_hash_table_size (params_table) > 0) { GList *keys; GList *l; g_string_append_c (str, '?'); keys = g_hash_table_get_keys (params_table); for (l = keys; l; l = l->next) { const char *value = g_hash_table_lookup (params_table, l->data); g_assert (value); g_string_append (str, (const char *)l->data); g_string_append_c (str, '='); g_string_append (str, value); if (l->next != NULL) g_string_append_c (str, '&'); } g_list_free (keys); } g_hash_table_unref (params_table); return g_string_free (str, FALSE); } static void parse_json_async (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { const char *payload = task_data; JsonParser *parser; JsonNode *root_node; GError *error = NULL; parser = json_parser_new (); json_parser_load_from_data (parser, payload, -1, &error); if (error) { g_task_return_error (task, error); return; } if (g_cancellable_is_cancelled (cancellable)) { g_task_return_pointer (task, NULL, NULL); return; } root_node = json_parser_get_root (parser); g_assert (root_node); g_task_return_pointer (task, json_node_ref (root_node), (GDestroyNotify)json_node_unref); g_object_unref (parser); } static void call_done_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { RestProxyCall *call = REST_PROXY_CALL (source_object); GTask *task = user_data; GError *error = NULL; char *payload; /* Get the json data, run another GTask that actually parses the json and returns the root node */ rest_proxy_call_invoke_finish (call, result, &error); if (error != NULL) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_warning ("%s(%s): %p, %s %s", __FILE__, __FUNCTION__, call, rest_proxy_call_get_function(call), error->message); } g_task_return_error (task, error); return; } payload = g_strdup(rest_proxy_call_get_payload (call)); g_task_set_task_data (task, payload, g_free); g_task_run_in_thread (task, parse_json_async); } void cb_utils_load_threaded_async (RestProxyCall *call, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GTask *task = g_task_new (call, cancellable, callback, user_data); #ifdef DEBUG { char *s = cb_utils_rest_proxy_call_to_string (call); g_debug ("REST: %s", s); g_free (s); } #else g_debug("REST: %s %s", rest_proxy_call_get_method(call), rest_proxy_call_get_function(call)); #endif rest_proxy_call_invoke_async (call, cancellable, call_done_cb, task); } JsonNode * cb_utils_load_threaded_finish (GAsyncResult *result, GError **error) { JsonNode *node = g_task_propagate_pointer (G_TASK (result), error); g_object_unref (result); return node; } static void users_received_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { RestProxyCall *call = REST_PROXY_CALL (source_object); GTask *task = user_data; GError *error = NULL; JsonNode *root_node; JsonArray *root_arr; guint i, len; struct { CbUserIdentity *ids; int length; } *data; /* We are in the main thread again here. */ root_node = cb_utils_load_threaded_finish (result, &error); if (error != NULL) { g_task_return_error (task, error); goto out; } if (g_cancellable_is_cancelled (g_task_get_cancellable (task))) { g_task_return_pointer (task, NULL, NULL); goto out; } g_assert (root_node != NULL); root_arr = json_node_get_array (root_node); len = json_array_get_length (root_arr); data = g_malloc (sizeof (*data)); data->ids = g_new (CbUserIdentity, len); data->length = len; for (i = 0; i < len; i ++) { JsonObject *obj = json_array_get_object_element (root_arr, i); data->ids[i].id = json_object_get_int_member (obj, "id"); data->ids[i].user_name = g_strdup (json_object_get_string_member (obj, "name")); data->ids[i].screen_name = g_strdup (json_object_get_string_member (obj, "screen_name")); data->ids[i].verified = json_object_get_boolean_member (obj, "verified"); data->ids[i].protected_account = json_object_get_boolean_member (obj, "protected"); } json_node_unref (root_node); g_task_return_pointer (task, data, NULL); out: g_object_unref (G_OBJECT (call)); } void cb_utils_query_users_async (RestProxy *proxy, const char *query, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GTask *task; RestProxyCall *call; g_return_if_fail (REST_IS_PROXY (proxy)); g_return_if_fail (query != NULL); call = rest_proxy_new_call (proxy); task = g_task_new (call, cancellable, callback, user_data); rest_proxy_call_set_function (call, "1.1/users/search.json"); rest_proxy_call_set_method (call, "GET"); rest_proxy_call_add_param (call, "q", query); rest_proxy_call_add_param (call, "count", "10"); rest_proxy_call_add_param (call, "include_entities", "false"); cb_utils_load_threaded_async (call, cancellable, users_received_cb, task); } CbUserIdentity * cb_utils_query_users_finish (GAsyncResult *result, int *out_length, GError **error) { struct { CbUserIdentity *ids; int length; } *data = g_task_propagate_pointer (G_TASK (result), error); CbUserIdentity *ids; if (data == NULL) { g_object_unref (result); *out_length = 0; return NULL; } *out_length = data->length; g_object_unref (result); ids = data->ids; g_free (data); return ids; }cawbird-1.4.2/src/CbUtils.h000066400000000000000000000066611416632607600154620ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ #ifndef _CB_UTILS_H_ #define _CB_UTILS_H_ #include #ifdef GDK_WINDOWING_X11 #include #endif //#ifdef GDK_WINDOWING_WAYLAND //#include //#endif #include #include "CbTypes.h" #include #include typedef GtkWidget * (*CbUtilsCreateWidgetFunc) (gpointer *data, gpointer *user_data); void cb_utils_bind_model (GtkWidget *listbox, GListModel *model, GtkListBoxCreateWidgetFunc func, void *data); void cb_utils_bind_non_gobject_model (GtkWidget *listbox, GListModel *model, CbUtilsCreateWidgetFunc func, gpointer *user_data); void cb_utils_unbind_non_gobject_model (GtkWidget *listbox, GListModel *model); void cb_utils_linkify_user (const CbUserIdentity *user, GString *str); void cb_utils_write_reply_text (const CbMiniTweet *t, GString *str); char * cb_utils_escape_quotes (const char *in); char * cb_utils_escape_ampersands (const char *in); GDateTime * cb_utils_parse_date (const char *_in); char * cb_utils_get_file_type (const char *url); char * cb_utils_rest_proxy_call_to_string (RestProxyCall *call); void cb_utils_load_threaded_async (RestProxyCall *call, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); JsonNode *cb_utils_load_threaded_finish (GAsyncResult *result, GError **error); void cb_utils_query_users_async (RestProxy *proxy, const char *query, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); CbUserIdentity * cb_utils_query_users_finish (GAsyncResult *result, int *out_length, GError **error); void cb_utils_init_gui (gint *argc, gchar ***argv); static inline void cb_clear_source (guint *id) { if (*id == 0) return; g_source_remove (*id); *id = 0; } #endif cawbird-1.4.2/src/ComposedTweet.vala000066400000000000000000000054021416632607600173630ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ComposedTweet { private string _text; private GLib.GenericArray _uploads; private int64 _reply_to_id = -1; public int64 reply_to_id { get { return _reply_to_id; } set { assert(_quoted_tweet_url == null); _reply_to_id = value; } } private string? _quoted_tweet_url; public signal void ready(); public ComposedTweet(string text) { _text = text; _uploads = new GLib.GenericArray(4); } public void add_attachment(MediaUpload upload) { upload.progress_complete.connect((message) => { if (message == null) { this.ready(); } }); _uploads.add(upload); } public MediaUpload[] get_attachments() { return _uploads.data; } public string get_text() { if (_quoted_tweet_url != null && _uploads.length > 0) { return "%s %s".printf(_text, get_quoted_url()); } else { return _text; } } public bool has_quote_attachment() { return _quoted_tweet_url != null && _uploads.length == 0; } public void set_quoted_tweet(Cb.Tweet tweet) { assert(reply_to_id == -1); // We can't do this with a ternary to get the relevant mini tweet and then have one format string for that because // it somehow causes segfaults when the minitweet ends up having gibberish values (like an ID of 0 and thousands of media attachments!) if (tweet.retweeted_tweet != null) { _quoted_tweet_url = "https://twitter.com/%s/status/%s".printf(tweet.retweeted_tweet.author.screen_name, tweet.retweeted_tweet.id.to_string()); } else { _quoted_tweet_url = "https://twitter.com/%s/status/%s".printf(tweet.source_tweet.author.screen_name, tweet.source_tweet.id.to_string()); } } public string get_quoted_url() { return _quoted_tweet_url == null ? "" : _quoted_tweet_url; } }cawbird-1.4.2/src/DMManager.vala000066400000000000000000000206751416632607600164050ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class DMManager : GLib.Object { private unowned Account account; private DMThreadsModel threads_model; public bool empty { get { return threads_model.get_n_items () == 0; } } public signal void message_received (DMThread thread, int64 message_id, string text); public signal void thread_changed (DMThread thread); public DMManager.for_account (Account account) { this.account = account; this.threads_model = new DMThreadsModel (); } public void load_cached_threads () { account.db.select ("dm_threads") .cols ("user_id", "screen_name", "name", "last_message", "last_message_id") .order ("last_message_id") .run ((vals) => { DMThread thread = new DMThread (); thread.user.id = int64.parse (vals[0]); thread.user.screen_name = vals[1]; thread.user.user_name = vals[2]; thread.last_message_id = int64.parse (vals[4]); thread.last_message = vals[3]; threads_model.add (thread); return true; }); } public GLib.ListModel get_threads_model () { return this.threads_model; } public bool has_thread (int64 user_id) { return this.threads_model.has_thread (user_id); } public bool has_dm (int64 dm_id) { int64 id = account.db.select ("dms") .cols ("id") .where_eqi ("id", dm_id) .once_i64 (); return id == dm_id; } private bool has_dm_json (int64 dm_id) { int64 id = account.db.select ("dms") .cols ("id") .where_eqi ("id", dm_id) .and ().where_eq ("message_json", "") .once_i64 (); return id == dm_id; } public int reset_unread_count (int64 user_id) { if (!threads_model.has_thread (user_id)) { debug ("No thread found for user id %s", user_id.to_string ()); return 0; } int prev_count = threads_model.reset_unread_count (user_id); this.thread_changed (threads_model.get_thread (user_id)); return prev_count; } public string? reset_notification_id (int64 user_id) { if (!threads_model.has_thread (user_id)) { debug ("No thread found for user id %s", user_id.to_string ()); return null; } return threads_model.reset_notification_id (user_id); } public async void load_newest_dms () { } public void insert_message (Json.Object dm_obj) { update_thread.begin (dm_obj); } private async void update_thread (Json.Object dm_obj) { Json.Object dm_msg = dm_obj.get_object_member ("message_create"); int64 message_id = int64.parse(dm_obj.get_string_member ("id")); bool _has_json = has_dm_json (message_id); bool _has_dm = has_dm (message_id); if (_has_dm && _has_json) { // The API now returns all recent DMs, and we can't say "since", so we have to // check whether the ID exists each time return; } Json.Generator generator = new Json.Generator (); Json.Node node = new Json.Node(Json.NodeType.OBJECT); node.set_object (dm_obj); generator.set_root (node); string json = generator.to_data (null); if (_has_dm && !_has_json) { account.db.update ("dms") .val ("message_json", json) .where_eqi ("id", message_id) .run(); return; } // Else it is new int64 recipient_id = int64.parse(dm_msg.get_object_member ("target").get_string_member ("recipient_id")); int64 sender_id = int64.parse(dm_msg.get_string_member ("sender_id")); Json.Object dm_msg_data = dm_msg.get_object_member ("message_data"); string source_text = dm_msg_data.get_string_member ("text"); var urls = dm_msg_data.get_object_member ("entities").get_array_member ("urls"); var url_list = new Cb.TextEntity[urls.get_length ()]; urls.foreach_element((arr, index, node) => { var url = node.get_object(); string expanded_url = url.get_string_member("expanded_url"); Json.Array indices = url.get_array_member ("indices"); url_list[index] = Cb.TextEntity() { from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1) , original_text = url.get_string_member ("url"), display_text = url.get_string_member ("display_url"), target = expanded_url.replace ("&", "&"), tooltip_text = expanded_url }; }); string text = Cb.TextTransform.text (source_text, url_list, Cb.TransformFlags.EXPAND_LINKS, 0, 0); string? sender_user_name = yield Twitter.get ().get_user_name (account, sender_id); string? sender_screen_name = yield Twitter.get ().get_screen_name (account, sender_id); string? recipient_user_name = yield Twitter.get ().get_user_name (account, recipient_id); string? recipient_screen_name = yield Twitter.get ().get_screen_name (account, recipient_id); int64 thread_user_id = 0; string? thread_screen_name = null; string? thread_user_name = null; /* User -> Other Other -> User User -> User */ if (sender_id == account.id) { thread_user_id = recipient_id; thread_user_name = recipient_user_name; thread_screen_name = recipient_screen_name; } else { thread_user_id = sender_id; thread_user_name = sender_user_name; thread_screen_name = sender_screen_name; } if (!threads_model.has_thread (thread_user_id)) { DMThread thread = new DMThread (); thread.user.id = thread_user_id; thread.user.screen_name = thread_screen_name; thread.user.user_name = thread_user_name; thread.last_message = text; thread.last_message_id = message_id; this.threads_model.add (thread); account.db.insert ("dm_threads") .vali64 ("user_id", thread_user_id) .val ("screen_name", thread_screen_name) .val ("name", thread_user_name) .val ("last_message", text) .vali64 ("last_message_id", message_id) .run (); } else if (sender_id != account.id || recipient_id == account.id) { DMThread thread = threads_model.get_thread (sender_id); if (message_id > thread.last_message_id) { this.threads_model.update_last_message (sender_id, message_id, text); account.db.update ("dm_threads").val ("last_message", text) .vali64 ("last_message_id", message_id) .where_eqi ("user_id", sender_id).run (); this.thread_changed (thread); } } account.user_counter.user_seen (thread_user_id, thread_screen_name, thread_user_name); account.db.insert ("dms").vali64 ("id", message_id) .vali64 ("from_id", sender_id) .vali64 ("to_id", recipient_id) .val ("from_screen_name", sender_screen_name) .val ("to_screen_name", recipient_screen_name) .val ("from_name", sender_user_name) .val ("to_name", recipient_user_name) // Note: We now get time stamps in milliseconds from the API! .vali64 ("timestamp", int64.parse(dm_obj.get_string_member ("created_timestamp")) / 1000) .val ("text", text) .val ("message_json", json) .run (); /* Update unread count for the thread */ if (sender_id != account.id && threads_model.has_thread (sender_id)) { DMThread thread = threads_model.get_thread (sender_id); threads_model.increase_unread_count (sender_id); this.message_received (thread, message_id, text); this.thread_changed (thread); } } } cawbird-1.4.2/src/DMPage.vala000066400000000000000000000715241416632607600157060ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/dm-page.ui")] class DMPage : IPage, Cb.MessageReceiver, Gtk.Box { public const int KEY_SENDER_ID = 0; public const int KEY_SCREEN_NAME = 1; public const int KEY_USER_NAME = 2; public const int KEY_AVATAR_URL = 3; public int unread_count { get { return 0; } } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; } } public unowned Account account; public int id { get; set; } [GtkChild] private unowned Gtk.Button send_button; [GtkChild] private unowned CompletionTextView text_view; [GtkChild] private unowned Gtk.ListBox messages_list; [GtkChild] private unowned ScrollWidget scroll_widget; [GtkChild] private unowned Gtk.Stack action_stack; [GtkChild] private unowned Gtk.Box reply_box; [GtkChild] private unowned Gtk.Button delete_button; [GtkChild] private unowned ComposeImageManager compose_image_manager; [GtkChild] private unowned Gtk.Button add_media_button; [GtkChild] private unowned FavImageView fav_image_view; [GtkChild] private unowned Gtk.Button fav_image_button; [GtkChild] private unowned Gtk.Box add_button_box; [GtkChild] private unowned Gtk.Label image_error_label; private Cb.EmojiChooser? emoji_chooser = null; private Gtk.Button? emoji_button = null; private GLib.Cancellable? cancellable; private DMPlaceholderBox placeholder_box = new DMPlaceholderBox (); private int64 first_dm_id; public int64 user_id; private string user_name; private string screen_name; private bool was_scrolled_down = false; private bool first_load = true; private uint update_time_delta_timeout = 0; private MediaUpload media_upload; public DMPage (int id, Account account) { this.id = id; this.account = account; this.cancellable = new GLib.Cancellable(); send_button.sensitive = false; text_view.buffer.changed.connect (set_send_sensitive_state); messages_list.set_sort_func (twitter_item_sort_func_inv); placeholder_box.show (); messages_list.set_placeholder(placeholder_box); text_view.size_allocate.connect (() => { if (was_scrolled_down) scroll_widget.scroll_down_next (false, false); }); scroll_widget.vadjustment.value_changed.connect (() => { if (scroll_widget.scrolled_down) { this.was_scrolled_down = true; } else { this.was_scrolled_down = false; } }); scroll_widget.scrolled_to_start.connect((value) => { load_dms.begin(); }); compose_image_manager.max_images = 1; var upload_proxy = new Rest.OAuthProxy (account.proxy.consumer_key, account.proxy.consumer_secret, "https://upload.twitter.com/", false); upload_proxy.token = account.proxy.token; upload_proxy.token_secret = account.proxy.token_secret; compose_image_manager.proxy = upload_proxy; compose_image_manager.image_removed.connect ((uuid) => { media_upload = null; this.add_media_button.sensitive = true; this.fav_image_button.sensitive = true; this.compose_image_manager.hide (); set_send_sensitive_state(); }); compose_image_manager.image_reloaded.connect ((upload) => { if (upload.cancellable != null) { upload.cancellable.cancel(); } TweetUtils.upload_media.begin (upload, account, null); set_send_sensitive_state(); }); } public void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.DIRECT_MESSAGE) { handle_dm.begin (type, root); } } private bool has_dm (int64 dm_id) { bool found = false; List dm_entries = messages_list.get_children (); uint length = dm_entries.length (); unowned List current = dm_entries.last (); for (uint i = 0; i < length; i++) { if (((DMListEntry)current.data).id == dm_id) { found = true; break; } current = current.nth_prev (1); } return found; } private async void handle_dm (Cb.StreamMessageType type, Json.Node root) { Json.Object dm_obj = root.get_object (); int64 dm_id = int64.parse(dm_obj.get_string_member ("id")); if (has_dm (dm_id)) { return; } Json.Object dm_msg = dm_obj.get_object_member ("message_create"); int64 sender_id = int64.parse(dm_msg.get_string_member ("sender_id")); int64 recipient_id = int64.parse(dm_msg.get_object_member ("target").get_string_member ("recipient_id")); /* Only handle DMs for our current chat */ if (sender_id != this.user_id && recipient_id != this.user_id) return; Json.Object dm_msg_data = dm_msg.get_object_member ("message_data"); var text = dm_msg_data.get_string_member ("text"); string? sender_user_name = yield Twitter.get ().get_user_name (account, sender_id); string? sender_screen_name = yield Twitter.get ().get_screen_name (account, sender_id); int64 timestamp = int64.parse(dm_obj.get_string_member ("created_timestamp")) / 1000; yield add_entry (dm_id, sender_id, text, sender_user_name, sender_screen_name, dm_msg_data, timestamp); } private async void add_entry (int64 dm_id, int64 sender_id, string text, string sender_user_name, string sender_screen_name, Json.Object? message_data, int64 timestamp) { var new_msg = create_list_entry(dm_id, sender_id, text, sender_user_name, sender_screen_name, message_data, timestamp); messages_list.add (new_msg); if (dm_id < first_dm_id) { first_dm_id = dm_id; } if (scroll_widget.scrolled_down) { scroll_widget.scroll_down_next (); } else { scroll_widget.balance_next_upper_change (TOP); } } private DMListEntry create_list_entry(int64 dm_id, int64 sender_id, string text, string sender_user_name, string sender_screen_name, Json.Object? message_data, int64 timestamp) { Cb.TextEntity[] entities; Cb.Media? media = null; if (message_data != null && message_data.has_member ("entities")) { var entity_nodes = message_data.get_object_member ("entities"); var url_nodes = entity_nodes.get_array_member ("urls"); var hashtag_nodes = entity_nodes.get_array_member ("hashtags"); var user_mention_nodes = entity_nodes.get_array_member("user_mentions"); entities = new Cb.TextEntity[url_nodes.get_length () + hashtag_nodes.get_length() + user_mention_nodes.get_length()]; url_nodes.foreach_element((arr, index, node) => { var url = node.get_object(); string expanded_url = url.get_string_member("expanded_url"); Json.Array indices = url.get_array_member ("indices"); entities[index] = Cb.TextEntity() { from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1) , target = expanded_url.replace ("&", "&"), original_text = url.get_string_member ("url"), tooltip_text = expanded_url, display_text = url.get_string_member ("display_url") }; }); var offset = url_nodes.get_length(); hashtag_nodes.foreach_element((arr, index, node) => { var hashtag = node.get_object(); Json.Array indices = hashtag.get_array_member ("indices"); var hashtag_text = "#%s".printf(hashtag.get_string_member("text")); entities[offset + index] = Cb.TextEntity() { from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1) , target = null, original_text = hashtag_text, tooltip_text = hashtag_text, display_text = hashtag_text }; }); offset += hashtag_nodes.get_length(); user_mention_nodes.foreach_element((arr, index, node) => { var user_mention = node.get_object(); Json.Array indices = user_mention.get_array_member ("indices"); var mention_screen_name = "@%s".printf(user_mention.get_string_member("screen_name")); var id_str = user_mention.get_string_member("id_str"); entities[offset + index] = Cb.TextEntity() { from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1) , target = "@%s/%s".printf(id_str, mention_screen_name), original_text = mention_screen_name, tooltip_text = user_mention.get_string_member("name"), display_text = mention_screen_name }; }); if (entities.length > 1) { var swapped = false; do { swapped = false; for (int i = 0; i < entities.length - 1; i++) { int j = i + 1; if (entities[j].from < entities[i].from) { Cb.TextEntity tmp = entities[i]; entities[i] = entities[j]; entities[j] = tmp; swapped = true; } } } while (swapped); } } else { entities = new Cb.TextEntity[0]; } if (message_data != null && message_data.has_member("attachment")) { var attachment = message_data.get_object_member("attachment"); if (attachment.get_string_member("type") == "media") { var media_node = attachment.get_object_member("media"); media = new Cb.Media(); var media_type = media_node.get_string_member("type"); var url = media_node.get_string_member("media_url_https"); var sizes = media_node.get_object_member("sizes"); var small_size = sizes.get_object_member("small"); media.thumb_width = (int)small_size.get_int_member("w"); media.thumb_height = (int)small_size.get_int_member("h"); media.thumb_url = "%s:small".printf(url); var large_size = sizes.get_object_member("large"); media.thumb_width = (int)large_size.get_int_member("w"); media.thumb_height = (int)large_size.get_int_member("h"); if (media_node.has_member("ext_alt_text")) { media.alt_text = media_node.get_string_member("ext_alt_text"); } if (media_type == "photo") { media.target_url = "%s:orig".printf(url); media.url = "%s:large".printf(url); media.type = Cb.MediaType.IMAGE; } else if (media_type == "video" || media_type == "animated_gif") { if (media.alt_text == null && media_node.has_member("additional_media_info")) { var additional_media_info = media_node.get_object_member("additional_media_info"); if (additional_media_info.has_member("title") && additional_media_info.has_member("description")) { media.alt_text = "%s\n\n%s".printf(additional_media_info.get_string_member("title"), additional_media_info.get_string_member("description")) .strip(); } } Json.Object? variant = null; Json.Array variants = media_node.get_object_member("video_info").get_array_member("variants"); uint variant_count = variants.get_length(); for (int i = 0; i < variant_count; i++) { var media_variant = variants.get_object_element(i); if (media_variant.get_string_member("content_type") == "application/x-mpegURL") { variant = media_variant; media.target_url = media_node.get_string_member("expanded_url"); break; } } if (variant == null && variant_count > 0) { variant = variants.get_object_element(0); } if (variant != null) { media.url = variant.get_string_member("url"); media.type = Cb.MediaType.TWITTER_VIDEO; } else { // It all went wrong, so trash the object media = null; } } else { media = null; } } if (media != null) { if (media.url.has_prefix("https://ton.twitter.com/")) { media.consumer_key = account.proxy.consumer_key; media.consumer_secret = account.proxy.consumer_secret; media.token = account.proxy.token; media.token_secret = account.proxy.token_secret; } Cb.MediaDownloader.get_default().load_async.begin (media); } } var new_msg = new DMListEntry (); new_msg.id = dm_id; new_msg.text = text; new_msg.name = sender_user_name; new_msg.screen_name = sender_screen_name; new_msg.timestamp = timestamp; new_msg.main_window = main_window; new_msg.user_id = sender_id; new_msg.media = media; new_msg.message_data = message_data; new_msg.set_entities(entities); new_msg.update_time_delta (); new_msg.avatar_clicked.connect(() => { if (has_checked_items()) { action_stack.visible_child = delete_button; } else { action_stack.visible_child = reply_box; } }); Twitter.get ().get_avatar_url.begin (account, sender_id, (obj, res) => { new_msg.load_avatar (Twitter.get ().get_avatar_url.end(res)); }); return new_msg; } public void on_join (int page_id, Cb.Bundle? args) { int64 user_id = args.get_int64 (KEY_SENDER_ID); if (user_id == 0) return; if (first_load) { setup_emoji_chooser (); first_load = false; } media_upload = null; image_error_label.visible = false; first_dm_id = int64.MAX; this.user_id = user_id; if ((screen_name = args.get_string (KEY_SCREEN_NAME)) != null) { // If the screen name is set then it's a new conversation // So show the placeholder with name and avatar user_name = args.get_string (KEY_USER_NAME); placeholder_box.user_id = user_id; placeholder_box.screen_name = screen_name; placeholder_box.name = name; placeholder_box.avatar_url = args.get_string (KEY_AVATAR_URL); placeholder_box.load_avatar (); } messages_list.get_accessible().set_name(_("Direct messages with %s").printf(user_name)); messages_list.get_accessible().set_description(_("Direct messages with %s").printf(user_name)); text_view.set_account (this.account); // Clear list messages_list.foreach ((w) => {messages_list.remove (w);}); // Update unread count DMThreadsPage threads_page = ((DMThreadsPage)main_window.get_page (Page.DM_THREADS)); threads_page.adjust_unread_count_for_user_id (user_id); load_dms.begin((obj, res) => { load_dms.end(res); messages_list.get_accessible().set_name(_("Direct messages with %s").printf(user_name)); messages_list.get_accessible().set_description(_("Direct messages with %s").printf(user_name)); account.user_counter.user_seen (user_id, screen_name, user_name); scroll_widget.scroll_down_next (false, true); // Focus the text entry text_view.grab_focus (); if (this.update_time_delta_timeout != 0) { GLib.Source.remove(this.update_time_delta_timeout); } this.update_time_delta_timeout = GLib.Timeout.add(1000 * 60, () => { messages_list.get_children().foreach((dm_list_entry) => { ((DMListEntry)dm_list_entry).update_time_delta(); }); return GLib.Source.CONTINUE; }); }); action_stack.visible_child = reply_box; } private async void load_dms() { // Load messages var query = account.db.select ("dms") .cols ("from_id", "to_id", "text", "message_json", "from_name", "from_screen_name", "timestamp", "id"); var dm_is_in_thread = ""; if (user_id == account.id) { dm_is_in_thread = @"(`from_id`='$user_id' AND `to_id`='$user_id')"; } else { dm_is_in_thread = @"(`from_id`='$user_id' OR `to_id`='$user_id')"; } if (first_dm_id != int64.MAX) { query.where_lt("id", first_dm_id).and().where(dm_is_in_thread); } else { query.where(dm_is_in_thread); } string[,] values = new string[35,8]; int row_num = 0; // We can't `yield` async methods in the callback // so load DMs in order by loading into memory and then parsing/adding query.order ("timestamp DESC") .limit (35) .run ((vals) => { for (int i = 0; i < vals.length; i++) { values[row_num,i] = vals[i]; } row_num++; return true; }); for (int i = 0; i < row_num; i++) { int64 id = int64.parse (values[i,7]); string json = values[i,3]; if (json != "") { try { Json.Parser parser = new Json.Parser (); parser.load_from_data (json); Json.Node node = parser.get_root (); yield handle_dm(Cb.StreamMessageType.DIRECT_MESSAGE, node); } catch (Error e) { warning ("Unable to parse the DM json string: %s\n", e.message); } } else { yield add_entry (id, int64.parse (values[i,0]), values[i,2], values[i,4], values[i,5], null, int64.parse (values[i,6])); } if (user_name == null) { user_name = values[i,3]; } if (screen_name == null) { screen_name = values[i,4]; } } } public void on_leave () { if (this.update_time_delta_timeout != 0) { GLib.Source.remove(this.update_time_delta_timeout); this.update_time_delta_timeout = 0; } } [GtkCallback] private void send_button_clicked_cb () { if (text_view.buffer.text.length == 0 && compose_image_manager.n_images == 0) return; send_button.sensitive = false; // Withdraw the notification if there is one DMThreadsPage threads_page = ((DMThreadsPage)main_window.get_page (Page.DM_THREADS)); string notification_id = threads_page.get_notification_id_for_user_id (this.user_id); if (notification_id != null) GLib.Application.get_default ().withdraw_notification (notification_id); var gen = new Json.Generator(); var root = new Json.Node(Json.NodeType.OBJECT); var object = new Json.Object(); root.set_object(object); gen.set_root(root); var event = new Json.Object(); object.set_object_member("event", event); event.set_string_member("type", "message_create"); var msg_create = new Json.Object(); event.set_object_member("message_create", msg_create); var target = new Json.Object(); msg_create.set_object_member("target", target); target.set_string_member("recipient_id", user_id.to_string()); var msg_data = new Json.Object(); msg_create.set_object_member("message_data", msg_data); msg_data.set_string_member("text", text_view.buffer.text); if (media_upload != null) { var attachment = new Json.Object(); msg_data.set_object_member("attachment", attachment); attachment.set_string_member("type", "media"); var media = new Json.Object(); attachment.set_object_member("media", media); media.set_int_member("id", media_upload.media_id); } string json_dump = gen.to_data (null); var call = new OAuthProxyCallWithBody(account.proxy, json_dump); call.set_function ("1.1/direct_messages/events/new.json"); call.set_method ("POST"); call.invoke_async.begin (null, (obj, res) => { try { send_button.sensitive = true; call.invoke_async.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this.main_window); return; } unowned string back = call.get_payload(); account.user_stream.inject_tweet (Cb.StreamMessageType.DIRECT_MESSAGE, back); text_view.buffer.text = ""; compose_image_manager.clear(); compose_image_manager.hide(); media_upload = null; }); } [GtkCallback] private bool text_view_key_press_cb (Gdk.EventKey evt) { if (evt.keyval == Gdk.Key.Return && (evt.state & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) { send_button_clicked_cb (); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } private void set_send_sensitive_state() { uint text_length = text_view.buffer.text.length; send_button.sensitive = (text_length > 0 && media_upload == null) || (media_upload != null && media_upload.is_uploaded()); } private bool has_checked_items () { var dm_messages = messages_list.get_children(); // Assume the user is deleting recent messages and keeping older content, so work from the bottom up dm_messages.reverse(); foreach (Gtk.Widget widget in dm_messages) { if (widget is DMListEntry) { var dm_message_entry = (DMListEntry)widget; if (dm_message_entry.is_checked) { return true; } } } return false; } [GtkCallback] private void delete_button_clicked_cb (Gtk.Button button) { delete_button.sensitive = false; var deletable_widgets = new GLib.GenericArray(); messages_list.foreach((widget) => { if (widget is DMListEntry) { var dm_message_entry = (DMListEntry)widget; if (dm_message_entry.is_checked) { deletable_widgets.add(dm_message_entry); } } }); var collect_obj = new Collect(deletable_widgets.length); collect_obj.finished.connect((e) => { if (e != null) { Utils.show_error_dialog (e, this.main_window); } delete_button.sensitive = true; action_stack.visible_child = reply_box; }); deletable_widgets.foreach((dm_message_entry) => { delete_dm(dm_message_entry, collect_obj); }); } private void delete_dm(DMListEntry dm_message_entry, Collect collect_obj) { dm_message_entry.sensitive = false; var call = new OAuthProxyCallWithQueryString(account.proxy); call.set_function ("1.1/direct_messages/events/destroy.json"); call.set_method ("DELETE"); call.add_param("id", dm_message_entry.id.to_string()); call.invoke_async.begin(null, (obj, res) => { try { call.invoke_async.end (res); collect_obj.emit(); account.db.delete("dms").where_eqi("id", dm_message_entry.id).run(); messages_list.remove(dm_message_entry); } catch (GLib.Error e) { var err = TweetUtils.failed_request_to_error (call, e); if (err.code == 34) { // Already deleted collect_obj.emit(); account.db.delete("dms").where_eqi("id", dm_message_entry.id).run(); messages_list.remove(dm_message_entry); return; } dm_message_entry.sensitive = true; dm_message_entry.is_checked = false; collect_obj.emit(err); return; } }); } [GtkCallback] private void add_media_clicked_cb (Gtk.Button source) { var filechooser = new Gtk.FileChooserNative (_("Select Media"), this.main_window, Gtk.FileChooserAction.OPEN, _("Open"), _("Cancel")); var filter = new Gtk.FileFilter (); filter.add_mime_type ("image/png"); filter.add_mime_type ("image/jpeg"); filter.add_mime_type ("image/webp"); filter.add_mime_type ("image/gif"); if (compose_image_manager.n_images == 0) { filter.add_mime_type ("video/mpeg"); filter.add_mime_type ("video/mp4"); } filechooser.set_filter (filter); if (filechooser.run () == Gtk.ResponseType.ACCEPT) { var filename = filechooser.get_filename (); try { load_image (filename); } catch (GLib.Error e) { // TODO: Proper error checking/reporting // But it shouldn't happen because we only just picked it, so the file info // should just work warning ("%s (%s)", e.message, filename); } } } private void load_image (string filename) throws GLib.Error { /* Get file size */ var file = GLib.File.new_for_path (filename); GLib.FileInfo info = file.query_info (GLib.FileAttribute.STANDARD_TYPE + "," + GLib.FileAttribute.STANDARD_CONTENT_TYPE + "," + GLib.FileAttribute.STANDARD_SIZE, 0); var content_type = info.get_content_type(); #if MSWINDOWS // We can't trust Windows with mime types, it only understands extensions, so fudge something and hope for the best var fname = filename.down(); var is_image = fname.has_suffix(".png") || fname.has_suffix(".gif") || fname.has_suffix(".jpg") || fname.has_suffix(".jpeg") || fname.has_suffix(".webp"); var is_video = !is_image; #else var is_video = content_type.has_prefix("video/"); var is_image = content_type.has_prefix("image/"); #endif var is_animated_gif = is_image && Utils.is_animated_gif(filename); var file_size = info.get_size(); if (!is_image && !is_video) { image_error_label.label = _("Selected file is not an image or video."); image_error_label.visible = true; } else if (is_video && file_size > Twitter.MAX_BYTES_PER_VIDEO) { image_error_label.label = _("The selected video is too big. The maximum file size per video is %'d MB") .printf (Twitter.MAX_BYTES_PER_VIDEO / 1024 / 1024); image_error_label.visible = true; } else if (is_image && !is_animated_gif && file_size > Twitter.MAX_BYTES_PER_IMAGE) { image_error_label.label = _("The selected image is too big. The maximum file size per image is %'d MB") .printf (Twitter.MAX_BYTES_PER_IMAGE / 1024 / 1024); image_error_label.visible = true; } else if (is_image && is_animated_gif && file_size > Twitter.MAX_BYTES_PER_GIF) { image_error_label.label = _("The selected GIF is too big. The maximum file size per GIF is %'d MB") .printf (Twitter.MAX_BYTES_PER_GIF / 1024 / 1024); image_error_label.visible = true; } else { media_upload = new MediaUpload(filename, true); media_upload.progress_complete.connect((err) => { if (err != null) { Utils.show_error_dialog(err, this.main_window); } else { set_send_sensitive_state(); } }); this.compose_image_manager.show (); this.compose_image_manager.load_media (media_upload); TweetUtils.upload_media.begin (media_upload, account, cancellable); this.add_media_button.sensitive = false; this.fav_image_button.sensitive = false; image_error_label.visible = false; } set_send_sensitive_state(); } [GtkCallback] public void fav_image_button_clicked_cb () { action_stack.visible_child_name = "fav-images"; this.fav_image_view.load_images (); } [GtkCallback] public void favorite_image_selected_cb (string path) { try { load_image (path); action_stack.visible_child = reply_box; } catch (GLib.Error e) { // TODO: Proper error checking/reporting // But it shouldn't happen because we only just picked it from the fav list, // so the file info should just work warning ("%s (%s)", e.message, path); } } private void setup_emoji_chooser () { this.emoji_chooser = new Cb.EmojiChooser (); if (!emoji_chooser.try_init ()) { this.emoji_chooser = null; return; } emoji_chooser.emoji_picked.connect ((text) => { this.text_view.insert_at_cursor (text); action_stack.visible_child = reply_box; }); emoji_chooser.show_all (); action_stack.add (emoji_chooser); this.emoji_button = new Gtk.Button.with_label ("😀"); emoji_button.set_tooltip_text(_("Insert Emoji")); emoji_button.get_accessible().set_name(_("Insert Emoji")); emoji_button.clicked.connect (() => { this.emoji_chooser.populate (); action_stack.visible_child = this.emoji_chooser; }); emoji_button.show_all (); add_button_box.add (emoji_button); } public string get_title () { return _("Direct Conversation"); } public void create_radio_button (Gtk.RadioButton? group) {} public Gtk.RadioButton? get_radio_button() {return null;} } cawbird-1.4.2/src/DMThreadsPage.vala000066400000000000000000000244501416632607600172150ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class DMThreadsPage : IPage, Cb.MessageReceiver, ScrollWidget { private bool initialized = false; private int _unread_count = 0; public int unread_count { get { return _unread_count; } set { //debug ("Changing unread_count from %d to %d", this._unread_count, value); this._unread_count = value; radio_button.show_badge = (this._unread_count > 0); } } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; } } public unowned Account account; public int id { get; set; } private BadgeRadioButton radio_button; private StartConversationEntry start_conversation_entry; private Gtk.ListBox thread_list; private Gtk.ListBox top_list; private Gtk.ListBoxRow? progress_row = null; private GLib.GenericSet suppressed_dms; private DMManager manager; public DMThreadsPage (int id, Account account) { this.id = id; this.account = account; this.suppressed_dms = new GLib.GenericSet(GLib.int64_hash, GLib.int64_equal); this.manager = new DMManager.for_account (account); this.manager.message_received.connect (dm_received_cb); this.manager.thread_changed.connect (thread_changed_cb); /* Create UI */ this.hscrollbar_policy = Gtk.PolicyType.NEVER; var frame = new Gtk.Frame (null); frame.margin = 25; frame.set_valign (Gtk.Align.START); frame.set_shadow_type (Gtk.ShadowType.IN); frame.show (); var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); frame.add (box); box.show (); this.top_list = new Gtk.ListBox (); top_list.show (); top_list.set_selection_mode (Gtk.SelectionMode.NONE); top_list.keynav_failed.connect (top_list_keynav_failed_cb); box.add (top_list); var sep = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); sep.show (); box.add (sep); thread_list = new Gtk.ListBox (); thread_list.set_valign (Gtk.Align.START); thread_list.set_selection_mode (Gtk.SelectionMode.NONE); var accessible_name = _("Direct message threads"); thread_list.get_accessible().set_name(accessible_name); thread_list.get_accessible().set_description(accessible_name); thread_list.keynav_failed.connect (thread_list_keynav_failed_cb); thread_list.set_header_func (default_header_func); box.add (thread_list); this.add (frame); top_list.row_activated.connect ((row) => { if (row is StartConversationEntry) { ((StartConversationEntry)row).reveal (); } }); thread_list.row_activated.connect ((row) => { if (row is DMThreadEntry) { var entry = (DMThreadEntry) row; /* We can withdraw the notification here since activating the notification will dismiss it */ if (manager.has_thread (entry.user_id)) { string? notification_id = manager.reset_notification_id (entry.user_id); if (notification_id != null) GLib.Application.get_default ().withdraw_notification (notification_id); } var bundle = new Cb.Bundle (); bundle.put_int64 (DMPage.KEY_SENDER_ID, entry.user_id); bundle.put_string (DMPage.KEY_SCREEN_NAME, entry.screen_name); bundle.put_string (DMPage.KEY_USER_NAME, entry.name); main_window.main_widget.switch_page (Page.DM, bundle); } else warning ("activated row is not a DMThreadEntry"); }); start_conversation_entry = new StartConversationEntry (account); start_conversation_entry.start.connect((user_id, screen_name, name, avatar_url) => { if (manager.has_thread (user_id)) { this.unread_count -= manager.reset_unread_count (user_id); } var bundle = new Cb.Bundle (); bundle.put_int64 (DMPage.KEY_SENDER_ID, user_id); bundle.put_string (DMPage.KEY_SCREEN_NAME, screen_name); bundle.put_string (DMPage.KEY_USER_NAME, name); bundle.put_string (DMPage.KEY_AVATAR_URL, avatar_url); main_window.main_widget.switch_page (Page.DM, bundle); }); Cb.Utils.bind_model (thread_list, manager.get_threads_model (), thread_widget_func); top_list.add (start_conversation_entry); /* We need to do this here so we know which threads we already have cached */ manager.load_cached_threads (); } private Gtk.Widget thread_widget_func (GLib.Object item) { DMThread thread = (DMThread) item; var row = new DMThreadEntry (thread.user.id); row.screen_name = thread.user.screen_name; row.name = thread.user.user_name; row.last_message = thread.last_message; row.unread_count = thread.unread_count; thread.load_avatar.begin (this.account, this.get_scale_factor (), () => { row.avatar = thread.avatar_surface; }); return row; } void dm_received_cb (DMThread thread, int64 message_id, string text) { assert (thread.user.id != account.id); if (thread.user.id != account.id) { if (!user_id_visible (thread.user.id)) { this.unread_count ++; debug ("Increasing global unread count by 1"); } } this.notify_new_dm (thread, message_id, text); } void thread_changed_cb (DMThread thread) { foreach (Gtk.Widget w in this.thread_list.get_children ()) { if (w is DMThreadEntry) { var entry = (DMThreadEntry) w; if (entry.user_id == thread.user.id) { entry.last_message = thread.last_message; entry.unread_count = thread.unread_count; break; } } } } public void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.DIRECT_MESSAGE) { var obj = root.get_object (); if (account.suppress_dm_notifications) { var dm_id = int64.parse(obj.get_string_member ("id")); suppressed_dms.add(dm_id); } this.manager.insert_message (obj); } else if (type == Cb.StreamMessageType.DIRECT_MESSAGES_LOADED) { account.unsuppress_dm_notifications(); } } public void on_join (int page_id, Cb.Bundle? args) { if (!GLib.NetworkMonitor.get_default ().get_network_available ()) return; if (!initialized) { bool was_empty = manager.empty; if (was_empty) { top_list.hide (); this.progress_row = new Gtk.ListBoxRow (); var spinner = new Gtk.Spinner (); spinner.set_size_request (16, 16); spinner.margin = 12; spinner.visible = true; spinner.start (); progress_row.add (spinner); progress_row.activatable = false; progress_row.visible = true; thread_list.add (progress_row); } this.manager.load_newest_dms.begin (() => { if (was_empty) { if (this.progress_row != null) { thread_list.remove (progress_row); this.progress_row = null; } top_list.show_all (); foreach (Gtk.Widget w in thread_list.get_children ()) { w.show (); } } }); this.initialized = true; } } public void on_leave () { start_conversation_entry.unreveal (); } private void notify_new_dm (DMThread thread, int64 message_id, string msg_text) { if (!Settings.notify_new_dms ()) return; if (suppressed_dms.contains(message_id)) { suppressed_dms.remove(message_id); return; } string sender_screen_name = thread.user.screen_name; int64 sender_id = thread.user.id; string summary; string text; if (thread.notification_id != null) { summary = ngettext ("%d new Message from %s", "%d new Messages from %s", thread.unread_count).printf (thread.unread_count, thread.user.user_name); text = ""; } else { summary = _("New direct message from %s").printf (sender_screen_name); text = msg_text; } thread.notification_id = account.notifications.send_dm (sender_id, thread.notification_id, summary, Utils.unescape_html (text)); } public void create_radio_button(Gtk.RadioButton? group) { radio_button = new BadgeRadioButton(group, "cawbird-dms-symbolic", _("Direct Messages")); } public Gtk.RadioButton? get_radio_button() { return radio_button; } private bool user_id_visible (int64 sender_id) { return (main_window.cur_page_id == Page.DM && ((DMPage)main_window.get_page (Page.DM)).user_id == sender_id); } public string get_title () { return _("Direct Messages"); } public void adjust_unread_count_for_user_id (int64 user_id) { int unread_count = manager.reset_unread_count (user_id); this.unread_count -= unread_count; debug ("unread_count -= %d", unread_count); } public string? get_notification_id_for_user_id (int64 user_id) { if (!manager.has_thread (user_id)) { warning ("No thread for user id %s", user_id.to_string ()); return null; } string? id = manager.reset_notification_id (user_id); return id; } private bool top_list_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.DOWN) { if (thread_list.visible) { thread_list.child_focus (direction); } return true; } return false; } private bool thread_list_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.UP) { top_list.child_focus (direction); return true; } return false; } } cawbird-1.4.2/src/DefaultTimeline.vala000066400000000000000000000324631416632607600176630ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public abstract class DefaultTimeline : ScrollWidget, IPage { public const int REST = 25; protected bool initialized = false; public int id { get; set; } private int _unread_count = 0; public int unread_count { set { debug ("Unread count for %s from %d to %d", get_title (), _unread_count, value); _unread_count = int.max (value, 0); debug ("New unread count for %s: %d", this.get_title (), value); radio_button.show_badge = (_unread_count > 0); } get { return this._unread_count; } } protected unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; tweet_list.main_window = main_window; } } public TweetListBox tweet_list = new TweetListBox (); public unowned Account account; protected BadgeRadioButton radio_button; protected uint tweet_remove_timeout = 0; protected abstract string function { get; } protected bool loading = false; protected Gtk.Widget? last_focus_widget = null; private double last_value = 0.0; protected bool preload_is_complete = false; protected abstract string accessibility_name { get; } protected DefaultTimeline (int id) { this.id = id; this.hscrollbar_policy = Gtk.PolicyType.NEVER; this.scrolled_to_start.connect (handle_scrolled_to_start); this.scrolled_to_end.connect (() => { if (!loading) { load_older (); } }); this.vadjustment.notify["value"].connect (() => { mark_seen_on_scroll (vadjustment.value); }); this.add (tweet_list); tweet_list.get_accessible().set_name(accessibility_name); tweet_list.get_accessible().set_description(accessibility_name); tweet_list.row_activated.connect ((row) => { if (row is TweetListEntry) { var bundle = new Cb.Bundle (); bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (TweetInfoPage.KEY_TWEET, ((TweetListEntry)row).tweet); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); } last_focus_widget = row; }); tweet_list.retry_button_clicked.connect (() => { tweet_list.model.clear (); this.load_newest (); }); this.hexpand = true; } protected bool handle_core_stream_messages (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.DELETE) { int64 id = root.get_object ().get_object_member ("delete") .get_object_member ("status").get_int_member ("id"); delete_tweet (id); } else if (type == Cb.StreamMessageType.RT_DELETE) { Utils.unrt_tweet (root, this.tweet_list.model); } else if (type == Cb.StreamMessageType.EVENT_FAVORITE) { int64 id = root.get_object ().get_int_member ("id"); toggle_favorite (id, true); } else if (type == Cb.StreamMessageType.EVENT_UNFAVORITE) { int64 id = root.get_object ().get_object_member ("target_object").get_int_member ("id"); toggle_favorite (id, false); } else { return false; } return true; } protected virtual void stream_message_received (Cb.StreamMessageType type, Json.Node root) { bool handled = handle_core_stream_messages (type, root); if (!handled) { if (type == Cb.StreamMessageType.EVENT_BLOCK) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_MUTE) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_HIDE_RTS) { tweet_list.hide_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } else if (type == Cb.StreamMessageType.EVENT_SHOW_RTS) { tweet_list.show_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } } } protected int64 get_user_id (Json.Node root) { return root.get_object ().get_object_member ("target").get_int_member ("id"); } protected void show_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.show_tweets_from (user_id, tweet_reason); tweet_list.show_retweets_from (user_id, retweet_reason); } protected void hide_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.hide_tweets_from (user_id, tweet_reason); tweet_list.hide_retweets_from (user_id, retweet_reason); } public virtual void on_join (int page_id, Cb.Bundle? args) { if (!initialized) { if (!Settings.auto_scroll_on_new_tweets ()) { /* we are technically not scrolling up, but due to missing content, we can't really not be scrolled up... */ mark_seen (-1); } initialized = true; } if (Settings.auto_scroll_on_new_tweets () && scrolled_up) { mark_seen (-1); } if (last_focus_widget != null) { /* We might have a reference to a row that's been removed from the listbox */ if (last_focus_widget.parent == tweet_list) last_focus_widget.grab_focus (); else last_focus_widget = null; } this.get_vadjustment ().value = this.last_value; } public virtual void on_leave () { this.last_focus_widget = main_window.get_focus (); if (tweet_list.action_entry != null && tweet_list.action_entry.shows_actions) tweet_list.action_entry.toggle_mode (); last_value = this.get_vadjustment ().value; } public bool handles_double_open () { return true; } public void double_open () { if (!loading) { this.scroll_up_next (true, true); tweet_list.get_row_at_index (0).grab_focus (); } } public void load_newest () { this.loading = true; this.load_newest_internal.begin (() => { this.loading = false; }); } public void load_older () { if (!initialized) return; this.balance_next_upper_change (BOTTOM); this.loading = true; this.load_older_internal.begin (() => { this.loading = false; }); } public abstract string get_title (); public override void destroy () { if (tweet_remove_timeout > 0) { GLib.Source.remove (tweet_remove_timeout); tweet_remove_timeout = 0; } base.destroy (); } public virtual void create_radio_button(Gtk.RadioButton? group){} public Gtk.RadioButton? get_radio_button() { return radio_button; } /** * Handle the case of the user scrolling to the start of the list, * i.e. remove all the items except a few ones after a timeout. */ protected void handle_scrolled_to_start() { if (tweet_remove_timeout != 0) return; if (tweet_list.model.get_n_items () > DefaultTimeline.REST) { tweet_remove_timeout = GLib.Timeout.add (500, () => { if (!scrolled_up) { tweet_remove_timeout = 0; return GLib.Source.REMOVE; } /* Check again in case this changed in the last 500ms */ if (tweet_list.model.get_n_items () > DefaultTimeline.REST) { tweet_list.model.remove_oldest_n_visible (tweet_list.model.get_n_items () - DefaultTimeline.REST); } tweet_remove_timeout = 0; return GLib.Source.REMOVE; }); } else if (tweet_remove_timeout != 0) { GLib.Source.remove (tweet_remove_timeout); tweet_remove_timeout = 0; } } public void delete_tweet (int64 tweet_id) { bool was_seen; bool removed = this.tweet_list.model.delete_id (tweet_id, out was_seen); if (removed && !was_seen) this.unread_count --; } public void toggle_favorite (int64 id, bool mode) { Cb.Tweet? t = this.tweet_list.model.get_for_id (id, 0); if (t != null) { if (mode) this.tweet_list.model.set_tweet_flag (t, Cb.TweetState.FAVORITED); else this.tweet_list.model.unset_tweet_flag (t, Cb.TweetState.FAVORITED); } } /** * So, we don't want to display a retweet in the following situations: * 1) If the original tweet was a tweet by the authenticated user * 2) In any case, if the user follows the author of the tweet * (not the author of the retweet!), we already get the source * tweet by other means, so don't display it again. * 3) It's a retweet from the authenticating user itself * 4) If the retweet is already in the timeline. There's no other * way of checking the case where 2 independend users retweet * the same tweet. */ protected Cb.TweetState get_rt_flags (Cb.Tweet t) { uint flags = 0; /* First case */ if (t.get_user_id () == account.id) flags |= Cb.TweetState.HIDDEN_FORCE; /* Second case */ if (account.follows_id (t.get_user_id ())) flags |= Cb.TweetState.HIDDEN_RT_BY_FOLLOWEE; /* third case */ if (t.retweeted_tweet != null && t.source_tweet.author.id == account.id) flags |= Cb.TweetState.HIDDEN_FORCE; if (t.retweeted_tweet != null) { /* Fourth case */ foreach (Gtk.Widget w in tweet_list.get_children ()) { if (w is TweetListEntry) { var tt = ((TweetListEntry)w).tweet; if (tt.retweeted_tweet != null && tt.retweeted_tweet.id == t.retweeted_tweet.id) { flags |= Cb.TweetState.HIDDEN_FORCE; break; } } } } return (Cb.TweetState)flags; } protected void mark_seen (int64 id) { foreach (Gtk.Widget w in tweet_list.get_children ()) { if (w == null || !(w is TweetListEntry)) continue; var tle = (TweetListEntry) w; if (tle.tweet.id == id || id == -1) { if (!tle.tweet.get_seen ()) { this.unread_count--; } tle.tweet.set_seen (true); if (id != -1) break; } } } protected bool scroll_up (Cb.Tweet t) { bool auto_scroll = Settings.auto_scroll_on_new_tweets (); if (this.scrolled_up && (t.get_user_id () == account.id || auto_scroll)) { this.scroll_up_next (true, main_window.cur_page_id != this.id); return true; } return false; } /** * Default implementation for loading the newest tweets * from the given function of the twitter api. */ protected async void load_newest_internal () { // This should now be unnecessary since the change to stream-by-polling return; } /** * Default implementation to load older tweets. * */ protected async void load_older_internal () { int requested_tweet_count = 28; var call = account.proxy.new_call (); call.set_function (this.function); call.set_method ("GET"); call.add_param ("count", requested_tweet_count.to_string ()); call.add_param ("include_my_retweet", "true"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); call.add_param ("max_id", (tweet_list.model.min_id - 1).to_string ()); Json.Node? root_node = null; try { root_node = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return; } var root = root_node.get_array (); if (root.get_length () == 0) { tweet_list.set_empty (); return; } TweetUtils.work_array (root, tweet_list, account); } /** * Mark the TweetListEntries the user has already seen. * * @param value The scrolling value as from Gtk.Adjustment */ protected void mark_seen_on_scroll (double value) { if (unread_count == 0) return; // We HAVE to use widgets here. tweet_list.forall_internal (false, (w) => { if (!(w is TweetListEntry)) return; var tle = (TweetListEntry)w; if (tle.tweet.get_seen ()) return; Gtk.Allocation alloc; tle.get_allocation (out alloc); if (alloc.y + (alloc.height / 2.0) >= value) { tle.tweet.set_seen (true); unread_count--; } }); } public void rerun_filters () { unread_count -= TweetUtils.rerun_filters(tweet_list, account); } } cawbird-1.4.2/src/FavoritesTimeline.vala000066400000000000000000000066601416632607600202410ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class FavoritesTimeline : Cb.MessageReceiver, DefaultTimeline { protected override string function { get { return "1.1/favorites/list.json"; } } protected override string accessibility_name { get { return _("Liked tweets timeline"); } } public FavoritesTimeline (int id, Account account) { base (id); this.account = account; this.tweet_list.account = account; } protected override void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.TWEET) { if (root.get_object ().has_member ("retweeted_status")) { Utils.set_rt_from_tweet (root, this.tweet_list.model, this.account); } } else if (type == Cb.StreamMessageType.DELETE) { int64 id = root.get_object ().get_object_member ("delete") .get_object_member ("status").get_int_member ("id"); delete_tweet (id); } else if (type == Cb.StreamMessageType.RT_DELETE) { Utils.unrt_tweet (root, this.tweet_list.model); } else if (type == Cb.StreamMessageType.EVENT_FAVORITE) { Json.Node tweet_obj = root; int64 tweet_id = tweet_obj.get_object ().get_int_member ("id"); Cb.Tweet? existing_tweet = this.tweet_list.model.get_for_id (tweet_id, 0); if (existing_tweet != null) { /* This tweet is already in the model, so just mark it as favorited */ tweet_list.model.set_tweet_flag (existing_tweet, Cb.TweetState.FAVORITED); return; } var tweet = new Cb.Tweet (); tweet.load_from_json (tweet_obj, account.id, new GLib.DateTime.now_local ()); tweet.set_flag (Cb.TweetState.FAVORITED); this.tweet_list.model.add (tweet); } else if (type == Cb.StreamMessageType.EVENT_UNFAVORITE) { int64 id = root.get_object ().get_object_member ("target_object").get_int_member ("id"); toggle_favorite (id, false); } else { // We don't do a full fall back to DefaultTimeline here because we want to keep content // we liked from people we then blocked/muted so that we can remove it handle_core_stream_messages (type, root); } } public override void on_leave () { for (uint i = 0; i < tweet_list.model.get_n_items (); i ++) { var tweet = (Cb.Tweet) tweet_list.model.get_item (i); if (!tweet.is_flag_set (Cb.TweetState.FAVORITED)) { tweet_list.model.remove_tweet (tweet); i --; } } base.on_leave (); } public override string get_title () { return _("Likes"); } public override void create_radio_button (Gtk.RadioButton? group) { radio_button = new BadgeRadioButton(group, "cawbird-favorite-symbolic", _("Likes")); } } cawbird-1.4.2/src/FilterPage.vala000066400000000000000000000216171416632607600166310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/filter-page.ui")] class FilterPage : Gtk.ScrolledWindow, IPage, Cb.MessageReceiver { public int id { get; set; } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; } } public unowned Account account; private BadgeRadioButton radio_button; [GtkChild] private unowned Gtk.ListBox filter_list; [GtkChild] private unowned Gtk.ListBox user_list; [GtkChild] private unowned Gtk.Frame user_list_frame; [GtkChild] private unowned Gtk.Revealer user_list_revealer; private bool filters_loaded = false; private bool users_loaded = false; /* TODO: We might want to use a custom model impl for the muted/blocked list... */ public FilterPage (int id, Account account) { this.id = id; this.account = account; filter_list.set_header_func (default_header_func); filter_list.add (new AddListEntry (_("Add new Filter"))); filter_list.row_activated.connect ((row) => { if (row is AddListEntry) { var dialog = new ModifyFilterDialog (main_window, account); dialog.filter_added.connect (filter_added_cb); dialog.show_all (); } else if (row is FilterListEntry) { var filter_row = (FilterListEntry) row; var dialog = new ModifyFilterDialog (main_window, account, filter_row.filter); dialog.filter_added.connect (filter_added_cb); dialog.show_all (); } }); user_list.set_header_func (default_header_func); } public void on_join (int page_id, Cb.Bundle? args) { if (!filters_loaded) { for (int i = 0; i < account.filters.length; i ++) { var f = account.filters.get (i); var entry = new FilterListEntry (f, account, main_window); filter_list.add (entry); } filters_loaded = true; } if (!GLib.NetworkMonitor.get_default ().get_network_available ()) return; if (users_loaded) return; users_loaded = true; var collect_obj = new Collect (2); collect_obj.finished.connect (() => { if (user_list.get_children ().length () > 0) { user_list_frame.show (); user_list_revealer.reveal_child = true; } }); var call = account.proxy.new_call (); call.set_function ("1.1/blocks/list.json"); call.set_method ("GET"); call.add_param ("include_entities", "false"); call.add_param ("skip_status", "true"); Cb.Utils.load_threaded_async.begin (call, null, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { warning (e.message); return; } if (root == null) { collect_obj.emit (); return; } Json.Array users = root.get_object ().get_array_member ("users"); users.foreach_element ((arr, index, node) => { var obj = node.get_object (); add_user (obj, false); }); collect_obj.emit (); }); var call2 = account.proxy.new_call (); call2.set_function ("1.1/mutes/users/list.json"); call2.set_method ("GET"); call2.add_param ("include_entities", "false"); call2.add_param ("skip_status", "true"); Cb.Utils.load_threaded_async.begin (call2, null, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { warning (e.message); return; } if (root == null) { collect_obj.emit (); return; } Json.Array users = root.get_object ().get_array_member ("users"); users.foreach_element ((arr, index, node) => { var obj = node.get_object (); add_user (obj, true); }); collect_obj.emit (); }); } /** * Called when the user adds a new Cb.Filter via the AddFilterDialog * **/ private void filter_added_cb (Cb.Filter f, bool created) { if (created) { var entry = new FilterListEntry (f, account, main_window); filter_list.add (entry); } else { var children = filter_list.get_children (); foreach (Gtk.Widget w in children) { if (!(w is FilterListEntry)) continue; var le = (FilterListEntry) w; if (le.filter.get_id () == f.get_id ()) { le.content = f.get_contents (); break; } } } } public void stream_message_received (Cb.StreamMessageType type, Json.Node root_node) { if (type == Cb.StreamMessageType.EVENT_BLOCK) { var obj = root_node.get_object ().get_object_member ("target"); add_user (obj, false); } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) { var obj = root_node.get_object ().get_object_member ("target"); int64 user_id = obj.get_int_member ("id"); remove_user (user_id, false); } else if (type == Cb.StreamMessageType.EVENT_MUTE) { var obj = root_node.get_object ().get_object_member ("target"); add_user (obj, true); } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) { var obj = root_node.get_object ().get_object_member ("target"); int64 user_id = obj.get_int_member ("id"); remove_user (user_id, true); } } private void add_user (Json.Object user_obj, bool muted) { int64 id = user_obj.get_int_member ("id"); foreach (Gtk.Widget w in user_list.get_children ()) { if (!(w is UserFilterEntry)) continue; var ufe = (UserFilterEntry) w; if (ufe.user_id == id) { if (muted) ufe.muted = true; else ufe.blocked = true; return; } } string avatar_url = user_obj.get_string_member ("profile_image_url_https"); if (this.get_scale_factor () >= 2) avatar_url = avatar_url.replace ("_normal", "_bigger"); var entry = new UserFilterEntry (); entry.user_id = id; entry.muted = muted; entry.blocked = !muted; entry.name = user_obj.get_string_member ("name"); entry.screen_name = user_obj.get_string_member ("screen_name"); entry.avatar_url = avatar_url; entry.deleted.connect ((id) => { if (entry.muted) unmute_user (id); if (entry.blocked) unblock_user (id); }); user_list.add (entry); user_list_frame.show (); } private void remove_user (int64 id, bool muted) { foreach (Gtk.Widget w in user_list.get_children ()) { if (!(w is UserFilterEntry)) continue; var ufe = (UserFilterEntry) w; if (ufe.user_id == id) { if (muted) ufe.muted = false; else ufe.blocked = false; if (!ufe.blocked && !ufe.muted) user_list.remove (w); break; } } if (user_list.get_children ().length () == 0) { user_list_frame.hide (); } } private void unblock_user (int64 id) { UserUtils.block_user.begin (account, id, false, (obj, res) => { try { UserUtils.block_user.end (res); remove_user (id, false); } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); } }); } private void unmute_user (int64 id) { UserUtils.mute_user.begin (account, id, false, (obj, res) => { try { UserUtils.mute_user.end (res); remove_user (id, true); } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); } }); } [GtkCallback] private bool filter_list_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.DOWN) { if (user_list.visible){ user_list.child_focus (direction); } return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } [GtkCallback] private bool user_list_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.UP) { filter_list.child_focus (direction); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } public void on_leave () {} public void create_radio_button (Gtk.RadioButton? group) { radio_button = new BadgeRadioButton(group, "cawbird-filter-symbolic", _("Filters")); } public Gtk.RadioButton? get_radio_button() { return radio_button; } public string get_title () { return _("Filters"); } } cawbird-1.4.2/src/HomeTimeline.vala000066400000000000000000000147571416632607600171750ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class HomeTimeline : Cb.MessageReceiver, DefaultTimeline { private int64 last_tweet_id = 0; protected override string function { get { return "1.1/statuses/home_timeline.json"; } } protected override string accessibility_name { get { return _("Home timeline"); } } public HomeTimeline(int id, Account account) { base (id); this.account = account; this.tweet_list.account = account; } protected override void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.TWEET) { add_tweet (root); } else if (type == Cb.StreamMessageType.TIMELINE_LOADED) { this.preload_is_complete = true; } else if (type == Cb.StreamMessageType.EVENT_UNFOLLOW) { hide_tweets_from (root, Cb.TweetState.HIDDEN_UNFOLLOWED); } else if (type == Cb.StreamMessageType.EVENT_FOLLOW) { show_tweets_from (root, Cb.TweetState.HIDDEN_UNFOLLOWED); load_tweets_from_follow.begin (root); } else { base.stream_message_received (type, root); } } protected void add_tweet (Json.Node obj) { GLib.DateTime now = new GLib.DateTime.now_local (); Cb.Tweet t = new Cb.Tweet (); t.load_from_json (obj, this.account.id, now); /* We don't use the set_state version from Cb.TweetModel here since we just decide the initial visibility of the tweet */ if (t.retweeted_tweet != null) { if (t.source_tweet.author.id == account.id) { // Don't show our own RTs if we inject them, because Twitter // doesn't provide them in a normal home timeline request. // But we should update the RT status. Utils.set_rt_from_tweet (obj, this.tweet_list.model, this.account); return; } t.set_flag (get_rt_flags (t)); } TweetUtils.set_tweet_hidden_flags(t, account); bool auto_scroll = Settings.auto_scroll_on_new_tweets (); bool is_new_unread = false; if (t.id < last_tweet_id && !tweet_list.model.contains_id (t.id)) { int64 age_diff = (int64)(((last_tweet_id >> 22) / 1000) - ((t.id >> 22) / 1000)); debug("Loaded missing tweet %lld (%lld seconds older than %lld)", t.id, age_diff, last_tweet_id); is_new_unread = true; } else if (t.id > last_tweet_id && t.source_tweet.author.id != account.id) { // Keep track of the last ID we saw. // Ignore our own tweets because they get injected and come out of sequence. // This is also the reason we can't just use the model's max_id last_tweet_id = t.id; is_new_unread = true; } // Else we've seen it before, so change nothing t.set_seen (t.source_tweet.author.id == account.id || (t.retweeted_tweet != null && t.retweeted_tweet.author.id == account.id) || (this.scrolled_up && main_window.cur_page_id == this.id && auto_scroll) || ! preload_is_complete); bool focused = tweet_list.get_first_visible_row () != null && tweet_list.get_first_visible_row ().is_focus; bool should_focus = (focused && this.scrolled_up); tweet_list.model.add (t); if (!t.is_hidden ()) { if (auto_scroll) { base.scroll_up (t); } else if (preload_is_complete) { /* We need to balance even if we don't scroll up, in case auto-scroll-on-new-tweets is disabled */ this.balance_next_upper_change (TOP); } if (!t.get_seen () && preload_is_complete && is_new_unread) { this.unread_count ++; } } else { t.set_seen (true); } if (should_focus) { tweet_list.get_first_visible_row ().grab_focus (); } /* The rest of this function deals with notifications which we certainly don't want to show for invisible tweets */ if (t.is_hidden ()) return; // We never show any notifications if auto-scroll-on-new-tweet is enabled // or if it's our tweet or an initial load, or if it's not a new tweet if (t.get_user_id () == account.id || auto_scroll || !preload_is_complete || !is_new_unread) return; int stack_size = Settings.get_tweet_stack_count (); if (stack_size == 1 && !auto_scroll) { string summary = ""; if (t.retweeted_tweet != null){ summary = _("%s retweeted %s").printf (t.source_tweet.author.user_name, t.retweeted_tweet.author.user_name); } else { summary = _("%s tweeted").printf (t.source_tweet.author.user_name); } string id_suffix = "tweet-%s".printf (t.id.to_string ()); t.notification_id = account.notifications.send (summary, t.get_real_text (), id_suffix); } else if(stack_size != 0 && unread_count % stack_size == 0 && unread_count > 0) { string summary = ngettext("%d new Tweet!", "%d new Tweets!", unread_count).printf (unread_count); account.notifications.send (summary, ""); } } private async void load_tweets_from_follow (Json.Node follow_root) { var follow_id = get_user_id (follow_root); try { var root_array = yield UserUtils.load_user_timeline_by_id (account, follow_id, 200, tweet_list.model.min_id); root_array.foreach_element((array, idx, node) => { add_tweet (node); }); } catch (GLib.Error e) { // If we can't load tweets then oh well, never mind warning(e.message); } } public override string get_title () { return "@" + account.screen_name; } public override void create_radio_button (Gtk.RadioButton? group) { radio_button = new BadgeRadioButton(group, "cawbird-user-home-symbolic", _("Home")); } } cawbird-1.4.2/src/IPage.vala000066400000000000000000000024561416632607600155740ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public interface IPage : Gtk.Widget { public abstract int id { get; set; } public abstract void on_join(int page_id, Cb.Bundle? args); public abstract void on_leave (); public abstract void create_radio_button(Gtk.RadioButton? group); public abstract Gtk.RadioButton? get_radio_button(); public abstract string get_title (); public abstract unowned MainWindow window { set; } public virtual bool handles_double_open () { return false; } public virtual void double_open () {} public virtual void rerun_filters () {} } cawbird-1.4.2/src/ListStatusesPage.vala000066400000000000000000000342731416632607600200550ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/list-statuses-page.ui")] class ListStatusesPage : ScrollWidget, Cb.MessageReceiver, IPage { public const int KEY_USER_LIST = 0; public const int KEY_NAME = 1; public const int KEY_DESCRIPTION = 2; public const int KEY_CREATOR = 3; public const int KEY_N_SUBSCRIBERS = 4; public const int KEY_N_MEMBERS = 5; public const int KEY_CREATED_AT = 6; public const int KEY_MODE = 7; public const int KEY_LIST_ID = 8; public const int KEY_TITLE = 9; public int id { get; set; } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; tweet_list.main_window = main_window; } } public unowned Account account; private int64 list_id; private uint tweet_remove_timeout = 0; [GtkChild] private unowned TweetListBox tweet_list; [GtkChild] private unowned Gtk.MenuButton delete_button; [GtkChild] private unowned Gtk.Button edit_button; [GtkChild] private unowned Gtk.Label description_label; [GtkChild] private unowned Gtk.Label title_label; [GtkChild] private unowned Gtk.Label creator_label; [GtkChild] private unowned Gtk.Label subscribers_label; [GtkChild] private unowned Gtk.Label members_label; [GtkChild] private unowned Gtk.Label created_at_label; [GtkChild] private unowned Gtk.Stack title_stack; [GtkChild] private unowned Gtk.Entry title_entry; [GtkChild] private unowned Gtk.Stack description_stack; [GtkChild] private unowned Gtk.Entry description_entry; [GtkChild] private unowned Gtk.Stack delete_stack; [GtkChild] private unowned Gtk.Button cancel_button; [GtkChild] private unowned Gtk.Stack edit_stack; [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Stack mode_stack; [GtkChild] private unowned Gtk.Label mode_label; [GtkChild] private unowned Gtk.ComboBoxText mode_combo_box; [GtkChild] private unowned Gtk.Button refresh_button; private bool loading = false; public ListStatusesPage (int id, Account account) { this.id = id; this.account = account; this.tweet_list.account = account; this.scrolled_to_end.connect (load_older); this.scrolled_to_start.connect (handle_scrolled_to_start); tweet_list.set_adjustment (this.get_vadjustment ()); } protected virtual void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.EVENT_BLOCK) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_MUTE) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_HIDE_RTS) { tweet_list.hide_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } else if (type == Cb.StreamMessageType.EVENT_SHOW_RTS) { tweet_list.show_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } } private int64 get_user_id (Json.Node root) { return root.get_object ().get_object_member ("target").get_int_member ("id"); } protected void show_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.show_tweets_from (user_id, tweet_reason); tweet_list.show_retweets_from (user_id, retweet_reason); } protected void hide_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.hide_tweets_from (user_id, tweet_reason); tweet_list.hide_retweets_from (user_id, retweet_reason); } /** * va_list params: * - int64 list_id - The id of the list to show * - string name - The lists's name * - bool user_list - true if the list belongs to the user, false otherwise * - string description - the lists's description * - string creator * - int subscribers_count * - int memebers_count * - int64 created_at * - string mode */ public void on_join (int page_id, Cb.Bundle? args) { int64 list_id = args.get_int64 (KEY_LIST_ID); if (list_id == 0) { list_id = this.list_id; return; // Continue } string? list_name = args.get_string (KEY_NAME); if (list_name != null) { string list_title = args.get_string (KEY_TITLE); bool user_list = args.get_bool (KEY_USER_LIST); string description = args.get_string (KEY_DESCRIPTION); string creator = args.get_string (KEY_CREATOR); int n_subscribers = args.get_int (KEY_N_SUBSCRIBERS); int n_members = args.get_int (KEY_N_MEMBERS); int64 created_at = args.get_int64 (KEY_CREATED_AT); string mode = args.get_string (KEY_MODE); delete_button.sensitive = user_list; edit_button.sensitive = user_list; title_label.label = list_title; description_label.label = description; creator_label.label = creator; members_label.label = "%'d".printf (n_members); subscribers_label.label = "%'d".printf (n_subscribers); created_at_label.label = new GLib.DateTime.from_unix_local (created_at).format ("%x, %X"); set_mode_label(mode); // TRANSLATORS: "%s" is the user's name for the list - e.g. "Contributors" when looking at https://twitter.com/i/lists/1285277968676331522 var accessible_name = _("%s list tweets").printf(list_title); tweet_list.get_accessible().set_name(accessible_name); tweet_list.get_accessible().set_description(accessible_name); } debug (@"Showing list with id $list_id"); if (list_id == this.list_id) { this.list_id = list_id; load_newer.begin (); } else { this.list_id = list_id; tweet_list.model.clear (); load_newest.begin (); } } public void on_leave () {} private async void load_newest () { loading = true; tweet_list.set_unempty (); uint requested_tweet_count = 25; var call = account.proxy.new_call (); call.set_function ("1.1/lists/statuses.json"); call.set_method ("GET"); debug ("USING LIST ID %s", list_id.to_string ()); call.add_param ("list_id", list_id.to_string ()); call.add_param ("count", requested_tweet_count.to_string ()); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { if (e.message.down () == "not found") { tweet_list.set_empty (); } warning (e.message); loading = false; return; } var root_array = root.get_array (); if (root_array.get_length () == 0) { tweet_list.set_empty (); loading = false; return; } TweetUtils.work_array (root_array, tweet_list, account); loading = false; } private async void load_older () { if (loading) return; loading = true; uint requested_tweet_count = 25; var call = account.proxy.new_call (); call.set_function ("1.1/lists/statuses.json"); call.set_method ("GET"); call.add_param ("list_id", list_id.to_string ()); call.add_param ("max_id", (tweet_list.model.min_id -1).to_string ()); call.add_param ("count", requested_tweet_count.to_string ()); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return; } var root_array = root.get_array (); TweetUtils.work_array (root_array, tweet_list, account); loading = false; } private void set_mode_label(string mode) { mode_label.label = mode == "private" ? _("Private") : _("Public"); } [GtkCallback] private void edit_button_clicked_cb () { title_stack.visible_child = title_entry; description_stack.visible_child = description_entry; delete_stack.visible_child = cancel_button; edit_stack.visible_child = save_button; mode_stack.visible_child = mode_combo_box; title_entry.text = title_label.label; description_entry.text = description_label.label; mode_combo_box.active_id = mode_label.label == _("Private") ? "private" : "public"; } [GtkCallback] private void cancel_button_clicked_cb () { title_stack.visible_child = title_label; description_stack.visible_child = description_label; delete_stack.visible_child = delete_button; edit_stack.visible_child = edit_button; mode_stack.visible_child = mode_label; } [GtkCallback] private void save_button_clicked_cb () { // Make everything go back to normal title_label.label = title_entry.get_text(); description_label.label = description_entry.text; set_mode_label(mode_combo_box.active_id); cancel_button_clicked_cb (); edit_button.sensitive = false; delete_button.sensitive = false; var call = account.proxy.new_call (); call.set_function ("1.1/lists/update.json"); call.set_method ("POST"); call.add_param ("list_id", list_id.to_string ()); call.add_param ("name", title_label.label); call.add_param ("mode", mode_combo_box.active_id); call.add_param ("description", description_label.label); main_window.set_window_title (this.get_title ()); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this.main_window); } account.user_stream.inject_tweet(Cb.StreamMessageType.EVENT_LIST_UPDATED, call.get_payload()); edit_button.sensitive = true; delete_button.sensitive = true; }); } [GtkCallback] private void delete_confirmation_item_clicked_cb () { ListUtils.delete_list.begin (account, list_id, (obj, res) => { try { ListUtils.delete_list.end (res); // Go back to the ListsPage and tell it to remove this list var bundle = new Cb.Bundle (); bundle.put_int (ListsPage.KEY_MODE, ListsPage.MODE_DELETE); bundle.put_int64 (ListsPage.KEY_LIST_ID, list_id); main_window.main_widget.switch_page (Page.LISTS, bundle); } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); } }); } [GtkCallback] private void refresh_button_clicked_cb () { refresh_button.sensitive = false; load_newer.begin (() => { refresh_button.sensitive = true; }); } [GtkCallback] private void tweet_activated_cb (Gtk.ListBoxRow row) { if (row is TweetListEntry) { var bundle = new Cb.Bundle (); bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (TweetInfoPage.KEY_TWEET, ((TweetListEntry)row).tweet); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); } else warning ("row is of unknown type"); } private async void load_newer () { var call = account.proxy.new_call (); call.set_function ("1.1/lists/statuses.json"); call.set_method ("GET"); call.add_param ("list_id", list_id.to_string ()); call.add_param ("count", "30"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); int64 since_id = tweet_list.model.max_id; if (since_id < 0) since_id = 1; call.add_param ("since_id", since_id.to_string ()); debug ("Getting statuses since %s for list_id %s", since_id.to_string (), list_id.to_string ()); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return; } var root_array = root.get_array (); if (root_array.get_length () > 0) { TweetUtils.work_array (root_array, tweet_list, account); } } protected void handle_scrolled_to_start() { if (tweet_remove_timeout != 0) return; if (tweet_list.model.get_n_items () > DefaultTimeline.REST) { tweet_remove_timeout = GLib.Timeout.add (500, () => { if (!scrolled_up) { tweet_remove_timeout = 0; return false; } tweet_list.model.remove_oldest_n_visible (tweet_list.model.get_n_items () - DefaultTimeline.REST); tweet_remove_timeout = 0; return GLib.Source.REMOVE; }); } else if (tweet_remove_timeout != 0) { GLib.Source.remove (tweet_remove_timeout); tweet_remove_timeout = 0; } } public string get_title () { return title_label.label; } public void create_radio_button (Gtk.RadioButton? group) {} public Gtk.RadioButton? get_radio_button () {return null;} public void rerun_filters () { TweetUtils.rerun_filters(tweet_list, account); } } cawbird-1.4.2/src/ListsPage.vala000066400000000000000000000111171416632607600164740ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ListsPage : IPage, ScrollWidget, Cb.MessageReceiver { public const int KEY_MODE = 0; public const int KEY_LIST_ID = 1; public const int MODE_DELETE = 1; private BadgeRadioButton radio_button; public int unread_count { get; set; } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; user_lists_widget.main_window = value; } } public unowned Account account { get; set; } public unowned Cb.DeltaUpdater delta_updater { get; set; } public int id { get; set; } private int64 user_id; private UserListsWidget user_lists_widget; private GLib.DateTime last_load = new GLib.DateTime.from_unix_utc (0); public ListsPage (int id, Account account) { this.id = id; this.account = account; this.set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); this.user_lists_widget = new UserListsWidget (); this.user_lists_widget.account = account; this.add (user_lists_widget); } public void on_join (int page_id, Cb.Bundle? args) { int mode = 0; if (!GLib.NetworkMonitor.get_default ().get_network_available ()) return; if (args != null) mode = args.get_int (KEY_MODE); if (mode == 0) { this.user_id = account.id; load_newest.begin (); } else if (mode == MODE_DELETE) { int64 list_id = args.get_int64 (KEY_LIST_ID); message (@"Deleting list with id $list_id"); user_lists_widget.remove_list (list_id); } } public void on_leave () { user_lists_widget.unreveal (); } private async void load_newest () { var now = new GLib.DateTime.now_local (); if (now.difference (last_load) > GLib.TimeSpan.MINUTE) { last_load = now; yield user_lists_widget.load_lists (user_id); } } private void stream_message_received (Cb.StreamMessageType type, Json.Node root) { // {{{ if (type == Cb.StreamMessageType.EVENT_LIST_CREATED || type == Cb.StreamMessageType.EVENT_LIST_SUBSCRIBED) { var obj = root.get_object (); var entry = new ListListEntry.from_json_data (obj, account); user_lists_widget.add_list (entry); } else if (type == Cb.StreamMessageType.EVENT_LIST_DESTROYED || type == Cb.StreamMessageType.EVENT_LIST_UNSUBSCRIBED) { var obj = root.get_object (); int64 list_id = obj.get_int_member ("id"); user_lists_widget.remove_list (list_id); } else if (type == Cb.StreamMessageType.EVENT_LIST_UPDATED) { var obj = root.get_object (); int64 list_id = obj.get_int_member ("id"); update_list (list_id, obj); } else if (type == Cb.StreamMessageType.EVENT_LIST_MEMBER_ADDED) { var obj = root.get_object (); int64 list_id = obj.get_int_member ("id"); user_lists_widget.update_member_count (list_id, 1); } else if (type == Cb.StreamMessageType.EVENT_LIST_MEMBER_REMOVED) { var obj = root.get_object (); int64 list_id = obj.get_int_member ("id"); user_lists_widget.update_member_count (list_id, -1); } } // }}} public async TwitterList[] get_user_lists () { yield load_newest(); return user_lists_widget.get_user_lists (); } private void update_list (int64 list_id, Json.Object obj) { string title = obj.get_string_member ("name"); string name = obj.get_string_member ("full_name"); string description = obj.get_string_member ("description"); string mode = obj.get_string_member ("mode"); user_lists_widget.update_list (list_id, title, name, description, mode); } public void create_radio_button (Gtk.RadioButton? group) { radio_button = new BadgeRadioButton (group, "view-list-symbolic", _("Lists")); } public string get_title () { return _("Lists"); } public Gtk.RadioButton? get_radio_button () { return radio_button; } } cawbird-1.4.2/src/MainWidget.vala000066400000000000000000000160141416632607600166320ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class MainWidget : Gtk.Box { private unowned Account account; private Gtk.RadioButton dummy_button = new Gtk.RadioButton (null); private IPage[] pages; private Cb.BundleHistory history = new Cb.BundleHistory (); private bool page_switch_lock = false; private ImpostorWidget stack_impostor = new ImpostorWidget (); private Gtk.Box top_box; private Gtk.Stack stack; private Gtk.Revealer topbar_revealer; public int cur_page_id { get { return history.get_current (); } } public MainWidget (Account account, MainWindow parent, Cawbird app) { this.account = account; app.start_account (account); /* Create widgets */ this.set_orientation (Gtk.Orientation.VERTICAL); this.topbar_revealer = new Gtk.Revealer (); topbar_revealer.set_reveal_child (true); topbar_revealer.set_transition_type (Gtk.RevealerTransitionType.SLIDE_UP); this.top_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); top_box.set_hexpand (true); top_box.set_homogeneous (true); top_box.get_style_context ().add_class ("topbar"); topbar_revealer.add (top_box); this.add (topbar_revealer); this.stack = new Gtk.Stack (); stack.set_hexpand (true); stack.set_vexpand (true); stack.notify["transition-running"].connect(() => { if (stack.transition_running == false) { var visible_child = stack.visible_child; stack.get_children().foreach((w) => { if (w != visible_child && w != stack_impostor) { stack.remove(w); } }); } }); this.add (stack); stack.add (stack_impostor); pages = new IPage[11]; pages[0] = new HomeTimeline (Page.STREAM, account); pages[1] = new MentionsTimeline (Page.MENTIONS, account); pages[2] = new FavoritesTimeline (Page.FAVORITES, account); pages[3] = new DMThreadsPage (Page.DM_THREADS, account); pages[4] = new ListsPage (Page.LISTS, account); pages[5] = new FilterPage (Page.FILTERS, account); pages[6] = new SearchPage (Page.SEARCH, account); pages[7] = new ProfilePage (Page.PROFILE, account); pages[8] = new TweetInfoPage (Page.TWEET_INFO, account); pages[9] = new DMPage (Page.DM, account); pages[10] = new ListStatusesPage (Page.LIST_STATUSES, account); /* Initialize all containers */ for (int i = 0; i < pages.length; i++) { IPage page = pages[i]; page.window = parent; if (page is Cb.MessageReceiver) account.user_stream.register ((Cb.MessageReceiver)page); page.create_radio_button (dummy_button); if (page.get_radio_button () != null) { top_box.add (page.get_radio_button ()); page.get_radio_button ().clicked.connect (() => { if (page.get_radio_button ().active && !page_switch_lock) { switch_page (page.id); } }); } } Settings.get ().bind ("sidebar-visible", this.topbar_revealer, "reveal-child", SettingsBindFlags.DEFAULT); } /** * Switches the window's main notebook to the given page. * * @param page_id The id of the page to switch to. * See the Page.* constants. * */ public void switch_page (int page_id, Cb.Bundle? args = null) { if (page_id == history.get_current ()) { if (pages[page_id].handles_double_open ()) pages[page_id].double_open (); if ((history.get_current_bundle () != null && history.get_current_bundle ().equals (args)) || history.get_current_bundle () == args) return; } bool push = true; // Set the correct transition type if (page_id == Page.PREVIOUS || page_id < history.get_current ()) stack.transition_type = Gtk.StackTransitionType.SLIDE_RIGHT; else if (page_id == Page.NEXT || page_id > history.get_current ()) stack.transition_type = Gtk.StackTransitionType.SLIDE_LEFT; int current_page = history.get_current (); // If we go forward/back, we don't need to update the history. if (page_id == Page.PREVIOUS) { if (history.at_start ()) return; push = false; page_id = history.back (); args = history.get_current_bundle (); } else if (page_id == Page.NEXT) { if (history.at_end ()) return; push = false; page_id = history.forward (); args = history.get_current_bundle (); } IPage page = pages[page_id]; if (page_id == current_page) { stack_impostor.clone (page); var transition_type = stack.transition_type; stack.transition_type = Gtk.StackTransitionType.NONE; stack.set_visible_child (stack_impostor); stack.transition_type = transition_type; stack.remove(page); } if (current_page != -1) pages[current_page].on_leave (); if (push) { history.push (page_id, args); } /* XXX The following will cause switch_page to be called twice because setting the active property of the button will cause the clicked event to be emitted, which will call switch_page. */ Gtk.ToggleButton button = page.get_radio_button (); page_switch_lock = true; if (button != null) button.active = true; else dummy_button.active = true; if (page.parent == null) { stack.add(page); } /* on_join first, then set_visible_child so the new page is still !child-visible, so e.g. GtkStack transitions inside the page aren't animated */ page.on_join (page_id, args); page.show_all(); stack.set_visible_child (page); ((MainWindow)this.parent).set_window_title (page.get_title (), stack.transition_type); page_switch_lock = false; ((MainWindow)this.parent).back_button.sensitive = !history.at_start (); } public void remove_current_page () { this.history.remove_current (); this.switch_page (Page.PREVIOUS); } public IPage get_page (int page_id) { return pages[page_id]; } public void rerun_filters() { foreach (IPage page in pages) { page.rerun_filters(); } } public void stop () { for (int i = 0; i < pages.length; i++) { IPage page = pages[i]; if (page is Cb.MessageReceiver) account.user_stream.unregister ((Cb.MessageReceiver)page); } ((Cawbird)GLib.Application.get_default ()).stop_account (this.account); } } cawbird-1.4.2/src/MainWindow.vala000066400000000000000000000444771416632607600166740ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class MainWindow : Gtk.ApplicationWindow { private const GLib.ActionEntry[] win_entries = { {"compose-tweet", show_hide_compose_window}, {"toggle-topbar", Settings.toggle_topbar_visible}, {"switch-page", simple_switch_page, "i"}, {"show-account-dialog", show_account_dialog}, {"show-account-list", show_account_list}, {"previous", previous}, {"next", next} }; private Gtk.HeaderBar headerbar; private AvatarWidget avatar_image; private Gtk.ListBox account_list; private Gtk.Popover account_popover; private Gtk.Box header_box; private Gtk.ToggleButton account_button; private Gtk.Label title_label; private Gtk.Label last_page_label; private Gtk.Stack title_stack; public Gtk.Button back_button; public Gtk.ToggleButton compose_tweet_button; private Gtk.MenuButton app_menu_button = null; public MainWidget main_widget; public unowned Account? account; private ComposeTweetWindow? compose_tweet_window = null; private Gtk.GestureMultiPress thumb_button_gesture; public int cur_page_id { get { return main_widget.cur_page_id; } } public MainWindow (Gtk.Application app, Account? account = null) { set_default_size (530, 700); var group = new Gtk.WindowGroup (); group.add_window (this); #if DEBUG this.set_focus.connect ((w) => { debug ("Focus widget now: %s %p", w != null ? __class_name (w) : "(null)", w); }); #endif this.get_style_context ().add_class ("cawbird-window"); /* Create widgets */ this.set_show_menubar (false); this.set_icon_name ("uk.co.ibboard.cawbird"); this.delete_event.connect (window_delete_cb); this.headerbar = new Gtk.HeaderBar (); headerbar.set_title ("Cawbird"); headerbar.set_show_close_button (true); this.title_stack = new Gtk.Stack (); this.title_label = new Gtk.Label (""); title_label.set_ellipsize (Pango.EllipsizeMode.MIDDLE); title_label.get_style_context ().add_class ("title"); title_stack.add (title_label); this.last_page_label = new Gtk.Label (""); last_page_label.set_ellipsize (Pango.EllipsizeMode.MIDDLE); last_page_label.get_style_context ().add_class ("title"); title_stack.add (last_page_label); headerbar.set_custom_title (title_stack); this.header_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); header_box.set_no_show_all (true); this.account_button = new Gtk.ToggleButton (); account_button.set_tooltip_text (_("Show configured accounts")); account_button.clicked.connect (account_button_clicked_cb); account_button.get_style_context ().add_class ("account-button"); this.avatar_image = new AvatarWidget (); avatar_image.size = 24; avatar_image.set_valign (Gtk.Align.CENTER); account_button.add (avatar_image); account_button.show_all (); header_box.add (account_button); this.compose_tweet_button = new Gtk.ToggleButton (); compose_tweet_button.add (new Gtk.Image.from_icon_name ("cawbird-compose-symbolic", Gtk.IconSize.BUTTON)); compose_tweet_button.set_tooltip_text (_("Compose Tweet")); compose_tweet_button.set_action_name ("win.compose-tweet"); compose_tweet_button.get_style_context ().add_class ("image-button"); compose_tweet_button.show_all (); header_box.add (compose_tweet_button); this.back_button = new Gtk.Button.from_icon_name ("go-previous-symbolic", Gtk.IconSize.BUTTON); back_button.clicked.connect (back_button_clicked_cb); back_button.show_all (); header_box.add (back_button); header_box.show_all (); headerbar.pack_start (header_box); headerbar.show_all (); this.set_titlebar (headerbar); this.account_popover = new Gtk.Popover (account_button); account_popover.closed.connect (account_popover_closed_cb); var frame = new Gtk.Frame (null); frame.set_margin_start (6); frame.set_margin_end (6); frame.set_margin_top (6); frame.set_margin_bottom (6); this.account_list = new Gtk.ListBox (); account_list.set_selection_mode (Gtk.SelectionMode.NONE); account_list.row_activated.connect (account_row_activated_cb); frame.add (account_list); account_popover.add (frame); account_popover.show_all (); change_account (account); account_list.set_sort_func (account_sort_func); account_list.set_header_func (default_header_func); var add_entry = new AddListEntry (_("Add new Account")); add_entry.show_all (); account_list.add (add_entry); for (uint i = 0; i < Account.get_n (); i ++) { var acc = Account.get_nth (i); var e = new UserListEntry.from_account (acc); e.show_settings = true; e.action_clicked.connect (() => { account_popover.popdown (); }); account_list.add (e); } ((Cawbird)app).account_added.connect ((new_acc) => { var entries = account_list.get_children (); foreach (Gtk.Widget ule in entries) if (ule is UserListEntry && new_acc.screen_name == ((UserListEntry)ule).screen_name) return; var ule = new UserListEntry.from_account (new_acc); ule.show_settings = true; ule.action_clicked.connect (() => { account_popover.popdown (); }); account_list.add (ule); ule.show (); }); ((Cawbird)app).account_removed.connect ((acc) => { var entries = account_list.get_children (); foreach (Gtk.Widget ule in entries) if (ule is UserListEntry && acc.screen_name == ((UserListEntry)ule).screen_name) { account_list.remove (ule); break; } }); this.add_action_entries (win_entries, this); headerbar.key_press_event.connect ((evt) => { if (evt.keyval == Gdk.Key.Down && main_widget != null) { main_widget.get_page (main_widget.cur_page_id).focus (Gtk.DirectionType.RIGHT); return true; } return false; }); this.thumb_button_gesture = new Gtk.GestureMultiPress (this); thumb_button_gesture.set_button (0); thumb_button_gesture.set_propagation_phase (Gtk.PropagationPhase.CAPTURE); thumb_button_gesture.pressed.connect (thumb_button_pressed_cb); load_geometry (); } public override void size_allocate(Gtk.Allocation allocation) { if ((allocation.width < Cawbird.RESPONSIVE_LIMIT) != (get_allocated_width() < Cawbird.RESPONSIVE_LIMIT) || get_allocated_width() == 1) { var new_length = (allocation.width < Cawbird.RESPONSIVE_LIMIT) ? 15 : 25; foreach (Gtk.Widget ule in account_list.get_children ()) { if (ule is UserListEntry) { ((UserListEntry)ule).name_display_length = new_length; } } } base.size_allocate(allocation); } private void back_button_clicked_cb () { main_widget.switch_page (Page.PREVIOUS); } public void change_account (Account? account) { int64? old_user_id = null; if (this.account != null) { old_user_id = this.account.id; this.account.info_changed.disconnect (account_info_changed); } this.account = account; if (main_widget != null) { main_widget.stop (); } if (get_child () != null) { remove (get_child ()); } Cawbird cb = (Cawbird) GLib.Application.get_default (); if (account != null) { header_box.show (); main_widget = new MainWidget (account, this, cb); main_widget.show_all (); this.add (main_widget); main_widget.switch_page (0); this.set_window_title (main_widget.get_page (0).get_title ()); avatar_image.surface = account.avatar_small; account.notify["avatar-small"].connect(() => { avatar_image.surface = this.account.avatar_small; }); account.info_changed.connect (account_info_changed); this.set_title ("Cawbird - @%s".printf (account.screen_name)); cb.account_window_changed (old_user_id, account.id); if (!Gtk.Settings.get_default ().gtk_shell_shows_app_menu) { if (app_menu_button == null) { app_menu_button = new Gtk.MenuButton (); app_menu_button.image = new Gtk.Image.from_icon_name ("emblem-system-symbolic", Gtk.IconSize.MENU); app_menu_button.get_style_context ().add_class ("image-button"); app_menu_button.menu_model = cb.app_menu; app_menu_button.show_all (); headerbar.pack_end (app_menu_button); } else app_menu_button.show (); } } else { /* "Special case" when creating a new account */ header_box.hide (); if (app_menu_button != null) app_menu_button.hide (); this.title_label.label = "Cawbird"; this.set_title ("Cawbird"); var create_widget = new AccountCreateWidget (cb, this); create_widget.account_created.connect ((acc) => { MainWindow? account_window = null; if (cb.is_window_open_for_user_id (acc.id, out account_window)) { account_window.present (); this.dispose(); } else { change_account (acc); } }); this.add (create_widget); } } private void account_row_activated_cb (Gtk.ListBoxRow row) { if (row is AddListEntry) { account_popover.popdown (); var window = new MainWindow (application, null); get_application ().add_window (window); window.show_all (); return; } var e = (UserListEntry)row; int64 user_id = e.user_id; Cawbird cb = (Cawbird)this.get_application (); MainWindow? account_window = null; if (user_id == this.account.id || cb.is_window_open_for_user_id (user_id, out account_window)) { account_popover.popdown (); if (account_window != null) account_window.present (); return; } Account? acc = Account.query_account_by_id (user_id); if (acc != null) { change_account (acc); account_popover.popdown (); } else warning ("account == null"); } private void thumb_button_pressed_cb (Gtk.GestureMultiPress gesture, int n_press, double x, double y) { uint button = gesture.get_current_button (); if (button == 9) { // Forward thumb button main_widget.switch_page (Page.NEXT); gesture.set_state (Gtk.EventSequenceState.CLAIMED); } else if (button == 8) { // backward thumb button main_widget.switch_page (Page.PREVIOUS); gesture.set_state (Gtk.EventSequenceState.CLAIMED); } } private void show_hide_compose_window () { if (this.account == null) return; if (compose_tweet_window == null) { compose_tweet_window = new ComposeTweetWindow (this, account, null, ComposeTweetWindow.Mode.NORMAL); compose_tweet_window.show (); compose_tweet_window.hide.connect (() => { compose_tweet_button.active = false; }); compose_tweet_window.destroy.connect (() => { compose_tweet_window = null; }); } else { compose_tweet_window.hide (); compose_tweet_window.destroy (); } } /** * GSimpleActionActivateCallback version of switch_page, used * for keyboard accelerators. */ private void simple_switch_page (GLib.SimpleAction a, GLib.Variant? param) { main_widget.switch_page (param.get_int32 ()); } private void previous (GLib.SimpleAction a, GLib.Variant? param) { if (this.account == null) return; main_widget.switch_page (Page.PREVIOUS); } private void next (GLib.SimpleAction a, GLib.Variant? param) { if (this.account == null) return; main_widget.switch_page (Page.NEXT); } /* result of the show-account-dialog GAction */ private void show_account_dialog () { if (this.account == null) return; var dialog = new AccountDialog (this.account); dialog.set_transient_for (this); dialog.modal = true; dialog.show (); } /* for show-account-list GAction */ private void show_account_list () { if (this.account != null) { this.account_popover.popup (); } } public IPage get_page (int page_id) { return main_widget.get_page (page_id); } private void account_button_clicked_cb () { if (account_popover.visible) { account_popover.popdown (); } else { account_popover.popup (); } } private void account_popover_closed_cb () { account_button.active = false; } private bool window_delete_cb (Gdk.EventAny evt) { if (main_widget != null) main_widget.stop (); if (account == null) return Gdk.EVENT_PROPAGATE; unowned GLib.List ws = this.application.get_windows (); debug("Windows: %u", ws.length ()); string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); if (startup_accounts.length == 1 && startup_accounts[0] == "") startup_accounts.resize (0); save_geometry (); if (startup_accounts.length > 0) return Gdk.EVENT_PROPAGATE; int n_main_windows = 0; foreach (Gtk.Window win in ws) if (win is MainWindow && ((MainWindow) win).account != null) n_main_windows ++; if (n_main_windows == 1) { // This is the last window so we save this one anyways... string[] new_startup_accounts = new string[1]; new_startup_accounts[0] = ((MainWindow)ws.nth_data (0)).account.screen_name; Settings.get ().set_strv ("startup-accounts", new_startup_accounts); debug ("Saving the account %s", ((MainWindow)ws.nth_data (0)).account.screen_name); } return Gdk.EVENT_PROPAGATE; } private void account_info_changed (string screen_name, string name, Cairo.Surface small_avatar, Cairo.Surface avatar) { this.set_window_title (main_widget.get_page (main_widget.cur_page_id).get_title ()); this.set_title ("Cawbird - @%s".printf (screen_name)); } /** * */ private void load_geometry () { if (account == null) { return; } GLib.Variant win_geom = Settings.get ().get_value ("window-geometry"); int x = 0, y = 0, w = 0, h = 0; if (!win_geom.lookup (account.screen_name, "(iiii)", &x, &y, &w, &h)) { warning ("Couldn't load window geometry for screen_name `%s'", account.screen_name); return; } if (w == 0 || h == 0) return; move (x, y); this.set_default_size (w, h); } /** * Saves this window's geometry in the window-geometry gsettings key. */ public void save_geometry () { if (account == null || account.id == 0) return; GLib.Variant win_geom = Settings.get ().get_value ("window-geometry"); GLib.Variant new_geom; GLib.VariantBuilder builder = new GLib.VariantBuilder (new GLib.VariantType("a{s(iiii)}")); var iter = win_geom.iterator (); string? key = null; int x = 0, y = 0, w = 0, h = 0; while (iter.next ("{s(iiii)}", &key, &x, &y, &w, &h)) { if (key != account.screen_name) { builder.add ("{s(iiii)}", key, x, y, w, h); } key = null; // Otherwise we leak key } /* Finally, add this window */ this.get_position (out x, out y); this.get_size (out w, out h); builder.add ("{s(iiii)}", account.screen_name, x, y, w, h); new_geom = builder.end (); debug ("Saving geomentry for %s: %d,%d,%d,%d", account.screen_name, x, y, w, h); Settings.get ().set_value ("window-geometry", new_geom); } private int account_sort_func (Gtk.ListBoxRow a, Gtk.ListBoxRow b) { if (a is AddListEntry) return 1; return ((UserListEntry)a).screen_name.ascii_casecmp (((UserListEntry)b).screen_name); } public void rerun_filters () { main_widget.rerun_filters(); } public void set_window_title (string title, Gtk.StackTransitionType transition_type = Gtk.StackTransitionType.NONE) { this.last_page_label.label = this.title_label.label; this.title_stack.transition_type = Gtk.StackTransitionType.NONE; this.title_stack.visible_child = last_page_label; this.title_stack.transition_type = transition_type; this.title_label.label = title; this.title_stack.visible_child = title_label; } public void reply_to_tweet (int64 tweet_id) { Cb.Tweet? tweet = null; tweet = ((DefaultTimeline)this.main_widget.get_page(Page.STREAM)).tweet_list.model.get_for_id (tweet_id, 0); if (tweet == null) { warning ("tweet with id %s could not be found", tweet_id.to_string ()); return; } var ctw = new ComposeTweetWindow (this, this.account, tweet, ComposeTweetWindow.Mode.REPLY); ctw.show_all (); } public void mark_tweet_as_read (int64 tweet_id) { DefaultTimeline home_timeline = ((DefaultTimeline)this.main_widget.get_page(Page.STREAM)); DefaultTimeline mentions_timeline = ((DefaultTimeline)this.main_widget.get_page(Page.MENTIONS)); Cb.Tweet? tweet = null; tweet = home_timeline.tweet_list.model.get_for_id (tweet_id, 0); if (tweet != null) { tweet.set_seen (true); home_timeline.unread_count --; } // and now with the MentionsTimeline tweet = mentions_timeline.tweet_list.model.get_for_id (tweet_id, 0); if (tweet != null) { tweet.set_seen (true); mentions_timeline.unread_count --; } } } cawbird-1.4.2/src/MediaUpload.vala000066400000000000000000000077061416632607600167760ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class MediaUpload : GLib.Object { public string id { get; private set; } public string? filepath { owned get { return file.get_path(); } } public string filetype { get { #if MSWINDOWS var filename = filepath.down(); // We can't trust Windows with mime types, it only understands extensions, so fudge something and hope for the best // This time we have to re-implement mime types, but based on file extensions! if (filename.has_suffix(".gif")) { return "image/gif"; } else if (filename.has_suffix(".jpg") || filename.has_suffix(".jpeg")) { return "image/jpeg"; } else if (filename.has_suffix(".webp")) { return "image/webp"; } else if (filename.has_suffix(".png")) { return "image/png"; } else { return "video/mp4"; } #else return fileinfo.get_content_type (); #endif } } private string? cat; public string media_category { owned get { if (cat == null) { var prefix = dm ? "dm" : "tweet"; if (filetype.has_prefix("video/")) { cat = "%s_video".printf(prefix); } else if (filetype == "image/gif") { // Animated GIFs are "blah_gif" but static GIFs are "blah_image" if (Utils.is_animated_gif(filepath)) { cat = "%s_gif".printf(prefix); } else { cat = "%s_image".printf(prefix); } } else { cat = "%s_image".printf(prefix); } } return cat; } } private int64 _media_id = -1; public int64 media_id { get { return _media_id; } set { _media_id = value; media_id_assigned(); } } private File file; private FileInfo fileinfo; private bool dm; public int64 filesize { get { return fileinfo.get_size(); } } private double _progress = 0; public double progress { get { return _progress; } set { _progress = value; progress_updated(_progress); } } private bool upload_finalized; public GLib.Cancellable cancellable { get; private set; } public signal void progress_updated(double progress); public signal void progress_complete(GLib.Error? error = null); public signal void media_id_assigned(); public MediaUpload(string filepath, bool for_dm = false) throws GLib.Error { id = GLib.Uuid.string_random(); file = File.new_for_path(filepath); fileinfo = file.query_info(GLib.FileAttribute.STANDARD_TYPE + "," + GLib.FileAttribute.STANDARD_CONTENT_TYPE + "," + GLib.FileAttribute.STANDARD_SIZE, 0); dm = for_dm; cancellable = new GLib.Cancellable(); } public FileInputStream read() throws GLib.Error { return file.read(); } public void finalize_upload() { debug("Finalizing upload"); progress = 1.0; upload_finalized = true; progress_complete(); } public bool is_uploaded() { return upload_finalized; } }cawbird-1.4.2/src/MentionsTimeline.vala000066400000000000000000000111351416632607600200640ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class MentionsTimeline : Cb.MessageReceiver, DefaultTimeline { protected override string function { get { return "1.1/statuses/mentions_timeline.json"; } } protected override string accessibility_name { get { return _("Mentions timeline"); } } public MentionsTimeline(int id, Account account) { base (id); this.account = account; this.tweet_list.account= account; } protected override void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.TWEET) { Utils.set_rt_from_tweet (root, this.tweet_list.model, this.account); } else if (type == Cb.StreamMessageType.MENTION) { add_tweet (root); } else if (type == Cb.StreamMessageType.MENTIONS_LOADED) { this.preload_is_complete = true; account.unsuppress_mention_notifications(); } else { base.stream_message_received (type, root); } } private void add_tweet (Json.Node root_node) { /* Mark tweets as seen the user has already replied to */ var root = root_node.get_object (); if (root.has_member ("retweeted_status")) { Utils.set_rt_from_tweet (root_node, this.tweet_list.model, this.account); } var author = root.get_object_member ("user"); if (author.get_int_member ("id") == account.id && !root.get_null_member ("in_reply_to_status_id")) { mark_seen (root.get_int_member ("in_reply_to_status_id")); return; } GLib.DateTime now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); t.load_from_json (root_node, account.id, now); if (t.get_user_id () == account.id) return; if (t.retweeted_tweet != null && get_rt_flags (t) > 0) return; TweetUtils.set_tweet_hidden_flags(t, account); if (preload_is_complete) { this.balance_next_upper_change (TOP); t.set_seen (false); } tweet_list.model.add (t); base.scroll_up (t); if (preload_is_complete && !t.is_hidden()) this.unread_count ++; // Notify if: // 1) Preload is complete (we've done our first load - prevents notifications for old mentions on startup) // 2) We're not suppressing notifications for this account (same purpose, but reusable setting to other parts of the UI) // 3) The user wants to be notified // 4) The tweet isn't hidden (i.e. the user doesn't want to see something in the content) if (preload_is_complete && !account.suppress_mention_notifications && Settings.notify_new_mentions () && !t.is_hidden()) { string text; if (t.retweeted_tweet != null) text = Utils.unescape_html (t.retweeted_tweet.text); else text = Utils.unescape_html (t.source_tweet.text); /* Ignore the mention if both accounts are configured in Cawbird */ if (Account.query_account_by_id (t.get_user_id ()) == null) { string summary = _("%s mentioned %s").printf (Utils.unescape_html (t.get_user_name ()), account.name); string id = "%s-%s".printf (account.id.to_string (), "mention"); var tuple = new GLib.Variant.tuple ({account.id, t.id}); var notification = new GLib.Notification (summary); notification.set_body (text); notification.set_default_action_and_target_value ("app.show-window", account.id); notification.add_button_with_target_value ("Mark read", "app.mark-read", tuple); notification.add_button_with_target_value ("Reply", "app.reply-to-tweet", tuple); t.notification_id = id; GLib.Application.get_default ().send_notification (id, notification); } } } public override string get_title () { return _("Mentions"); } public override void create_radio_button (Gtk.RadioButton? group) { radio_button = new BadgeRadioButton (group, "cawbird-mentions-symbolic", _("Mentions")); } } cawbird-1.4.2/src/NotificationManager.vala000066400000000000000000000042341416632607600205240ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class NotificationManager : GLib.Object { private unowned Account account; public NotificationManager (Account account) { this.account = account; } public void withdraw (string id) { GLib.Application.get_default ().withdraw_notification (id); } public string send (string summary, string body, string? id_suffix = null) { var n = new GLib.Notification (summary); n.set_body (body); string id = "%s-%s".printf (account.id.to_string (), id_suffix ?? ""); /* Default action: Bring the account window to the front */ n.set_default_action_and_target_value ("app.show-window", account.id); GLib.Application.get_default ().send_notification (id, n); return id; } public string send_dm (int64 sender_id, string? existing_id, string summary, string text) { if (existing_id != null) { this.withdraw (existing_id); } string new_id = "new-dm-%s".printf (sender_id.to_string ()); var n = new GLib.Notification (summary); var value = new GLib.Variant.tuple ({new GLib.Variant.int64 (account.id), new GLib.Variant.int64 (sender_id)}); n.set_default_action_and_target_value ("app.show-dm-thread", value); n.set_body (text); GLib.Application.get_default ().send_notification (new_id, n); return new_id; } } cawbird-1.4.2/src/OAuthProxyCallWithBody.vala000066400000000000000000000024411416632607600211310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class OAuthProxyCallWithBody : Rest.OAuthProxyCall { private string message_content = ""; public OAuthProxyCallWithBody(Rest.Proxy proxy, string content) { Object(proxy: proxy); message_content = content; } public override bool serialize_params (out string content_type, out string content, out size_t content_len) throws Error { content_type = "application/json"; content = this.message_content; content_len = this.message_content.length; return true; } } cawbird-1.4.2/src/OAuthProxyCallWithQueryString.vala000066400000000000000000000030731416632607600225320ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class OAuthProxyCallWithQueryString : Rest.OAuthProxyCall { public OAuthProxyCallWithQueryString(Rest.Proxy proxy) { Object(proxy: proxy); } public override bool serialize_params (out string content_type, out string content, out size_t content_len) throws Error { var call_params = this.get_params().as_string_hash_table(); string[] parts = new string[call_params.length]; int pos = 0; call_params.foreach((key, value) => { parts[pos] = "%s=%s".printf(key, value); pos++; }); string query_string = string.joinv("&", parts); this.set_function("%s?%s".printf(this.get_function(), query_string)); content_type = "text/plain"; content = ""; content_len = 0; return true; } } cawbird-1.4.2/src/ProfilePage.vala000066400000000000000000001131451416632607600170020ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/profile-page.ui")] class ProfilePage : ScrollWidget, IPage, Cb.MessageReceiver { private const GLib.ActionEntry[] action_entries = { {"write-dm", write_dm_activated}, {"tweet-to", tweet_to_activated}, {"add-remove-list", add_remove_list_activated}, }; public const int KEY_SCREEN_NAME = 0; public const int KEY_USER_ID = 1; public int unread_count { get { return 0; } } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; user_lists.main_window = value; tweet_list.main_window = main_window; followers_list.main_window = main_window; followers_list.main_window = main_window; } } public unowned Account account; public int id { get; set; } [GtkChild] private unowned AspectImage banner_image; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Label name_label; [GtkChild] private unowned Gtk.Label screen_name_label; [GtkChild] private unowned Gtk.Label description_label; [GtkChild] private unowned Gtk.Label url_label; [GtkChild] private unowned Gtk.Label tweets_label; [GtkChild] private unowned Gtk.Label following_label; [GtkChild] private unowned Gtk.Label followers_label; [GtkChild] private unowned Gtk.Label location_label; [GtkChild] private unowned FollowButton follow_button; [GtkChild] private unowned TweetListBox tweet_list; [GtkChild] private unowned TweetListBox followers_list; [GtkChild] private unowned TweetListBox following_list; [GtkChild] private unowned Gtk.Spinner progress_spinner; [GtkChild] private unowned Gtk.Label follows_you_label; [GtkChild] private unowned UserListsWidget user_lists; [GtkChild] private unowned Gtk.Stack user_stack; [GtkChild] private unowned Gtk.MenuButton more_button; [GtkChild] private unowned Gtk.Stack loading_stack; [GtkChild] private unowned Gtk.RadioButton tweets_button; [GtkChild] private unowned Gtk.RadioButton followers_button; [GtkChild] private unowned Gtk.RadioButton following_button; [GtkChild] private unowned Gtk.RadioButton lists_button; [GtkChild] private unowned Gtk.Label loading_error_label; [GtkChild] private unowned Gtk.Box user_blocked_page; [GtkChild] private unowned Gtk.Label user_blocked_label; private int64 user_id; private new string name; private string screen_name; private string avatar_url; private int follower_count = -1; private GLib.Cancellable data_cancellable; private bool lists_page_inited = false; private bool retweet_item_blocked = false; private bool tweets_loading = false; private bool followers_loading = false; private JsonCursor? followers_cursor = null; private bool following_loading = false; private JsonCursor? following_cursor = null; private GLib.SimpleActionGroup actions; private bool override_block = false; public ProfilePage (int id, Account account) { this.id = id; this.account = account; this.user_lists.account = account; this.tweet_list.account = account; this.scrolled_to_end.connect (() => { if (user_stack.visible_child == tweet_list) { this.load_older_tweets.begin (); } else if (user_stack.visible_child == followers_list) { this.load_followers.begin (); } else if (user_stack.visible_child == following_list) { this.load_following.begin (); } }); tweet_list.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (TweetInfoPage.KEY_TWEET, ((TweetListEntry)row).tweet); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); }); tweet_list.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { tweets_button.grab_focus(); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; }); Utils.connect_vadjustment (this, tweet_list); followers_list.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, ((UserListEntry)row).user_id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, ((UserListEntry)row).screen_name); main_window.main_widget.switch_page (Page.PROFILE, bundle); }); followers_list.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { followers_button.grab_focus(); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; }); Utils.connect_vadjustment (this, followers_list); following_list.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, ((UserListEntry)row).user_id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, ((UserListEntry)row).screen_name); main_window.main_widget.switch_page (Page.PROFILE, bundle); }); following_list.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { following_button.grab_focus(); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; }); Utils.connect_vadjustment (this, following_list); user_lists.hide_user_list_entry (); user_lists.connect_nav(this, lists_button); actions = new GLib.SimpleActionGroup (); actions.add_action_entries (action_entries, this); GLib.SimpleAction block_action = new GLib.SimpleAction.stateful ("toggle-blocked", null, new GLib.Variant.boolean (false)); block_action.activate.connect (toggle_blocked_activated); actions.add_action (block_action); GLib.SimpleAction mute_action = new GLib.SimpleAction.stateful ("toggle-muted", null, new GLib.Variant.boolean (false)); mute_action.activate.connect (toggle_muted_activated); actions.add_action (mute_action); GLib.SimpleAction rt_action = new GLib.SimpleAction.stateful ("toggle-retweets", null, new GLib.Variant.boolean (false)); rt_action.activate.connect (retweet_action_activated); actions.add_action (rt_action); this.insert_action_group ("user", actions); } public override void size_allocate(Gtk.Allocation allocation) { if (allocation.width < Cawbird.RESPONSIVE_LIMIT) { follow_button.set_property("margin-end", 6); follow_button.compact = true; } else { follow_button.set_property("margin-end", 16); follow_button.compact = false; } base.size_allocate(allocation); } private void set_user_id (int64 user_id) { this.user_id = user_id; follow_button.sensitive = (user_id != account.id); loading_stack.visible_child_name = "progress"; progress_spinner.start (); set_banner (null); load_friendship.begin (); load_profile_data.begin (user_id); } private async void load_friendship () { /* Set muted and blocked status now, let the friendship update it */ set_user_muted (account.is_muted (user_id)); set_user_blocked (account.is_blocked (user_id)); /* We (maybe) re-enable this later when the friendship object has arrived */ ((SimpleAction)actions.lookup_action ("toggle-retweets")).set_enabled (false); ((SimpleAction)actions.lookup_action ("add-remove-list")).set_enabled (user_id != account.id); ((SimpleAction)actions.lookup_action ("write-dm")).set_enabled (user_id != account.id); ((SimpleAction)actions.lookup_action ("toggle-blocked")).set_enabled (user_id != account.id); ((SimpleAction)actions.lookup_action ("toggle-muted")).set_enabled (user_id != account.id); uint fr = yield UserUtils.load_friendship (account, this.user_id, this.screen_name); follows_you_label.visible = (fr & FRIENDSHIP_FOLLOWED_BY) > 0; set_user_muted ((fr & FRIENDSHIP_MUTING) > 0); set_user_blocked ((fr & FRIENDSHIP_BLOCKING) > 0); set_retweets_disabled ((fr & FRIENDSHIP_FOLLOWING) > 0 && (fr & FRIENDSHIP_WANT_RETWEETS) == 0); if ((fr & FRIENDSHIP_CAN_DM) == 0) ((SimpleAction)actions.lookup_action ("write-dm")).set_enabled (false); ((SimpleAction)actions.lookup_action ("toggle-retweets")).set_enabled ((fr & FRIENDSHIP_FOLLOWING) > 0); } private async void load_profile_data (int64 user_id) { follow_button.sensitive = false; var call = account.proxy.new_call (); call.set_method ("GET"); call.set_function ("1.1/users/show.json"); if (user_id != 0) call.add_param ("user_id", user_id.to_string ()); else call.add_param ("screen_name", this.screen_name); call.add_param ("include_entities", "false"); Json.Node? root_node = null; try { root_node = yield Cb.Utils.load_threaded_async (call, data_cancellable); } catch (GLib.Error e) { if (e.message == "Forbidden") { loading_error_label.label = _("Suspended Account"); loading_stack.visible_child = loading_error_label; } else if (e.domain == Rest.ProxyError.quark() && e.code == new Rest.ProxyError.SSL ("workaround").code) { debug ("Reloading user profile on SSL failure"); load_profile_data.begin (user_id); } else { warning (e.message); } return; } if (root_node == null) return; var root = root_node.get_object(); int64 id = root.get_int_member ("id"); this.user_id = id; string avatar_url = root.get_string_member("profile_image_url_https"); int scale = this.get_scale_factor (); /* Always load the 200x200 px version even in loDPI since there's no 100x100px version */ avatar_url = avatar_url.replace ("_normal", "_200x200"); // We don't use our AvatarCache here because this (100×100) avatar is only // ever loaded here. TweetUtils.download_avatar.begin (avatar_url, 100 * scale, data_cancellable, (obj, res) => { Cairo.Surface surface; try { var pixbuf = TweetUtils.download_avatar.end (res); if (pixbuf == null) { surface = scale_surface ((Cairo.ImageSurface)Twitter.no_avatar, 100, 100); } else { surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, scale, null); } } catch (GLib.Error e) { warning (e.message); surface = Twitter.no_avatar; } avatar_image.surface = surface; progress_spinner.stop (); loading_stack.visible_child_name = "data"; }); string name = root.get_string_member("name").strip (); string screen_name = root.get_string_member("screen_name"); string description = root.get_string_member("description"); int followers = (int)root.get_int_member("followers_count"); int following = (int)root.get_int_member("friends_count"); int tweets = (int)root.get_int_member("statuses_count"); bool is_following = false; if (Utils.usable_json_value (root, "following")) is_following = root.get_boolean_member("following"); bool has_url = root.get_object_member("entities").has_member("url"); bool verified = root.get_boolean_member ("verified"); bool protected_user = root.get_boolean_member ("protected"); if (protected_user) { tweet_list.set_placeholder_text (_("Protected profile")); } string color = root.get_string_member ("profile_background_color"); banner_image.color_string = "#" + color; if (root.has_member ("profile_banner_url")) { string banner_base_url = root.get_string_member ("profile_banner_url"); load_profile_banner (banner_base_url); } string display_url = ""; Json.Object entities = root.get_object_member ("entities"); if (has_url) { var urls_object = entities.get_object_member("url").get_array_member("urls"). get_element(0).get_object(); var url = urls_object.get_string_member("expanded_url"); if (urls_object.has_member ("display_url")) { display_url = urls_object.get_string_member("expanded_url"); } else { url = urls_object.get_string_member("url"); display_url = url; } } string location = null; if (root.has_member("location")) { location = root.get_string_member("location"); } Cb.TextEntity[]? text_urls = null; if (root.has_member ("description")) { int n_tl_entities = 0; Tl.Entity[]? tl_entities = Tl.extract_entities (description, null); // We just add hashtags and mentions ourselves and leave links to Twitter foreach (Tl.Entity e in tl_entities) { if (e.type == Tl.EntityType.HASHTAG || e.type == Tl.EntityType.MENTION) n_tl_entities ++; } Json.Array urls = entities.get_object_member ("description").get_array_member ("urls"); text_urls = new Cb.TextEntity[urls.get_length () + n_tl_entities]; urls.foreach_element ((arr, i, node) => { var ent = node.get_object (); string expanded_url = ent.get_string_member ("expanded_url"); Json.Array indices = ent.get_array_member ("indices"); text_urls[i] = Cb.TextEntity(){ from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1), target = expanded_url, original_text = ent.get_string_member ("url"), display_text = ent.get_string_member ("display_url"), tooltip_text = expanded_url }; }); // Adding them now is fine since we will sort them later int i = (int)urls.get_length (); foreach (Tl.Entity e in tl_entities) { if (e.type != Tl.EntityType.HASHTAG && e.type != Tl.EntityType.MENTION) continue; if (e.type == Tl.EntityType.HASHTAG) { text_urls[i] = Cb.TextEntity () { from = (uint)e.start_character_index, to = (uint)(e.start_character_index + e.length_in_characters), target = e.start->substring(0, (long)e.length_in_bytes), original_text = e.start->substring(0, (long)e.length_in_bytes), display_text = e.start->substring(0, (long)e.length_in_bytes), tooltip_text = e.start->substring(0, (long)e.length_in_bytes) }; } else if (e.type == Tl.EntityType.MENTION) { text_urls[i] = Cb.TextEntity () { from = (uint)e.start_character_index, to = (uint)(e.start_character_index + e.length_in_characters), target = "@0/%.*s".printf (e.length_in_bytes, e.start), original_text = e.start->substring(0, (long)e.length_in_bytes), display_text = e.start->substring(0, (long)e.length_in_bytes), tooltip_text = e.start->substring(0, (long)e.length_in_bytes) }; } i ++; } } account.user_counter.user_seen_full (id, screen_name, name, verified, protected_user); this.follow_button.following = is_following; this.follow_button.sensitive = (this.user_id != this.account.id); var section = (GLib.Menu)more_button.menu_model.get_item_link (0, GLib.Menu.LINK_SECTION); var user_item = new GLib.MenuItem (_("Tweet to @%s").printf (screen_name.replace ("_", "__")), "user.tweet-to"); section.remove (1); section.insert_item (1, user_item); var _name = name.strip (); name_label.set_text (name); name_label.tooltip_text = _name; var _screen_name = "@" + screen_name; screen_name_label.set_label (_screen_name); screen_name_label.tooltip_text = _screen_name; TweetUtils.sort_entities (ref text_urls); string desc = Cb.TextTransform.text (description, text_urls, 0, 0, 0); this.follower_count = followers; description_label.label = "%s".printf (desc); tweets_label.label = "%'d".printf(tweets); tweets_button.get_accessible().set_name(ngettext("%d tweet", "%d tweets", tweets).printf(tweets)); following_label.label = "%'d".printf(following); following_button.get_accessible().set_name(ngettext("Following %d account", "Following %d accounts", following).printf(following)); update_follower_label (); if (location != null && location != "") { location_label.visible = true; location_label.label = location; location_label.get_accessible().set_name(_("Location: %s".printf(location))); } else location_label.visible = false; avatar_image.verified = verified; avatar_image.protected_account = protected_user; if (display_url.length > 0) { display_url = GLib.Markup.escape_text (display_url); url_label.visible = true; url_label.set_markup ("%s" .printf (display_url, display_url)); description_label.margin_bottom = 6; } else { url_label.visible = false; description_label.margin_bottom = 12; } this.name = name; this.screen_name = screen_name; this.avatar_url = avatar_url; // TRANSLATORS: Value is user's name - used for accessibility text for profile timeline view tweet_list.get_accessible().set_name(_("%s timeline").printf(name)); // TRANSLATORS: Value is user's name - used for accessibility text for list of users following the user followers_list.get_accessible().set_name(_("%s followers").printf(name)); // TRANSLATORS: Value is user's name - used for accessibility text for list of users followed by the user following_list.get_accessible().set_name(_("%s following").printf(name)); } private async void load_tweets () { if (account.blocked_or_muted (user_id) && !override_block) { return; } if ((!account.blocked_or_muted(user_id) || override_block) && user_stack.visible_child == user_blocked_page) { user_stack.visible_child = tweet_list; } tweet_list.set_unempty (); tweets_loading = true; Json.Array root_array; try { if (user_id != 0) { root_array = yield UserUtils.load_user_timeline_by_id(account, user_id, 10); } else { root_array = yield UserUtils.load_user_timeline_by_screen_name(account, screen_name, 10); } } catch (GLib.Error e) { if (e.message != "Authorization Required") { warning (e.message); } tweet_list.set_empty (); return; } if (root_array.get_length () == 0) { tweet_list.set_empty (); return; } TweetUtils.work_array (root_array, tweet_list, account); tweets_loading = false; } private async void load_older_tweets (int count_multiplier = 1) { if (tweets_loading) return; if (user_stack.visible_child != tweet_list) return; tweets_loading = true; int requested_tweet_count = 15 * count_multiplier; Json.Array root_array; try { if (user_id != 0) { root_array = yield UserUtils.load_user_timeline_by_id(account, user_id, requested_tweet_count, -1, tweet_list.model.min_id); } else { root_array = yield UserUtils.load_user_timeline_by_screen_name(account, screen_name, requested_tweet_count, -1, tweet_list.model.min_id); } } catch (GLib.Error e) { warning (e.message); return; } TweetUtils.work_array (root_array, tweet_list, account); tweets_loading = false; } private async void load_followers () { if (this.followers_cursor != null && this.followers_cursor.full) return; if (this.followers_loading) return; this.followers_loading = true; this.followers_cursor = yield UserUtils.load_followers (this.account, this.user_id, this.followers_cursor); if (this.followers_cursor == null) { this.followers_list.set_placeholder_text (_("Protected Profile")); this.followers_list.set_empty (); return; } var users_array = this.followers_cursor.json_object.get_array (); users_array.foreach_element ((array, index, node) => { var user_obj = node.get_object (); string avatar_url = user_obj.get_string_member ("profile_image_url_https"); if (this.get_scale_factor () >= 2) avatar_url = avatar_url.replace ("_normal", "_bigger"); var entry = new UserListEntry (); entry.show_settings = false; entry.user_id = user_obj.get_int_member ("id"); entry.set_screen_name ("@" + user_obj.get_string_member ("screen_name")); entry.name = user_obj.get_string_member ("name"); entry.avatar_url = avatar_url; entry.get_style_context ().add_class ("tweet"); entry.show (); this.followers_list.add (entry); }); this.followers_loading = false; } private async void load_following () { if (this.following_cursor != null && this.following_cursor.full) return; if (this.following_loading) return; this.following_loading = true; this.following_cursor = yield UserUtils.load_following (this.account, this.user_id, this.following_cursor); if (this.following_cursor == null) { message ("null cursor"); this.following_list.set_placeholder_text (_("Protected Profile")); this.following_list.set_empty (); return; } var users_array = this.following_cursor.json_object.get_array (); users_array.foreach_element ((array, index, node) => { var user_obj = node.get_object (); string avatar_url = user_obj.get_string_member ("profile_image_url_https"); if (this.get_scale_factor () >= 2) avatar_url = avatar_url.replace ("_normal", "_bigger"); var entry = new UserListEntry (); entry.show_settings = false; entry.user_id = user_obj.get_int_member ("id"); entry.set_screen_name ("@" + user_obj.get_string_member ("screen_name")); entry.name = user_obj.get_string_member ("name"); entry.avatar_url = avatar_url; entry.get_style_context ().add_class ("tweet"); entry.show (); this.following_list.add (entry); }); this.following_loading = false; } private void load_profile_banner (string base_url) { string banner_url = base_url + "/mobile_retina"; Utils.download_pixbuf.begin (banner_url, null, (obj, res) => { Gdk.Pixbuf? banner = Utils.download_pixbuf.end (res); set_banner (banner); }); } [GtkCallback] private void follow_button_clicked_cb () { follow_button.sensitive = false; var call = account.proxy.new_call(); call.set_method ("POST"); if (follow_button.following) { call.set_function( "1.1/friendships/destroy.json"); } else { call.set_function ("1.1/friendships/create.json"); call.add_param ("follow", "false"); } call.add_param ("id", user_id.to_string ()); call.invoke_async.begin (null, (obj, res) => { try { this.follow_button.sensitive = (this.user_id != this.account.id); call.invoke_async.end (res); } catch (GLib.Error e) { var err = TweetUtils.failed_request_to_error(call, e); if (err.domain != TweetUtils.get_error_domain() || err.code != 160) { debug("Code: %d", err.code); Utils.show_error_dialog (err, main_window); follow_button.sensitive = true; return; } } if (follow_button.following) { TweetUtils.inject_user_unfollow (this.user_id, account); follower_count --; account.unfollow_id (this.user_id); ((SimpleAction)actions.lookup_action ("toggle-retweets")).set_enabled (false); set_retweets_disabled (false); } else { TweetUtils.inject_user_follow (this.user_id, account); set_user_blocked (false); follower_count ++; account.follow_id (this.user_id); ((SimpleAction)actions.lookup_action ("toggle-retweets")).set_enabled (true); } this.follow_button.following = !this.follow_button.following; update_follower_label (); follow_button.sensitive = true; }); } [GtkCallback] private bool activate_link (string uri) { return TweetUtils.activate_link (uri, main_window); } private inline void set_banner (Gdk.Pixbuf? banner) { if (banner == null) banner_image.pixbuf = Twitter.no_banner; else banner_image.pixbuf = banner; } /** * see IPage#onJoin */ public void on_join (int page_id, Cb.Bundle? args) { int64 user_id = args.get_int64 (KEY_USER_ID); if (user_id == -1) return; string? screen_name = args.get_string (KEY_SCREEN_NAME); if (screen_name != null) { this.screen_name = screen_name; } data_cancellable = new GLib.Cancellable (); tweet_list.reset_placeholder_text (); followers_list.reset_placeholder_text (); following_list.reset_placeholder_text (); if (user_id != this.user_id) { reset_data (); followers_cursor = null; followers_list.remove_all (); following_cursor = null; following_list.remove_all (); set_user_id (user_id); if (account.follows_id (user_id)) { this.follow_button.following = true; this.follow_button.sensitive = true; } tweet_list.model.clear (); user_lists.clear_lists (); lists_page_inited = false; load_tweets.begin (() => { // Try to load more in case we loaded tweets with RTs disabled //and didn't fetch enough in one go fill_tweet_list.begin(); }); override_block = false; show_tweet_list(); } else { /* Still load the friendship since muted/blocked/etc. may have changed */ load_friendship.begin (); } tweets_button.active = true; } public void on_leave () { // We might otherwise overwrite the new user's data with that from the old one. data_cancellable.cancel (); more_button.get_popover ().hide (); } private void reset_data () { name_label.label = " "; screen_name_label.label = " "; description_label.label = " "; url_label.label = " "; location_label.label = " "; tweets_label.label = " "; following_label.label = " "; followers_label.label = " "; avatar_image.surface = null; } public void create_radio_button (Gtk.RadioButton? group) {} public string get_title () { return "@" + screen_name; } public Gtk.RadioButton? get_radio_button(){ return null; } private void write_dm_activated (GLib.SimpleAction a, GLib.Variant? v) { var bundle = new Cb.Bundle (); bundle.put_int64 (DMPage.KEY_SENDER_ID, user_id); bundle.put_string (DMPage.KEY_SCREEN_NAME, screen_name); bundle.put_string (DMPage.KEY_USER_NAME, name); bundle.put_string (DMPage.KEY_AVATAR_URL, avatar_url.replace ("_bigger", "_normal")); main_window.main_widget.switch_page (Page.DM, bundle); } private void tweet_to_activated (GLib.SimpleAction a, GLib.Variant? v) { var cw = new ComposeTweetWindow (main_window, account, null); cw.set_text ("@" + screen_name + " "); cw.show_all (); } private void add_remove_list_activated (GLib.SimpleAction a, GLib.Variant? v) { var uld = new UserListDialog (main_window, account, user_id); uld.load_lists (); uld.show_all (); } private void toggle_blocked_activated (GLib.SimpleAction a, GLib.Variant? v) { bool current_state = get_user_blocked (); a.set_enabled (false); UserUtils.block_user.begin (account, this.user_id, !current_state, (obj, res) => { try { UserUtils.block_user.end (res); a.set_state(!current_state); if (!current_state) { this.follow_button.following = false; this.follow_button.sensitive = (this.user_id != this.account.id); } } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); } finally { a.set_enabled (true); } }); } private void toggle_muted_activated (GLib.SimpleAction a, GLib.Variant? v) { bool setting = get_user_muted (); a.set_enabled (false); UserUtils.mute_user.begin (account,this.user_id, !setting, (obj, res) => { try { UserUtils.mute_user.end (res); a.set_state (!setting); } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); } finally { a.set_enabled (true); } }); } private void retweet_action_activated (GLib.SimpleAction a, GLib.Variant? v) { if (retweet_item_blocked) return; retweet_item_blocked = true; bool current_state = a.get_state ().get_boolean (); a.set_state (new GLib.Variant.boolean (!current_state)); var call = account.proxy.new_call (); call.set_function ("1.1/friendships/update.json"); call.set_method ("POST"); call.add_param ("user_id", this.user_id.to_string ()); call.add_param ("retweets", current_state.to_string ()); if (current_state) { account.remove_disabled_rts_id (this.user_id); TweetUtils.inject_user_show_rts(user_id, account); } else { account.add_disabled_rts_id (this.user_id); TweetUtils.inject_user_hide_rts (this.user_id, account); } call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this.main_window); /* Reset the state if the retweeting failed */ a.set_state (new GLib.Variant.boolean (current_state)); } retweet_item_blocked = false; }); } private void hide_tweets(Cb.TweetState reason, string message) { tweet_list.hide_tweets_from(user_id, reason); tweet_list.hide_retweets_from(user_id, reason); tweet_list.set_placeholder_text(message); tweet_list.set_empty(); user_blocked_label.label = message; user_stack.visible_child = user_blocked_page; override_block = false; } private void show_tweets(Cb.TweetState reason) { tweet_list.show_tweets_from(user_id, reason); tweet_list.show_retweets_from(user_id, reason); if (tweet_list.model.get_n_items() == 0 && (!account.blocked_or_muted(user_id) || override_block)) { load_tweets.begin(() => { if (override_block) { // It's either this or adding a "flag mask" to a util function tweet_list.show_tweets_from(user_id, reason); tweet_list.show_retweets_from(user_id, reason); } }); } } private void set_user_blocked (bool blocked) { ((SimpleAction)actions.lookup_action ("toggle-blocked")).set_state (new GLib.Variant.boolean (blocked)); var reason = Cb.TweetState.HIDDEN_AUTHOR_BLOCKED; if (blocked) { hide_tweets(reason, _("User is blocked")); } else { show_tweets(reason); } } private bool get_user_blocked () { return ((SimpleAction)actions.lookup_action ("toggle-blocked")).get_state ().get_boolean (); } private void set_user_muted (bool muted) { ((SimpleAction)actions.lookup_action ("toggle-muted")).set_state (new GLib.Variant.boolean (muted)); var reason = Cb.TweetState.HIDDEN_AUTHOR_MUTED; if (muted) { hide_tweets(reason, _("User is muted")); } else { show_tweets(reason); } } private bool get_user_muted () { return ((SimpleAction)actions.lookup_action ("toggle-muted")).get_state ().get_boolean (); } private void set_retweets_disabled (bool disabled) { ((SimpleAction)actions.lookup_action ("toggle-retweets")).set_state (new GLib.Variant.boolean (disabled)); } private void update_follower_label () { followers_label.label = "%'d".printf(follower_count); followers_button.get_accessible().set_name(ngettext("%d follower", "%d followers", follower_count).printf(follower_count)); } public void stream_message_received (Cb.StreamMessageType type, Json.Node root_node) { if (type == Cb.StreamMessageType.TWEET) { Utils.set_rt_from_tweet (root_node, this.tweet_list.model, this.account); var obj = root_node.get_object (); var user = obj.get_object_member ("user"); if (user.get_int_member ("id") != this.user_id) return; // Correct user! var tweet = new Cb.Tweet (); tweet.load_from_json (root_node, account.id, new GLib.DateTime.now_local ()); this.tweet_list.model.add (tweet); } else if (type == Cb.StreamMessageType.DELETE) { var status = root_node.get_object ().get_object_member ("delete").get_object_member ("status"); int64 user_id = status.get_int_member ("user_id"); if (user_id != this.user_id) { return; } int64 id = status.get_int_member ("id"); bool was_seen; this.tweet_list.model.delete_id (id, out was_seen); } else if (type == Cb.StreamMessageType.RT_DELETE) { Utils.unrt_tweet (root_node, this.tweet_list.model); } else if (type == Cb.StreamMessageType.EVENT_HIDE_RTS) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { tweet_list.hide_retweets_from (event_user_id, Cb.TweetState.HIDDEN_RTS_DISABLED); fill_tweet_list.begin(); } } else if (type == Cb.StreamMessageType.EVENT_SHOW_RTS) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { tweet_list.show_retweets_from (event_user_id, Cb.TweetState.HIDDEN_RTS_DISABLED); } } else if (type == Cb.StreamMessageType.EVENT_BLOCK) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { set_user_blocked (true); } } else if (type == Cb.StreamMessageType.EVENT_MUTE) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { set_user_muted (true); } } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { set_user_blocked (false); } } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) { var event_user_id = get_user_id (root_node); if (event_user_id == user_id) { set_user_muted (false); } } } private async void fill_tweet_list() { // Try to load more tweets if we may not have enough because we disabled RTs from this user // But don't try too many times or we'll burn up all of our requests var count_multiplier = 1; for (int i = 0; i < 5; i++) { GLib.Idle.add(() => { // Give the scroller time to update its status fill_tweet_list.callback(); return GLib.Source.REMOVE; }); yield; if (this.is_scrollable) { break; } var prev_min_id = tweet_list.model.min_id; yield load_older_tweets(count_multiplier); if (tweet_list.model.min_id == prev_min_id) { count_multiplier++; } } } private int64 get_user_id (Json.Node root) { return root.get_object ().get_object_member ("target").get_int_member ("id"); } private void show_tweet_list() { if (account.blocked_or_muted (user_id) && !override_block) { user_stack.visible_child = user_blocked_page; } else { user_stack.visible_child = tweet_list; } } [GtkCallback] private void tweets_button_toggled_cb (GLib.Object source) { if (((Gtk.RadioButton)source).active) { this.balance_next_upper_change (BOTTOM); show_tweet_list(); } } [GtkCallback] private void followers_button_toggled_cb (GLib.Object source) { if (((Gtk.RadioButton)source).active) { if (this.followers_cursor == null) { this.load_followers.begin (); } this.balance_next_upper_change (BOTTOM); user_stack.visible_child = followers_list; } } [GtkCallback] private void following_button_toggled_cb (GLib.Object source) { if (((Gtk.RadioButton)source).active) { if (this.following_cursor == null) { this.load_following.begin (); } this.balance_next_upper_change (BOTTOM); user_stack.visible_child = following_list; } } [GtkCallback] private void lists_button_toggled_cb (GLib.Object source) { if (((Gtk.RadioButton)source).active) { if (!lists_page_inited) { user_lists.load_lists.begin (user_id, name, screen_name); lists_page_inited = true; } this.balance_next_upper_change (BOTTOM); user_stack.visible_child = user_lists; } } [GtkCallback] private void show_anyway_clicked(GLib.Object source) { override_block = true; user_stack.visible_child = tweet_list; if (account.is_muted(user_id)) { show_tweets(Cb.TweetState.HIDDEN_AUTHOR_MUTED); } if (account.is_blocked(user_id)) { show_tweets(Cb.TweetState.HIDDEN_AUTHOR_BLOCKED); } } public void rerun_filters () { TweetUtils.rerun_filters(tweet_list, account); } } cawbird-1.4.2/src/SearchPage.vala000066400000000000000000000415361416632607600166130ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/search-page.ui")] class SearchPage : IPage, Cb.MessageReceiver, Gtk.Box { public const int KEY_QUERY = 0; private const int USER_COUNT = 3; /** The unread count here is always zero */ public int unread_count { get { return 0; } } public unowned Account account; public int id { get; set; } private unowned MainWindow main_window; public unowned MainWindow window { set { main_window = value; tweet_list.main_window = main_window; } } [GtkChild] private unowned Gtk.SearchEntry search_entry; [GtkChild] private unowned Gtk.Button search_button; [GtkChild] private unowned TweetListBox tweet_list; [GtkChild] private unowned ListBox user_list; [GtkChild] private unowned Gtk.Label users_header; [GtkChild] private unowned Gtk.Label tweets_header; [GtkChild] private unowned ScrollWidget scroll_widget; private Gtk.RadioButton radio_button; private GLib.Cancellable? cancellable = null; private LoadMoreEntry load_more_entry = new LoadMoreEntry (); private string search_query; private int user_page = 1; private Gtk.Widget last_focus_widget; private Collect collect_obj; private uint remove_content_timeout = 0; private string last_search_query; private bool loading_tweets = false; private bool loading_users = false; private Json.Node? pending_user = null; public SearchPage (int id, Account account) { this.id = id; this.account = account; tweet_list.set_header_func (header_func); tweet_list.row_activated.connect (tweet_row_activated_cb); tweet_list.retry_button_clicked.connect (retry_button_clicked_cb); tweet_list.account = account; tweet_list.set_placeholder_text(_("No tweets found")); Utils.connect_vadjustment (scroll_widget, tweet_list); user_list.set_header_func (header_func); user_list.set_sort_func (twitter_item_sort_func); user_list.row_activated.connect (user_row_activated_cb); user_list.retry_button_clicked.connect (retry_button_clicked_cb); user_list.set_placeholder_text(_("No users found")); // We could connect the vadjust for the user list as well, but as it's the top then we won't bother search_entry.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.DOWN) { Gtk.Widget? first_row = user_list.get_first_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } else { first_row = tweet_list.get_first_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } } } return false; }); search_button.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.DOWN) { Gtk.Widget? first_row = user_list.get_first_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } else { first_row = tweet_list.get_first_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } } } return false; }); user_list.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { search_entry.grab_focus(); return true; } else if (direction == Gtk.DirectionType.DOWN) { Gtk.Widget? first_row = tweet_list.get_first_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } } return false; }); tweet_list.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { Gtk.Widget? first_row = user_list.get_last_visible_row(); if (first_row != null) { first_row.grab_focus(); return true; } else { search_entry.grab_focus(); } return true; } return false; }); search_button.clicked.connect (() => { search_for (search_entry.get_text()); }); load_more_entry.get_button ().clicked.connect (() => { user_page++; load_users (); }); scroll_widget.scrolled_to_end.connect (load_tweets); tweet_list.get_placeholder ().hide (); user_list.get_placeholder ().hide (); } protected virtual void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.EVENT_BLOCK) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_UNBLOCK) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_BLOCKED, Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } else if (type == Cb.StreamMessageType.EVENT_MUTE) { hide_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_UNMUTE) { show_tweets_from (root, Cb.TweetState.HIDDEN_AUTHOR_MUTED, Cb.TweetState.HIDDEN_RETWEETER_MUTED); } else if (type == Cb.StreamMessageType.EVENT_HIDE_RTS) { tweet_list.hide_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } else if (type == Cb.StreamMessageType.EVENT_SHOW_RTS) { tweet_list.show_retweets_from (get_user_id (root), Cb.TweetState.HIDDEN_RTS_DISABLED); } } private int64 get_user_id (Json.Node root) { return root.get_object ().get_object_member ("target").get_int_member ("id"); } protected void show_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.show_tweets_from (user_id, tweet_reason); tweet_list.show_retweets_from (user_id, retweet_reason); } protected void hide_tweets_from (Json.Node root, Cb.TweetState tweet_reason, Cb.TweetState retweet_reason = 0) { if (retweet_reason == 0) { retweet_reason = tweet_reason; } int64 user_id = get_user_id(root); tweet_list.hide_tweets_from (user_id, tweet_reason); tweet_list.hide_retweets_from (user_id, retweet_reason); } [GtkCallback] private void search_entry_activate_cb () { search_for (search_entry.get_text ()); } private void retry_button_clicked_cb () { search_for (last_search_query); } /** * see IPage#onJoin */ public void on_join (int page_id, Cb.Bundle? args) { string? term = args != null ? args.get_string (KEY_QUERY) : null; if (this.remove_content_timeout != 0) { GLib.Source.remove (this.remove_content_timeout); this.remove_content_timeout = 0; } else { scroll_widget.hide(); } if (term == null) { if (last_focus_widget != null && last_focus_widget.parent != null) last_focus_widget.grab_focus (); else search_entry.grab_focus (); return; } search_for (term, true); } public override void dispose () { if (this.remove_content_timeout != 0) { GLib.Source.remove (this.remove_content_timeout); this.remove_content_timeout = 0; } base.dispose (); } public void on_leave () { this.remove_content_timeout = GLib.Timeout.add (3 * 1000 * 60, () => { tweet_list.remove_all (); tweet_list.get_placeholder ().hide (); user_list.remove_all(); user_list.get_placeholder ().hide (); scroll_widget.hide(); this.last_focus_widget = null; this.remove_content_timeout = 0; return GLib.Source.REMOVE; }); } public void search_for (string search_term, bool set_text = false) { if (search_term.length == 0) { tweet_list.set_empty(); user_list.set_empty(); return; } this.last_search_query = search_term; if (this.cancellable != null) { debug ("Cancelling earlier search..."); this.cancellable.cancel (); } this.cancellable = new GLib.Cancellable (); string q = this.last_search_query;//search_term.copy (); // clear the list tweet_list.remove_all (); tweet_list.set_unempty (); user_list.remove_all(); user_list.set_unempty(); user_list.get_placeholder().show(); scroll_widget.show(); // Set accessible text var accessible_name = _("Users matching \"%s\"".printf(q)); user_list.get_accessible().set_name(accessible_name); user_list.get_accessible().set_description(accessible_name); accessible_name = _("Tweets matching \"%s\"".printf(q)); tweet_list.get_accessible().set_name(accessible_name); tweet_list.get_accessible().set_description(accessible_name); if (set_text) search_entry.set_text(q); this.search_query = q; this.user_page = 1; collect_obj = new Collect (2); collect_obj.finished.connect (show_entries); load_tweets (); load_users (); } private void tweet_row_activated_cb (Gtk.ListBoxRow row) { this.last_focus_widget = row; var bundle = new Cb.Bundle (); bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (TweetInfoPage.KEY_TWEET, ((TweetListEntry)row).tweet); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); } private void user_row_activated_cb (Gtk.ListBoxRow row) { this.last_focus_widget = row; var user_row = (UserListEntry)row; var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, user_row.user_id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, user_row.screen_name); main_window.main_widget.switch_page (Page.PROFILE, bundle); } private void header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? before) { Gtk.Widget header = row.get_header (); if (header != null) return; if (before == null) { if (row is UserListEntry) { row.set_header (users_header); } else if (row is TweetListEntry) { row.set_header (tweets_header); } } } private void load_users () { if (this.loading_users) return; this.loading_users = true; var user_call = account.proxy.new_call (); user_call.set_method ("GET"); user_call.set_function ("1.1/users/search.json"); user_call.add_param ("q", this.search_query); user_call.add_param ("count", (USER_COUNT + 1).to_string ()); user_call.add_param ("include_entities", "false"); user_call.add_param ("page", user_page.to_string ()); Cb.Utils.load_threaded_async.begin (user_call, cancellable, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { warning (e.message); user_list.set_error (e.message); if (!collect_obj.done) collect_obj.emit (); this.loading_users = false; return; } if (root == null) { this.loading_users = false; debug ("load_users: root is null"); if (!collect_obj.done) collect_obj.emit (); return; } var users = root.get_array (); if (user_list.get_children().length() + users.get_length() <= 0) { user_list.set_empty (); user_list.get_placeholder().show(); } var final_page = false; if (user_page > 1) { add_user_to_list(pending_user); } pending_user = null; if (this.loading_tweets) { // Keep a "loading" placeholder showing tweet_list.get_placeholder ().show (); } users.foreach_element ((array, index, node) => { if (index > USER_COUNT - 1) { // Keep one item back so that we know there's more to load pending_user = node; return; } final_page |= add_user_to_list(node); }); if (!final_page && pending_user != null) { if (load_more_entry.parent == null) { user_list.add (load_more_entry); } load_more_entry.show (); } else { load_more_entry.hide (); } if (!collect_obj.done) collect_obj.emit (); this.loading_users = false; }); } private bool add_user_to_list(Json.Node node) { var final_page = false; var user_obj = node.get_object (); var screen_name = user_obj.get_string_member ("screen_name"); var exists = false; var children = user_list.get_children(); children.reverse(); foreach (Gtk.Widget widget in children) { if (widget is UserListEntry && ((UserListEntry)widget).screen_name == screen_name) { // We got overlap final_page = true; exists = true; break; } } if (!exists) { var entry = new UserListEntry (); string avatar_url = user_obj.get_string_member ("profile_image_url_https"); if (this.get_scale_factor () >= 2) avatar_url = avatar_url.replace ("_normal", "_bigger"); entry.user_id = user_obj.get_int_member ("id"); entry.set_screen_name ("@" + screen_name); entry.name = user_obj.get_string_member ("name").strip (); entry.avatar_url = avatar_url; entry.verified = user_obj.get_boolean_member ("verified"); entry.protected_account = user_obj.get_boolean_member ("protected"); entry.show_settings = false; user_list.add (entry); } return final_page; } private void load_tweets () { if (loading_tweets) return; this.loading_tweets = true; TweetUtils.search_for_tweets.begin (account, this.search_query + " -filter:retweets", (this.tweet_list.model.min_id - 1), -1, 35, cancellable, (_, res) => { Cb.Tweet[] tweets; try { tweets = TweetUtils.search_for_tweets.end (res); } catch (GLib.Error e) { warning (e.message); tweet_list.set_error (e.message); this.loading_tweets = false; if (!collect_obj.done) collect_obj.emit (); return; } if (tweets.length <= 0) { tweet_list.set_empty (); tweet_list.get_placeholder().show(); } foreach (Cb.Tweet tweet in tweets) { tweet_list.model.add (tweet); } this.loading_tweets = false; if (!collect_obj.done) collect_obj.emit (); }); } private void show_entries (GLib.Error? e) { if (e != null) { user_list.set_error (e.message); user_list.set_empty (); tweet_list.set_empty (); this.loading_tweets = false; this.loading_users = false; return; } this.loading_tweets = false; this.loading_users = false; /* Work around a problem with GtkListBox where the entries are not redrawn for some reason. This happened whenever we remove_all'd all the rows from the list while it was not mapped */ tweet_list.queue_draw (); } public void create_radio_button (Gtk.RadioButton? group){ radio_button = new BadgeRadioButton (group, "cawbird-edit-find-symbolic", _("Search")); } public Gtk.RadioButton? get_radio_button() { return radio_button; } public string get_title () { return _("Search"); } public bool handles_double_open () { return true; } public void rerun_filters () { TweetUtils.rerun_filters(tweet_list, account); } } class LoadMoreEntry : Gtk.ListBoxRow, Cb.TwitterItem { private GLib.TimeSpan last_timediff; public bool seen { get { return true; } set {} } private Gtk.Button load_more_button; public LoadMoreEntry () { this.activatable = false; this.load_more_button = new Gtk.Button.with_label (_("Load More")); load_more_button.get_style_context ().add_class ("dim-label"); load_more_button.set_halign (Gtk.Align.FILL); load_more_button.set_hexpand (true); load_more_button.set_relief (Gtk.ReliefStyle.NONE); load_more_button.show (); this.add (load_more_button); } public Gtk.Button get_button () { return load_more_button; } public int update_time_delta (GLib.DateTime? now = null) {return 0;} public int64 get_sort_factor () { return int64.MAX - 2; } public int64 get_timestamp () { return 0; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } } cawbird-1.4.2/src/Settings.vala000066400000000000000000000127041416632607600164040ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public enum MediaVisibility{ SHOW = 1, HIDE = 2, HIDE_IN_TIMELINES = 3 } public enum TranslationService { GOOGLE = 0, BING = 1, DEEPL = 2, CUSTOM = 3 } public enum ShortcutKey { ALT = 0, CTRL = 1, SHIFT = 2, SUPER = 3, PRIMARY = 4 } public class Settings : GLib.Object { private static GLib.Settings settings; public static void init(){ settings = new GLib.Settings("uk.co.ibboard.cawbird"); } public static new GLib.Settings get () { return settings; } /** * Returns how many tweets should be stacked before a * notification should be created. */ public static int get_tweet_stack_count() { int setting_val = settings.get_enum("new-tweets-notify"); return setting_val; } /** * Check whether the user wants Cawbird to always use the dark gtk theme variant. */ public static bool use_dark_theme(){ return settings.get_boolean("use-dark-theme"); } public static bool notify_new_mentions(){ return settings.get_boolean("new-mentions-notify"); } public static bool notify_new_dms(){ return settings.get_boolean("new-dms-notify"); } public static bool auto_scroll_on_new_tweets () { return settings.get_boolean ("auto-scroll-on-new-tweets"); } public static string get_accel (string accel_name) { return settings.get_string ("accel-" + accel_name); } public static double get_tweet_scale() { int scale_idx = settings.get_enum ("tweet-scale"); switch (scale_idx) { case 3: return Pango.Scale.XX_LARGE; case 2: return Pango.Scale.X_LARGE; case 1: return Pango.Scale.LARGE; default: return Pango.Scale.MEDIUM; } } public static void toggle_topbar_visible () { settings.set_boolean ("sidebar-visible", !settings.get_boolean ("sidebar-visible")); } public static string get_consumer_key (string default_key = Cawbird.consumer_k) { var override_key = settings.get_string ("consumer-key"); return override_key != "" ? override_key : default_key; } public static string get_consumer_secret (string default_secret = Cawbird.consumer_s) { var override_secret = settings.get_string ("consumer-secret"); return override_secret != "" ? override_secret : default_secret; } public static void add_text_transform_flag (Cb.TransformFlags flag) { settings.set_uint ("text-transform-flags", settings.get_uint ("text-transform-flags") | flag); } public static void remove_text_transform_flag (Cb.TransformFlags flag) { settings.set_uint ("text-transform-flags", settings.get_uint ("text-transform-flags") & ~flag); } public static Cb.TransformFlags get_text_transform_flags () { return (Cb.TransformFlags) settings.get_uint ("text-transform-flags"); } public static bool hide_nsfw_content () { return settings.get_boolean ("hide-nsfw-content"); } public static MediaVisibility get_media_visiblity () { return (MediaVisibility)settings.get_enum ("media-visibility"); } public static TranslationService get_translation_service() { return (TranslationService)settings.get_enum("translation-service"); } public static string get_custom_translation_service() { return settings.get_string("custom-translation-service"); } public static void set_custom_translation_service(string new_url) { // The custom translation service gets a setter because we can't do normal binding because we want to validate the input settings.set_string("custom-translation-service", new_url); } public static string get_translation_service_url() { var translation_service = get_translation_service(); switch (translation_service) { case TranslationService.GOOGLE: return "https://translate.google.com/?op=translate&sl={SOURCE_LANG}&tl={TARGET_LANG}&text={CONTENT}"; case TranslationService.BING: return "https://www.bing.com/translator/?from={SOURCE_LANG}&to={TARGET_LANG}&text={CONTENT}"; case TranslationService.DEEPL: return "https://www.deepl.com/translator#{SOURCE_LANG}/{TARGET_LANG}/{CONTENT}"; default: return get_custom_translation_service(); } } public static ShortcutKey get_shortcut_key() { return (ShortcutKey)settings.get_enum("shortcut-key"); } public static string get_shortcut_key_string() { var shortcut_key = get_shortcut_key(); switch (shortcut_key) { case ShortcutKey.ALT: return ""; case ShortcutKey.CTRL: return ""; case ShortcutKey.SHIFT: return ""; case ShortcutKey.SUPER: return ""; case ShortcutKey.PRIMARY: return ""; default: return ""; } } } cawbird-1.4.2/src/TweetInfoPage.vala000066400000000000000000001142311416632607600173030ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/tweet-info-page.ui")] class TweetInfoPage : IPage, ScrollWidget, Cb.MessageReceiver { public const int KEY_MODE = 0; public const int KEY_TWEET = 1; public const int KEY_EXISTING = 2; public const int KEY_TWEET_ID = 3; public const int KEY_SCREEN_NAME = 4; public const int BY_INSTANCE = 1; public const int BY_ID = 2; private const GLib.ActionEntry[] action_entries = { {"quote", quote_activated }, {"reply", reply_activated }, {"favorite", favorite_activated}, {"delete", delete_activated } }; public int unread_count { get {return 0;} } public int id { get; set; } public unowned MainWindow window { set { main_window = value; replied_to_list_box.main_window = main_window; replies_list_box.main_window = main_window; self_replies_list_box.main_window = main_window; mentioned_replies_list_box.main_window = main_window; } } public unowned Account account; private int64 tweet_id; private string screen_name; private bool values_set = false; private Cb.Tweet tweet; private GLib.SimpleActionGroup actions; private unowned MainWindow main_window; private GLib.Cancellable? cancellable = null; [GtkChild] private unowned Gtk.Grid grid; [GtkChild] private unowned Gtk.Box main_box; [GtkChild] private unowned Gtk.Box lower_content; [GtkChild] private unowned MultiMediaWidget mm_widget; [GtkChild] private unowned Gtk.Label text_label; [GtkChild] private unowned TextButton name_button; [GtkChild] private unowned Gtk.Label screen_name_label; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Box translate_box; [GtkChild] private unowned Gtk.Label translate_label; [GtkChild] private unowned Gtk.Label rt_label; [GtkChild] private unowned Gtk.Image rt_image; [GtkChild] private unowned Gtk.Label rts_label; [GtkChild] private unowned Gtk.Label favs_label; [GtkChild] private unowned TweetListBox replied_to_list_box; [GtkChild] private unowned TweetListBox replies_list_box; [GtkChild] private unowned TweetListBox self_replies_list_box; [GtkChild] private unowned TweetListBox mentioned_replies_list_box; [GtkChild] private unowned Gtk.ToggleButton favorite_button; [GtkChild] private unowned Gtk.ToggleButton retweet_button; [GtkChild] private unowned Gtk.MenuButton menu_button; [GtkChild] private unowned Gtk.Label time_label; [GtkChild] private unowned Gtk.Label source_label; [GtkChild] private unowned Gtk.Stack main_stack; [GtkChild] private unowned Gtk.Label missing_tweet_label; [GtkChild] private unowned Gtk.Label error_label; [GtkChild] private unowned Gtk.Label reply_label; [GtkChild] private unowned Gtk.Box reply_box; public TweetInfoPage (int id, Account account) { this.id = id; this.account = account; this.replies_list_box.account = account; this.replies_list_box.set_thread_mode (true); this.self_replies_list_box.account = account; this.self_replies_list_box.set_thread_mode (true); this.mentioned_replies_list_box.account = account; this.mentioned_replies_list_box.set_thread_mode (true); this.replied_to_list_box.account = account; this.replied_to_list_box.set_thread_mode (true); Utils.connect_vadjustment (this, replies_list_box, scroll_past_top); Utils.connect_vadjustment (this, self_replies_list_box, scroll_past_top); Utils.connect_vadjustment (this, mentioned_replies_list_box, scroll_past_top); Utils.connect_vadjustment (this, replied_to_list_box, scroll_past_top); replied_to_list_box.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.DOWN) { name_button.grab_focus(); } return false; }); self_replies_list_box.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.DOWN) { if (mentioned_replies_list_box.is_visible()) { mentioned_replies_list_box.get_first_visible_row().grab_focus(); } else if (replies_list_box.is_visible()) { replies_list_box.get_first_visible_row().grab_focus(); } return true; } else if (direction == Gtk.DirectionType.UP) { menu_button.grab_focus(); return true; } return false; }); mentioned_replies_list_box.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { if (self_replies_list_box.is_visible()) { self_replies_list_box.get_last_visible_row().grab_focus(); } else { menu_button.grab_focus(); } return true; } else if (direction == Gtk.DirectionType.DOWN && replies_list_box.is_visible()) { replies_list_box.get_first_visible_row().grab_focus(); return true; } return false; }); replies_list_box.keynav_failed.connect((direction) => { if (direction == Gtk.DirectionType.UP) { if (mentioned_replies_list_box.is_visible()) { mentioned_replies_list_box.get_last_visible_row().grab_focus(); } else if (self_replies_list_box.is_visible()) { self_replies_list_box.get_last_visible_row().grab_focus(); } else { menu_button.grab_focus(); } return true; } return false; }); grid.set_redraw_on_allocate (true); mm_widget.media_clicked.connect ((m, i) => TweetUtils.handle_media_click (tweet.get_medias (), main_window, i)); replied_to_list_box.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int (KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (KEY_TWEET, ((TweetListEntry)row).tweet); bundle.put_bool (KEY_EXISTING, true); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); }); replies_list_box.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int (KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (KEY_TWEET, ((TweetListEntry)row).tweet); bundle.put_bool (KEY_EXISTING, true); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); }); self_replies_list_box.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int (KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (KEY_TWEET, ((TweetListEntry)row).tweet); bundle.put_bool (KEY_EXISTING, true); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); }); mentioned_replies_list_box.row_activated.connect ((row) => { var bundle = new Cb.Bundle (); bundle.put_int (KEY_MODE, TweetInfoPage.BY_INSTANCE); bundle.put_object (KEY_TWEET, ((TweetListEntry)row).tweet); bundle.put_bool (KEY_EXISTING, true); main_window.main_widget.switch_page (Page.TWEET_INFO, bundle); }); this.actions = new GLib.SimpleActionGroup (); this.actions.add_action_entries (action_entries, this); this.insert_action_group ("tweet", this.actions); Settings.get ().changed["media-visibility"].connect (media_visiblity_changed_cb); Settings.get ().changed["tweet-scale"].connect (set_tweet_text_scale); this.mm_widget.visible = (Settings.get_media_visiblity () != MediaVisibility.HIDE); set_tweet_text_scale(); } private void scroll_past_top(Gtk.ScrolledWindow parent, Gtk.ListBox list_box, int over_scroll) { parent.vadjustment.value = 0; } public override void size_allocate(Gtk.Allocation allocation) { base.size_allocate(allocation); lower_content.set_size_request(-1, allocation.height); } private void media_visiblity_changed_cb () { if (Settings.get_media_visiblity () == MediaVisibility.HIDE) this.mm_widget.hide (); else this.mm_widget.show (); } [GtkCallback] private bool key_released_cb (Gdk.EventKey evt) { #if DEBUG switch(evt.keyval) { case Gdk.Key.k: TweetUtils.log_tweet(tweet); return Gdk.EVENT_STOP; } #endif return Gdk.EVENT_PROPAGATE; } public void on_join (int page_id, Cb.Bundle? args) { int mode = args.get_int (KEY_MODE); if (mode == 0) return; values_set = false; bool existing = args.get_bool (KEY_EXISTING); main_stack.visible_child = main_box; missing_tweet_label.hide (); /* If we have a tweet instance here already, we set the avatar now instead of in * set_tweet_data, since the rearrange_tweets() or list.model.clear() calls * might cause the avatar to get removed from the cache. */ if (existing) { // Only possible BY_INSTANCE var tweet = (Cb.Tweet) args.get_object (KEY_TWEET); if (Twitter.get ().has_avatar (tweet.get_user_id ())) avatar_image.surface = Twitter.get ().get_cached_avatar (tweet.get_user_id ()); rearrange_tweets (tweet.id); } else { replied_to_list_box.model.clear (); replied_to_list_box.hide (); replies_list_box.model.clear (); replies_list_box.set_unempty (); replies_list_box.show (); self_replies_list_box.model.clear (); self_replies_list_box.hide (); mentioned_replies_list_box.model.clear (); mentioned_replies_list_box.hide (); } if (mode == BY_INSTANCE) { Cb.Tweet tweet = (Cb.Tweet)args.get_object (KEY_TWEET); if (Twitter.get ().has_avatar (tweet.get_user_id ())) avatar_image.surface = Twitter.get ().get_cached_avatar (tweet.get_user_id ()); if (tweet.retweeted_tweet != null) this.tweet_id = tweet.retweeted_tweet.id; else this.tweet_id = tweet.id; this.screen_name = tweet.get_screen_name (); this.tweet = tweet; set_tweet_data (tweet); } else if (mode == BY_ID) { this.tweet = null; this.tweet_id = args.get_int64 (KEY_TWEET_ID); this.screen_name = args.get_string (KEY_SCREEN_NAME); } query_tweet_info (); } private void set_tweet_text_scale () { // Use the larger of the old scale and the user's scale // We don't multiply because 1.5 * XX-Large would be HUGE! var scale = double.max(Settings.get_tweet_scale(), 1.5); var new_attribs = new Pango.AttrList(); var scale_attr = Pango.attr_scale_new(scale); new_attribs.insert((owned)scale_attr); text_label.set_attributes(new_attribs); } private void load_user_avatar (string url) { string avatar_url; int scale = this.get_scale_factor (); if (scale == 1) avatar_url = url.replace ("_normal", "_bigger"); else avatar_url = url.replace ("_normal", "_200x200"); TweetUtils.download_avatar.begin (avatar_url, 73 * scale, cancellable, (obj, res) => { Cairo.Surface surface; try { var pixbuf = TweetUtils.download_avatar.end (res); if (pixbuf == null) { surface = scale_surface ((Cairo.ImageSurface)Twitter.no_avatar, 73, 73); } else { surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, scale, null); } } catch (GLib.Error e) { warning (e.message); surface = Twitter.no_avatar; } avatar_image.surface = surface; }); } private void rearrange_tweets (int64 new_id) { replies_list_box.model.clear (); replies_list_box.set_unempty (); replies_list_box.show (); if (replies_list_box.model.contains_id (new_id) || mentioned_replies_list_box.model.contains_id (new_id)) { // We're moving down the thread to a reply of the currently displayed tweet, // so move the current tweet up into replied_to_list_box replied_to_list_box.model.add (this.tweet); replied_to_list_box.show (); self_replies_list_box.model.clear (); self_replies_list_box.hide (); mentioned_replies_list_box.model.clear (); mentioned_replies_list_box.hide (); } else if (self_replies_list_box.model.contains_id (new_id)) { // We're moving down the thread to a self-reply of the currently displayed tweet, // so move all intervening tweets up into replied_to_list_box replied_to_list_box.model.add (this.tweet); replied_to_list_box.show (); mentioned_replies_list_box.model.clear (); mentioned_replies_list_box.hide (); var idx = self_replies_list_box.model.index_of (new_id); for (int i = 0; i < idx; i++) { replied_to_list_box.model.add ((Cb.Tweet)self_replies_list_box.model.get_item (i)); } self_replies_list_box.model.remove_oldest_n_visible (idx + 1); if (self_replies_list_box.model.get_n_items () == 0) { self_replies_list_box.hide (); } } else if (replied_to_list_box.model.contains_id (new_id)) { // We're moving up the thread to a replied-to tweet so // remove all tweets below the selected one from the "replied to" list box // (they'll now be replies) and add the direct successor to the replies list // or add the chain of self-replies to the self-reply list // Other replies will then be loaded by a separate process mentioned_replies_list_box.model.clear (); mentioned_replies_list_box.hide (); self_replies_list_box.hide (); var idx = replied_to_list_box.model.index_of (new_id); var new_tweet = (Cb.Tweet)replied_to_list_box.model.get_item (idx); var new_screen_name_lower = new_tweet.get_screen_name().down(); var self_replies_count = self_replies_list_box.model.get_n_items (); if (self_replies_count > 0) { Cb.Tweet? remove_from_tweet = null; for (int i = 0; i < self_replies_count; i++) { var tweet = (Cb.Tweet)self_replies_list_box.model.get_item (i); if (tweet.get_screen_name().down() != new_screen_name_lower) { remove_from_tweet = tweet; break; } } if (remove_from_tweet != null) { self_replies_list_box.model.remove_tweets_later_than (remove_from_tweet.id); } } var list_length = replied_to_list_box.model.get_n_items (); var prev_id = new_id; var mentions = new_tweet.get_mentions (); for (int i = 0; i < mentions.length; i++) { mentions[i] = mentions[i].down(); } for (int i = idx + 1; i < list_length; i++) { var tweet = (Cb.Tweet)replied_to_list_box.model.get_item (i); var tweet_screen_name = tweet.get_screen_name().down(); if (tweet_screen_name == new_screen_name_lower) { self_replies_list_box.model.add (tweet); self_replies_list_box.show (); prev_id = tweet.id; } else if (i == idx + 1) { if (tweet_screen_name in mentions) { mentioned_replies_list_box.model.add (tweet); mentioned_replies_list_box.show (); } else { replies_list_box.model.add (tweet); replies_list_box.show (); } self_replies_list_box.model.clear (); break; } else { var moved_item_count = i - idx; if (self_replies_list_box.model.get_n_items () > moved_item_count) { // Remove the remaining self-replies, which now aren't a self-reply thread self_replies_list_box.model.remove_tweets_later_than (tweet.id); } break; } } var cur_reply_id = this.tweet.source_tweet.reply_id; var screen_name_lower = screen_name.down(); if (cur_reply_id == new_id) { if (screen_name_lower == new_screen_name_lower) { self_replies_list_box.model.add (this.tweet); self_replies_list_box.show (); } else if (screen_name_lower in mentions) { mentioned_replies_list_box.model.add (this.tweet); mentioned_replies_list_box.show (); } else { replies_list_box.model.add (this.tweet); replies_list_box.show (); } } else if (self_replies_list_box.model.contains_id (cur_reply_id) && screen_name_lower == new_screen_name_lower) { self_replies_list_box.model.add (this.tweet); self_replies_list_box.show (); } replied_to_list_box.model.remove_tweets_later_than (new_id); if (replied_to_list_box.model.get_n_items () == 0) replied_to_list_box.hide (); } else { // New tweet - wipe the lot to be sure! // (It's most likely to be going back in the history from a previous // move *up* the thread, so we might be able to keep the replied_to, // but there's also the possibility that we're moving back from a // quoted tweet and its thread and we can't tell until we load everything // so just wipe it and be done with it) replied_to_list_box.model.clear (); replied_to_list_box.hide (); mentioned_replies_list_box.model.clear (); mentioned_replies_list_box.hide (); self_replies_list_box.model.clear (); self_replies_list_box.hide (); } } public void on_leave () { if (cancellable != null) { cancellable.cancel (); cancellable = null; } } [GtkCallback] private void favorite_button_toggled_cb () { toggle_favorite_status (); } private void toggle_favorite_status () { if (!values_set) return; favorite_button.sensitive = false; TweetUtils.set_favorite_status.begin (account, tweet, favorite_button.active, (obj, res) => { var success = false; try { success = TweetUtils.set_favorite_status.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { if (tweet.is_flag_set (Cb.TweetState.FAVORITED)) { this.tweet.favorite_count ++; } else { this.tweet.favorite_count --; } this.update_rts_favs_labels (); } else { favorite_button.active = tweet.is_flag_set (Cb.TweetState.FAVORITED); } favorite_button.sensitive = true; }); } [GtkCallback] private void retweet_button_toggled_cb () { if (!values_set) return; retweet_button.sensitive = false; TweetUtils.set_retweet_status.begin (account, tweet, retweet_button.active, (obj, res) => { var success = false; try { success = TweetUtils.set_retweet_status.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { if (tweet.is_flag_set (Cb.TweetState.RETWEETED)) { this.tweet.retweet_count ++; } else { this.tweet.retweet_count --; } this.update_rts_favs_labels (); } else { retweet_button.active = tweet.is_flag_set (Cb.TweetState.RETWEETED); } retweet_button.sensitive = true; }); } [GtkCallback] private void reply_button_clicked_cb () { ComposeTweetWindow ctw = new ComposeTweetWindow(main_window, this.account, this.tweet, ComposeTweetWindow.Mode.REPLY); ctw.show (); } [GtkCallback] private bool link_activated_cb (string uri) { return TweetUtils.activate_link (uri, main_window); } [GtkCallback] private void name_button_clicked_cb () { int64 id; string screen_name; if (this.tweet.retweeted_tweet != null) { id = this.tweet.retweeted_tweet.author.id; screen_name = this.tweet.retweeted_tweet.author.screen_name; } else { id = this.tweet.source_tweet.author.id; screen_name = this.tweet.source_tweet.author.screen_name; } var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, screen_name); main_window.main_widget.switch_page (Page.PROFILE, bundle); } private void query_tweet_info () { if (this.cancellable != null) { this.cancellable.cancel (); } this.cancellable = new Cancellable (); var now = new GLib.DateTime.now_local (); var call = account.proxy.new_call (); call.set_method ("GET"); call.set_function ("1.1/statuses/show.json"); call.add_param ("id", tweet_id.to_string ()); call.add_param ("include_my_retweet", "true"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); Cb.Utils.load_threaded_async.begin (call, cancellable, (__, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { error_label.label = "%s: %s".printf (_("Could not show tweet"), e.message); main_stack.visible_child = error_label; return; } if (root == null) return; Json.Object root_object = root.get_object (); if (this.tweet != null) { int n_retweets = (int)root_object.get_int_member ("retweet_count"); int n_favorites = (int)root_object.get_int_member ("favorite_count"); this.tweet.retweet_count = n_retweets; this.tweet.favorite_count = n_favorites; } else { this.tweet = new Cb.Tweet (); tweet.load_from_json (root, account.id, now); } string source_client = root_object.get_string_member ("source"); source_client = "" + extract_source (source_client) + ""; set_tweet_data (tweet, source_client); if (tweet.retweeted_tweet == null) load_replied_to_tweet (tweet.source_tweet.reply_id); else load_replied_to_tweet (tweet.retweeted_tweet.reply_id); values_set = true; }); // Pull the user's self-replies and user replies separately to ensure user replies don't overrun the thread // It doubles our query count, but we get 180 per 15 minutes, which is still 6 threads per minute! TweetUtils.search_for_tweets_json.begin (account, "to:" + this.screen_name + " from:" + this.screen_name, -1, tweet_id, 100, cancellable, add_replies); TweetUtils.search_for_tweets_json.begin (account, "to:" + this.screen_name + " -from:" + this.screen_name, -1, tweet_id, 100, cancellable, (src, res) => { add_replies(src, res); if (replies_list_box.model.get_n_items() == 0) { replies_list_box.hide(); } }); } private void add_replies (GLib.Object? src, GLib.AsyncResult res) { var now = new GLib.DateTime.now_local (); GLib.List statuses; try { statuses = TweetUtils.search_for_tweets_json.end (res); } catch (GLib.Error e) { if (!(e is GLib.IOError.CANCELLED)) warning (e.message); return; } // Get the screen name of the author and the mentions of the current tweet. // And lowercase them so we can compare them, because Twitter isn't consistent in its casing // even in internal fields! var screen_name_lower = screen_name.down(); var mentions = tweet.get_mentions (); for (int i = 0; i < mentions.length; i++) { mentions[i] = mentions[i].down(); } int64[] thread_ids = {tweet_id}; // Results come back in decreasing chronological order, but we need to work increasing statuses.reverse(); statuses.foreach ((node) => { var obj = node.get_object (); if (!obj.has_member ("in_reply_to_status_id") || obj.get_null_member ("in_reply_to_status_id")) return; int64 reply_id = obj.get_int_member ("in_reply_to_status_id"); if (!(reply_id in thread_ids)) { // Not relevant to the thread? Skip it return; } var user_obj = obj.get_object_member("user"); var reply_screen_name = user_obj.get_string_member("screen_name").down(); if (reply_id != tweet_id && reply_screen_name != screen_name_lower) { // Potentially relevant to the thread, but not from the author and not in reply to the current tweet? Skip it, it's something else return; } var t = new Cb.Tweet (); t.load_from_json (node, account.id, now); TweetUtils.set_tweet_hidden_flags(t, account); if (reply_screen_name == screen_name_lower) { // Must be relevant by now, so matching screen name means it's more of the author's thread thread_ids += t.id; self_replies_list_box.model.add (t); } else if (reply_screen_name in mentions) { mentioned_replies_list_box.model.add (t); } else { replies_list_box.model.add (t); } }); if (replies_list_box.model.get_n_items () > 0) { replies_list_box.show (); } if (mentioned_replies_list_box.model.get_n_items () > 0) { mentioned_replies_list_box.show (); } if (self_replies_list_box.model.get_n_items () > 0) { self_replies_list_box.show (); } } /** * Loads the tweet this tweet is a reply to. * This will recursively call itself until the end of the chain is reached. * * @param reply_id The id of the tweet the previous tweet was a reply to. */ private void load_replied_to_tweet (int64 reply_id) { if (reply_id == 0) { // Top of the thread, so stop return; } var replied_to_idx = replied_to_list_box.model.index_of (reply_id); if (replied_to_idx == -1) { replied_to_idx = replied_to_list_box.model.index_of_retweet (reply_id); } if (replied_to_idx != -1) { // We already have this tweet, so don't fetch it from the web // BUT we might not have the rest of the thread (because they pressed "Back" after we removed some of the thread) // so recurse anyway var replied_to_tweet = (Cb.Tweet)replied_to_list_box.model.get_item (replied_to_idx); if (replied_to_tweet.retweeted_tweet == null) { load_replied_to_tweet (replied_to_tweet.source_tweet.reply_id); } else { load_replied_to_tweet (replied_to_tweet.retweeted_tweet.reply_id); } return; } this.balance_next_upper_change(TOP); replied_to_list_box.show (); var call = account.proxy.new_call (); call.set_function ("1.1/statuses/show.json"); call.set_method ("GET"); call.add_param ("id", reply_id.to_string ()); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); call.invoke_async.begin (cancellable, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var err = TweetUtils.failed_request_to_error (call, e); if (err.domain == TweetUtils.get_error_domain()) { if (err.code == 179) { missing_tweet_label.label = _("This tweet is hidden by the author"); } else { // err.code == 144 missing_tweet_label.label = _("This tweet is unavailable"); } missing_tweet_label.show(); } else { Utils.show_error_dialog (err, this.main_window); } replied_to_list_box.visible = (replied_to_list_box.get_children ().length () > 0); return; } var parser = new Json.Parser (); try { parser.load_from_data (call.get_payload ()); } catch (GLib.Error e) { critical (e.message); return; } /* If we get here, the tweet is not protected so we can just use it */ var tweet = new Cb.Tweet (); tweet.load_from_json (parser.get_root (), account.id, new GLib.DateTime.now_local ()); TweetUtils.set_tweet_hidden_flags(tweet, account); replied_to_list_box.model.add (tweet); this.balance_next_upper_change(TOP); if (tweet.retweeted_tweet == null) load_replied_to_tweet (tweet.source_tweet.reply_id); else load_replied_to_tweet (tweet.retweeted_tweet.reply_id); }); } /** * */ private void set_tweet_data (Cb.Tweet tweet, string? with = null) { bool tweet_is_protected = tweet.is_flag_set (Cb.TweetState.PROTECTED); bool tweet_is_verified = tweet.is_flag_set (Cb.TweetState.VERIFIED); account.user_counter.user_seen_full (tweet.get_user_id (), tweet.get_screen_name (), tweet.get_user_name (), tweet_is_verified, tweet_is_protected); GLib.DateTime created_at = new GLib.DateTime.from_unix_local ( tweet.retweeted_tweet != null ? tweet.retweeted_tweet.created_at : tweet.source_tweet.created_at); string time_format = created_at.format ("%x, %X"); if (with != null) { time_format += " via " + with; } text_label.label = tweet.get_formatted_text (); name_button.set_text (tweet.get_user_name ()); name_button.tooltip_text = tweet.get_user_name (); var screen_name = "@" + tweet.get_screen_name (); screen_name_label.label = screen_name; screen_name_label.tooltip_text = screen_name; load_user_avatar (tweet.avatar_url); if (Utils.needs_translating(tweet)) { translate_box.show(); var buff = new StringBuilder (); buff.append ("") // TRANSLATORS: Insert your *actual* language here instead of English - e.g. "Ins Deutsche übersetzen" or "Traduire en français" .append (_("Translate to English")) .append (""); translate_label.label = buff.str; } else { translate_box.hide(); } if (tweet.retweeted_tweet != null) { rt_label.show (); rt_image.show (); var buff = new StringBuilder (); buff.append ("") .append (GLib.Markup.escape_text(tweet.source_tweet.author.user_name)) .append (" @") .append (tweet.source_tweet.author.screen_name); rt_label.label = buff.str; } else { rt_label.hide (); rt_image.hide (); } update_rts_favs_labels (); time_label.label = time_format; retweet_button.active = tweet.is_flag_set (Cb.TweetState.RETWEETED); favorite_button.active = tweet.is_flag_set (Cb.TweetState.FAVORITED); avatar_image.verified = tweet_is_verified; avatar_image.protected_account = tweet_is_protected; // Linking to a RT in New Twitter gives you a "RT …" page with no apparent way to get // to the original tweet, therefore we need to link to the RTed tweet to be useful. // Also, this used to mix the RT ID with the RTed username, which was wrong. if (tweet.retweeted_tweet != null) { set_source_link (tweet.retweeted_tweet.id, tweet.retweeted_tweet.author.screen_name); } else { set_source_link (tweet.source_tweet.id, tweet.source_tweet.author.screen_name); } if ((tweet.retweeted_tweet != null && tweet.retweeted_tweet.reply_id != 0) || tweet.source_tweet.reply_id != 0) { var author_id = (tweet.retweeted_tweet != null) ? tweet.retweeted_tweet.author.id : tweet.source_tweet.author.id; var reply_users = tweet.get_reply_users (); if (reply_users.length > 0) { reply_box.show (); reply_label.label = Utils.build_reply_to_string(reply_users, author_id, false); } else { reply_box.hide (); } } else { reply_box.hide (); } if (tweet.has_inline_media ()) { this.mm_widget.visible = (Settings.get_media_visiblity () != MediaVisibility.HIDE); mm_widget.set_all_media (tweet.get_medias ()); } else { mm_widget.hide (); } ((GLib.SimpleAction)actions.lookup_action ("delete")).set_enabled (tweet.get_user_id () == account.id); if (tweet.is_flag_set (Cb.TweetState.PROTECTED)) { retweet_button.hide (); ((GLib.SimpleAction)actions.lookup_action ("quote")).set_enabled (false); } else { retweet_button.show (); ((GLib.SimpleAction)actions.lookup_action ("quote")).set_enabled (true); } } private void update_rts_favs_labels () { rts_label.label = "%'d %s".printf (tweet.retweet_count, _("Retweets")); favs_label.label = "%'d %s".printf (tweet.favorite_count, _("Likes")); } private void set_source_link (int64 id, string screen_name) { var link = "https://twitter.com/%s/status/%s".printf (screen_name, id.to_string ()); source_label.label = "%s" .printf (link, _("Open in Browser"), _("Source")); } private void quote_activated () { ComposeTweetWindow ctw = new ComposeTweetWindow(main_window, this.account, this.tweet, ComposeTweetWindow.Mode.QUOTE); ctw.show (); } private void reply_activated () { ComposeTweetWindow ctw = new ComposeTweetWindow(main_window, this.account, this.tweet, ComposeTweetWindow.Mode.REPLY); ctw.show (); } private void favorite_activated () { if (!favorite_button.sensitive) return; toggle_favorite_status (); } private void delete_activated () { if (this.tweet == null || this.tweet.get_user_id () != account.id) { return; } TweetUtils.delete_tweet.begin (account, tweet, (obj, res) => { var success = false; try { success = TweetUtils.delete_tweet.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { this.main_window.main_widget.remove_current_page (); } }); } public string get_title () { return _("Tweet Details"); } /** * Twitter's source parameter of tweets includes a 'rel' parameter * that doesn't work as pango markup, so we rebuild a hyperlink that will work. * * Note: This assumes a certain format of source parameter hyperlink: * TweetDeck * * @param source_str The source string from twitter * * @return The rebuilt #source_string that's valid pango markup */ private string extract_source (string source_str) { if (source_str == "") { // TRANSLATORS: Used for the "via " line in Tweet Info view when the client is blank return _("an unknown client"); } int from, to; from = source_str.index_of_char ('"') + 1; to = source_str.index_of_char ('"', from); if (to == -1 || from == -1) return source_str; int name_start = source_str.index_of_char('>', to) + 1; int name_end = source_str.index_of_char('<', name_start); string client_name = source_str.substring(name_start, name_end - name_start); return "%s".printf(source_str.substring (from, to - from), client_name, client_name); } public void create_radio_button (Gtk.RadioButton? group) {} public Gtk.RadioButton? get_radio_button () { return null; } public void stream_message_received (Cb.StreamMessageType type, Json.Node root) { if (type == Cb.StreamMessageType.TWEET) { Json.Object root_obj = root.get_object (); if (Utils.usable_json_value (root_obj, "in_reply_to_status_id")) { int64 reply_id = root_obj.get_int_member ("in_reply_to_status_id"); if (reply_id == this.tweet_id) { var t = new Cb.Tweet (); t.load_from_json (root, account.id, new GLib.DateTime.now_local ()); var screen_name_lower = t.get_screen_name().down(); var mentions = tweet.get_mentions (); for (int i = 0; i < mentions.length; i++) { mentions[i] = mentions[i].down(); } if (screen_name_lower == screen_name.down()) { self_replies_list_box.model.add (t); self_replies_list_box.show (); } else if (screen_name_lower in mentions) { mentioned_replies_list_box.model.add (t); mentioned_replies_list_box.show (); } else { replies_list_box.model.add (t); replies_list_box.show (); } } } } else if (type == Cb.StreamMessageType.DELETE) { int64 tweet_id = root.get_object ().get_object_member ("delete") .get_object_member ("status") .get_int_member ("id"); if (tweet_id == this.tweet_id && main_window.cur_page_id == this.id) { /* TODO: We should probably remove this page with this bundle form the history, even if it's not the currently visible page */ debug ("Current tweet with id %s deleted!", tweet_id.to_string ()); this.main_window.main_widget.remove_current_page (); } } else if (type == Cb.StreamMessageType.EVENT_FAVORITE) { int64 id = root.get_object ().get_int_member ("id"); if (id == this.tweet_id) { this.values_set = false; this.favorite_button.active = true; this.tweet.favorite_count ++; this.update_rts_favs_labels (); this.values_set = true; } } else if (type == Cb.StreamMessageType.EVENT_UNFAVORITE) { int64 id = root.get_object ().get_object_member ("target_object").get_int_member ("id"); int64 source_id = root.get_object ().get_object_member ("source").get_int_member ("id"); if (source_id == account.id && id == this.tweet_id) { this.values_set = false; this.favorite_button.active = false; this.tweet.favorite_count --; this.update_rts_favs_labels (); this.values_set = true; } } } public void rerun_filters () { TweetUtils.rerun_filters(replied_to_list_box, account); TweetUtils.rerun_filters(self_replies_list_box, account); TweetUtils.rerun_filters(mentioned_replies_list_box, account); TweetUtils.rerun_filters(replies_list_box, account); } } cawbird-1.4.2/src/Twitter.vala000066400000000000000000000261151416632607600162470ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public struct UserInfo { int64 id; string name; string screen_name; string description; string avatar_url; string? banner_url; string website; } public class Twitter : GLib.Object { private static Twitter twitter; private Twitter () {} public static new Twitter get () { if (twitter == null) twitter = new Twitter (); return twitter; } [Signal (detailed = true)] private signal void avatar_downloaded (Cairo.Surface avatar); // Limit was once 3MB for GIFs and 5MB for images and Corebird used 3MB throughout. // In 2020 it's 5MB image, 15MB GIF and video - https://developer.twitter.com/en/docs/media/upload-media/overview public const int MAX_BYTES_PER_IMAGE = 1024 * 1024 * 5; public const int MAX_BYTES_PER_GIF = 1024 * 1024 * 15; public const int MAX_BYTES_PER_VIDEO = 1024 * 1024 * 15; public const int short_url_length = 23; public const int max_media_per_upload = 4; public static Cairo.Surface no_avatar; public static Gdk.Pixbuf no_banner; private Cb.AvatarCache avatar_cache; private GLib.HashTable user_json_cache; public void init () { try { Twitter.no_avatar = Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/no_avatar.png"), 1, null); Twitter.no_banner = new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/no_banner.png"); } catch (GLib.Error e) { error ("Error while loading assets: %s", e.message); } this.avatar_cache = new Cb.AvatarCache (); this.user_json_cache = new HashTable(GLib.int_hash, GLib.int_equal); } public void ref_avatar (Cairo.Surface surface) { this.avatar_cache.increase_refcount_for_surface (surface); } public void unref_avatar (Cairo.Surface surface) { this.avatar_cache.decrease_refcount_for_surface (surface); } public bool has_avatar (int64 user_id) { return (get_cached_avatar (user_id) != Twitter.no_avatar); } public Cairo.Surface get_cached_avatar (int64 user_id) { bool found; Cairo.Surface? surface = this.avatar_cache.get_surface_for_id (user_id, out found); if (surface == null) return Twitter.no_avatar; else return surface; } /* This is a get_avatar version for times where we don't have an at least relatively recent avatar_url for the given account. This will first query the account details of the given account, then use the avatar_url to download the avatar and insert it into the avatar cache */ public async Cairo.Surface? load_avatar_for_user_id (Account account, int64 user_id, int size) { Cairo.Surface? s; bool found = false; s = avatar_cache.get_surface_for_id (user_id, out found); if (s != null) { assert (found); return s; } if (s == null && found) { ulong handler_id = 0; handler_id = this.avatar_downloaded[user_id.to_string ()].connect ((ava) => { s = ava; this.disconnect (handler_id); this.load_avatar_for_user_id.callback (); }); yield; assert (s != null); return s; } this.avatar_cache.add (user_id, null, null); string avatar_url = yield this.get_user_string_member (account, user_id, "profile_image_url_https"); if (avatar_url == null) { return null; } this.avatar_cache.set_url (user_id, avatar_url); s = yield this.get_surface(user_id, avatar_url, size, true); if (s != null) return s; else yield; return s; } /** * Get the avatar with the given url. If the avatar exists on the * hard drive already, it is loaded and returned immediately. If * the avatar is in memory already, that version is returned. * If the avatar is neither on disk nor in memory, it will be downladed * first and set via the supplied `func`. */ public async void get_avatar (int64 user_id, string url, AvatarWidget dest_widget, int size = 48, bool force_download = false) { dest_widget.surface = yield this.get_surface (user_id, url, size, force_download); } private async Cairo.Surface? get_surface (int64 user_id, string url, int size = 48, bool force_download = false) { assert (user_id > 0); bool has_key = false; Cairo.Surface? a = this.avatar_cache.get_surface_for_id (user_id, out has_key); bool new_url = a == Twitter.no_avatar && url != this.avatar_cache.get_url_for_id (user_id); if (a != null && !new_url) { return a; } if (has_key && !new_url && !force_download) { // wait until the avatar has finished downloading ulong handler_id = 0; handler_id = this.avatar_downloaded[user_id.to_string ()].connect ((ava) => { this.disconnect (handler_id); a = ava; get_surface.callback (); }); yield; return a; } else { // download the avatar this.avatar_cache.add (user_id, null, url); Gdk.Pixbuf? avatar = null; try { avatar = yield TweetUtils.download_avatar (url, size); } catch (GLib.Error e) { warning ("%s for %s", e.message, url); } Cairo.Surface s; // E.g. in the 404 case... if (avatar == null) s = Twitter.no_avatar; else s = Gdk.cairo_surface_create_from_pixbuf (avatar, 1, null); // a NULL surface is already in the cache this.avatar_cache.set_avatar (user_id, s, url); // signal all the other waiters in the queue avatar_downloaded[user_id.to_string ()](s); return s; } } public async string get_user_name (Account account, int64 user_id) { return yield get_user_string_member (account, user_id, "name"); } public async string get_screen_name (Account account, int64 user_id) { return yield get_user_string_member (account, user_id, "screen_name"); } public async string get_avatar_url (Account account, int64 user_id) { return yield get_user_string_member (account, user_id, "profile_image_url_https"); } private async string get_user_string_member (Account account, int64 user_id, string field_name) { Json.Node? user_json = yield get_user_json_by_id (account.proxy, user_id); string username = null; if (user_json != null) { username = user_json.get_object ().get_string_member (field_name); } return username; } private async Json.Node? get_user_json_by_id (Rest.OAuthProxy proxy, int64 user_id) { if (this.user_json_cache.contains (user_id)) { return this.user_json_cache[user_id]; } Json.Node? root = null; try { root = yield get_user_json (proxy, null, user_id); this.user_json_cache[user_id] = root; } catch (GLib.Error e) { warning (e.message); } return root; } private async Json.Node? get_user_json (Rest.OAuthProxy proxy, string? screen_name, int64 user_id) throws GLib.Error { var call = proxy.new_call (); call.set_function ("1.1/users/show.json"); call.set_method ("GET"); if (screen_name != null) { call.add_param ("screen_name", screen_name); } else { call.add_param ("user_id", user_id.to_string ()); } return yield Cb.Utils.load_threaded_async (call, null); } public async UserInfo get_user_info_by_screen_name(Rest.OAuthProxy proxy, string screen_name) throws GLib.Error { return yield get_user_info (proxy, screen_name, -1); } public async UserInfo get_user_info_by_user_id(Rest.OAuthProxy proxy, int64 user_id) throws GLib.Error { return yield get_user_info (proxy, null, user_id); } private async UserInfo get_user_info(Rest.OAuthProxy proxy, string? screen_name, int64 user_id) throws GLib.Error { return parse_user_info (yield get_user_json(proxy, screen_name, user_id)); } public async UserInfo get_own_user_info(Rest.OAuthProxy proxy) throws GLib.Error { var call = proxy.new_call (); call.set_function ("1.1/account/verify_credentials.json"); call.set_method ("GET"); call.add_param ("include_entities", "true"); call.add_param ("skip_status", "true"); return parse_user_info(yield Cb.Utils.load_threaded_async (call, null)); } private UserInfo parse_user_info(Json.Node root_node) { var user_info = UserInfo(); var root = root_node.get_object (); user_info.id = root.get_int_member ("id"); user_info.name = root.get_string_member ("name"); user_info.screen_name = root.get_string_member ("screen_name"); Json.Array desc_urls = root.get_object_member ("entities").get_object_member ("description") .get_array_member ("urls"); var urls = new Cb.TextEntity[desc_urls.get_length ()]; desc_urls.foreach_element ((arr, index, node) => { Json.Object obj = node.get_object (); Json.Array indices = obj.get_array_member ("indices"); urls[index] = Cb.TextEntity () { from = (uint)indices.get_int_element (0), to = (uint)indices.get_int_element (1), original_text = obj.get_string_member ("url"), display_text = obj.get_string_member ("expanded_url"), target = null }; }); user_info.description = Cb.TextTransform.text (root.get_string_member ("description"), urls, Cb.TransformFlags.EXPAND_LINKS, 0, 0); if (root.has_member ("profile_banner_url")) user_info.banner_url = root.get_string_member ("profile_banner_url"); /* Website URL */ if (root.get_object_member ("entities").has_member ("url")) { user_info.website = root.get_object_member ("entities").get_object_member ("url") .get_array_member ("urls").get_object_element (0).get_string_member ("expanded_url"); } else { user_info.website = ""; } user_info.avatar_url = root.get_string_member ("profile_image_url_https"); return user_info; } } cawbird-1.4.2/src/UserEventReceiver.vala000066400000000000000000000126241416632607600202120ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class UserEventReceiver : GLib.Object, Cb.MessageReceiver { private unowned Account account; public UserEventReceiver (Account account) { this.account = account; } public void stream_message_received (Cb.StreamMessageType type, Json.Node root_node) { switch (type) { case Cb.StreamMessageType.FRIENDS: account.set_friends (root_node.get_object ().get_array_member ("friends")); break; case Cb.StreamMessageType.EVENT_FOLLOW: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.follow_id (user_id); break; case Cb.StreamMessageType.EVENT_UNFOLLOW: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.unfollow_id (user_id); break; case Cb.StreamMessageType.EVENT_MUTE: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.mute_id (user_id); break; case Cb.StreamMessageType.EVENT_UNMUTE: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.unmute_id (user_id); break; case Cb.StreamMessageType.EVENT_BLOCK: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.block_id (user_id); break; case Cb.StreamMessageType.EVENT_UNBLOCK: int64 user_id = root_node.get_object ().get_object_member ("target") .get_int_member ("id"); account.unblock_id (user_id); break; case Cb.StreamMessageType.EVENT_USER_UPDATE: var user_obj = root_node.get_object ().get_object_member ("target"); if (user_obj.get_int_member ("id") == account.id) { string old_screen_name = account.screen_name; account.name = user_obj.get_string_member ("name"); account.description = user_obj.get_string_member ("description"); account.screen_name = user_obj.get_string_member ("screen_name"); account.info_changed (account.screen_name, account.name, account.avatar_small, account.avatar); account.save_info (); Utils.update_startup_account (old_screen_name, account.screen_name); } else { warning ("USER_UPDATE: ids don't match"); } break; case Cb.StreamMessageType.DIRECT_MESSAGE: var cb = (Cawbird) GLib.Application.get_default (); if (!cb.is_window_open_for_user_id (account.id) && !account.suppress_dm_notifications && Settings.notify_new_dms ()) { var dm_obj = root_node.get_object ().get_object_member ("direct_message"); var sender_obj = dm_obj.get_object_member ("sender"); int64 sender_id = sender_obj.get_int_member ("id"); string sender_name = sender_obj.get_string_member ("name"); string dm_text = dm_obj.get_string_member ("text"); account.notifications.send_dm (sender_id, null, _("New direct message from %s").printf (sender_name), Utils.unescape_html (dm_text)); } break; case Cb.StreamMessageType.MENTION: var cb = (Cawbird) GLib.Application.get_default (); if (!cb.is_window_open_for_user_id (account.id) && !account.suppress_mention_notifications && Settings.notify_new_mentions ()) { Cb.Tweet tweet = new Cb.Tweet(); tweet.load_from_json(root_node, account.id, new GLib.DateTime.now_local()); TweetUtils.set_tweet_hidden_flags(tweet, account); // Only notify on non-hidden tweets - if the user is blocking it for some reason, so don't shove it in their face! if (!tweet.is_hidden()) { // TODO: Care about retweets/quotes! // XXX : And media? string summary = _("%s mentioned %s").printf (tweet.source_tweet.author.user_name, account.name); account.notifications.send (summary, tweet.get_real_text()); } } break; default: // Do nothing. There's no relevance to the user's account settings or notifications break; } } } cawbird-1.4.2/src/async/000077500000000000000000000000001416632607600150505ustar00rootroot00000000000000cawbird-1.4.2/src/async/Collect.vala000066400000000000000000000031041416632607600173000ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class Collect : GLib.Object { private int cur = 0; private int max; private GLib.Error? error = null; public bool done { get { return this.cur == this.max; } } public signal void finished (GLib.Error? error); public Collect (int max) requires (max >= 0) { this.max = max; } public void emit (GLib.Error? error = null) requires (cur < max) { /* If our global error is set, something previously went wrong and we ignore this call to emit(); */ if (this.error != null) return; /* If error is set, we call finished() with that error and ignore all following calls to emit() */ if (error != null) { finished (error); this.error = error; return; } cur++; if (cur == max) { finished (null); } } } cawbird-1.4.2/src/async/CollectById.vala000066400000000000000000000035361416632607600200610ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class CollectById : GLib.Object { private GLib.GenericSet ids; private GLib.Error? error = null; public bool done { get { return ids.length == 0; } } public bool errored { get { return error != null; } } public signal void finished (GLib.Error? error); public CollectById () { ids = new GLib.GenericSet(GLib.str_hash, GLib.str_equal, null); } public void add(string id) { ids.add(id); } public void emit (string id, GLib.Error? error = null) { /* If our global error is set, something previously went wrong and we ignore this call to emit(); */ if (this.error != null) { return; } /* If error is set, we call finished() with that error and ignore all following calls to emit() */ if (error != null) { finished (error); this.error = error; return; } ids.remove(id); if (done) { finished (null); } } } cawbird-1.4.2/src/cawbird.vapi000066400000000000000000000476231416632607600162430ustar00rootroot00000000000000/* cawbird.vapi generated by valac 0.38.7, do not modify. */ namespace TweetUtils { [CCode (cheader_filename = "cawbird.h")] public static GLib.Error failed_request_to_error (Rest.ProxyCall call, GLib.Error e); [CCode (cheader_filename = "cawbird.h")] public static void handle_media_click (Cb.Media[] media, MainWindow window, int index, double px = 0.0, double py = 0.0); [CCode (cheader_filename = "cawbird.h")] public static void sort_entities (ref Cb.TextEntity[] entities); } namespace Utils { [CCode (cheader_filename = "cawbird.h")] public static Cb.Filter create_persistent_filter (string content, Account account); [CCode (cheader_filename = "cawbird.h")] public static async void download_file (string url, GLib.OutputStream out_stream); [CCode (cheader_filename = "cawbird.h")] public static string get_media_display_name (Cb.Media media); [CCode (cheader_filename = "cawbird.h")] public static string get_time_delta (GLib.DateTime time, GLib.DateTime now); [CCode (cheader_filename = "cawbird.h")] public static void init_soup_session (); [CCode (cheader_filename = "cawbird.h")] public static void load_custom_css (); [CCode (cheader_filename = "cawbird.h")] public static void load_custom_icons (); [CCode (cheader_filename = "cawbird.h")] public static void update_startup_account (string old_screen_name, string new_screen_name); [CCode (cheader_filename = "cawbird.h")] public static bool usable_json_value (Json.Object node, string value_name); } namespace Dirs { [CCode (cheader_filename = "cawbird.h")] public static string config (string path); [CCode (cheader_filename = "cawbird.h")] public static void create_dirs (); } namespace ListUtils { } namespace UserUtils { } namespace Benchmark { [CCode (cheader_filename = "cawbird.h")] public class Bench { public GLib.DateTime first; public string name; public Bench (); public void stop (); } [CCode (cheader_filename = "cawbird.h")] public static Benchmark.Bench start (string name); } namespace Sql { [CCode (cheader_filename = "cawbird.h")] public class Database : GLib.Object { public Database (string filename, string init_file, int max_version); public void begin_transaction (); public void end_transaction (); public void exec (string sql, Sqlite.Callback? callback = null); public unowned Sqlite.Database get_sqlite_db (); public Sql.InsertStatement insert (string table_name); public Sql.InsertStatement replace (string table_name); public Sql.SelectStatement select (string table_name); public Sql.UpdateStatement update (string table_name); } [CCode (cheader_filename = "cawbird.h")] public class InsertStatement : GLib.Object { public weak Sqlite.Database db; public InsertStatement (string table_name, bool replace = false); public int64 run (); public Sql.InsertStatement val (string col_name, string col_value); public Sql.InsertStatement valb (string col_name, bool col_value); public Sql.InsertStatement vali (string col_name, int col_value); public Sql.InsertStatement vali64 (string col_name, int64 col_value); } [CCode (cheader_filename = "cawbird.h")] public class SelectStatement : GLib.Object { public weak Sqlite.Database db; public SelectStatement (string table_name); public Sql.SelectStatement cols (string first, ...); public Sql.SelectStatement limit (int limit); public Sql.SelectStatement nocase (); public int64 once_i64 (); public string? once_string (); public Sql.SelectStatement or (); public Sql.SelectStatement order (string order_by); public int run (Sql.SelectCallback callback); public Sql.SelectStatement where (string stmt); public Sql.SelectStatement where_eqi (string w, int64 v); public Sql.SelectStatement where_prefix (string field, string prefix); public Sql.SelectStatement where_prefix2 (string field, string prefix); } [CCode (cheader_filename = "cawbird.h")] public class UpdateStatement : GLib.Object { public weak Sqlite.Database db; public UpdateStatement (string table_name); public int64 run (); public Sql.UpdateStatement val (string col_name, string col_value); public Sql.UpdateStatement valb (string col_name, bool col_value); public Sql.UpdateStatement vali (string col_name, int col_value); public Sql.UpdateStatement vali64 (string col_name, int64 col_value); public Sql.UpdateStatement where (string where); public Sql.UpdateStatement where_eq (string col, string value); public Sql.UpdateStatement where_eqi (string col, int64 iv); } [CCode (cheader_filename = "cawbird.h")] public delegate bool SelectCallback (string[] vals); [CCode (cheader_filename = "cawbird.h")] public const string ACCOUNTS_INIT_FILE; [CCode (cheader_filename = "cawbird.h")] public const int ACCOUNTS_SQL_VERSION; [CCode (cheader_filename = "cawbird.h")] public const string CAWBIRD_INIT_FILE; [CCode (cheader_filename = "cawbird.h")] public const int CAWBIRD_SQL_VERSION; } [CCode (cheader_filename = "cawbird.h")] public enum MediaVisibility { SHOW, HIDE, HIDE_IN_TIMELINES } [CCode (cheader_filename = "cawbird.h")] public class Cawbird : Gtk.Application { public static Sql.Database db; public static Cb.SnippetManager snippet_manager; public Cawbird (); public override void activate (); public void add_window_for_account (Account account); public bool add_window_for_screen_name (string screen_name); public override int command_line (GLib.ApplicationCommandLine cmd); public bool is_window_open_for_screen_name (string screen_name, out MainWindow? window = null); public bool is_window_open_for_user_id (int64 user_id, out MainWindow? window = null); public override void shutdown (); public void start_account (Account acc); public override void startup (); public void stop_account (Account acc); public signal void account_added (Account acc); public signal void account_removed (Account acc); public signal void account_window_changed (int64? old_id, int64 new_id); } [CCode (cheader_filename = "cawbird.h")] public class MainWindow : Gtk.ApplicationWindow { public weak Account? account; public Gtk.Button back_button; public Gtk.ToggleButton compose_tweet_button; public MainWidget main_widget; public MainWindow (Gtk.Application app, Account? account = null); public void change_account (Account? account); public IPage get_page (int page_id); public void mark_tweet_as_read (int64 tweet_id); public void reply_to_tweet (int64 tweet_id); public void rerun_filters (); public void save_geometry (); public void set_window_title (string title, Gtk.StackTransitionType transition_type = Gtk.StackTransitionType.NONE); public int cur_page_id { get; } } [CCode (cheader_filename = "cawbird.h")] public class MainWidget : Gtk.Box { public MainWidget (Account account, MainWindow parent, Cawbird app); public IPage get_page (int page_id); public void remove_current_page (); public void stop (); public void switch_page (int page_id, Cb.Bundle? args = null); public int cur_page_id { get; } } [CCode (cheader_filename = "cawbird.h")] public class Account : GLib.Object { public string avatar_url; public string? banner_url; public int64[] blocked; public Sql.Database db; public string? description; public int64[] disabled_rts; public GLib.GenericArray filters; public int64[] friends; public int64 id; public int64[] muted; public string name; public NotificationManager notifications; public Rest.OAuthProxy proxy; public string screen_name; public Cb.UserCounter user_counter; public Cb.UserStream user_stream; public string? website; public Account (int64 id, string screen_name, string name); public static void add_account (Account acc); public void add_disabled_rts_id (int64 user_id); public void add_filter (owned Cb.Filter f); public void block_id (int64 id); public bool blocked_or_muted (int64 user_id); public bool filter_matches (Cb.Tweet t); public void follow_id (int64 user_id); public bool follows_id (int64 user_id); public static uint get_n (); public static Account get_nth (uint index); public void init_database (); public async void init_information (); public void init_proxy (bool load_secrets = true, bool force = false); public bool is_blocked (int64 user_id); public bool is_muted (int64 user_id); public void load_avatar (); public void mute_id (int64 id); public static unowned Account? query_account (string screen_name); public static unowned Account? query_account_by_id (int64 id); public async void query_user_info (); public static void remove_account (string screen_name); public void remove_disabled_rts_id (int64 user_id); public void save_info (); public void set_blocked (Json.Array blocked_array); public void set_disabled_rts (Json.Array disabled_rts_array); public void set_friends (Json.Array friends_array); public void set_muted (Json.Array muted_array); public void set_new_avatar (Cairo.Surface new_avatar); public void unblock_id (int64 id); public void unfollow_id (int64 user_id); public void uninit (); public void unmute_id (int64 id); public Cairo.Surface avatar { get; set; } public Cairo.Surface avatar_small { get; set; } public signal void info_changed (string screen_name, string name, Cairo.Surface avatar_small, Cairo.Surface avatar); } [CCode (cheader_filename = "cawbird.h")] public class HomeTimeline : Cb.MessageReceiver, DefaultTimeline { public HomeTimeline (int id, Account account); public override void create_radio_button (Gtk.RadioButton? group); public override string get_title (); public void hide_retweets_from (int64 user_id, Cb.TweetState reason); public void hide_tweets_from (int64 user_id, Cb.TweetState reason); public void show_retweets_from (int64 user_id, Cb.TweetState reason); public void show_tweets_from (int64 user_id, Cb.TweetState reason); protected override string function { get; } } [CCode (cheader_filename = "cawbird.h")] public abstract class DefaultTimeline : ScrollWidget, IPage { public weak Account account; protected bool initialized; protected Gtk.Widget? last_focus_widget; protected bool loading; protected weak MainWindow main_window; protected BadgeRadioButton radio_button; public TweetListBox tweet_list; protected uint tweet_remove_timeout; public const int REST; public DefaultTimeline (int id); public virtual void create_radio_button (Gtk.RadioButton? group); public void delete_tweet (int64 tweet_id); public override void destroy (); protected Cb.TweetState get_rt_flags (Cb.Tweet t); public abstract string get_title (); protected void handle_scrolled_to_start (); public void load_newest (); protected async void load_newest_internal (); public void load_older (); protected async void load_older_internal (); protected void mark_seen (int64 id); protected void mark_seen_on_scroll (double value); public virtual void on_join (int page_id, Cb.Bundle? args); public virtual void on_leave (); public void rerun_filters (); protected bool scroll_up (Cb.Tweet t); public void toggle_favorite (int64 id, bool mode); protected abstract string function { get; } public int unread_count { get; set; } } [CCode (cheader_filename = "cawbird.h")] public class Settings : GLib.Object { public Settings (); public static void add_text_transform_flag (Cb.TransformFlags flag); public static bool auto_scroll_on_new_tweets (); public static new GLib.Settings @get (); public static string get_accel (string accel_name); public static string get_consumer_key (); public static string get_consumer_secret (); public static MediaVisibility get_media_visiblity (); public static Cb.TransformFlags get_text_transform_flags (); public static int get_tweet_stack_count (); public static bool hide_nsfw_content (); public static void init (); public static bool notify_new_dms (); public static bool notify_new_mentions (); public static void remove_text_transform_flag (Cb.TransformFlags flag); public static void toggle_topbar_visible (); public static bool use_dark_theme (); } [CCode (cheader_filename = "cawbird.h")] public class NotificationManager : GLib.Object { public NotificationManager (Account account); public string send (string summary, string body, string? id_suffix = null); public string send_dm (int64 sender_id, string? existing_id, string summary, string text); public void withdraw (string id); } [CCode (cheader_filename = "cawbird.h")] public class Twitter : GLib.Object { public static Cairo.Surface no_avatar; public static Gdk.Pixbuf no_banner; public const int MAX_BYTES_PER_IMAGE; public const int max_media_per_upload; public const int short_url_length; public static new Twitter @get (); public async void get_avatar (int64 user_id, string url, AvatarWidget dest_widget, int size = 48, bool force_download = false); public Cairo.Surface get_cached_avatar (int64 user_id); public bool has_avatar (int64 user_id); public void init (); public async Cairo.Surface? load_avatar_for_user_id (Account account, int64 user_id, int size); public void ref_avatar (Cairo.Surface surface); public void unref_avatar (Cairo.Surface surface); } [CCode (cheader_filename = "cawbird.h")] public class DMManager : GLib.Object { public DMManager (); public DMManager.for_account (Account account); public GLib.ListModel get_threads_model (); public bool has_thread (int64 user_id); public void insert_message (Json.Object dm_obj); public void load_cached_threads (); public async void load_newest_dms (); public string? reset_notification_id (int64 user_id); public int reset_unread_count (int64 user_id); public bool empty { get; } public signal void message_received (DMThread thread, string text, bool initial); public signal void thread_changed (DMThread thread); } [CCode (cheader_filename = "cawbird.h")] public class TweetListBox : Gtk.ListBox { public weak Account account; public Cb.DeltaUpdater delta_updater; public Cb.TweetModel model; public TweetListBox (); public Gtk.Widget? get_first_visible_row (); public Gtk.Stack? get_placeholder (); public void remove_all (); public void reset_placeholder_text (); public void set_empty (); public void set_error (string err_msg); public void set_placeholder_text (string text); public void set_unempty (); public TweetListEntry? action_entry { get; } public signal void retry_button_clicked (); } [CCode (cheader_filename = "cawbird.h")] public class ScrollWidget : Gtk.ScrolledWindow { public ScrollWidget (); public void balance_next_upper_change (int mode); public void scroll_down_next (bool animate = true, bool force_wait = false); public void scroll_up_next (bool animate = true, bool force_start = false); public double end_diff { get; set; } public bool scrolled_down { get; } public bool scrolled_up { get; } public signal void scrolled_to_end (); public signal void scrolled_to_start (double value); } [CCode (cheader_filename = "cawbird.h")] public class TextButton : Gtk.Button { public TextButton (); public void set_text (string text); } [CCode (cheader_filename = "cawbird.h")] public class BadgeRadioButton : Gtk.RadioButton { public BadgeRadioButton (Gtk.RadioButton group, string icon_name, string text = ""); public override bool draw (Cairo.Context ct); public bool show_badge { get; set; } } [CCode (cheader_filename = "cawbird.h")] public class MultiMediaWidget : Gtk.Box { public bool restrict_height; public weak Gtk.Window window; public const int MAX_HEIGHT; public MultiMediaWidget (); public void set_all_media (Cb.Media[] medias); public void set_media (int index, Cb.Media media); public signal void media_clicked (Cb.Media m, int index, double px, double py); public signal void media_invalid (); } [CCode (cheader_filename = "cawbird.h")] public class AvatarWidget : Gtk.Widget { public AvatarWidget (); public override bool draw (Cairo.Context ctx); public override void get_preferred_height (out int min, out int nat); public override void get_preferred_width (out int min, out int nat); public override void size_allocate (Gtk.Allocation alloc); public bool make_round { get; set; } public bool overlap { get; set; } public int size { get; set; } public Cairo.Surface surface { get; set; } public bool verified { get; set; } } [CCode (cheader_filename = "cawbird.h")] public class AvatarBannerWidget : Gtk.Container { public AvatarBannerWidget (); public override void add (Gtk.Widget w); public override bool draw (Cairo.Context ct); public override void forall_internal (bool include_internals, Gtk.Callback cb); public override void get_preferred_height_for_width (int width, out int min, out int nat); public override void get_preferred_width (out int min, out int nat); public override Gtk.SizeRequestMode get_request_mode (); public override void remove (Gtk.Widget w); public void set_account (Account account); public void set_avatar (Gdk.Pixbuf avatar); public void set_banner (Gdk.Pixbuf banner); public override void size_allocate (Gtk.Allocation allocation); public signal void avatar_changed (Gdk.Pixbuf new_avatar); public signal void avatar_clicked (); public signal void banner_changed (Gdk.Pixbuf new_banner); public signal void banner_clicked (); } [CCode (cheader_filename = "cawbird.h")] public class LazyMenuButton : Gtk.ToggleButton { public LazyMenuButton (); public override void clicked (); public GLib.Menu menu_model { get; set; } } [CCode (cheader_filename = "cawbird.h")] [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/tweet-list-entry.ui")] public class TweetListEntry : Cb.TwitterItem, Gtk.ListBoxRow { public Cb.Tweet tweet; public TweetListEntry (Cb.Tweet tweet, MainWindow? main_window, Account account, bool restrict_height = false); public void fade_in (); public void set_avatar (Cairo.Surface surface); public void toggle_mode (); public bool read_only { set; } public bool shows_actions { get; } } [CCode (cheader_filename = "cawbird.h")] [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/list-list-entry.ui")] public class ListListEntry : Gtk.ListBoxRow { public int64 created_at; public string creator_screen_name; public int64 id; public string mode; public int n_members; public int n_subscribers; public bool user_list; public ListListEntry (); public ListListEntry.from_json_data (Json.Object obj, Account account); public static int sort_func (Gtk.ListBoxRow r1, Gtk.ListBoxRow r2); public string description { get; set; } public string name { get; set; } } [CCode (cheader_filename = "cawbird.h")] [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/account-dialog.ui")] public class AccountDialog : Gtk.Window { public AccountDialog (Account account); public override void destroy (); } [CCode (cheader_filename = "cawbird.h")] public class Collect : GLib.Object { public Collect (int max); public void emit (GLib.Error? error = null); public bool done { get; } public signal void finished (GLib.Error? error); } [CCode (cheader_filename = "cawbird.h")] public class DMThread : GLib.Object { public Cairo.Surface? avatar_surface; public string last_message; public int64 last_message_id; public string? notification_id; public int unread_count; public Cb.UserIdentity user; public DMThread (); public async void load_avatar (Account account, int scale_factor); } [CCode (cheader_filename = "cawbird.h")] public class DMThreadsModel : GLib.ListModel, GLib.Object { public DMThreadsModel (); public void add (DMThread thread); public DMThread? get_thread (int64 user_id); public bool has_thread (int64 user_id); public void increase_unread_count (int64 user_id, int amount = 1); public string? reset_notification_id (int64 user_id); public int reset_unread_count (int64 user_id); public void update_last_message (int64 sender_id, int64 message_id, string message_text); } [CCode (cheader_filename = "cawbird.h")] public interface IPage : Gtk.Widget { public abstract void create_radio_button (Gtk.RadioButton? group); public virtual void double_open (); public abstract Gtk.RadioButton? get_radio_button (); public abstract string get_title (); public virtual bool handles_double_open (); public abstract void on_join (int page_id, Cb.Bundle? args); public abstract void on_leave (); public abstract int id { get; set; } public abstract MainWindow window { set; } } [CCode (cheader_filename = "cawbird.h")] public static unowned string __class_name (GLib.Object o); cawbird-1.4.2/src/libtl/000077500000000000000000000000001416632607600150415ustar00rootroot00000000000000cawbird-1.4.2/src/libtl/data.h000066400000000000000000000657511416632607600161410ustar00rootroot00000000000000/* This file is part of libtweetlength * Copyright (C) 2017 Timm Bäder, 2020 IBBoard * * libtweetlength 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. * * libtweetlength 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 libtweetlength. If not, see . */ #ifndef __TL_DATA_H__ #define __TL_DATA_H__ #define PUNCTUATION "!'#%&\"()*+,\\-./:;<=>?@[]^_{|}~$`" #define SPACES " \0x0020\0085\00A0" #define INVALID_CHARS "\0xFFFE\0xFEFF\0xFFFF" #define INVALID_URL_CHARS (PUNCTUATION SPACES INVALID_CHARS) #define INVALID_AFTER_URL_CHARS "?,!`~&*^%\\|" #define INVALID_BEFORE_NON_PROTOCOL_URL_CHARS (".@_-)/") #define INVALID_BEFORE_URL_CHARS ("$") #define VALID_BEFORE_HASHTAG_CHARS "{}!<>()[]\\/?`~:;.,%*" #define INVALID_BEFORE_HASHTAG_CHARS "_&" #define INVALID_HASHTAG_CHARS "!'#%&\"()*+,\\-./:;<=>?@[]^{|}~$`" #define INVALID_BEFORE_MENTION_CHARS "!_$&#*" #define VALID_BEFORE_MENTION_CHARS ";,`=+" #define INVALID_MENTION_CHARS "!'#%&\"()*+,\\-./:;<=>?@[]^{|}~$`" // List from twitter-text static const struct { size_t length; const char *str; } GTLDS[] = { {2, "삼성"}, {2, "ë‹·ì»´"}, {2, "ë‹·ë„·"}, {2, "コム"}, {2, "世界"}, {2, "中信"}, {2, "ä¼ä¸š"}, {2, "佛山"}, {2, "ä¿¡æ¯"}, {2, "å¥åº·"}, {2, "å…«å¦"}, {2, "å…¬å¸"}, {2, "公益"}, {2, "商城"}, {2, "商店"}, {2, "商标"}, {2, "嘉里"}, {2, "在线"}, {2, "大拿"}, {2, "娱ä¹"}, {2, "å®¶é›»"}, {2, "工行"}, {2, "广东"}, {2, "å¾®åš"}, {2, "慈善"}, {2, "手机"}, {2, "手表"}, {2, "æ‹›è˜"}, {2, "政务"}, {2, "政府"}, {2, "æ–°é—»"}, {2, "æ—¶å°š"}, {2, "書ç±"}, {2, "机构"}, {2, "游æˆ"}, {2, "点看"}, {2, "ç å®"}, {2, "移动"}, {2, "网å€"}, {2, "网店"}, {2, "网站"}, {2, "网络"}, {2, "è”通"}, {2, "è°·æ­Œ"}, {2, "购物"}, {2, "通販"}, {2, "集团"}, {2, "食å“"}, {2, "é¤åŽ…"}, {3, "aaa"}, {3, "abb"}, {3, "abc"}, {3, "aco"}, {3, "ads"}, {3, "aeg"}, {3, "afl"}, {3, "aig"}, {3, "anz"}, {3, "aol"}, {3, "app"}, {3, "art"}, {3, "aws"}, {3, "axa"}, {3, "bar"}, {3, "bbc"}, {3, "bbt"}, {3, "bcg"}, {3, "bcn"}, {3, "bet"}, {3, "bid"}, {3, "bio"}, {3, "biz"}, {3, "bms"}, {3, "bmw"}, {3, "bnl"}, {3, "bom"}, {3, "boo"}, {3, "bot"}, {3, "box"}, {3, "buy"}, {3, "bzh"}, {3, "cab"}, {3, "cal"}, {3, "cam"}, {3, "car"}, {3, "cat"}, {3, "cba"}, {3, "cbn"}, {3, "cbs"}, {3, "ceb"}, {3, "ceo"}, {3, "cfa"}, {3, "cfd"}, {3, "com"}, {3, "cpa"}, {3, "crs"}, {3, "csc"}, {3, "dad"}, {3, "day"}, {3, "dds"}, {3, "dev"}, {3, "dhl"}, {3, "diy"}, {3, "dnp"}, {3, "dog"}, {3, "dot"}, {3, "dtv"}, {3, "dvr"}, {3, "eat"}, {3, "eco"}, {3, "edu"}, {3, "esq"}, {3, "eus"}, {3, "fan"}, {3, "fit"}, {3, "fly"}, {3, "foo"}, {3, "fox"}, {3, "frl"}, {3, "ftr"}, {3, "fun"}, {3, "fyi"}, {3, "gal"}, {3, "gap"}, {3, "gay"}, {3, "gdn"}, {3, "gea"}, {3, "gle"}, {3, "gmo"}, {3, "gmx"}, {3, "goo"}, {3, "gop"}, {3, "got"}, {3, "gov"}, {3, "hbo"}, {3, "hiv"}, {3, "hkt"}, {3, "hot"}, {3, "how"}, {3, "htc"}, {3, "ibm"}, {3, "ice"}, {3, "icu"}, {3, "ifm"}, {3, "inc"}, {3, "ing"}, {3, "ink"}, {3, "int"}, {3, "ist"}, {3, "itv"}, {3, "iwc"}, {3, "jcb"}, {3, "jcp"}, {3, "jio"}, {3, "jlc"}, {3, "jll"}, {3, "jmp"}, {3, "jnj"}, {3, "jot"}, {3, "joy"}, {3, "kfh"}, {3, "kia"}, {3, "kim"}, {3, "kpn"}, {3, "krd"}, {3, "lat"}, {3, "law"}, {3, "lds"}, {3, "llc"}, {3, "llp"}, {3, "lol"}, {3, "lpl"}, {3, "ltd"}, {3, "man"}, {3, "map"}, {3, "mba"}, {3, "mcd"}, {3, "med"}, {3, "men"}, {3, "meo"}, {3, "mil"}, {3, "mit"}, {3, "mlb"}, {3, "mls"}, {3, "mma"}, {3, "moe"}, {3, "moi"}, {3, "mom"}, {3, "mov"}, {3, "msd"}, {3, "mtn"}, {3, "mtr"}, {3, "nab"}, {3, "nba"}, {3, "nec"}, {3, "net"}, {3, "new"}, {3, "nfl"}, {3, "ngo"}, {3, "nhk"}, {3, "now"}, {3, "nra"}, {3, "nrw"}, {3, "ntt"}, {3, "nyc"}, {3, "obi"}, {3, "off"}, {3, "one"}, {3, "ong"}, {3, "onl"}, {3, "ooo"}, {3, "org"}, {3, "ott"}, {3, "ovh"}, {3, "pay"}, {3, "pet"}, {3, "phd"}, {3, "pid"}, {3, "pin"}, {3, "pnc"}, {3, "pro"}, {3, "pru"}, {3, "pub"}, {3, "pwc"}, {3, "qvc"}, {3, "red"}, {3, "ren"}, {3, "ril"}, {3, "rio"}, {3, "rip"}, {3, "run"}, {3, "rwe"}, {3, "sap"}, {3, "sas"}, {3, "sbi"}, {3, "sbs"}, {3, "sca"}, {3, "scb"}, {3, "ses"}, {3, "sew"}, {3, "sex"}, {3, "sfr"}, {3, "ski"}, {3, "sky"}, {3, "soy"}, {3, "srl"}, {3, "srt"}, {3, "stc"}, {3, "tab"}, {3, "tax"}, {3, "tci"}, {3, "tdk"}, {3, "tel"}, {3, "thd"}, {3, "tjx"}, {3, "top"}, {3, "trv"}, {3, "tui"}, {3, "tvs"}, {3, "ubs"}, {3, "uno"}, {3, "uol"}, {3, "ups"}, {3, "vet"}, {3, "vig"}, {3, "vin"}, {3, "vip"}, {3, "wed"}, {3, "win"}, {3, "wme"}, {3, "wow"}, {3, "wtc"}, {3, "wtf"}, {3, "xin"}, {3, "xxx"}, {3, "xyz"}, {3, "you"}, {3, "yun"}, {3, "zip"}, {3, "ком"}, {3, "орг"}, {3, "руÑ"}, {3, "קו×"}, {3, "عرب"}, {3, "كوم"}, {3, "कॉम"}, {3, "नेट"}, {3, "คอม"}, {3, "ストア"}, {3, "セール"}, {3, "ã¿ã‚“ãª"}, {3, "中文网"}, {3, "天主教"}, {3, "我爱你"}, {3, "淡马锡"}, {3, "诺基亚"}, {3, "飞利浦"}, {4, "aarp"}, {4, "able"}, {4, "adac"}, {4, "aero"}, {4, "aigo"}, {4, "akdn"}, {4, "ally"}, {4, "amex"}, {4, "arab"}, {4, "army"}, {4, "arpa"}, {4, "arte"}, {4, "asda"}, {4, "asia"}, {4, "audi"}, {4, "auto"}, {4, "baby"}, {4, "band"}, {4, "bank"}, {4, "bbva"}, {4, "beer"}, {4, "best"}, {4, "bike"}, {4, "bing"}, {4, "blog"}, {4, "blue"}, {4, "bofa"}, {4, "bond"}, {4, "book"}, {4, "buzz"}, {4, "cafe"}, {4, "call"}, {4, "camp"}, {4, "care"}, {4, "cars"}, {4, "casa"}, {4, "case"}, {4, "cash"}, {4, "cbre"}, {4, "cern"}, {4, "chat"}, {4, "citi"}, {4, "city"}, {4, "club"}, {4, "cool"}, {4, "coop"}, {4, "cyou"}, {4, "data"}, {4, "date"}, {4, "dclk"}, {4, "deal"}, {4, "dell"}, {4, "desi"}, {4, "diet"}, {4, "dish"}, {4, "docs"}, {4, "doha"}, {4, "duck"}, {4, "duns"}, {4, "dvag"}, {4, "erni"}, {4, "fage"}, {4, "fail"}, {4, "fans"}, {4, "farm"}, {4, "fast"}, {4, "fiat"}, {4, "fido"}, {4, "film"}, {4, "fire"}, {4, "fish"}, {4, "flir"}, {4, "food"}, {4, "ford"}, {4, "free"}, {4, "fund"}, {4, "game"}, {4, "gbiz"}, {4, "gent"}, {4, "ggee"}, {4, "gift"}, {4, "gmbh"}, {4, "gold"}, {4, "golf"}, {4, "goog"}, {4, "guge"}, {4, "guru"}, {4, "hair"}, {4, "haus"}, {4, "hdfc"}, {4, "help"}, {4, "here"}, {4, "hgtv"}, {4, "host"}, {4, "hsbc"}, {4, "icbc"}, {4, "ieee"}, {4, "imdb"}, {4, "immo"}, {4, "info"}, {4, "itau"}, {4, "java"}, {4, "jeep"}, {4, "jobs"}, {4, "jprs"}, {4, "kddi"}, {4, "kiwi"}, {4, "kpmg"}, {4, "kred"}, {4, "land"}, {4, "lego"}, {4, "lgbt"}, {4, "lidl"}, {4, "life"}, {4, "like"}, {4, "limo"}, {4, "link"}, {4, "live"}, {4, "loan"}, {4, "loft"}, {4, "love"}, {4, "ltda"}, {4, "luxe"}, {4, "maif"}, {4, "meet"}, {4, "meme"}, {4, "menu"}, {4, "mini"}, {4, "mint"}, {4, "mobi"}, {4, "moda"}, {4, "moto"}, {4, "mtpc"}, {4, "name"}, {4, "navy"}, {4, "news"}, {4, "next"}, {4, "nico"}, {4, "nike"}, {4, "ollo"}, {4, "open"}, {4, "page"}, {4, "pars"}, {4, "pccw"}, {4, "pics"}, {4, "ping"}, {4, "pink"}, {4, "play"}, {4, "plus"}, {4, "pohl"}, {4, "porn"}, {4, "post"}, {4, "prod"}, {4, "prof"}, {4, "qpon"}, {4, "raid"}, {4, "read"}, {4, "reit"}, {4, "rent"}, {4, "rest"}, {4, "rich"}, {4, "rmit"}, {4, "room"}, {4, "rsvp"}, {4, "ruhr"}, {4, "safe"}, {4, "sale"}, {4, "sapo"}, {4, "sarl"}, {4, "save"}, {4, "saxo"}, {4, "scor"}, {4, "scot"}, {4, "seat"}, {4, "seek"}, {4, "sexy"}, {4, "shaw"}, {4, "shia"}, {4, "shop"}, {4, "show"}, {4, "silk"}, {4, "sina"}, {4, "site"}, {4, "skin"}, {4, "sncf"}, {4, "sohu"}, {4, "song"}, {4, "sony"}, {4, "spot"}, {4, "star"}, {4, "surf"}, {4, "talk"}, {4, "taxi"}, {4, "team"}, {4, "tech"}, {4, "teva"}, {4, "tiaa"}, {4, "tips"}, {4, "town"}, {4, "toys"}, {4, "tube"}, {4, "vana"}, {4, "visa"}, {4, "viva"}, {4, "vivo"}, {4, "vote"}, {4, "voto"}, {4, "wang"}, {4, "weir"}, {4, "wien"}, {4, "wiki"}, {4, "wine"}, {4, "work"}, {4, "xbox"}, {4, "yoga"}, {4, "zara"}, {4, "zero"}, {4, "zone"}, {4, "дети"}, {4, "Ñайт"}, {4, "بيتك"}, {4, "شبكة"}, {4, "موقع"}, {4, "グーグル"}, {4, "クラウド"}, {4, "ãƒã‚¤ãƒ³ãƒˆ"}, {4, "大众汽车"}, {4, "组织机构"}, {4, "電訊盈科"}, {4, "香格里拉"}, {5, "actor"}, {5, "adult"}, {5, "aetna"}, {5, "amfam"}, {5, "amica"}, {5, "apple"}, {5, "archi"}, {5, "audio"}, {5, "autos"}, {5, "azure"}, {5, "baidu"}, {5, "beats"}, {5, "bible"}, {5, "bingo"}, {5, "black"}, {5, "boats"}, {5, "boots"}, {5, "bosch"}, {5, "build"}, {5, "canon"}, {5, "cards"}, {5, "chase"}, {5, "cheap"}, {5, "chloe"}, {5, "cisco"}, {5, "citic"}, {5, "click"}, {5, "cloud"}, {5, "coach"}, {5, "codes"}, {5, "crown"}, {5, "cymru"}, {5, "dabur"}, {5, "dance"}, {5, "deals"}, {5, "delta"}, {5, "dodge"}, {5, "drive"}, {5, "dubai"}, {5, "earth"}, {5, "edeka"}, {5, "email"}, {5, "epost"}, {5, "epson"}, {5, "faith"}, {5, "fedex"}, {5, "final"}, {5, "forex"}, {5, "forum"}, {5, "gallo"}, {5, "games"}, {5, "gifts"}, {5, "gives"}, {5, "glade"}, {5, "glass"}, {5, "globo"}, {5, "gmail"}, {5, "green"}, {5, "gripe"}, {5, "group"}, {5, "gucci"}, {5, "guide"}, {5, "homes"}, {5, "honda"}, {5, "horse"}, {5, "house"}, {5, "hyatt"}, {5, "iinet"}, {5, "ikano"}, {5, "intel"}, {5, "irish"}, {5, "iveco"}, {5, "jetzt"}, {5, "koeln"}, {5, "kyoto"}, {5, "lamer"}, {5, "lease"}, {5, "legal"}, {5, "lexus"}, {5, "lilly"}, {5, "linde"}, {5, "lipsy"}, {5, "lixil"}, {5, "loans"}, {5, "locus"}, {5, "lotte"}, {5, "lotto"}, {5, "lupin"}, {5, "macys"}, {5, "mango"}, {5, "media"}, {5, "miami"}, {5, "money"}, {5, "mopar"}, {5, "movie"}, {5, "nadex"}, {5, "nexus"}, {5, "nikon"}, {5, "ninja"}, {5, "nokia"}, {5, "nowtv"}, {5, "omega"}, {5, "onion"}, {5, "osaka"}, {5, "paris"}, {5, "parts"}, {5, "party"}, {5, "phone"}, {5, "photo"}, {5, "pizza"}, {5, "place"}, {5, "poker"}, {5, "praxi"}, {5, "press"}, {5, "prime"}, {5, "promo"}, {5, "quest"}, {5, "radio"}, {5, "rehab"}, {5, "reise"}, {5, "ricoh"}, {5, "rocks"}, {5, "rodeo"}, {5, "rugby"}, {5, "salon"}, {5, "sener"}, {5, "seven"}, {5, "sharp"}, {5, "shell"}, {5, "shoes"}, {5, "skype"}, {5, "sling"}, {5, "smart"}, {5, "smile"}, {5, "solar"}, {5, "space"}, {5, "sport"}, {5, "stada"}, {5, "store"}, {5, "study"}, {5, "style"}, {5, "sucks"}, {5, "swiss"}, {5, "tatar"}, {5, "tires"}, {5, "tirol"}, {5, "tmall"}, {5, "today"}, {5, "tokyo"}, {5, "tools"}, {5, "toray"}, {5, "total"}, {5, "tours"}, {5, "trade"}, {5, "trust"}, {5, "tunes"}, {5, "tushu"}, {5, "ubank"}, {5, "vegas"}, {5, "video"}, {5, "vista"}, {5, "vodka"}, {5, "volvo"}, {5, "wales"}, {5, "watch"}, {5, "weber"}, {5, "weibo"}, {5, "works"}, {5, "world"}, {5, "xerox"}, {5, "yahoo"}, {5, "zippo"}, {5, "بازار"}, {5, "همراه"}, {5, "संगठन"}, {5, "嘉里大酒店"}, {6, "abarth"}, {6, "abbott"}, {6, "abbvie"}, {6, "active"}, {6, "africa"}, {6, "agency"}, {6, "airbus"}, {6, "airtel"}, {6, "alipay"}, {6, "alsace"}, {6, "alstom"}, {6, "anquan"}, {6, "aramco"}, {6, "author"}, {6, "bayern"}, {6, "beauty"}, {6, "berlin"}, {6, "bharti"}, {6, "blanco"}, {6, "bostik"}, {6, "boston"}, {6, "broker"}, {6, "camera"}, {6, "career"}, {6, "caseih"}, {6, "casino"}, {6, "center"}, {6, "chanel"}, {6, "chrome"}, {6, "church"}, {6, "circle"}, {6, "claims"}, {6, "clinic"}, {6, "coffee"}, {6, "comsec"}, {6, "condos"}, {6, "coupon"}, {6, "credit"}, {6, "cruise"}, {6, "dating"}, {6, "datsun"}, {6, "dealer"}, {6, "degree"}, {6, "dental"}, {6, "design"}, {6, "direct"}, {6, "doctor"}, {6, "doosan"}, {6, "dunlop"}, {6, "dupont"}, {6, "durban"}, {6, "emerck"}, {6, "energy"}, {6, "estate"}, {6, "events"}, {6, "expert"}, {6, "family"}, {6, "flickr"}, {6, "futbol"}, {6, "gallup"}, {6, "garden"}, {6, "george"}, {6, "giving"}, {6, "global"}, {6, "google"}, {6, "gratis"}, {6, "health"}, {6, "hermes"}, {6, "hiphop"}, {6, "hockey"}, {6, "hotels"}, {6, "hughes"}, {6, "imamat"}, {6, "insure"}, {6, "intuit"}, {6, "jaguar"}, {6, "joburg"}, {6, "juegos"}, {6, "kaufen"}, {6, "kinder"}, {6, "kindle"}, {6, "kosher"}, {6, "lancia"}, {6, "latino"}, {6, "lawyer"}, {6, "lefrak"}, {6, "living"}, {6, "locker"}, {6, "london"}, {6, "luxury"}, {6, "madrid"}, {6, "maison"}, {6, "makeup"}, {6, "market"}, {6, "mattel"}, {6, "mobile"}, {6, "mobily"}, {6, "monash"}, {6, "mormon"}, {6, "moscow"}, {6, "museum"}, {6, "mutual"}, {6, "nagoya"}, {6, "natura"}, {6, "nissan"}, {6, "nissay"}, {6, "norton"}, {6, "nowruz"}, {6, "office"}, {6, "olayan"}, {6, "online"}, {6, "oracle"}, {6, "orange"}, {6, "otsuka"}, {6, "pfizer"}, {6, "photos"}, {6, "physio"}, {6, "piaget"}, {6, "pictet"}, {6, "quebec"}, {6, "racing"}, {6, "realty"}, {6, "reisen"}, {6, "repair"}, {6, "report"}, {6, "review"}, {6, "rocher"}, {6, "rogers"}, {6, "ryukyu"}, {6, "safety"}, {6, "sakura"}, {6, "sanofi"}, {6, "school"}, {6, "schule"}, {6, "search"}, {6, "secure"}, {6, "select"}, {6, "shouji"}, {6, "soccer"}, {6, "social"}, {6, "stream"}, {6, "studio"}, {6, "supply"}, {6, "suzuki"}, {6, "swatch"}, {6, "sydney"}, {6, "taipei"}, {6, "taobao"}, {6, "target"}, {6, "tattoo"}, {6, "tennis"}, {6, "tienda"}, {6, "tjmaxx"}, {6, "tkmaxx"}, {6, "toyota"}, {6, "travel"}, {6, "unicom"}, {6, "viajes"}, {6, "viking"}, {6, "villas"}, {6, "virgin"}, {6, "vision"}, {6, "voting"}, {6, "voyage"}, {6, "vuelos"}, {6, "walter"}, {6, "warman"}, {6, "webcam"}, {6, "xihuan"}, {6, "xperia"}, {6, "yachts"}, {6, "yandex"}, {6, "zappos"}, {6, "моÑква"}, {6, "онлайн"}, {6, "ابوظبي"}, {6, "ارامكو"}, {6, "ファッション"}, {7, "abogado"}, {7, "academy"}, {7, "agakhan"}, {7, "alibaba"}, {7, "android"}, {7, "athleta"}, {7, "auction"}, {7, "audible"}, {7, "auspost"}, {7, "avianca"}, {7, "banamex"}, {7, "bauhaus"}, {7, "bentley"}, {7, "bestbuy"}, {7, "booking"}, {7, "brother"}, {7, "bugatti"}, {7, "capital"}, {7, "caravan"}, {7, "careers"}, {7, "cartier"}, {7, "channel"}, {7, "charity"}, {7, "chintai"}, {7, "citadel"}, {7, "clubmed"}, {7, "college"}, {7, "cologne"}, {7, "comcast"}, {7, "company"}, {7, "compare"}, {7, "contact"}, {7, "cooking"}, {7, "corsica"}, {7, "country"}, {7, "coupons"}, {7, "courses"}, {7, "cricket"}, {7, "cruises"}, {7, "dentist"}, {7, "digital"}, {7, "domains"}, {7, "exposed"}, {7, "express"}, {7, "farmers"}, {7, "fashion"}, {7, "ferrari"}, {7, "ferrero"}, {7, "finance"}, {7, "fishing"}, {7, "fitness"}, {7, "flights"}, {7, "florist"}, {7, "flowers"}, {7, "forsale"}, {7, "frogans"}, {7, "fujitsu"}, {7, "gallery"}, {7, "genting"}, {7, "godaddy"}, {7, "grocery"}, {7, "guitars"}, {7, "hamburg"}, {7, "hangout"}, {7, "hitachi"}, {7, "holiday"}, {7, "hosting"}, {7, "hoteles"}, {7, "hotmail"}, {7, "hyundai"}, {7, "iselect"}, {7, "ismaili"}, {7, "jewelry"}, {7, "juniper"}, {7, "kitchen"}, {7, "komatsu"}, {7, "lacaixa"}, {7, "lancome"}, {7, "lanxess"}, {7, "lasalle"}, {7, "latrobe"}, {7, "leclerc"}, {7, "liaison"}, {7, "limited"}, {7, "lincoln"}, {7, "markets"}, {7, "metlife"}, {7, "monster"}, {7, "netbank"}, {7, "netflix"}, {7, "network"}, {7, "neustar"}, {7, "okinawa"}, {7, "oldnavy"}, {7, "organic"}, {7, "origins"}, {7, "panerai"}, {7, "philips"}, {7, "pioneer"}, {7, "politie"}, {7, "realtor"}, {7, "recipes"}, {7, "rentals"}, {7, "reviews"}, {7, "rexroth"}, {7, "samsung"}, {7, "sandvik"}, {7, "schmidt"}, {7, "schwarz"}, {7, "science"}, {7, "shiksha"}, {7, "shriram"}, {7, "singles"}, {7, "spiegel"}, {7, "staples"}, {7, "starhub"}, {7, "statoil"}, {7, "storage"}, {7, "support"}, {7, "surgery"}, {7, "systems"}, {7, "temasek"}, {7, "theater"}, {7, "theatre"}, {7, "tickets"}, {7, "tiffany"}, {7, "toshiba"}, {7, "trading"}, {7, "walmart"}, {7, "wanggou"}, {7, "watches"}, {7, "weather"}, {7, "website"}, {7, "wedding"}, {7, "whoswho"}, {7, "windows"}, {7, "winners"}, {7, "xfinity"}, {7, "yamaxun"}, {7, "youtube"}, {7, "zuerich"}, {7, "католик"}, {7, "اتصالات"}, {7, "العليان"}, {7, "كاثوليك"}, {7, "موبايلي"}, {8, "abudhabi"}, {8, "airforce"}, {8, "allstate"}, {8, "attorney"}, {8, "barclays"}, {8, "barefoot"}, {8, "bargains"}, {8, "baseball"}, {8, "boutique"}, {8, "bradesco"}, {8, "broadway"}, {8, "brussels"}, {8, "budapest"}, {8, "builders"}, {8, "business"}, {8, "capetown"}, {8, "catering"}, {8, "catholic"}, {8, "chrysler"}, {8, "cipriani"}, {8, "cityeats"}, {8, "cleaning"}, {8, "clinique"}, {8, "clothing"}, {8, "commbank"}, {8, "computer"}, {8, "delivery"}, {8, "deloitte"}, {8, "democrat"}, {8, "diamonds"}, {8, "discount"}, {8, "discover"}, {8, "download"}, {8, "engineer"}, {8, "ericsson"}, {8, "esurance"}, {8, "etisalat"}, {8, "everbank"}, {8, "exchange"}, {8, "feedback"}, {8, "fidelity"}, {8, "firmdale"}, {8, "flsmidth"}, {8, "football"}, {8, "frontier"}, {8, "goodyear"}, {8, "grainger"}, {8, "graphics"}, {8, "guardian"}, {8, "hdfcbank"}, {8, "helsinki"}, {8, "holdings"}, {8, "hospital"}, {8, "infiniti"}, {8, "ipiranga"}, {8, "istanbul"}, {8, "jpmorgan"}, {8, "lighting"}, {8, "lundbeck"}, {8, "marriott"}, {8, "maserati"}, {8, "mckinsey"}, {8, "memorial"}, {8, "merckmsd"}, {8, "mortgage"}, {8, "movistar"}, {8, "mutuelle"}, {8, "observer"}, {8, "partners"}, {8, "pharmacy"}, {8, "pictures"}, {8, "plumbing"}, {8, "property"}, {8, "redstone"}, {8, "reliance"}, {8, "saarland"}, {8, "samsclub"}, {8, "security"}, {8, "services"}, {8, "shopping"}, {8, "showtime"}, {8, "softbank"}, {8, "software"}, {8, "stcgroup"}, {8, "supplies"}, {8, "symantec"}, {8, "telecity"}, {8, "training"}, {8, "uconnect"}, {8, "vanguard"}, {8, "ventures"}, {8, "verisign"}, {8, "woodside"}, {8, "yokohama"}, {9, "accenture"}, {9, "alfaromeo"}, {9, "allfinanz"}, {9, "amsterdam"}, {9, "analytics"}, {9, "aquarelle"}, {9, "barcelona"}, {9, "bloomberg"}, {9, "christmas"}, {9, "community"}, {9, "directory"}, {9, "education"}, {9, "equipment"}, {9, "fairwinds"}, {9, "financial"}, {9, "firestone"}, {9, "fresenius"}, {9, "frontdoor"}, {9, "fujixerox"}, {9, "furniture"}, {9, "goldpoint"}, {9, "goodhands"}, {9, "hisamitsu"}, {9, "homedepot"}, {9, "homegoods"}, {9, "homesense"}, {9, "honeywell"}, {9, "institute"}, {9, "insurance"}, {9, "kuokgroup"}, {9, "ladbrokes"}, {9, "lancaster"}, {9, "landrover"}, {9, "lifestyle"}, {9, "marketing"}, {9, "marshalls"}, {9, "mcdonalds"}, {9, "melbourne"}, {9, "microsoft"}, {9, "montblanc"}, {9, "panasonic"}, {9, "passagens"}, {9, "pramerica"}, {9, "richardli"}, {9, "scjohnson"}, {9, "shangrila"}, {9, "solutions"}, {9, "statebank"}, {9, "statefarm"}, {9, "stockholm"}, {9, "travelers"}, {9, "vacations"}, {9, "yodobashi"}, {10, "accountant"}, {10, "apartments"}, {10, "associates"}, {10, "basketball"}, {10, "bnpparibas"}, {10, "boehringer"}, {10, "capitalone"}, {10, "consulting"}, {10, "creditcard"}, {10, "cuisinella"}, {10, "eurovision"}, {10, "extraspace"}, {10, "foundation"}, {10, "healthcare"}, {10, "immobilien"}, {10, "industries"}, {10, "management"}, {10, "mitsubishi"}, {10, "nationwide"}, {10, "newholland"}, {10, "nextdirect"}, {10, "onyourside"}, {10, "properties"}, {10, "protection"}, {10, "prudential"}, {10, "realestate"}, {10, "republican"}, {10, "restaurant"}, {10, "schaeffler"}, {10, "swiftcover"}, {10, "tatamotors"}, {10, "technology"}, {10, "telefonica"}, {10, "university"}, {10, "vistaprint"}, {10, "vlaanderen"}, {10, "volkswagen"}, {11, "accountants"}, {11, "barclaycard"}, {11, "blackfriday"}, {11, "blockbuster"}, {11, "bridgestone"}, {11, "calvinklein"}, {11, "contractors"}, {11, "creditunion"}, {11, "engineering"}, {11, "enterprises"}, {11, "foodnetwork"}, {11, "investments"}, {11, "kerryhotels"}, {11, "lamborghini"}, {11, "motorcycles"}, {11, "olayangroup"}, {11, "photography"}, {11, "playstation"}, {11, "productions"}, {11, "progressive"}, {11, "redumbrella"}, {11, "rightathome"}, {11, "williamhill"}, {12, "construction"}, {12, "lplfinancial"}, {12, "pamperedchef"}, {12, "scholarships"}, {12, "versicherung"}, {13, "international"}, {13, "lifeinsurance"}, {13, "orientexpress"}, {13, "spreadbetting"}, {13, "travelchannel"}, {13, "wolterskluwer"}, {14, "afamilycompany"}, {14, "americanfamily"}, {14, "bananarepublic"}, {14, "cancerresearch"}, {14, "cookingchannel"}, {14, "kerrylogistics"}, {14, "weatherchannel"}, {15, "americanexpress"}, {15, "kerryproperties"}, {15, "sandvikcoromant"}, {16, "vermögensberater"}, {17, "vermögensberatung"}, {18, "northwesternmutual"}, {18, "travelersinsurance"}, }; static const struct { size_t length; const char *str; } CCTLDS[] = { {2, "한국"}, {2, "ac"}, {2, "ad"}, {2, "ae"}, {2, "af"}, {2, "ag"}, {2, "ai"}, {2, "al"}, {2, "am"}, {2, "an"}, {2, "ao"}, {2, "aq"}, {2, "ar"}, {2, "as"}, {2, "at"}, {2, "au"}, {2, "aw"}, {2, "ax"}, {2, "az"}, {2, "ba"}, {2, "bb"}, {2, "bd"}, {2, "be"}, {2, "bf"}, {2, "bg"}, {2, "bh"}, {2, "bi"}, {2, "bj"}, {2, "bl"}, {2, "bm"}, {2, "bn"}, {2, "bo"}, {2, "bq"}, {2, "br"}, {2, "bs"}, {2, "bt"}, {2, "bv"}, {2, "bw"}, {2, "by"}, {2, "bz"}, {2, "ca"}, {2, "cc"}, {2, "cd"}, {2, "cf"}, {2, "cg"}, {2, "ch"}, {2, "ci"}, {2, "ck"}, {2, "cl"}, {2, "cm"}, {2, "cn"}, {2, "co"}, {2, "cr"}, {2, "cu"}, {2, "cv"}, {2, "cw"}, {2, "cx"}, {2, "cy"}, {2, "cz"}, {2, "de"}, {2, "dj"}, {2, "dk"}, {2, "dm"}, {2, "do"}, {2, "dz"}, {2, "ec"}, {2, "ee"}, {2, "eg"}, {2, "eh"}, {2, "er"}, {2, "es"}, {2, "et"}, {2, "eu"}, {2, "fi"}, {2, "fj"}, {2, "fk"}, {2, "fm"}, {2, "fo"}, {2, "fr"}, {2, "ga"}, {2, "gb"}, {2, "gd"}, {2, "ge"}, {2, "gf"}, {2, "gg"}, {2, "gh"}, {2, "gi"}, {2, "gl"}, {2, "gm"}, {2, "gn"}, {2, "gp"}, {2, "gq"}, {2, "gr"}, {2, "gs"}, {2, "gt"}, {2, "gu"}, {2, "gw"}, {2, "gy"}, {2, "hk"}, {2, "hm"}, {2, "hn"}, {2, "hr"}, {2, "ht"}, {2, "hu"}, {2, "id"}, {2, "ie"}, {2, "il"}, {2, "im"}, {2, "in"}, {2, "io"}, {2, "iq"}, {2, "ir"}, {2, "is"}, {2, "it"}, {2, "je"}, {2, "jm"}, {2, "jo"}, {2, "jp"}, {2, "ke"}, {2, "kg"}, {2, "kh"}, {2, "ki"}, {2, "km"}, {2, "kn"}, {2, "kp"}, {2, "kr"}, {2, "kw"}, {2, "ky"}, {2, "kz"}, {2, "la"}, {2, "lb"}, {2, "lc"}, {2, "li"}, {2, "lk"}, {2, "lr"}, {2, "ls"}, {2, "lt"}, {2, "lu"}, {2, "lv"}, {2, "ly"}, {2, "ma"}, {2, "mc"}, {2, "md"}, {2, "me"}, {2, "mf"}, {2, "mg"}, {2, "mh"}, {2, "mk"}, {2, "ml"}, {2, "mm"}, {2, "mn"}, {2, "mo"}, {2, "mp"}, {2, "mq"}, {2, "mr"}, {2, "ms"}, {2, "mt"}, {2, "mu"}, {2, "mv"}, {2, "mw"}, {2, "mx"}, {2, "my"}, {2, "mz"}, {2, "na"}, {2, "nc"}, {2, "ne"}, {2, "nf"}, {2, "ng"}, {2, "ni"}, {2, "nl"}, {2, "no"}, {2, "np"}, {2, "nr"}, {2, "nu"}, {2, "nz"}, {2, "om"}, {2, "pa"}, {2, "pe"}, {2, "pf"}, {2, "pg"}, {2, "ph"}, {2, "pk"}, {2, "pl"}, {2, "pm"}, {2, "pn"}, {2, "pr"}, {2, "ps"}, {2, "pt"}, {2, "pw"}, {2, "py"}, {2, "qa"}, {2, "re"}, {2, "ro"}, {2, "rs"}, {2, "ru"}, {2, "rw"}, {2, "sa"}, {2, "sb"}, {2, "sc"}, {2, "sd"}, {2, "se"}, {2, "sg"}, {2, "sh"}, {2, "si"}, {2, "sj"}, {2, "sk"}, {2, "sl"}, {2, "sm"}, {2, "sn"}, {2, "so"}, {2, "sr"}, {2, "ss"}, {2, "st"}, {2, "su"}, {2, "sv"}, {2, "sx"}, {2, "sy"}, {2, "sz"}, {2, "tc"}, {2, "td"}, {2, "tf"}, {2, "tg"}, {2, "th"}, {2, "tj"}, {2, "tk"}, {2, "tl"}, {2, "tm"}, {2, "tn"}, {2, "to"}, {2, "tp"}, {2, "tr"}, {2, "tt"}, {2, "tv"}, {2, "tw"}, {2, "tz"}, {2, "ua"}, {2, "ug"}, {2, "uk"}, {2, "um"}, {2, "us"}, {2, "uy"}, {2, "uz"}, {2, "va"}, {2, "vc"}, {2, "ve"}, {2, "vg"}, {2, "vi"}, {2, "vn"}, {2, "vu"}, {2, "wf"}, {2, "ws"}, {2, "ye"}, {2, "yt"}, {2, "za"}, {2, "zm"}, {2, "zw"}, {2, "ελ"}, {2, "ευ"}, {2, "бг"}, {2, "ею"}, {2, "рф"}, {2, "გე"}, {2, "中国"}, {2, "中國"}, {2, "å°æ¹¾"}, {2, "å°ç£"}, {2, "澳門"}, {2, "香港"}, {3, "бел"}, {3, "қаз"}, {3, "мкд"}, {3, "мон"}, {3, "Ñрб"}, {3, "укр"}, {3, "Õ°Õ¡Õµ"}, {3, "قطر"}, {3, "مصر"}, {3, "ไทย"}, {3, "ລາວ"}, {3, "新加å¡"}, {4, "بارت"}, {4, "ڀارت"}, {4, "تونس"}, {4, "عراق"}, {4, "عمان"}, {4, "भारत"}, {4, "ভারত"}, {4, "ভাৰত"}, {4, "ਭਾਰਤ"}, {4, "ભારત"}, {4, "ଭାରତ"}, {4, "ಭಾರತ"}, {4, "ලංකà·"}, {5, "ایران"}, {5, "بھارت"}, {5, "سودان"}, {5, "سورية"}, {5, "भारोत"}, {5, "বাংলা"}, {5, "భారతà±"}, {5, "ഭാരതം"}, {6, "الاردن"}, {6, "المغرب"}, {6, "امارات"}, {6, "Ùلسطين"}, {6, "مليسيا"}, {6, "भारतमà¥"}, {6, "இலஙà¯à®•ை"}, {7, "البحرين"}, {7, "الجزائر"}, {7, "پاکستان"}, {7, "இநà¯à®¤à®¿à®¯à®¾"}, {8, "السعودية"}, {9, "موريتانيا"}, {11, "சிஙà¯à®•பà¯à®ªà¯‚à®°à¯"}, }; #endifcawbird-1.4.2/src/libtl/libtweetlength.c000066400000000000000000001521721416632607600202360ustar00rootroot00000000000000/* This file is part of libtweetlength * Copyright (C) 2017 Timm Bäder * * libtweetlength 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. * * libtweetlength 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 libtweetlength. If not, see . */ #include "libtweetlength.h" #include "data.h" #include #define LINK_LENGTH 23 #define UNWEIGHTED_VALUE 1 #define WEIGHTED_VALUE 2 #define END_SEQUENCE_WEIGHT 0 #define REGIONAL_INDICATOR_OFFSET 0x1F1E6 #define MAKE_KEY(prev_char, cur_char) GUINT_TO_POINTER(prev_char + (cur_char << 8)) // Map of current character type to a list of potential replacements based on the previous character GHashTable *chartype_map; // Lookup table of valid Regional Indicator strings gboolean valid_ri_strings[26*26]; gboolean ri_validator_generated = FALSE; typedef struct { guint type; const char *start; gsize start_character_index; gsize length_in_bytes; gsize length_in_characters; gsize length_in_weighted_characters; } Token; #ifdef LIBTL_DEBUG static char * G_GNUC_UNUSED token_str (const Token *t) { return g_strdup_printf ("Type: %u, Text: '%.*s'", t->type, (int)t->length_in_bytes, t->start); } static char * G_GNUC_UNUSED entity_str (const TlEntity *e) { return g_strdup_printf ("Type: %u, Text: '%.*s'", e->type, (int)e->length_in_bytes, e->start); } #endif enum { TOK_TEXT = 1, TOK_NUMBER, TOK_WHITESPACE, TOK_COLON, TOK_SLASH, TOK_OPEN_PAREN, TOK_CLOSE_PAREN, TOK_QUESTIONMARK, TOK_DOT, TOK_HASH, TOK_AT, TOK_EQUALS, TOK_DASH, TOK_UNDERSCORE, TOK_APOSTROPHE, TOK_QUOTE, TOK_DOLLAR, TOK_AMPERSAND, TOK_EXCLAMATION, TOK_TILDE }; enum { CHARTYPE_NONE, // Used for initial setup ("none") and other special situations (Fitzpatrick modifier on its own) CHARTYPE_UNWEIGHTED, CHARTYPE_KEYCAPPABLE, CHARTYPE_WEIGHTED_OTHER, CHARTYPE_FITZPATRICK, CHARTYPE_WOMAN, CHARTYPE_MAN, CHARTYPE_UNGENDERED_ADULT, CHARTYPE_CHILD, CHARTYPE_FAMILY_PARENTS, CHARTYPE_FAMILY_1_CHILD, CHARTYPE_FAMILY_2_CHILD, CHARTYPE_PERSON, CHARTYPE_GENDERABLE_PERSON, CHARTYPE_UNTONED_GENDERABLE_PERSON, CHARTYPE_FITZPATRICKED_PERSON, CHARTYPE_FITZPATRICKED_GENDERABLE_PERSON, CHARTYPE_FITZPATRICKED_ADULT, CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_HAIRSTYLE, CHARTYPE_HAIRSTYLED_ADULT, CHARTYPE_JOB, CHARTYPE_JOB_TEXT, CHARTYPE_JOB_PERSON_TEXT, CHARTYPE_JOB_PERSON, CHARTYPE_WHITE_FLAG, CHARTYPE_WHITE_FLAG_VS16, CHARTYPE_BLACK_FLAG, CHARTYPE_GENDER_TEXT, CHARTYPE_GENDER, CHARTYPE_GENDERED_PERSON_TEXT, CHARTYPE_GENDERED_PERSON, CHARTYPE_HAIR, CHARTYPE_HEART, CHARTYPE_LOVE_BASE_TEXT, CHARTYPE_LOVE_BASE, CHARTYPE_LOVE_BASE_TEXT_POSSIBLE, CHARTYPE_LOVE_BASE_POSSIBLE, CHARTYPE_LOVE, CHARTYPE_KISS_MARK, CHARTYPE_KISSING_BASE, CHARTYPE_KISSING_BASE_POSSIBLE, CHARTYPE_KISSING, CHARTYPE_RAINBOW, CHARTYPE_TRANSGENDER_SYMBOL, CHARTYPE_SKULL_AND_CROSSBONES, CHARTYPE_PARTIAL_COMBINED_FLAG, CHARTYPE_COMBINED_FLAG, CHARTYPE_CHRISTMAS_TREE, CHARTYPE_DOG, CHARTYPE_SAFETY_VEST, CHARTYPE_CAT, CHARTYPE_COLOUR_BLACK, CHARTYPE_BEAR, CHARTYPE_SNOWFLAKE, CHARTYPE_ZWJ_ANIMAL_TEXT, CHARTYPE_ZWJ_ANIMAL, CHARTYPE_REGIONAL_INDICATOR, CHARTYPE_REGIONAL_INDICATOR_FLAG, CHARTYPE_TAG, CHARTYPE_TAGGED_FLAG, CHARTYPE_TAG_CLOSE, CHARTYPE_VS16, CHARTYPE_ZWJ }; typedef struct _CharTypeOption { guint8 new_chartype; guint8 carry_weight; } CharTypeOption; static inline CharTypeOption* new_chartypeoption(guint new_chartype, guint carry_weight) { CharTypeOption *opt = malloc(sizeof(CharTypeOption)); opt->new_chartype = new_chartype; opt->carry_weight = carry_weight; return opt; } static inline GHashTable* get_chartype_options () { if (chartype_map != NULL) { return chartype_map; } chartype_map = g_hash_table_new(g_direct_hash, g_direct_equal); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_WOMAN), new_chartypeoption(CHARTYPE_FAMILY_PARENTS, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_MAN), new_chartypeoption(CHARTYPE_FAMILY_PARENTS, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_WOMAN), new_chartypeoption(CHARTYPE_FAMILY_PARENTS, WEIGHTED_VALUE)); // But not Woman then Man for the family g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_CHILD), new_chartypeoption(CHARTYPE_FAMILY_1_CHILD, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_CHILD), new_chartypeoption(CHARTYPE_FAMILY_1_CHILD, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FAMILY_PARENTS, CHARTYPE_CHILD), new_chartypeoption(CHARTYPE_FAMILY_1_CHILD, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FAMILY_1_CHILD, CHARTYPE_CHILD), new_chartypeoption(CHARTYPE_FAMILY_2_CHILD, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_JOB_TEXT), new_chartypeoption(CHARTYPE_JOB_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_JOB_TEXT), new_chartypeoption(CHARTYPE_JOB_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_JOB_TEXT), new_chartypeoption(CHARTYPE_JOB_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_ADULT, CHARTYPE_JOB_TEXT), new_chartypeoption(CHARTYPE_JOB_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_JOB_TEXT), new_chartypeoption(CHARTYPE_JOB_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_JOB), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_JOB), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_JOB), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_ADULT, CHARTYPE_JOB), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_JOB), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_JOB_PERSON_TEXT, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_CHRISTMAS_TREE), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_CHRISTMAS_TREE), new_chartypeoption(CHARTYPE_JOB_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_PERSON, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_GENDERABLE_PERSON, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_GENDERABLE_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_CHILD, CHARTYPE_FITZPATRICK), new_chartypeoption(CHARTYPE_FITZPATRICKED_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_ADULT, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_HAIRSTYLE), new_chartypeoption(CHARTYPE_HAIRSTYLED_ADULT, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNGENDERED_ADULT, CHARTYPE_GENDER_TEXT), new_chartypeoption(CHARTYPE_GENDERED_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_UNTONED_GENDERABLE_PERSON, CHARTYPE_GENDER_TEXT), new_chartypeoption(CHARTYPE_GENDERED_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_UNGENDERED_ADULT, CHARTYPE_GENDER_TEXT), new_chartypeoption(CHARTYPE_GENDERED_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_GENDERABLE_PERSON, CHARTYPE_GENDER_TEXT), new_chartypeoption(CHARTYPE_GENDERED_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_FITZPATRICKED_GENDERABLE_PERSON, CHARTYPE_GENDER_TEXT), new_chartypeoption(CHARTYPE_GENDERED_PERSON_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_GENDERED_PERSON_TEXT, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_GENDERED_PERSON, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_GENDER_TEXT, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_GENDER, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WHITE_FLAG, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_WHITE_FLAG_VS16, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WHITE_FLAG, CHARTYPE_RAINBOW), new_chartypeoption(CHARTYPE_COMBINED_FLAG, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WHITE_FLAG_VS16, CHARTYPE_RAINBOW), new_chartypeoption(CHARTYPE_COMBINED_FLAG, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WHITE_FLAG, CHARTYPE_TRANSGENDER_SYMBOL), new_chartypeoption(CHARTYPE_PARTIAL_COMBINED_FLAG, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WHITE_FLAG_VS16, CHARTYPE_TRANSGENDER_SYMBOL), new_chartypeoption(CHARTYPE_PARTIAL_COMBINED_FLAG, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_BLACK_FLAG, CHARTYPE_SKULL_AND_CROSSBONES), new_chartypeoption(CHARTYPE_PARTIAL_COMBINED_FLAG, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_PARTIAL_COMBINED_FLAG, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_COMBINED_FLAG, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_WOMAN, CHARTYPE_HEART), new_chartypeoption(CHARTYPE_LOVE_BASE_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_MAN, CHARTYPE_HEART), new_chartypeoption(CHARTYPE_LOVE_BASE_TEXT_POSSIBLE, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE_TEXT, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_LOVE_BASE, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE_TEXT_POSSIBLE, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_LOVE_BASE_POSSIBLE, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE, CHARTYPE_MAN), new_chartypeoption(CHARTYPE_LOVE, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE, CHARTYPE_WOMAN), new_chartypeoption(CHARTYPE_LOVE, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE_POSSIBLE, CHARTYPE_MAN), new_chartypeoption(CHARTYPE_LOVE, END_SEQUENCE_WEIGHT)); // But not Man Heart Woman g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE, CHARTYPE_KISS_MARK), new_chartypeoption(CHARTYPE_KISSING_BASE, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_LOVE_BASE_POSSIBLE, CHARTYPE_KISS_MARK), new_chartypeoption(CHARTYPE_KISSING_BASE_POSSIBLE, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_KISSING_BASE, CHARTYPE_MAN), new_chartypeoption(CHARTYPE_KISSING, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_KISSING_BASE, CHARTYPE_WOMAN), new_chartypeoption(CHARTYPE_KISSING, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_KISSING_BASE_POSSIBLE, CHARTYPE_MAN), new_chartypeoption(CHARTYPE_KISSING, END_SEQUENCE_WEIGHT)); // But not Man Heart Kiss Woman g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_BEAR, CHARTYPE_SNOWFLAKE), new_chartypeoption(CHARTYPE_ZWJ_ANIMAL_TEXT, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_ZWJ_ANIMAL_TEXT, CHARTYPE_VS16), new_chartypeoption(CHARTYPE_ZWJ_ANIMAL, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_DOG, CHARTYPE_SAFETY_VEST), new_chartypeoption(CHARTYPE_ZWJ_ANIMAL, END_SEQUENCE_WEIGHT)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_CAT, CHARTYPE_COLOUR_BLACK), new_chartypeoption(CHARTYPE_ZWJ_ANIMAL, END_SEQUENCE_WEIGHT)); // We assume that CHARTYPE_TAG strings are valid because it's too much trouble if they're not. // There's a near-zero probability of people writing them by hand, so we should be safe. g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_BLACK_FLAG, CHARTYPE_TAG), new_chartypeoption(CHARTYPE_TAGGED_FLAG, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_TAGGED_FLAG, CHARTYPE_TAG), new_chartypeoption(CHARTYPE_TAGGED_FLAG, WEIGHTED_VALUE)); g_hash_table_insert(chartype_map, MAKE_KEY(CHARTYPE_TAGGED_FLAG, CHARTYPE_TAG_CLOSE), new_chartypeoption(CHARTYPE_TAGGED_FLAG, END_SEQUENCE_WEIGHT)); return chartype_map; } static gboolean is_valid_regional_indicator (gunichar ri_char1, gunichar ri_char2) { if (!ri_validator_generated) { // Strings taken from https://en.wikipedia.org/wiki/Regional_indicator_symbol // Note: We need the trailing space to align everything for the loop! gchar *indicators = "AC AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ" " CA CC CD CF CG CH CI CK CL CM CN CO CP CR CU CV CW CX CY CZ DE DG DJ DK DM DO DZ EA EC EE EG EH ER ES ET EU FI FJ FK FM FO FR" " GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU IC ID IE IL IM IN IO IQ IR IS IT JE JM JO JP" " KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ" " NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW" " SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TA TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ" " UA UG UM UN US UY UZ VA VC VE VG VI VN VU WF WS XK YE YT ZA ZM ZW" " AN BU CS DD FX NT QU SU TP YD YU ZR "; gchar *char1 = indicators; gchar *char2 = char1 + 1; do { valid_ri_strings[((*char1 - 0x41) * 26) + (*char2 - 0x41)] = TRUE; char1 += 3; char2 += 3; } while (*char1 != '\0'); ri_validator_generated = TRUE; } return valid_ri_strings[((ri_char1 - REGIONAL_INDICATOR_OFFSET) * 26) + (ri_char2 - REGIONAL_INDICATOR_OFFSET)]; } static inline guint token_type_from_char (gunichar c) { switch (c) { case '@': return TOK_AT; case '#': return TOK_HASH; case ':': return TOK_COLON; case '/': return TOK_SLASH; case '(': return TOK_OPEN_PAREN; case ')': return TOK_CLOSE_PAREN; case '.': return TOK_DOT; case '?': return TOK_QUESTIONMARK; case '=': return TOK_EQUALS; case '-': return TOK_DASH; case '_': return TOK_UNDERSCORE; case '\'': return TOK_APOSTROPHE; case '"': return TOK_QUOTE; case '$': return TOK_DOLLAR; case '&': return TOK_AMPERSAND; case '!': return TOK_EXCLAMATION; case '~': return TOK_TILDE; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return TOK_NUMBER; case ' ': case '\n': case '\t': return TOK_WHITESPACE; default: return TOK_TEXT; } } static inline gboolean token_ends_in_accented (const Token *t) { const char *p = t->start; gunichar c; gsize i; if (t->length_in_bytes == 1 || t->type != TOK_TEXT) { return FALSE; } // The rules here aren't exactly clear... // We read the last character of the text pointed to by the given token. // If that's not an ascii character, we return TRUE. for (i = 0; i < t->length_in_characters - 1; i ++) { p = g_utf8_next_char (p); } c = g_utf8_get_char (p); if (c > 127) return TRUE; return FALSE; } static inline gboolean token_in (const Token *t, const char *haystack) { const int haystack_len = strlen (haystack); int i; if (t->length_in_bytes > 1) { return FALSE; } for (i = 0; i < haystack_len; i ++) { if (haystack[i] == t->start[0]) { return TRUE; } } return FALSE; } static inline void emplace_token (GArray *array, const char *token_start, gsize token_length, gsize start_character_index, gsize length_in_characters, gsize length_in_weighted_characters) { Token *t; g_array_set_size (array, array->len + 1); t = &g_array_index (array, Token, array->len - 1); t->type = token_type_from_char (token_start[0]); t->start = token_start; t->length_in_bytes = token_length; t->start_character_index = start_character_index; t->length_in_characters = length_in_characters; t->length_in_weighted_characters = length_in_weighted_characters; } static inline void emplace_entity_for_tokens (GArray *array, const Token *tokens, guint entity_type, guint start_token_index, guint end_token_index) { TlEntity *e; guint i; g_array_set_size (array, array->len + 1); e = &g_array_index (array, TlEntity, array->len - 1); e->type = entity_type; e->start = tokens[start_token_index].start; e->length_in_bytes = 0; e->length_in_characters = 0; e->length_in_weighted_characters = 0; e->start_character_index = tokens[start_token_index].start_character_index; for (i = start_token_index; i <= end_token_index; i ++) { e->length_in_bytes += tokens[i].length_in_bytes; e->length_in_characters += tokens[i].length_in_characters; e->length_in_weighted_characters += tokens[i].length_in_weighted_characters; } } static inline gboolean is_valid_mention_char (gunichar c) { // Just ASCII if (c > 127) return FALSE; return TRUE; } static inline gboolean token_is_tld (const Token *t, gboolean has_protocol) { guint i; if (t->length_in_characters > GTLDS[G_N_ELEMENTS (GTLDS) - 1].length) { return FALSE; } for (i = 0; i < G_N_ELEMENTS (GTLDS); i ++) { if (t->length_in_characters == GTLDS[i].length && strncasecmp (t->start, GTLDS[i].str, t->length_in_bytes) == 0) { return TRUE; } } for (i = 0; i < G_N_ELEMENTS (CCTLDS); i ++) { if (t->length_in_characters == CCTLDS[i].length && strncasecmp (t->start, CCTLDS[i].str, t->length_in_bytes) == 0) { return TRUE; } } return FALSE; } static inline gboolean token_is_protocol (const Token *t) { if (t->type != TOK_TEXT) { return FALSE; } if (t->length_in_bytes != 4 && t->length_in_bytes != 5) { return FALSE; } return strncasecmp (t->start, "http", t->length_in_bytes) == 0 || strncasecmp (t->start, "https", t->length_in_bytes) == 0; } static inline gboolean char_splits (gunichar c) { switch (c) { case ',': case '.': case '/': case '?': case '(': case ')': case ':': case ';': case '=': case '@': case '#': case '-': case '_': case '\n': case '\t': case '\0': case ' ': case '\'': case '"': case '$': case '|': case '&': case '^': case '%': case '+': case '*': case '\\': case '{': case '}': case '[': case ']': case '`': case '~': case '!': return TRUE; default: return FALSE; } return FALSE; } static inline gsize entity_length_in_characters (const TlEntity *e) { switch (e->type) { case TL_ENT_LINK: return LINK_LENGTH; default: return e->length_in_characters; } } static inline gboolean is_weighted_character (gunichar ch) { // Based on https://developer.twitter.com/en/docs/developer-utilities/twitter-text // then the following ranges count as "1", everything else is "2": // * 0 - 4351 (0x0 - 0x10FF) = Latin through to Georgian // * 8192 - 8205 (0x2000 - 0x200D) = Unicode spaces // * 8208 - 8223 (0x2010 - 0x201F) = Unicode hyphens and smart quotes // * 8242 - 8247 (0x2032 - 0x2037) = Prime marks return !((ch >= 0 && ch <= 4351) || (ch >= 8192 && ch <= 8205) || (ch >= 8208 && ch <= 8223) || (ch >= 8242 && ch <= 8247)); } static inline guint chartype_for_char (gunichar c) { if (c == 0x200D) { return CHARTYPE_ZWJ; } else if (!is_weighted_character (c)) { return CHARTYPE_UNWEIGHTED; } else if (c == 0xFE0F) { return CHARTYPE_VS16; } else if ((c >= 0x1100 && c < 0x2000) || (c >= 0x2800 && c <= 0x1F1E5 && c != 0x2B1B)) { // Hangul Jamo through Greek Extended // and Braille Patterns through Enclosed Alphanumeric Supplemental (that aren't Regional Indicators) // We specifically exclude U+2B1B to keep the range as big as possible return CHARTYPE_WEIGHTED_OTHER; } else if (c >= 0x1F3FB && c <= 0x1F3FF) { return CHARTYPE_FITZPATRICK; } else if (c == 0x1f466 || c == 0x1f467) { return CHARTYPE_CHILD; } else if (c == 0x1F468) { return CHARTYPE_MAN; } else if (c == 0x1F469) { return CHARTYPE_WOMAN; } else if (c == 0x1F9D1) { return CHARTYPE_UNGENDERED_ADULT; } else if (c == 0x26F9 || c == 0x1F3C3 || c == 0x1F3C4 || (c >= 0x1F3CA && c == 0x1F3CC) || c == 0x1F46E || c == 0x1F470 || c == 0x1F471 || c == 0x1F473 || c == 0x1F477 || c == 0x1F481 || c == 0x1F482 || c == 0x1F486 || c == 0x1F487 || c == 0x1F575 || (c >= 0x1F645 && c<= 0x1F647) || c == 0x1F64B || c == 0x1F64D || c == 0x1F64E || c == 0x1F6A3 || (c >= 0x1F6B4 && c <= 0x1F6B6) || c == 0x1F926 || c == 0x1F935 || (c >= 0x1F937 && c <= 0x1F939) || c == 0x1F93D || c == 0x1F93E || c == 0x1F9B8 || c == 0x1F9B9 || (c >= 0x1F9CD && c <= 0x1F9CF) || (c >= 0x1F9D6 && c <= 0x1F9DE) ) { return CHARTYPE_GENDERABLE_PERSON; } else if (c == 0x1F46F || c == 0x1F93C || c == 0x1F9DD) { // Zombies, wrestlers and bunnie people, oh my! return CHARTYPE_UNTONED_GENDERABLE_PERSON; } else if (c == 0x261D || (c >= 0x270A && c<= 0x270C) || c == 0x270D || c == 0x1F385 || c == 0x1F3C2 || c == 0x1F3C7 || c == 0x1F442 || c == 0x1F443 || (c >= 0x1F446 && c <= 0x1F450) || (c >= 0x1F466 && c <= 0x1F46D) || c == 0x1F47C || c == 0x1F483 || c == 0x1F485 || c == 0x1F48F || c == 0x1F491 || c == 0x1F4AA || c == 0x1F574 || c == 0x1F57A || c == 0x1F590 || c == 0x1F595 || c == 0x1F596 || c == 0x1F64C || c == 0x1F6C0 || c == 0x1F6CC || c == 0x1F90C || c == 0x1F90F || c == 0x1F918 || (c >= 0x1F919 && c <= 0x1F91E) || c == 0x1F91F || (c >= 0x1F930 && c <= 0x1F934) || c == 0x1F936 || c == 0x1F977 || c == 0x1F9B5 || c == 0x1F9B6 || c == 0x1F9BB || (c >= 0x1F9D1 && c<= 0x1F9D5)) { return CHARTYPE_PERSON; } else if (c == 0xE007F) { return CHARTYPE_TAG_CLOSE; } else if (c == 0x1F33E || c == 0x1F373 || c == 0x1F37C || c == 0x1F393 || c == 0x1F3A4 || c == 0x1F3A8 || c == 0x1F3EB || c == 0x1F3ED || c == 0x1F4BB || c == 0x1F4BC || c == 0x1F527 || c == 0x1F52C || c == 0x1F680 || c == 0x1F692 || c == 0x1F9AF || c == 0x1F9BC || c == 0x1F9BD ) { return CHARTYPE_JOB; } else if (c == 0x2695 || c == 0x2696 || c == 0x2708) { return CHARTYPE_JOB_TEXT; } else if (c >= 0x1F1E6 && c <= 0x1F1FF) { return CHARTYPE_REGIONAL_INDICATOR; } else if (c == 0x1F3F3) { return CHARTYPE_WHITE_FLAG; } else if (c == 0x1F3F4) { return CHARTYPE_BLACK_FLAG; } else if (c == 0x1F308) { return CHARTYPE_RAINBOW; } else if (c == 0x26A7) { return CHARTYPE_TRANSGENDER_SYMBOL; } else if (c == 0x2620) { return CHARTYPE_SKULL_AND_CROSSBONES; } else if (c == 0x2764) { return CHARTYPE_HEART; } else if (c == 0x1F48B) { return CHARTYPE_KISS_MARK; } else if (c >= 0x1F9B0 && c <= 0x1F9B3) { return CHARTYPE_HAIRSTYLE; } else if (c == 0x2640 || c == 0x2642) { return CHARTYPE_GENDER_TEXT; } else if (c == 0x1F384) { return CHARTYPE_CHRISTMAS_TREE; } else if (c == 0x1F408) { return CHARTYPE_CAT; } else if (c == 0x1F415) { return CHARTYPE_DOG; } else if (c == 0x1F43B) { return CHARTYPE_BEAR; } else if (c == 0x1F9BA) { return CHARTYPE_SAFETY_VEST; } else if (c == 0x2B1B) { return CHARTYPE_COLOUR_BLACK; } else if (c == 0x2744) { return CHARTYPE_SNOWFLAKE; } else if ((c >= 0xE0030 && c <= 0xE0039) || (c >= 0xE0041 && c <= 0xE005A) || (c >= 0xE0061 && c <= 0xE007A)) { // Capital letters and digits, as per https://www.unicode.org/L2/L2015/15190-pri299-additional-flags-bkgnd.html // But Twitter takes lower-case return CHARTYPE_TAG; } else { #ifdef LIBTL_DEBUG g_debug("Fell through to \"other\" for 0x%08X", c); #endif return CHARTYPE_WEIGHTED_OTHER; } } /* * tokenize: * * Returns: (transfer full): Tokens */ static GArray * tokenize (const char *input, gsize length_in_bytes, gboolean compact_emoji) { GArray *tokens = g_array_new (FALSE, TRUE, sizeof (Token)); const char *p = input; gsize cur_character_index = 0; GHashTable *chartype_map = get_chartype_options(); while (p - input < (long)length_in_bytes) { const char *cur_start = p; gunichar cur_char = g_utf8_get_char (p); gsize cur_length = 0; gsize length_in_chars = 0; gsize length_in_weighted_chars = 0; guint last_token_type = 0; guint prev_char_type = CHARTYPE_NONE; guint cur_char_type = CHARTYPE_NONE; guint carry_weight = 0; gboolean is_zwjed = FALSE; gboolean matched = FALSE; gunichar prev_ri_char = '\0'; CharTypeOption *data; /* If this char already splits, it's a one-char token */ if (char_splits (cur_char)) { const char *old_p = p; p = g_utf8_next_char (p); emplace_token (tokens, cur_start, p - old_p, cur_character_index, 1, is_weighted_character (cur_char) ? WEIGHTED_VALUE : UNWEIGHTED_VALUE); cur_character_index ++; continue; } last_token_type = token_type_from_char (cur_char); do { if (compact_emoji) { matched = FALSE; cur_char_type = chartype_for_char (cur_char); if (cur_char_type == CHARTYPE_ZWJ) { if (!is_zwjed) { matched = TRUE; is_zwjed = TRUE; carry_weight += UNWEIGHTED_VALUE; } cur_char_type = prev_char_type; } else if (cur_char_type == CHARTYPE_REGIONAL_INDICATOR) { if (prev_char_type == CHARTYPE_REGIONAL_INDICATOR && is_valid_regional_indicator (prev_ri_char, cur_char)) { matched = TRUE; cur_char_type = CHARTYPE_REGIONAL_INDICATOR_FLAG; } prev_ri_char = cur_char; } else { if (is_zwjed || cur_char_type == CHARTYPE_FITZPATRICK || cur_char_type == CHARTYPE_VS16 || cur_char_type == CHARTYPE_TAG || prev_char_type == CHARTYPE_TAGGED_FLAG) { data = g_hash_table_lookup(chartype_map, MAKE_KEY(prev_char_type, cur_char_type)); if (data != NULL) { matched = TRUE; int char_carry_weight = data->carry_weight; cur_char_type = data->new_chartype; if (char_carry_weight == END_SEQUENCE_WEIGHT) { // It was a completing character carry_weight = 0; } else { carry_weight += char_carry_weight; } } // Else it didn't have a mapping } is_zwjed = FALSE; } if (!matched) { // If we didn't match a rule then any partially built sequence (carry_weight) length_in_weighted_chars += carry_weight + (is_weighted_character (cur_char) ? WEIGHTED_VALUE : UNWEIGHTED_VALUE); carry_weight = 0; } prev_char_type = cur_char_type; } else { length_in_weighted_chars += is_weighted_character (cur_char) ? WEIGHTED_VALUE : UNWEIGHTED_VALUE; } const char *old_p = p; p = g_utf8_next_char (p); cur_char = g_utf8_get_char (p); cur_length += p - old_p; length_in_chars ++; if (token_type_from_char (cur_char) != last_token_type) { length_in_weighted_chars += carry_weight; carry_weight = 0; break; } } while (!char_splits (cur_char) && p - input < (long)length_in_bytes); length_in_weighted_chars += carry_weight; emplace_token (tokens, cur_start, cur_length, cur_character_index, length_in_chars, length_in_weighted_chars); cur_character_index += length_in_chars; } return g_steal_pointer (&tokens); } static gboolean parse_link_tail (GArray *entities, const Token *tokens, gsize n_tokens, guint *current_position) { guint i = *current_position; const Token *t; gsize paren_level = 0; int first_paren_index = -1; for (;;) { t = &tokens[i]; if (t->type == TOK_WHITESPACE || t->type == TOK_APOSTROPHE) { i --; break; } if (tokens[i].type == TOK_OPEN_PAREN) { if (first_paren_index == -1) { first_paren_index = i; } paren_level ++; if (paren_level == 3) { break; } } else if (tokens[i].type == TOK_CLOSE_PAREN) { if (first_paren_index == -1) { first_paren_index = i; } paren_level --; } i ++; if (i == n_tokens) { i --; break; } } if (paren_level != 0) { g_assert (first_paren_index != -1); i = first_paren_index - 1; // Before that paren } t = &tokens[i]; /* Whatever happened, don't count trailing punctuation */ if (token_in (t, INVALID_AFTER_URL_CHARS)) { i --; } *current_position = i; return TRUE; } // Returns whether a link has been parsed or not. static gboolean parse_link (GArray *entities, const Token *tokens, gsize n_tokens, guint *current_position) { guint i = *current_position; const Token *t; guint start_token = *current_position; guint end_token; gboolean has_protocol = FALSE; t = &tokens[i]; // Some may not even appear before a protocol if (i > 0 && token_in (&tokens[i - 1], INVALID_BEFORE_URL_CHARS)) { return FALSE; } if (token_is_protocol (t)) { // need "://" now. t = &tokens[i + 1]; if (t->type != TOK_COLON) { return FALSE; } i ++; t = &tokens[i + 1]; if (t->type != TOK_SLASH) { return FALSE; } i ++; t = &tokens[i + 1]; if (t->type != TOK_SLASH) { return FALSE; } // If we are at the end now, this is not a link, just the protocol. if (i + 1 == n_tokens - 1) { return FALSE; } i += 2; // Skip to token after second slash has_protocol = TRUE; } else { // Lookbehind: Token before may not be an @, they are not supported. if (i > 0 && token_in (&tokens[i - 1], INVALID_BEFORE_NON_PROTOCOL_URL_CHARS)) { return FALSE; } } if (token_in (&tokens[i], INVALID_URL_CHARS)) { return FALSE; } // Now read until .tld. There can be multiple (e.g. in http://foobar.com.com.com"), // so we need to do this in a greedy way. guint tld_index = i; guint tld_iter = i; gboolean tld_found = FALSE; guint fragment_length = 0; while (tld_iter < n_tokens - 1) { const Token *t = &tokens[tld_iter]; if (t->type == TOK_WHITESPACE) { if (!tld_found) { return FALSE; } } if (!(t->type == TOK_NUMBER || t->type == TOK_TEXT || t->type == TOK_DOT || t->type == TOK_DASH)) { if (!tld_found) { return FALSE; } else { break; } } if (t->type != TOK_DOT) { // Approximate some rules for handling Punycode. This may not be perfect, but it should be good enough and rarely hit. // And it passes Twitter's test case! if (t->length_in_characters != t->length_in_weighted_characters) { gsize unicode_chars = t->length_in_weighted_characters - t->length_in_characters; gsize ascii_ish_chars = t->length_in_characters - unicode_chars; fragment_length += ascii_ish_chars + ((((unicode_chars * 100) / 5) * 6) / 100) + 1; } else { fragment_length += t->length_in_characters; } if (fragment_length > 63) { return FALSE; } } else { fragment_length = 0; } if (t->type == TOK_DOT && token_is_tld (&tokens[tld_iter + 1], has_protocol)) { tld_index = tld_iter; tld_found = TRUE; } tld_iter ++; } if (tld_index >= n_tokens - 1 || !tld_found || token_in (&tokens[tld_index - 1], INVALID_URL_CHARS)) { return FALSE; } // tld_index is the TOK_DOT g_assert (tokens[tld_index].type == TOK_DOT); i = tld_index + 1; // If the next token is a colon, we are reading a port if (i < n_tokens - 1 && tokens[i + 1].type == TOK_COLON) { i ++; // i == COLON if (tokens[i + 1].type != TOK_NUMBER) { // According to twitter.com, the link reaches until before the COLON i --; } else { // Skip port number i ++; } } // To continue a link, the next token must be a slash or a question mark // If it isn't, we stop here. if (i < n_tokens - 1) { // A trailing slash is part of the link, other punctuation is not. if (tokens[i + 1].type == TOK_SLASH || tokens[i + 1].type == TOK_QUESTIONMARK) { i ++; if (i < n_tokens - 1) { if (!parse_link_tail (entities, tokens, n_tokens, &i)) { return FALSE; } } else if (tokens[i].type == TOK_QUESTIONMARK) { // Trailing questionmark is not part of the link i --; } } else if (tokens[i + 1].type == TOK_AT) { // We cannot just return FALSE for all non-slash/non-questionmark tokens here since // The Rules say some of them make a link until this token and some of them cause the // entire parsing to produce no link at all, like in the @ case (don't want to turn // email addresses into links). return FALSE; } } end_token = i; g_assert (end_token < n_tokens); emplace_entity_for_tokens (entities, tokens, TL_ENT_LINK, start_token, end_token); *current_position = end_token + 1; // Hop to the next token! return TRUE; } static gboolean parse_mention (GArray *entities, const Token *tokens, gsize n_tokens, guint *current_position) { guint i = *current_position; const guint start_token = i; guint end_token; g_assert (tokens[i].type == TOK_AT); // Lookback at the previous token. If it was a text token // without whitespace between, this is not going to be a mention... if (i > 0) { // Text tokens before an @-token generally destroy the mention, // except in a few cases... if (tokens[i - 1].type == TOK_TEXT && !token_in (&tokens[i - 1], VALID_BEFORE_MENTION_CHARS) && !token_ends_in_accented (&tokens[i - 1])) { return FALSE; } // Numbers and special invalid chars always ruin the mention if (tokens[i - 1].type == TOK_NUMBER || token_in (&tokens[i - 1], INVALID_BEFORE_MENTION_CHARS)) { return FALSE; } } // Skip @ i ++; for (;;) { if (i >= n_tokens) { i --; break; } if (token_in (&tokens[i], INVALID_MENTION_CHARS)) { i --; break; } if (tokens[i].type != TOK_TEXT && tokens[i].type != TOK_NUMBER && tokens[i].type != TOK_UNDERSCORE) { i --; break; } if (tokens[i].type == TOK_TEXT) { const char *text = tokens[i].start; // Special rules apply about what characters may appear in a @screen_name const char *p = text; while (p - text < (long)tokens[i].length_in_bytes) { gunichar c = g_utf8_get_char (p); if (!is_valid_mention_char (c)) { return FALSE; } p = g_utf8_next_char (p); } } i ++; } if (i == start_token) { return FALSE; } // Mentions ending in an '@' are no mentions, e.g. @_@ if (i < n_tokens - 1 && tokens[i + 1].type == TOK_AT) { return FALSE; } end_token = i; g_assert (end_token < n_tokens); emplace_entity_for_tokens (entities, tokens, TL_ENT_MENTION, start_token, end_token); *current_position = end_token + 1; // Hop to the next token! return TRUE; } static gboolean parse_hashtag (GArray *entities, const Token *tokens, gsize n_tokens, guint *current_position) { gsize i = *current_position; const guint start_token = i; guint end_token; gboolean text_found = FALSE; g_assert (tokens[i].type == TOK_HASH); // Lookback at the previous token. If it was a text token // without whitespace between, this is not going to be a mention... if (i > 0 && tokens[i - 1].type == TOK_TEXT && !token_in (&tokens[i - 1], VALID_BEFORE_HASHTAG_CHARS)) { return FALSE; } // Some chars make the entire hashtag invalid if (i > 0 && token_in (&tokens[i - 1], INVALID_BEFORE_HASHTAG_CHARS)) { return FALSE; } //skip # i ++; for (; i < n_tokens; i ++) { if (token_in (&tokens[i], INVALID_HASHTAG_CHARS)) { break; } if (tokens[i].type != TOK_TEXT && tokens[i].type != TOK_NUMBER && tokens[i].type != TOK_UNDERSCORE) { break; } text_found |= tokens[i].type == TOK_TEXT; } if (!text_found) { return FALSE; } end_token = i - 1; g_assert (end_token < n_tokens); emplace_entity_for_tokens (entities, tokens, TL_ENT_HASHTAG, start_token, end_token); *current_position = end_token + 1; // Hop to the next token! return TRUE; } /* * parse: * * Returns: (transfer full): list of tokens */ static GArray * parse (const Token *tokens, gsize n_tokens, gboolean extract_text_entities, guint *n_relevant_entities) { GArray *entities = g_array_new (FALSE, TRUE, sizeof (TlEntity)); guint i = 0; guint relevant_entities = 0; while (i < n_tokens) { const Token *token = &tokens[i]; // We always have to do this since links can begin with whatever word if (parse_link (entities, tokens, n_tokens, &i)) { relevant_entities ++; continue; } switch (token->type) { case TOK_AT: if (parse_mention (entities, tokens, n_tokens, &i)) { relevant_entities ++; continue; } break; case TOK_HASH: if (parse_hashtag (entities, tokens, n_tokens, &i)) { relevant_entities ++; continue; } break; } if (extract_text_entities && token->type == TOK_TEXT) { relevant_entities ++; } emplace_entity_for_tokens (entities, tokens, token->type == TOK_TEXT ? TL_ENT_TEXT : TL_ENT_WHITESPACE, i, i); i ++; } if (n_relevant_entities) { *n_relevant_entities = relevant_entities; } return entities; } static gsize count_entities_in_characters (GArray *entities) { guint i; gsize sum = 0; for (i = 0; i < entities->len; i ++) { const TlEntity *e = &g_array_index (entities, TlEntity, i); sum += entity_length_in_characters (e); } return sum; } /* * tl_count_chars: * input: (nullable): NUL-terminated tweet text * * Returns: The length of @input, in characters. */ gsize tl_count_characters (const char *input) { if (input == NULL || input[0] == '\0') { return 0; } return tl_count_characters_n (input, strlen (input)); } /* * tl_count_characters_n: * input: (nullable): Text to measure * length_in_bytes: Length of @input, in bytes. * * Returns: The length of @input, in characters. */ gsize tl_count_characters_n (const char *input, gsize length_in_bytes) { GArray *tokens; const Token *token_array; gsize n_tokens; GArray *entities; gsize length; if (input == NULL || input[0] == '\0') { return 0; } // From here on, input/length_in_bytes are trusted to be OK tokens = tokenize (input, length_in_bytes, FALSE); n_tokens = tokens->len; token_array = (const Token *)g_array_free (tokens, FALSE); entities = parse (token_array, n_tokens, FALSE, NULL); length = count_entities_in_characters (entities); g_array_free (entities, TRUE); g_free ((char *)token_array); return length; } static inline gsize entity_length_in_weighted_characters (const TlEntity *e) { switch (e->type) { case TL_ENT_LINK: return LINK_LENGTH; default: return e->length_in_weighted_characters; } } static gsize count_entities_in_weighted_characters (GArray *entities) { guint i; gsize sum = 0; for (i = 0; i < entities->len; i ++) { const TlEntity *e = &g_array_index (entities, TlEntity, i); sum += entity_length_in_weighted_characters (e); } return sum; } /* * tl_count_weighted_chararacters: * input: (nullable): NUL-terminated tweet text * count_mode: COUNT_BASIC to do a dumb weighting count, * COUNT_SHORT_URLS to do dumb weighting count but with URLs only counting as short url * or COUNT_COMPACT for full short URL and compact emoji behaviour * * Returns: The length of @input, in Twitter's weighted characters. */ gsize tl_count_weighted_characters (const char *input, guint count_mode) { if (input == NULL || input[0] == '\0') { return 0; } char *normalised = g_utf8_normalize (input, -1, G_NORMALIZE_DEFAULT_COMPOSE); gsize size = 0; if (count_mode == COUNT_SHORT_URLS) { size = tl_count_weighted_characters_n (normalised, strlen (normalised), FALSE); } else if (count_mode == COUNT_COMPACT) { size = tl_count_weighted_characters_n (normalised, strlen (normalised), TRUE); } else { const char *p = normalised; gunichar c; c = g_utf8_get_char (p); while (c != '\0') { size += is_weighted_character (c) ? WEIGHTED_VALUE : UNWEIGHTED_VALUE; p = g_utf8_next_char (p); c = g_utf8_get_char (p); } } g_free(normalised); return size; } /* * tl_count_weighted_characters_n: * input: (nullable): Text to measure * length_in_bytes: Length of @input, in bytes. * compact_emoji: whether to count joined emoji as a compacted single character * * Returns: The length of @input, in characters. */ gsize tl_count_weighted_characters_n (const char *input, gsize length_in_bytes, gboolean compact_emoji) { GArray *tokens; const Token *token_array; gsize n_tokens; GArray *entities; gsize length; if (input == NULL || input[0] == '\0') { return 0; } // From here on, input/length_in_bytes are trusted to be OK tokens = tokenize (input, length_in_bytes, compact_emoji); n_tokens = tokens->len; token_array = (const Token *)g_array_free (tokens, FALSE); entities = parse (token_array, n_tokens, FALSE, NULL); length = count_entities_in_weighted_characters (entities); g_array_free (entities, TRUE); g_free ((char *)token_array); return length; } /** * tl_extract_entities: * @input: The input text to extract entities from * @out_n_entities: (out): Location to store the amount of entities in the returned * array. If 0, the return value is %NULL. * @out_text_length: (out) (optional): Return location for the complete * length of @input, in characters. This is the same value one would * get from calling tl_count_characters() or tl_count_characters_n() * on @input. * * Returns: An array of #TlEntity. If no entities are found, %NULL is returned. */ TlEntity * tl_extract_entities (const char *input, gsize *out_n_entities, gsize *out_text_length) { gsize dummy; g_return_val_if_fail (out_n_entities != NULL, NULL); if (out_text_length == NULL) { out_text_length = &dummy; } if (input == NULL || input[0] == '\0') { *out_n_entities = 0; *out_text_length = 0; return NULL; } return tl_extract_entities_n (input, strlen (input), out_n_entities, out_text_length); } static TlEntity * tl_extract_entities_internal (const char *input, gsize length_in_bytes, gsize *out_n_entities, gsize *out_text_length, gboolean extract_text_entities) { GArray *tokens; const Token *token_array; gsize n_tokens; GArray *entities; guint n_relevant_entities; TlEntity *result_entities; guint result_index = 0; tokens = tokenize (input, length_in_bytes, FALSE); #ifdef LIBTL_DEBUG g_debug ("############ %s: %.*s", __FUNCTION__, (guint)length_in_bytes, input); for (guint i = 0; i < tokens->len; i ++) { const Token *t = &g_array_index (tokens, Token, i); g_debug ("Token %u: Type: %d, Length: %u, Text:%.*s, start char: %u, chars: %u", i, t->type, (guint)t->length_in_bytes, (int)t->length_in_bytes, t->start, (guint)t->start_character_index, (guint)t->length_in_characters); } #endif n_tokens = tokens->len; token_array = (const Token *)g_array_free (tokens, FALSE); entities = parse (token_array, n_tokens, extract_text_entities, &n_relevant_entities); *out_text_length = count_entities_in_characters (entities); g_free ((char *)token_array); #ifdef LIBTL_DEBUG for (guint i = 0; i < entities->len; i ++) { const TlEntity *e = &g_array_index (entities, TlEntity, i); g_debug ("TlEntity %u: Text: '%.*s', Type: %u, Bytes: %u, Length: %u, start character: %u", i, (int)e->length_in_bytes, e->start, e->type, (guint)e->length_in_bytes, (guint)entity_length_in_characters (e), (guint)e->start_character_index); } #endif // Only pass mentions, hashtags and links out result_entities = g_malloc (sizeof (TlEntity) * n_relevant_entities); for (guint i = 0; i < entities->len; i ++) { const TlEntity *e = &g_array_index (entities, TlEntity, i); switch (e->type) { case TL_ENT_LINK: case TL_ENT_HASHTAG: case TL_ENT_MENTION: memcpy (&result_entities[result_index], e, sizeof (TlEntity)); result_index ++; break; case TL_ENT_TEXT: if (extract_text_entities) { memcpy (&result_entities[result_index], e, sizeof (TlEntity)); result_index ++; } break; default: {} } } *out_n_entities = n_relevant_entities; g_array_free (entities, TRUE); return result_entities; } /** * tl_extract_entities_n: * @input: The input text to extract entities from * @length_in_bytes: The length of @input, in bytes * @out_n_entities: (out): Location to store the amount of entities in the returned * array. If 0, the return value is %NULL. * @out_text_length: (out) (optional): Return location for the complete * length of @input, in characters. This is the same value one would * get from calling tl_count_characters() or tl_count_characters_n() * on @input. * * Returns: An array of #TlEntity. If no entities are found, %NULL is returned. */ TlEntity * tl_extract_entities_n (const char *input, gsize length_in_bytes, gsize *out_n_entities, gsize *out_text_length) { gsize dummy; g_return_val_if_fail (out_n_entities != NULL, NULL); if (out_text_length == NULL) { out_text_length = &dummy; } if (input == NULL || input[0] == '\0') { *out_n_entities = 0; *out_text_length = 0; return NULL; } return tl_extract_entities_internal (input, length_in_bytes, out_n_entities, out_text_length, FALSE); } /** * tl_extract_entities_and_text: * @input: The input text to extract entities from * @out_n_entities: (out): Location to store the amount of entities in the returned * array. If 0, the return value is %NULL. * @out_text_length: (out) (optional): Return location for the complete * length of @input, in characters. This is the same value one would * get from calling tl_count_characters() or tl_count_characters_n() * on @input. * * This is different from tl_extract_entities() in that it returns all entities * and not just hashtags, links and mentions. This allows for further post-processing * from the caller. * * Returns: An array of #TlEntity. If no entities are found, %NULL is returned. */ TlEntity * tl_extract_entities_and_text (const char *input, gsize *out_n_entities, gsize *out_text_length) { gsize dummy; g_return_val_if_fail (out_n_entities != NULL, NULL); if (out_text_length == NULL) { out_text_length = &dummy; } if (input == NULL || input[0] == '\0') { *out_n_entities = 0; *out_text_length = 0; return NULL; } return tl_extract_entities_internal (input, strlen (input), out_n_entities, out_text_length, TRUE); } /** * tl_extract_entities_and_text_n: * @input: The input text to extract entities from * @length_in_bytes: The length of @input, in bytes * @out_n_entities: (out): Location to store the amount of entities in the returned * array. If 0, the return value is %NULL. * @out_text_length: (out) (optional): Return location for the complete * length of @input, in characters. This is the same value one would * get from calling tl_count_characters() or tl_count_characters_n() * on @input. * * This is different from tl_extract_entities_n() in that it returns all entities * and not just hashtags, links and mentions. This allows for further post-processing * from the caller. * * Returns: An array of #TlEntity. If no entities are found, %NULL is returned. */ TlEntity * tl_extract_entities_and_text_n (const char *input, gsize length_in_bytes, gsize *out_n_entities, gsize *out_text_length) { gsize dummy; g_return_val_if_fail (out_n_entities != NULL, NULL); if (out_text_length == NULL) { out_text_length = &dummy; } if (input == NULL || input[0] == '\0') { *out_n_entities = 0; *out_text_length = 0; return NULL; } return tl_extract_entities_internal (input, length_in_bytes, out_n_entities, out_text_length, TRUE); } cawbird-1.4.2/src/libtl/libtweetlength.h000066400000000000000000000053501416632607600202360ustar00rootroot00000000000000/* This file is part of libtweetlength * Copyright (C) 2017 Timm Bäder * * libtweetlength 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. * * libtweetlength 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 libtweetlength. If not, see . */ #ifndef __LIBTWEETLENGTH_H__ #define __LIBTWEETLENGTH_H__ #include struct _TlEntity { guint type; const char *start; gsize length_in_bytes; gsize start_character_index; gsize length_in_characters; gsize length_in_weighted_characters; }; typedef struct _TlEntity TlEntity; typedef enum { TL_ENT_TEXT = 1, TL_ENT_HASHTAG = 2, TL_ENT_LINK = 3, TL_ENT_MENTION = 4, TL_ENT_WHITESPACE = 5, } TlEntityType; typedef enum { COUNT_BASIC, COUNT_SHORT_URLS, COUNT_COMPACT } TlCountType; gsize tl_count_characters (const char *input); gsize tl_count_characters_n (const char *input, gsize length_in_bytes); gsize tl_count_weighted_characters (const char *input, guint count_mode); gsize tl_count_weighted_characters_n (const char *input, gsize length_in_bytes, gboolean compact_emoji); TlEntity * tl_extract_entities (const char *input, gsize *out_n_entities, gsize *out_text_length); TlEntity * tl_extract_entities_n (const char *input, gsize length_in_bytes, gsize *out_n_entities, gsize *out_text_length); TlEntity * tl_extract_entities_and_text (const char *input, gsize *out_n_entities, gsize *out_text_length); TlEntity * tl_extract_entities_and_text_n (const char *input, gsize length_in_bytes, gsize *out_n_entities, gsize *out_text_length); #endif cawbird-1.4.2/src/list/000077500000000000000000000000001416632607600147065ustar00rootroot00000000000000cawbird-1.4.2/src/list/AddListEntry.vala000066400000000000000000000024331416632607600201230ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class AddListEntry : Gtk.ListBoxRow { public AddListEntry (string label) { var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 5); var img = new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.DIALOG); img.pixel_size = 32; img.margin_start = 10; img.hexpand = true; img.halign = Gtk.Align.END; box.add (img); var l = new Gtk.Label (label); l.hexpand = true; l.halign = Gtk.Align.START; box.add (l); box.margin_bottom = 4; box.margin_top = 4; add (box); } } cawbird-1.4.2/src/list/DMListEntry.vala000066400000000000000000000224501416632607600177340ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class DMListEntry : Gtk.ListBoxRow, Cb.TwitterItem { private Gtk.Grid grid; private AvatarWidget avatar_image; private Gtk.CheckButton delete_checkbutton; private Gtk.Label text_label; private Gtk.Label screen_name_label; private TextButton name_button; private Gtk.Label time_delta_label; private MediaButton media_button; private string _text; private Json.Object? _message_data; private Cb.Media? _media; private Cb.TextEntity[] _entities; public bool is_checked { get { return delete_checkbutton.active; } set { set_checked(value); } } public signal void avatar_clicked(); public string text { set { _text = value; if (_message_data == null) { text_label.label = value; text_label.visible = value != null && value != ""; } } } public string screen_name { set { screen_name_label.label = "@" + value; screen_name_label.tooltip_text = "@" + value; } } public new string name { set { name_button.set_text (value); name_button.tooltip_text = value; } } public Cb.Media media { get { return _media; } set { _media = value; if (_media != null) { if (media_button == null) { media_button = new MediaButton(media); grid.attach (media_button, 1, 2, 3, 1); media_button.clicked.connect(media_clicked_cb); media_button.show(); } else { media_button.media = _media; } } } } // A property would be nice, but Cb.TextEntity isn't a GLib.Object so we can't public void set_entities(Cb.TextEntity[] value) { _entities = value; set_dm_text(); } public Json.Object? message_data { get { return _message_data; } set { _message_data = value; set_dm_text(); } } public Cairo.Surface avatar { set { avatar_image.surface = value; } } public bool seen { get { return true; } set {} } private GLib.TimeSpan last_timediff; public int64 timestamp; public int64 id; public int64 user_id; public unowned MainWindow main_window; public DMListEntry () { this.set_activatable (false); this.get_style_context ().add_class ("tweet"); grid = new Gtk.Grid (); grid.margin = 6; grid.show (); this.add (grid); this.avatar_image = new AvatarWidget (); avatar_image.size = 48; avatar_image.set_valign (Gtk.Align.START); avatar_image.show(); delete_checkbutton = new Gtk.CheckButton(); delete_checkbutton.halign = Gtk.Align.CENTER; delete_checkbutton.valign = Gtk.Align.CENTER; delete_checkbutton.button_release_event.connect(avatar_button_release_cb); delete_checkbutton.enter_notify_event.connect(Utils.set_pointer_on_mouseover); delete_checkbutton.leave_notify_event.connect(Utils.set_pointer_on_mouseover); var avatar_overlay = new Gtk.Overlay(); avatar_overlay.add(avatar_image); avatar_overlay.add_overlay(delete_checkbutton); avatar_overlay.show(); var event_box = new Gtk.EventBox(); event_box.margin = 4; event_box.margin_end = 12; event_box.valign = Gtk.Align.START; event_box.add(avatar_overlay); event_box.button_release_event.connect(avatar_button_release_cb); event_box.enter_notify_event.connect(Utils.set_pointer_on_mouseover); event_box.leave_notify_event.connect(Utils.set_pointer_on_mouseover); event_box.show(); grid.attach (event_box, 0, 0, 1, 3); this.name_button = new TextButton (); name_button.set_valign (Gtk.Align.BASELINE); name_button.show (); grid.attach (name_button, 1, 0, 1, 1); this.screen_name_label = new Gtk.Label (null); screen_name_label.set_margin_start (6); screen_name_label.set_margin_end (6); screen_name_label.set_valign (Gtk.Align.BASELINE); screen_name_label.get_style_context ().add_class ("dim-label"); screen_name_label.show (); grid.attach (screen_name_label, 2, 0, 1, 1); this.time_delta_label = new Gtk.Label (null); time_delta_label.set_halign (Gtk.Align.END); time_delta_label.set_valign (Gtk.Align.BASELINE); time_delta_label.set_hexpand (true); time_delta_label.show (); time_delta_label.get_style_context ().add_class ("dim-label"); grid.attach (time_delta_label, 3, 0, 1, 1); this.text_label = new Gtk.Label (null); text_label.set_margin_top (6); text_label.set_margin_end (6); text_label.set_margin_bottom (6); text_label.set_hexpand (true); text_label.set_vexpand (true); text_label.set_xalign (0.0f); text_label.set_line_wrap (true); text_label.set_line_wrap_mode (Pango.WrapMode.WORD_CHAR); text_label.set_use_markup (true); text_label.set_use_markup (true); text_label.set_selectable (true); text_label.show (); text_label.activate_link.connect((uri) => { this.grab_focus (); return TweetUtils.activate_link (uri, main_window); }); grid.attach (text_label, 1, 1, 3, 1); name_button.clicked.connect (() => { var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, user_id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, screen_name_label.label.substring (1)); main_window.main_widget.switch_page (Page.PROFILE, bundle); }); this._entities = new Cb.TextEntity[0]; this.key_release_event.connect(key_released_cb); Settings.get ().changed["text-transform-flags"].connect(set_dm_text); Settings.get ().changed["tweet-scale"].connect (set_dm_text_scale); set_dm_text_scale(); this.show (); } ~DMListEntry() { Settings.get ().changed["text-transform-flags"].disconnect(set_dm_text); } [GtkCallback] private bool key_released_cb (Gdk.EventKey evt) { #if DEBUG switch(evt.keyval) { case Gdk.Key.k: if (_message_data != null) { var gen = new Json.Generator(); var node = new Json.Node(Json.NodeType.OBJECT); node.set_object(_message_data); gen.set_root(node); gen.set_pretty(true); string json = gen.to_data(null); stderr.printf(json + "\n"); } else { stderr.printf("Old format DM - no JSON\n"); } return Gdk.EVENT_STOP; } #endif return Gdk.EVENT_PROPAGATE; } private bool avatar_button_release_cb(Gtk.Widget widget, Gdk.EventButton event) { if (event.button == Gdk.BUTTON_PRIMARY) { set_checked(!is_checked); avatar_clicked(); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } private void set_dm_text() { if (_message_data != null) { var msg = _message_data.get_string_member ("text"); // Force removing media links, because they don't actually work for DMs var flags = Settings.get_text_transform_flags () | Cb.TransformFlags.REMOVE_MEDIA_LINKS; msg = Cb.TextTransform.text (msg, _entities, flags, 0, 0); text_label.label = msg; text_label.visible = msg != ""; } } private void set_dm_text_scale () { var scale = Settings.get_tweet_scale(); var new_attribs = new Pango.AttrList(); var scale_attr = Pango.attr_scale_new(scale); new_attribs.insert((owned)scale_attr); text_label.set_attributes(new_attribs); } public void load_avatar (string avatar_url) { string url = avatar_url; if (this.get_scale_factor () >= 2) url = url.replace ("_normal", "_bigger"); Twitter.get ().get_avatar.begin (user_id, url, avatar_image, 48 * this.get_scale_factor ()); } public int update_time_delta (GLib.DateTime? now = null) { GLib.DateTime cur_time; if (now == null) cur_time = new GLib.DateTime.now_local (); else cur_time = now; GLib.DateTime then = new GLib.DateTime.from_unix_local (timestamp); time_delta_label.label = Utils.get_time_delta (then, cur_time); return (int)(cur_time.difference (then) / 1000.0 / 1000.0); } public int64 get_sort_factor () { return timestamp; } public int64 get_timestamp () { return timestamp; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } private void media_clicked_cb(MediaButton button, double px, double py) { TweetUtils.handle_media_click ({media}, this.main_window, 0); } private void set_checked(bool checked) { if (checked) { avatar_image.opacity = 0.5; delete_checkbutton.active = true; delete_checkbutton.show(); } else { avatar_image.opacity = 1; delete_checkbutton.active = false; delete_checkbutton.hide(); } } } cawbird-1.4.2/src/list/DMThreadEntry.vala000066400000000000000000000045331416632607600202320ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/dm-thread-entry.ui")] class DMThreadEntry : Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Label name_label; [GtkChild] private unowned Gtk.Label screen_name_label; [GtkChild] private unowned Gtk.Label last_message_label; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Label unread_count_label; public int64 user_id; public new string name { get { return name_label.label; } set { name_label.label = value; name_label.tooltip_text = value; } } public string screen_name { get { return screen_name_label.label; } set { screen_name_label.label = "@" + value; screen_name_label.tooltip_text = "@" + value; } } public string last_message { set { last_message_label.label = value; } } public Cairo.Surface? avatar { set { avatar_image.surface = value;} } private int _unread_count = 0; public int unread_count { get { return this._unread_count; } set { this._unread_count = value; this.update_unread_count (); } } public DMThreadEntry (int64 user_id) { this.user_id = user_id; update_unread_count (); } private void update_unread_count () { if (unread_count == 0) unread_count_label.hide (); else { unread_count_label.show (); unread_count_label.label = ngettext ("(%d unread)", "(%d unread)", unread_count).printf(unread_count); } } } cawbird-1.4.2/src/list/FavImageRow.vala000066400000000000000000000116361416632607600177310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class FavImageRow : Gtk.FlowBoxChild { private const int THUMB_WIDTH = 80; private const int THUMB_HEIGHT = 50; private static Cairo.ImageSurface play_icon; private Gtk.EventBox event_box; private Gtk.Image image; private string file_path; private Gtk.GestureMultiPress gesture; public bool is_gif = false; static construct { try { play_icon = (Cairo.ImageSurface)Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/play.png"), 1, null); } catch (GLib.Error e) { critical (e.message); } } public FavImageRow (string path) { this.file_path = path; event_box = new Gtk.EventBox (); event_box.show (); image = new Gtk.Image (); image.set_size_request (THUMB_WIDTH, THUMB_HEIGHT); image.set_halign (Gtk.Align.CENTER); image.set_valign (Gtk.Align.CENTER); image.margin = 3; image.show (); event_box.add (image); this.add (event_box); this.set_valign (Gtk.Align.START); /* Sigh */ event_box.enter_notify_event.connect (() => { var flags = this.get_state_flags (); this.set_state_flags (flags | Gtk.StateFlags.PRELIGHT, true); return false; }); event_box.leave_notify_event.connect (() => { this.unset_state_flags (Gtk.StateFlags.PRELIGHT); return false; }); gesture = new Gtk.GestureMultiPress (event_box); gesture.set_propagation_phase (Gtk.PropagationPhase.CAPTURE); gesture.set_button (0); gesture.pressed.connect (() => { Gdk.EventSequence sequence = this.gesture.get_current_sequence (); Gdk.EventButton event = (Gdk.EventButton)this.gesture.get_last_event (sequence); if (event.triggers_context_menu ()) { var menu = new Gtk.Menu (); var delete_item = new Gtk.MenuItem.with_label (_("Delete")); delete_item.activate.connect (() => { var flowbox = this.get_parent (); if (!(flowbox is Gtk.FlowBox)) { warning ("Parent is not a flowbox"); return; } try { var file = GLib.File.new_for_path (this.file_path); file.trash (); flowbox.remove (this); } catch (GLib.Error e) { warning (e.message); } }); menu.add (delete_item); menu.attach_to_widget (this, null); menu.show_all (); menu.popup_at_widget (this, Gdk.Gravity.CENTER, Gdk.Gravity.CENTER, event); } else { this.set_state_flags (this.get_state_flags () | Gtk.StateFlags.ACTIVE, true); } gesture.set_state (Gtk.EventSequenceState.CLAIMED); }); gesture.released.connect (() => { Gdk.EventSequence sequence = this.gesture.get_current_sequence (); Gdk.EventButton? event = (Gdk.EventButton?)this.gesture.get_last_event (sequence); this.unset_state_flags (Gtk.StateFlags.ACTIVE); if (event != null && event.button == Gdk.BUTTON_PRIMARY) { /* This gesture blocks the flowbox gesture so implement activating manually. */ if (this.get_parent () is Gtk.FlowBox) { ((Gtk.FlowBox)this.get_parent ()).child_activated (this); } } }); this.get_style_context ().add_class ("fav-image-item"); load_image.begin (); } public override bool draw (Cairo.Context ct) { base.draw (ct); if (this.is_gif) { double scale = 0.6; int width = this.get_allocated_width (); int height = this.get_allocated_height (); double x = (width / 2.0) / scale - (play_icon.get_width () / 2.0); double y = (height / 2.0) / scale - (play_icon.get_height () / 2.0); ct.scale (scale, scale); ct.set_source_surface (play_icon, x, y); ct.paint (); } return false; } public unowned string get_image_path () { return file_path; } private async void load_image () { try { var in_stream = GLib.File.new_for_path (file_path).read (); var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async (in_stream, THUMB_WIDTH, THUMB_HEIGHT, true); in_stream.close (); this.image.set_from_pixbuf (pixbuf); } catch (GLib.Error e) { warning (e.message); } } } cawbird-1.4.2/src/list/FilterListEntry.vala000066400000000000000000000052011416632607600206540ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/filter-list-entry.ui")] class FilterListEntry : Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Label content_label; [GtkChild] private unowned Gtk.Revealer revealer; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Grid normal_box; [GtkChild] private unowned Gtk.Box delete_box; private unowned Cb.Filter _filter; public unowned Cb.Filter filter { set { content_label.label = value.get_contents (); _filter = value; } get { return _filter; } } public string content { set { content_label.label = value; } get { return content_label.label; } } private unowned Account account; private unowned MainWindow main_window; public FilterListEntry (Cb.Filter f, Account account, MainWindow main_window) { this.filter = f; this.account = account; this.main_window = main_window; } construct { revealer.notify["child-revealed"].connect (() => { if (!revealer.child_revealed) { ((Gtk.Container)this.get_parent ()).remove (this); } }); } [GtkCallback] private void menu_button_clicked_cb () { stack.visible_child = delete_box; this.activatable = false; } [GtkCallback] private void cancel_button_clicked_cb () { stack.visible_child = normal_box; this.activatable = true; } [GtkCallback] private void delete_button_clicked_cb () { for (int i = 0; i < account.filters.length; i ++) { var f = account.filters.get (i); if (f.get_id () == this.filter.get_id ()) { account.filters.remove (f); account.db.exec ("DELETE FROM `filters` WHERE `id`='%d'".printf (f.get_id ())); revealer.reveal_child = false; main_window.rerun_filters (); return; } } } } cawbird-1.4.2/src/list/ListListEntry.vala000066400000000000000000000135321416632607600203500ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/list-list-entry.ui")] public class ListListEntry : Gtk.ListBoxRow { public static int sort_func (Gtk.ListBoxRow r1, Gtk.ListBoxRow r2) { if (!(r1 is ListListEntry)) return -1; return ((ListListEntry)r1).name.ascii_casecmp (((ListListEntry)r2).name); } [GtkChild] private unowned Gtk.Label title_label; public string title { set { title_label.label = value; } get { return title_label.label; } } [GtkChild] private unowned Gtk.Label description_label; public string description { set { description_label.label = value; } get { return description_label.label; } } [GtkChild] private unowned Gtk.Label name_label; public new string name { set { name_label.label = normalize_name (value); } get { return name_label.label; } } [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Button subscribe_button; [GtkChild] private unowned Gtk.Button unsubscribe_button; [GtkChild] private unowned Gtk.Button delete_button; public int64 id; public bool user_list = false; public string creator_screen_name; public int n_subscribers; public int n_members = 0; public int64 created_at; public string mode; private unowned Account account; public ListListEntry.from_json_data (Json.Object obj, Account account) { this.account = account; var user = obj.get_object_member ("user"); title = obj.get_string_member ("name"); name = obj.get_string_member ("full_name"); description = obj.get_string_member ("description"); id = obj.get_int_member ("id"); creator_screen_name = user.get_string_member ("screen_name"); n_subscribers = (int)obj.get_int_member ("subscriber_count"); n_members = (int)obj.get_int_member ("member_count"); created_at = Cb.Utils.parse_date (obj.get_string_member ("created_at")).to_unix (); mode = obj.get_string_member ("mode"); bool following = obj.get_boolean_member ("following"); if (following || user.get_int_member ("id") == account.id) { unsubscribe_button.show (); subscribe_button.hide (); } else { unsubscribe_button.hide (); subscribe_button.show (); } if (user.get_int_member ("id") == account.id) { user_list = true; unsubscribe_button.hide (); } else { delete_button.hide (); } } private string normalize_name (string name) { if (name.contains ("/lists/")) { return name.replace ("/lists/", "/"); } return name; } [GtkCallback] private void delete_button_clicked_cb () { this.sensitive = false; ListUtils.delete_list.begin (account, id, (obj, res) => { try { ListUtils.delete_list.end (res); cancel_more_mode(); } catch (GLib.Error e) { Utils.show_error_dialog (e, (Gtk.Window)this.get_toplevel()); this.sensitive = true; return; } }); } [GtkCallback] private void subscribe_button_clicked_cb () { this.sensitive = false; var call = account.proxy.new_call (); call.set_function ("1.1/lists/subscribers/create.json"); call.set_method ("POST"); call.add_param ("list_id", id.to_string ()); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { // Subscribing to a subscribed list doesn't appear to cause errors, // so there's nothing to ignore as "accidental success" Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), (Gtk.Window)this.get_toplevel()); return; } finally { this.sensitive = true; } subscribe_button.hide (); unsubscribe_button.show (); }); } [GtkCallback] private void unsubscribe_button_clicked_cb () { this.sensitive = false; var call = account.proxy.new_call (); call.set_function ("1.1/lists/subscribers/destroy.json"); call.set_method ("POST"); call.add_param ("list_id", id.to_string ()); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var err = TweetUtils.failed_request_to_error (call, e); if (err.domain != TweetUtils.get_error_domain() || err.code != 109) { // 109 is "user isn't subscribed to the list", so assume they were unsubscribed by another source Utils.show_error_dialog (err, (Gtk.Window)this.get_toplevel()); return; } } finally { this.sensitive = true; } subscribe_button.show (); unsubscribe_button.hide (); }); } [GtkCallback] private void more_button_clicked_cb () { stack.visible_child_name = "more"; this.activatable = false; } [GtkCallback] private void cancel_button_clicked_cb () { cancel_more_mode(); } private void cancel_more_mode () { stack.visible_child_name = "default"; this.activatable = true; } [GtkCallback] private bool focus_out_cb () { stack.visible_child_name = "default"; return false; } } cawbird-1.4.2/src/list/NewListEntry.vala000066400000000000000000000072631416632607600201720ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/new-list-entry.ui")] class NewListEntry : Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Box create_box; [GtkChild] private unowned Gtk.Grid grid; [GtkChild] private unowned Gtk.Label list_name_label; [GtkChild] private unowned Gtk.Entry list_name_entry; [GtkChild] private unowned Gtk.Revealer revealer; [GtkChild] private unowned Gtk.Button create_list_button; public signal void create_activated (string list_name); construct { list_name_entry.buffer.notify["text"].connect (name_text_changed_cb); } public void reveal () { revealer.reveal_child = true; this.activatable = false; list_name_entry.grab_focus (); } public void unreveal () { revealer.reveal_child = false; this.activatable = true; list_name_entry.text = ""; } public override void get_preferred_width (out int min, out int nat) { min = nat = 0; int child_min, child_nat; list_name_label.get_preferred_width (out child_min, out child_nat); min += child_min; nat += child_nat; list_name_entry.get_preferred_width (out child_min, out child_nat); min += child_min; nat += child_nat; create_list_button.get_preferred_width (out child_min, out child_nat); // We can wrap when narrow enough, so ignore the create button for min and only include in natural size (because we'll take the space if possible) nat += child_nat; } public override void get_preferred_height_for_width (int width, out int min, out int nat) { int child_min, child_nat; create_box.get_preferred_height_for_width (width, out min, out nat); if (revealer.reveal_child) { list_name_entry.get_preferred_height_for_width (width, out child_min, out child_nat); min += child_min; nat += child_nat; if (width < Cawbird.RESPONSIVE_LIMIT) { create_list_button.get_preferred_height_for_width (width, out child_min, out child_nat); min += child_min; nat += child_nat; } } } public override void size_allocate(Gtk.Allocation allocation) { if (allocation.width < Cawbird.RESPONSIVE_LIMIT) { grid.child_set(create_list_button, "left-attach", 0); grid.child_set(create_list_button, "top-attach", 1); grid.child_set(create_list_button, "width", 2); } else { grid.child_set(create_list_button, "left-attach", 2); grid.child_set(create_list_button, "top-attach", 0); grid.child_set(create_list_button, "width", 1); } base.size_allocate(allocation); } [GtkCallback] private void create_list_button_clicked_cb () { create_activated (list_name_entry.text); } private void name_text_changed_cb () { string name = list_name_entry.text; create_list_button.sensitive = false; if (name.length == 0 || name.char_count () > 25) return; if (name.get_char (0).isdigit()) return; create_list_button.sensitive = true; } } cawbird-1.4.2/src/list/SnippetListEntry.vala000066400000000000000000000041251416632607600210550ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class SnippetListEntry : Gtk.ListBoxRow { private Gtk.Label key_label; private Gtk.Label value_label; private Gtk.Revealer revealer; public string key { get { return key_label.label; } set { key_label.label = value; } } public string value { get { return value_label.label; } set { value_label.label = value; } } public SnippetListEntry (string key, string value) { this.revealer = new Gtk.Revealer (); revealer.reveal_child = true; var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); box.margin = 6; box.homogeneous = true; key_label = new Gtk.Label (key); key_label.halign = Gtk.Align.START; key_label.hexpand = true; key_label.ellipsize = Pango.EllipsizeMode.END; box.add (key_label); value_label = new Gtk.Label (value); value_label.halign = Gtk.Align.START; value_label.hexpand = true; value_label.xalign = 0; value_label.ellipsize = Pango.EllipsizeMode.END; box.add (value_label); revealer.add (box); this.add (revealer); this.get_style_context ().add_class ("tweet"); } public void reveal () { revealer.notify["child-revealed"].connect (() => { if (!revealer.child_revealed) { this.get_parent ().remove (this); } }); revealer.reveal_child = false; } } cawbird-1.4.2/src/list/StartConversationEntry.vala000066400000000000000000000165221416632607600222730ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/start-conversation-entry.ui")] class StartConversationEntry : Gtk.ListBoxRow { private const int MAX_RESULTS = 7; [GtkChild] private unowned Gtk.Revealer revealer; [GtkChild] private unowned ReplyEntry name_entry; [GtkChild] private unowned Gtk.Stack go_stack; [GtkChild] private unowned Gtk.Spinner go_spinner; private UserCompletion user_completion; private Gtk.Window completion_window; private Gtk.ListBox completion_list = new Gtk.ListBox (); private int current_match = -1; public signal void start (int64 user_id, string screen_name, string name, string avatar_url); private unowned Account account; public StartConversationEntry (Account account) { this.account = account; completion_window = new Gtk.Window (Gtk.WindowType.POPUP); completion_window.set_type_hint (Gdk.WindowTypeHint.COMBO); completion_window.set_screen (name_entry.get_screen ()); completion_window.destroy_with_parent = true; completion_window.focus_out_event.connect (() => { completion_window.hide (); return false; }); name_entry.focus_out_event.connect (() => { completion_window.hide (); return false; }); var popup_frame = new Gtk.Frame (null); var scroller = new Gtk.ScrolledWindow (null, null); popup_frame.add (scroller); scroller.add (completion_list); completion_window.add (popup_frame); completion_list.activate_on_single_click = false; completion_list.row_activated.connect (go_button_clicked_cb); user_completion = new UserCompletion (account, MAX_RESULTS); user_completion.connect_to (name_entry.buffer, "text"); user_completion.start_completion.connect (() => { completion_window.set_attached_to (this.name_entry); completion_window.set_transient_for ((Gtk.Window)this.get_toplevel ()); position_popup_window (); completion_window.show_all (); completion_list.foreach ((w) => { completion_list.remove (w); }); }); user_completion.populate_completion.connect ((screen_name, name) => { var l = new CompletionListEntry (screen_name, name); l.show_all (); completion_list.add (l); }); name_entry.key_press_event.connect (name_entry_key_pressed); // activate.connect (() => { // go_stack.visible_child_name = "spinner"; // go_spinner.start (); // }); } private void position_popup_window () { int x, y; Gtk.Allocation alloc; name_entry.get_allocation (out alloc); name_entry.get_window ().get_origin (out x, out y); x += alloc.x; y += alloc.y + alloc.height; completion_window.move (x, y); completion_window.resize (alloc.width, 50); } private bool name_entry_key_pressed (Gdk.EventKey evt) { uint num_results = completion_list.get_children ().length (); if (num_results == 0) return Gdk.EVENT_PROPAGATE; if (evt.keyval == Gdk.Key.Down) { current_match = (current_match + 1) % (int)num_results; var row = completion_list.get_row_at_index (current_match); completion_list.select_row (row); return Gdk.EVENT_STOP; } else if (evt.keyval == Gdk.Key.Up) { current_match --; if (current_match < 0) current_match = (int)num_results - 1; var row = completion_list.get_row_at_index (current_match); completion_list.select_row (row); return Gdk.EVENT_STOP; } else if (evt.keyval == Gdk.Key.Return) { } return Gdk.EVENT_PROPAGATE; } construct { name_entry.cancelled.connect (() => { unreveal (); this.grab_focus (); }); } public void reveal () { revealer.reveal_child = true; name_entry.grab_focus (); this.activatable = false; } public void unreveal () { revealer.reveal_child = false; completion_window.hide (); this.activatable = true; } [GtkCallback] private void go_button_clicked_cb () { // if (name_entry.text.length > 0) // activated (); string screen_name; if (completion_list.get_selected_row () != null) { screen_name = ((CompletionListEntry)completion_list.get_selected_row ()).get_screen_name (); name_entry.text = screen_name; } else screen_name = name_entry.text; if (screen_name.has_prefix ("@")) screen_name = screen_name.substring (1); if (screen_name.length <= 0) return; go_stack.visible_child_name = "spinner"; go_spinner.start(); name_entry.sensitive = false; var call = account.proxy.new_call (); call.set_function ("1.1/users/show.json"); call.set_method ("GET"); call.add_param ("include_entities", "false"); call.add_param ("screen_name", screen_name); call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { critical (e.message); go_stack.visible_child_name = "button"; name_entry.sensitive = true; return; } // do stuff Json.Parser parser = new Json.Parser (); try { parser.load_from_data (call.get_payload ()); } catch (GLib.Error e) { critical (e.message); go_stack.visible_child_name = "button"; name_entry.sensitive = true; return; } var root = parser.get_root ().get_object (); int64 user_id = root.get_int_member ("id"); UserUtils.load_friendship.begin (account, user_id, "", (obj, res) => { uint fr = UserUtils.load_friendship.end (res); if ((fr & FRIENDSHIP_CAN_DM) == 0) { go_stack.visible_child_name = "button"; name_entry.sensitive = true; return; } string name = root.get_string_member ("name"); string avatar_url = root.get_string_member ("profile_image_url_https"); start (user_id, screen_name, name, avatar_url); name_entry.sensitive = true; go_stack.visible_child_name = "button"; }); }); } } class CompletionListEntry : Gtk.ListBoxRow { private Gtk.Label name_label; private Gtk.Label screen_name_label; public CompletionListEntry (string screen_name, string name) { var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); name_label = new Gtk.Label (name); name_label.ellipsize = Pango.EllipsizeMode.END; screen_name_label = new Gtk.Label ("@" + screen_name); name_label.set_valign (Gtk.Align.BASELINE); screen_name_label.set_valign (Gtk.Align.BASELINE); screen_name_label.get_style_context ().add_class ("dim-label"); box.add (name_label); box.add (screen_name_label); add (box); } public string get_screen_name () { return screen_name_label.get_label ().substring (1); } } cawbird-1.4.2/src/list/TweetListEntry.vala000066400000000000000000001046051416632607600205270ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/tweet-list-entry.ui")] public class TweetListEntry : Cb.TwitterItem, Gtk.ListBoxRow { [GtkChild] private unowned Gtk.Label name_label; [GtkChild] private unowned Gtk.Label time_delta_label; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned ChildSizedScroller scroller; [GtkChild] private unowned Gtk.Label text_label; [GtkChild] private unowned Gtk.Label rt_label; [GtkChild] private unowned Gtk.Image rt_image; [GtkChild] private unowned Gtk.Image rt_status_image; [GtkChild] private unowned Gtk.Image fav_status_image; [GtkChild] private unowned DoubleTapButton retweet_button; [GtkChild] private unowned Gtk.ToggleButton favorite_button; [GtkChild] private unowned Gtk.Grid grid; [GtkChild] private unowned Gtk.Box action_box; [GtkChild] private unowned Gtk.Label reply_label; /* Conditionally created widgets... */ private Gtk.Label? quote_label = null; private Gtk.Label? quote_name = null; private Gtk.Label? quote_time_delta = null; private Gtk.Label? quote_reply_label = null; private Gtk.Grid? quote_grid = null; private Gtk.Stack? media_stack = null; private MultiMediaWidget? mm_widget = null; private Gtk.Stack? quoted_media_stack = null; private MultiMediaWidget? quoted_mm_widget = null; private bool _read_only = false; public bool read_only { set { assert (value); if (mm_widget != null) mm_widget.sensitive = !value; if (quoted_mm_widget != null) quoted_mm_widget.sensitive = !value; name_label.set_markup("" + GLib.Markup.escape_text(tweet.get_user_name()) + " @" + tweet.get_screen_name()); this.get_style_context ().add_class ("read-only"); this._read_only = value; } } public bool shows_actions { get { return action_box.visible; } } private unowned Account account; private unowned MainWindow main_window; public Cb.Tweet tweet; private bool values_set = false; private bool delete_first_activated = false; private GLib.TimeSpan last_timediff; [Signal (action = true)] private signal void reply_tweet (); [Signal (action = true)] private signal void favorite_tweet (); [Signal (action = true)] private signal void retweet_tweet (); [Signal (action = true)] private signal void delete_tweet (); [Signal (action = true)] private signal void quote_tweet (); public TweetListEntry (Cb.Tweet tweet, MainWindow? main_window, Account account, bool restrict_height = false) { this.account = account; this.tweet = tweet; this.main_window = main_window; var name = tweet.get_user_name (); var screen_name = "@" + tweet.get_screen_name (); Cb.UserIdentity author; if (tweet.retweeted_tweet != null) { author = tweet.retweeted_tweet.author; } else { author = tweet.source_tweet.author; } name_label.set_markup ("%s ⁨%s⁩".printf(Utils.linkify_user (author, true), screen_name)); name_label.tooltip_text = "%s \u2068%s\u2069".printf(name, screen_name); if (tweet.avatar_url != null) { string avatar_url = tweet.avatar_url; if (this.get_scale_factor () >= 2) avatar_url = avatar_url.replace ("_normal", "_bigger"); Twitter.get ().get_avatar.begin (tweet.get_user_id (), avatar_url, avatar_image, 48 * this.get_scale_factor ()); } avatar_image.verified = tweet.is_flag_set (Cb.TweetState.VERIFIED); avatar_image.protected_account = tweet.is_flag_set (Cb.TweetState.PROTECTED); if (tweet.retweeted_tweet != null) { var rt_author = tweet.source_tweet.author; rt_label.show (); rt_image.show (); rt_label.label = "%s ⁨@%s⁩".printf(Utils.linkify_user (rt_author), rt_author.screen_name); // Set the accessible text as a single string rather than trying to make screen readers read // separate text for the RT icon and the names // TRANSLATORS: replacements are name and handle (without the "@") var rt_by = _("Retweeted by %s (@%s)").printf(rt_author.user_name, rt_author.screen_name); rt_label.get_accessible().set_name(rt_by); } var reply_users = tweet.get_reply_users(); if (reply_users.length > 0) { var author_id = (tweet.retweeted_tweet != null) ? tweet.retweeted_tweet.author.id : tweet.source_tweet.author.id; reply_label.label = Utils.build_reply_to_string(reply_users, author_id); reply_label.show (); } else { reply_label.hide (); } if (tweet.quoted_tweet != null) { this.create_quote_grid (tweet.quoted_tweet.reply_id != 0); var quoted_screen_name = "@" + tweet.quoted_tweet.author.screen_name; quote_name.set_markup ("%s ⁨%s⁩".printf(Utils.linkify_user (tweet.quoted_tweet.author, true), quoted_screen_name)); quote_name.tooltip_text = "%s \u2068%s\u2069".printf(tweet.quoted_tweet.author.user_name, quoted_screen_name); var quoted_reply_users = tweet.quoted_tweet.reply_users; if (quoted_reply_users.length != 0) { quote_reply_label.label = Utils.build_reply_to_string(quoted_reply_users, tweet.quoted_tweet.author.id); quote_reply_label.show (); } else if (quote_reply_label != null) { quote_reply_label.hide (); } // Else it hasn't been created because it wasn't a reply } retweet_button.active = tweet.is_flag_set (Cb.TweetState.RETWEETED); retweet_button.sensitive = !(tweet.is_flag_set (Cb.TweetState.PROTECTED) && tweet.get_user_id () != account.id); favorite_button.active = tweet.is_flag_set (Cb.TweetState.FAVORITED); retweet_button.size_allocate.connect_after(() => { // XXX: Grab focus after the button is allocated (if it has a size) to avoid // grabbing focus before the widget is ready (which jumps us // to the top of the list) if (retweet_button.visible && retweet_button.get_allocated_width() > 1) { retweet_button.grab_focus(); } }); grab_focus.connect_after(() => { // XXX: Also grab focus when the list item gets focus // because the size_allocate doesn't re-trigger when re-showing // the button if (action_box.visible && retweet_button.get_allocated_width() > 1) { retweet_button.grab_focus(); } }); tweet.state_changed.connect (state_changed_cb); if (tweet.has_inline_media ()) { this.create_media_widget (tweet.is_flag_set (Cb.TweetState.NSFW), out this.mm_widget, out this.media_stack); Gtk.Widget w = media_stack != null ? ((Gtk.Widget)media_stack) : ((Gtk.Widget)mm_widget); this.grid.attach (w, 2, 3, 5, 1); mm_widget.restrict_height = restrict_height; mm_widget.set_all_media (tweet.get_medias ()); mm_widget.media_clicked.connect (media_clicked_cb); mm_widget.media_invalid.connect (media_invalid_cb); mm_widget.window = main_window; } if (tweet.has_quoted_inline_media ()) { this.create_media_widget (tweet.is_quoted_flag_set (Cb.TweetState.NSFW), out this.quoted_mm_widget, out this.quoted_media_stack); quoted_mm_widget.margin_start = 12; Gtk.Widget w = quoted_media_stack != null ? ((Gtk.Widget)quoted_media_stack) : ((Gtk.Widget)quoted_mm_widget); this.quote_grid.attach (w, 0, 3, 3, 1); quoted_mm_widget.restrict_height = restrict_height; quoted_mm_widget.set_all_media (tweet.get_quoted_medias ()); quoted_mm_widget.media_clicked.connect (quoted_media_clicked_cb); quoted_mm_widget.media_invalid.connect (quoted_media_invalid_cb); quoted_mm_widget.window = main_window; } if (tweet.has_inline_media () || tweet.has_quoted_inline_media ()) { Settings.get ().changed["media-visibility"].connect (media_visibility_changed_cb); if (tweet.is_flag_set (Cb.TweetState.NSFW) || tweet.is_quoted_flag_set (Cb.TweetState.NSFW)) Settings.get ().changed["hide-nsfw-content"].connect (hide_nsfw_content_changed_cb); } var quote_action = new GLib.SimpleAction("quote", null); quote_action.activate.connect(quote_activated); var translate_action = new GLib.SimpleAction("translate", null); translate_action.activate.connect(translate_activated); translate_action.set_enabled(Utils.needs_translating(tweet)); var non_destructive_actions = new GLib.SimpleActionGroup (); non_destructive_actions.add_action (quote_action); non_destructive_actions.add_action (translate_action); this.insert_action_group ("tweet", non_destructive_actions); var delete_action = new GLib.SimpleAction("delete", null); delete_action.activate.connect(delete_activated); var destructive_actions = new GLib.SimpleActionGroup (); destructive_actions.add_action (delete_action); this.insert_action_group("destructive-actions", destructive_actions); if (tweet.get_user_id () != account.id) { delete_action.set_enabled (false); } if (tweet.is_flag_set (Cb.TweetState.PROTECTED)) { quote_action.set_enabled (false); } reply_tweet.connect (reply_tweet_activated); delete_tweet.connect (delete_tweet_activated); quote_tweet.connect (quote_activated); favorite_tweet.connect (() => { favorite_button.active = !favorite_button.active; }); retweet_tweet.connect (() => { retweet_button.tap (); }); if (tweet.is_flag_set (Cb.TweetState.FAVORITED)) fav_status_image.show (); if (tweet.is_flag_set (Cb.TweetState.RETWEETED)) rt_status_image.show (); values_set = true; if (restrict_height) { scroller.set_max_content_height(600); } set_tweet_text(); set_tweet_text_scale(); update_time_delta (); // TODO All these settings signal connections with lots of tweets could be costly... Settings.get ().changed["text-transform-flags"].connect (set_tweet_text); Settings.get ().changed["tweet-scale"].connect (set_tweet_text_scale); } ~TweetListEntry () { Settings.get ().changed["text-transform-flags"].disconnect (set_tweet_text); if (tweet.is_flag_set (Cb.TweetState.NSFW) || tweet.is_quoted_flag_set (Cb.TweetState.NSFW)) Settings.get ().changed["hide-nsfw-content"].disconnect (hide_nsfw_content_changed_cb); if (this.mm_widget != null || this.mm_widget != null) Settings.get ().changed["media-visibility"].disconnect (media_visibility_changed_cb); } private void media_visibility_changed_cb () { if (Settings.get_media_visiblity () == MediaVisibility.SHOW) { if (mm_widget != null) { this.mm_widget.show (); } if (quoted_mm_widget != null) { this.quoted_mm_widget.show (); } } else { if (mm_widget != null) { this.mm_widget.hide (); } if (quoted_mm_widget != null) { this.quoted_mm_widget.hide (); } } set_tweet_text(); } private void set_tweet_text() { Cb.TransformFlags transform_flags = Settings.get_text_transform_flags (); if (Settings.get_media_visiblity () != MediaVisibility.SHOW) { // Forcefully unset "remove media links" so people can see there's media without loading it transform_flags &= ~Cb.TransformFlags.REMOVE_MEDIA_LINKS; } text_label.label = tweet.get_trimmed_text (transform_flags); if (text_label.label == "") { scroller.hide(); } else { scroller.show(); } if (this.tweet.quoted_tweet != null) { this.quote_label.label = Cb.TextTransform.tweet (ref tweet.quoted_tweet, transform_flags, 0); if (quote_label.label == "") { quote_label.hide (); } else { quote_label.show(); } } } private void set_tweet_text_scale () { var scale = Settings.get_tweet_scale(); set_scale_attribute(text_label, scale); if (tweet.quoted_tweet != null) { set_scale_attribute(quote_label, scale); } } private void set_scale_attribute (Gtk.Label label, double scale) { var new_attribs = new Pango.AttrList(); var scale_attr = Pango.attr_scale_new(scale); new_attribs.insert((owned)scale_attr); label.set_attributes(new_attribs); } private void hide_nsfw_content_changed_cb () { if (this.media_stack != null) { if (this.tweet.is_flag_set (Cb.TweetState.NSFW) && Settings.hide_nsfw_content ()) this.media_stack.visible_child_name = "nsfw"; else this.media_stack.visible_child = mm_widget; } if (this.quoted_media_stack != null) { if (this.tweet.is_quoted_flag_set (Cb.TweetState.NSFW) && Settings.hide_nsfw_content ()) this.quoted_media_stack.visible_child_name = "nsfw"; else this.quoted_media_stack.visible_child = quoted_mm_widget; } } private void media_clicked_cb (Cb.Media m, int index, double px, double py) { TweetUtils.handle_media_click (this.tweet.get_medias (), this.main_window, index); } private void quoted_media_clicked_cb (Cb.Media m, int index, double px, double py) { TweetUtils.handle_media_click (this.tweet.get_quoted_medias (), this.main_window, index); } private void delete_tweet_activated () { if (tweet.get_user_id () != account.id) return; // Nope. if (delete_first_activated) { TweetUtils.delete_tweet.begin (account, tweet, (obj, res) => { var success = false; try { success = TweetUtils.delete_tweet.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { sensitive = false; if (shows_actions) { toggle_mode (); } } }); } else delete_first_activated = true; } static construct { unowned Gtk.BindingSet binding_set = Gtk.BindingSet.by_class ((GLib.ObjectClass)typeof (TweetListEntry).class_ref ()); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.r, 0, "reply-tweet", 0, null); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.d, 0, "delete-tweet", 0, null); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.t, 0, "retweet-tweet", 0, null); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.f, 0, "favorite-tweet", 0, null); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.l, 0, "favorite-tweet", 0, null); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.q, 0, "quote-tweet", 0, null); } [GtkCallback] private bool focus_out_cb (Gdk.EventFocus evt) { delete_first_activated = false; retweet_button.reset (); return false; } [GtkCallback] private bool key_released_cb (Gdk.EventKey evt) { #if DEBUG switch(evt.keyval) { case Gdk.Key.k: TweetUtils.log_tweet(tweet); return Gdk.EVENT_STOP; } #endif return Gdk.EVENT_PROPAGATE; } /** * Retweets or un-retweets the tweet. */ [GtkCallback] private void retweet_button_toggled_cb () { bool retweetable = tweet.get_user_id () == account.id || !tweet.is_flag_set (Cb.TweetState.PROTECTED); if (!retweetable || !values_set) return; retweet_button.sensitive = false; TweetUtils.set_retweet_status.begin (account, tweet, retweet_button.active, (obj, res) => { var success = false; try { success = TweetUtils.set_retweet_status.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { if (shows_actions) { toggle_mode (); } } else { retweet_button.active = tweet.is_flag_set (Cb.TweetState.RETWEETED); } retweet_button.sensitive = true; }); } [GtkCallback] private void favorite_button_toggled_cb () { if (!values_set) return; favorite_button.sensitive = false; TweetUtils.set_favorite_status.begin (account, tweet, favorite_button.active, (obj, res) => { var success = false; try { success = TweetUtils.set_favorite_status.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, main_window); } if (success) { if (shows_actions) { toggle_mode (); } } else { favorite_button.active = tweet.is_flag_set (Cb.TweetState.FAVORITED); } favorite_button.sensitive = true; }); } [GtkCallback] private void reply_button_clicked_cb () { ComposeTweetWindow ctw = new ComposeTweetWindow (this.main_window, this.account, this.tweet, ComposeTweetWindow.Mode.REPLY); ctw.show (); if (shows_actions) toggle_mode (); } private void quote_activated () { ComposeTweetWindow ctw = new ComposeTweetWindow (this.main_window, this.account, this.tweet, ComposeTweetWindow.Mode.QUOTE); ctw.show (); if (shows_actions) toggle_mode (); } private void translate_activated () { TweetUtils.activate_link(Utils.create_translate_url(tweet), main_window); if (shows_actions) { toggle_mode (); } } private void reply_tweet_activated () { ComposeTweetWindow ctw = new ComposeTweetWindow (this.main_window, this.account, this.tweet, ComposeTweetWindow.Mode.REPLY); ctw.show (); } private void delete_activated () { var dialog = new Gtk.MessageDialog(main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("Are you sure you want to delete this tweet?")); var result = dialog.run(); dialog.destroy(); if (result == Gtk.ResponseType.YES) { delete_first_activated = true; delete_tweet (); } else { if (shows_actions) { toggle_mode (); } } } [GtkCallback] private bool link_activated_cb (string uri) { if (this._read_only) { return false; } this.grab_focus (); return TweetUtils.activate_link (uri, main_window); } [GtkCallback] private void populate_popup_cb (Gtk.Label source, Gtk.Menu menu) { var link_text = source.get_current_uri (); if (link_text.has_prefix ("#")) { var item = new Gtk.MenuItem.with_label (_("Block %s").printf (link_text)); item.show (); item.activate.connect (() => { Utils.create_persistent_filter (link_text, account); main_window.rerun_filters (); }); menu.add (item); } } private void media_invalid_cb () { Cb.TransformFlags flags = Settings.get_text_transform_flags () & ~Cb.TransformFlags.REMOVE_MEDIA_LINKS; string new_text; if (tweet.retweeted_tweet != null) new_text = Cb.TextTransform.tweet (ref tweet.retweeted_tweet, flags, 0); else new_text = Cb.TextTransform.tweet (ref tweet.source_tweet, flags, 0); this.text_label.label = new_text; } private void quoted_media_invalid_cb () { //FIXME: Use quoted flags, once/if implemented Cb.TransformFlags flags = Settings.get_text_transform_flags () & ~Cb.TransformFlags.REMOVE_MEDIA_LINKS; if (tweet.quoted_tweet != null) { string new_quote_text = Cb.TextTransform.tweet (ref tweet.quoted_tweet, flags, 0); this.quote_label.label = new_quote_text; } } private void state_changed_cb () { this.values_set = false; this.fav_status_image.visible = tweet.is_flag_set (Cb.TweetState.FAVORITED); this.favorite_button.active = tweet.is_flag_set (Cb.TweetState.FAVORITED); this.retweet_button.active = tweet.is_flag_set (Cb.TweetState.RETWEETED); this.rt_status_image.visible = tweet.is_flag_set (Cb.TweetState.RETWEETED); if (tweet.is_flag_set (Cb.TweetState.DELETED)) { this.sensitive = false; grid.show(); action_box.hide(); } this.values_set = true; } public void set_avatar (Cairo.Surface surface) { /* This should only ever be called from the settings page. */ this.avatar_image.surface = surface; } /** * Updates the time delta label in the upper right * * @return The seconds between the current time and * the time the tweet was created */ public int update_time_delta (GLib.DateTime? now = null) { GLib.DateTime cur_time; if (now == null) cur_time = new GLib.DateTime.now_local (); else cur_time = now; GLib.DateTime then = new GLib.DateTime.from_unix_local ( tweet.retweeted_tweet != null ? tweet.retweeted_tweet.created_at : tweet.source_tweet.created_at); var link = "https://twitter.com/%s/status/%s".printf (tweet.source_tweet.author.screen_name, tweet.id.to_string()); time_delta_label.label = "%s" .printf (link, _("Open in Browser"), GLib.Markup.escape_text(Utils.get_time_delta(then, cur_time))); var long_delta = Utils.get_time_delta(then, cur_time, true); time_delta_label.get_accessible().set_name(long_delta); time_delta_label.get_accessible().set_description(long_delta); if (quote_time_delta != null) { then = new GLib.DateTime.from_unix_local (tweet.quoted_tweet.created_at); link = "https://twitter.com/%s/status/%s".printf (tweet.quoted_tweet.author.screen_name, tweet.quoted_tweet.id.to_string()); quote_time_delta.label = "%s" .printf (link, _("Open in Browser"), GLib.Markup.escape_text(Utils.get_time_delta(then, cur_time))); var long_quote_delta = Utils.get_time_delta(then, cur_time, true); quote_time_delta.get_accessible().set_name(long_quote_delta); quote_time_delta.get_accessible().set_description(long_quote_delta); } return (int)(cur_time.difference (then) / 1000.0 / 1000.0); } public int64 get_sort_factor () { return tweet.source_tweet.id; } public int64 get_timestamp () { return tweet.source_tweet.created_at; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } public void toggle_mode () { if (this._read_only) return; if (action_box.visible) { grid.show(); action_box.hide(); this.activatable = true; } else { // We can't use size groups because they only work when all elements are visible // So kludge an approximation with set_size_request on the action box (which will always be the shortest widget) var grid_height = grid.get_allocated_height() + grid.get_margin_top() + grid.get_margin_bottom(); action_box.set_size_request(-1, grid_height); action_box.show(); grid.hide(); this.activatable = false; } this.grab_focus(); } private int64 start_time; private int64 end_time; private bool anim_tick (Gtk.Widget widget, Gdk.FrameClock frame_clock) { int64 now = frame_clock.get_frame_time (); if (now > end_time) { this.opacity = 1.0; return false; } double t = (now - start_time) / (double)(end_time - start_time); t = ease_out_cubic (t); this.opacity = t; return true; } public void fade_in () { if (this.get_realized ()) { this.show (); return; } ulong realize_id = 0; realize_id = this.realize.connect (() => { this.start_time = this.get_frame_clock ().get_frame_time (); this.end_time = start_time + TRANSITION_DURATION; this.add_tick_callback (anim_tick); this.disconnect (realize_id); }); this.show (); } private void create_media_widget (bool nsfw, out MultiMediaWidget? mm_widget, out Gtk.Stack? media_stack) { // Note: We need to use local variables first, because the anonymous function won't capture an "out" parameter MultiMediaWidget _mm_widget = new MultiMediaWidget (); _mm_widget.halign = Gtk.Align.FILL; _mm_widget.hexpand = true; _mm_widget.margin_top = 6; if (nsfw) { Gtk.Stack _media_stack = new Gtk.Stack (); _media_stack.transition_type = Gtk.StackTransitionType.CROSSFADE; _media_stack.add (_mm_widget); var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12); box.valign = Gtk.Align.CENTER; var label = new Gtk.Label (_("This tweet contains images marked as inappropriate")); label.margin_start = 12; label.margin_end = 12; label.wrap = true; label.wrap_mode = Pango.WrapMode.WORD_CHAR; box.add (label); var button = new Gtk.Button.with_label (_("Show anyway")); button.halign = Gtk.Align.CENTER; button.valign = Gtk.Align.CENTER; button.clicked.connect (() => { _media_stack.visible_child = _mm_widget; }); box.add (button); _media_stack.add_named (box, "nsfw"); _media_stack.show_all (); if (Settings.hide_nsfw_content ()) _media_stack.visible_child_name = "nsfw"; else _media_stack.visible_child = _mm_widget; media_stack = _media_stack; } else { /* We will never have to hide mm_widget */ _mm_widget.show_all (); media_stack = null; } mm_widget = _mm_widget; mm_widget.visible = (Settings.get_media_visiblity () == MediaVisibility.SHOW); } public override void get_preferred_height_for_width (int width, out int min, out int nat) { if ((width < Cawbird.RESPONSIVE_LIMIT) == (get_allocated_width() < Cawbird.RESPONSIVE_LIMIT) && get_allocated_width() > 1 ) { // We're staying the same side of the limit, so let GTK do everything base.get_preferred_height_for_width(width, out min, out nat); return; } // We're crossing the responsive threshold, so approximate a calculation. int margins = 0; var orig_width = width; var style = this.get_style_context(); var style_margin = style.get_margin(0); var style_padding = style.get_padding(0); var style_borders = style.get_border(0); margins += style_margin.left + style_margin.right; margins += style_padding.left + style_padding.right; margins += style_borders.left + style_borders.right; GLib.Value val = GLib.Value(typeof(int)); grid.get_property("margin-start", ref val); margins += val.get_int(); grid.get_property("margin-end", ref val); margins += val.get_int(); width -= margins; int avatar_min, avatar_nat; avatar_image.get_preferred_height_for_width(width, out avatar_min, out avatar_nat); int child_min, child_nat; min = nat = 0; int avatar_width; avatar_image.get_preferred_width (out avatar_width, out child_nat); // Name and reply label are always next to the avatar, no matter the scale name_label.get_preferred_height_for_width(width - avatar_width, out child_min, out child_nat); min += child_min; nat += child_nat; reply_label.get_preferred_height_for_width(width - avatar_width, out child_min, out child_nat); min += child_min; nat += child_nat; if (orig_width < Cawbird.RESPONSIVE_LIMIT) { // In "responsive" mode the text sits under the avatar, so take whichever is taller: // the avatar or the user's name and the "Replying to" line (if it was set) min = int.max(avatar_min, min); nat = int.max(avatar_nat, nat); // Subtract the extra margin we'll add during allocation width -= 6; } else { // All the other widgets don't fill the column under the avatar, so reduce the width // that they calculate from width -= avatar_width; // But pretend it is a little wider because they still have their margins until allocation width += 6; } scroller.get_preferred_height_for_width(width, out child_min, out child_nat); min += child_min; nat += child_nat; // If the text is short in "wide mode" then the avatar may still be the tallest thing // so check our minimums min = int.max(avatar_min, min); nat = int.max(avatar_nat, nat); if (mm_widget != null) { Gtk.Widget w = media_stack != null ? ((Gtk.Widget)media_stack) : ((Gtk.Widget)mm_widget); w.get_preferred_height_for_width(width, out child_min, out child_nat); min += child_min; nat += child_nat; } if (quote_grid != null) { quote_grid.get_preferred_height_for_width(width, out child_min, out child_nat); min += child_min; nat += child_nat; } if (rt_label.visible) { int child2_min, child2_nat; rt_label.get_preferred_height_for_width(width, out child_min, out child_nat); rt_image.get_preferred_height_for_width(width, out child2_min, out child2_nat); min += int.max(child_min, child2_min); nat += int.max(child_nat, child2_nat); } min = int.max(avatar_min, min); nat = int.max(avatar_nat, nat); // Add the vertical GTK margins grid.get_property("margin-top", ref val); min += val.get_int(); nat += val.get_int(); grid.get_property("margin-bottom", ref val); min += val.get_int(); nat += val.get_int(); // And any CSS values var css_extra_height = style_margin.top + style_margin.bottom + style_padding.top + style_padding.bottom + style_borders.top + style_borders.bottom; min += css_extra_height; nat += css_extra_height; } public override void size_allocate(Gtk.Allocation allocation) { var cur_width = get_allocated_width(); var new_width = allocation.width; if (shows_actions && cur_width != new_width) { // The grid widget controls our height, but we can't get it right unless it is visible toggle_mode(); } var new_size_is_responsive = new_width < Cawbird.RESPONSIVE_LIMIT; var cur_size_is_responsive = cur_width < Cawbird.RESPONSIVE_LIMIT; if (new_size_is_responsive != cur_size_is_responsive || cur_width <= 1) { // We've crossed the threshold, so reallocate as appropriate if (new_size_is_responsive) { grid.child_set (avatar_image, "height", 2); grid.child_set (scroller, "left-attach", 0); grid.child_set (scroller, "width", 7); scroller.set ("margin-start", 6); grid.child_set (rt_image, "left-attach", 0); grid.child_set (rt_label, "left-attach", 1); if (mm_widget != null) { Gtk.Widget w = media_stack != null ? ((Gtk.Widget)media_stack) : ((Gtk.Widget)mm_widget); grid.child_set (w, "left-attach", 0); grid.child_set (w, "width", 7); w.set ("margin-start", 6); } if (quote_grid != null) { grid.child_set (quote_grid, "left-attach", 0); grid.child_set (quote_grid, "width", 7); quote_grid.set ("margin-start", 6); } } else { grid.child_set (avatar_image, "height", 5); grid.child_set (scroller, "left-attach", 2); grid.child_set (scroller, "width", 5); scroller.set ("margin-start", 0); grid.child_set (rt_image, "left-attach", 1); grid.child_set (rt_label, "left-attach", 2); if (mm_widget != null) { Gtk.Widget w = media_stack != null ? ((Gtk.Widget)media_stack) : ((Gtk.Widget)mm_widget); grid.child_set (w, "left-attach", 2); grid.child_set (w, "width", 5); w.set ("margin-start", 0); } if (quote_grid != null) { grid.child_set (quote_grid, "left-attach", 2); grid.child_set (quote_grid, "width", 5); quote_grid.set ("margin-start", 0); } } } base.size_allocate(allocation); } private bool quote_link_activated_cb (string uri) { if (this._read_only) { return false; } this.grab_focus (); return TweetUtils.activate_link (uri, main_window); } private void create_quote_grid (bool reply) { this.quote_grid = new Gtk.Grid (); quote_grid.margin_top = 12; quote_grid.margin_end = 6; quote_grid.get_style_context ().add_class ("quote"); this.quote_name = new Gtk.Label (""); quote_name.halign = Gtk.Align.START; quote_name.valign = Gtk.Align.BASELINE; quote_name.margin_start = 12; quote_name.margin_end = 6; quote_name.ellipsize = Pango.EllipsizeMode.END; quote_name.activate_link.connect (quote_link_activated_cb); quote_name.get_style_context ().add_class ("name"); quote_grid.attach (quote_name, 0, 0, 2, 1); if (reply) { this.quote_reply_label = new Gtk.Label (""); quote_reply_label.halign = Gtk.Align.START; quote_reply_label.set_use_markup (true); quote_reply_label.xalign = 0; quote_reply_label.set_margin_start (12); quote_reply_label.set_margin_bottom (4); quote_reply_label.activate_link.connect (quote_link_activated_cb); quote_reply_label.get_style_context ().add_class ("dim-label"); quote_reply_label.get_style_context ().add_class ("invisible-links"); quote_reply_label.set_no_show_all (true); quote_reply_label.wrap = true; quote_grid.attach (quote_reply_label, 0, 1, 3, 1); } this.quote_label = new Gtk.Label (""); quote_label.halign = Gtk.Align.START; quote_label.hexpand = true; quote_label.xalign = 0; quote_label.use_markup = true; quote_label.wrap = true; quote_label.wrap_mode = Pango.WrapMode.WORD_CHAR; quote_label.track_visited_links = false; quote_label.margin_start = 12; quote_label.activate_link.connect (quote_link_activated_cb); quote_label.populate_popup.connect (populate_popup_cb); var attrs = new Pango.AttrList (); attrs.insert (Pango.attr_style_new (Pango.Style.ITALIC)); quote_label.set_attributes (attrs); if (reply) quote_grid.attach (quote_label, 0, 2, 3, 1); else quote_grid.attach (quote_label, 0, 1, 3, 1); this.quote_time_delta = new Gtk.Label (""); quote_time_delta.halign = Gtk.Align.END; quote_time_delta.set_use_markup (true); quote_grid.attach (quote_time_delta, 2, 0, 1, 1); quote_grid.show_all (); this.grid.attach (quote_grid, 2, 4, 5, 1); } } cawbird-1.4.2/src/list/UserFilterEntry.vala000066400000000000000000000060731416632607600206670ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/user-filter-entry.ui")] class UserFilterEntry : Gtk.ListBoxRow, Cb.TwitterItem { [GtkChild] private unowned Gtk.Label name_label; [GtkChild] private unowned Gtk.Label screen_name_label; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Box delete_box; [GtkChild] private unowned Gtk.Grid grid; [GtkChild] private unowned Gtk.Revealer revealer; [GtkChild] private unowned Gtk.Button delete_button; public new string name { set { name_label.label = value; name_label.set_tooltip_text(value); } } public string screen_name { set { screen_name_label.label = "@" + value; } } public string avatar_url { set { real_set_avatar (value); } } public bool seen { get { return true; } set {} } public int64 user_id; public signal void deleted (int64 id); private bool _muted = false; private bool _blocked = false; public bool muted { get { return _muted; } set { _muted = value; if (muted && !blocked) { delete_button.label = _("Unmute"); } // Else the blocking takes priority } } public bool blocked { get { return _blocked; } set { _blocked = value; if (blocked) { delete_button.label = _("Unblock"); } } } private GLib.TimeSpan last_timediff; private void real_set_avatar (string avatar_url) { Twitter.get ().get_avatar.begin (user_id, avatar_url, avatar_image, 48 * this.get_scale_factor ()); } public int update_time_delta (GLib.DateTime? now = null) {return 0;} public int64 get_sort_factor () { return 2; } public int64 get_timestamp () { return 0; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } [GtkCallback] private void menu_button_clicked_cb () { stack.visible_child = delete_box; } [GtkCallback] private void cancel_button_clicked_cb () { stack.visible_child = grid; } [GtkCallback] private void delete_button_clicked_cb () { revealer.reveal_child = false; revealer.notify["child-revealed"].connect (() => { deleted (user_id); }); } } cawbird-1.4.2/src/list/UserListEntry.vala000066400000000000000000000127341416632607600203560ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/user-list-entry.ui")] class UserListEntry : Gtk.ListBoxRow, Cb.TwitterItem { [GtkChild] private unowned Gtk.Label name_label; [GtkChild] private unowned Gtk.Label screen_name_label; [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Button settings_button; [GtkChild] private unowned Gtk.Button new_window_button; [GtkChild] private unowned Gtk.Button profile_button; public new string name { set { name_label.label = value; name_label.tooltip_text = value; } } public string screen_name { owned get { return screen_name_label.label.substring (1); } } public int name_display_length { get { return name_label.max_width_chars; } set { name_label.max_width_chars = value; } } public void set_screen_name (string sn) { screen_name_label.label = sn; screen_name_label.tooltip_text = sn; } public string avatar_url { set { real_set_avatar (value); } } public Cairo.Surface avatar_surface { set { avatar_image.surface = value; } } public bool verified { set { this.avatar_image.verified = value; } } public bool protected_account { set { this.avatar_image.protected_account = value; } } public bool seen { get { return true; } set {} } public bool show_settings { set { settings_button.visible = value; new_window_button.visible = value; profile_button.visible = value; } } public int64 user_id { get; set; } private GLib.TimeSpan last_timediff; public signal void action_clicked (); private unowned Account account; public UserListEntry.from_account (Account acc) { this.screen_name_label.label = "@" + acc.screen_name; this.name = acc.name; this.avatar_surface = acc.avatar; this.account = acc; this.user_id = acc.id; acc.info_changed.connect ((screen_name, name, nop, avatar) => { this.screen_name_label.label = "@" + acc.screen_name; this.name = name; this.avatar_surface = avatar; }); acc.notify["avatar"].connect (() => { this.avatar_surface = this.account.avatar; }); var cb = (Cawbird) GLib.Application.get_default (); cb.window_added.connect ((window) => { if (window is MainWindow) { update_window_button_sensitivity ((MainWindow)window, false); } }); cb.window_removed.connect ((window) => { if (window is MainWindow) { update_window_button_sensitivity ((MainWindow)window, true); } }); cb.account_window_changed.connect ((old_id, new_id) => { if (old_id == this.user_id) new_window_button.sensitive = true; else if (new_id == this.user_id) new_window_button.sensitive = false; }); // Set initial sensitivitiy of new_window_button new_window_button.sensitive = !(cb.is_window_open_for_user_id (acc.id)); } private void real_set_avatar (string avatar_url) { Twitter.get ().get_avatar.begin (user_id, avatar_url, avatar_image, 48 * this.get_scale_factor ()); } public int update_time_delta (GLib.DateTime? now = null) {return 0;} public int64 get_sort_factor () { return int64.MAX - 1; } public int64 get_timestamp () { return 0; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } private void update_window_button_sensitivity (MainWindow window, bool new_value) { if (window.account != null && window.account.screen_name == this.account.screen_name) { new_window_button.sensitive = new_value; } } [GtkCallback] private void settings_button_clicked_cb () { action_clicked (); var active_window = ((Gtk.Application)GLib.Application.get_default ()).active_window; var dialog = new AccountDialog (this.account); dialog.set_transient_for (active_window); dialog.modal = true; dialog.show (); } [GtkCallback] private void new_window_button_clicked_cb () { var cb = (Cawbird) GLib.Application.get_default (); var window = new MainWindow (cb, this.account); cb.add_window (window); window.show_all (); action_clicked (); } [GtkCallback] private void profile_button_clicked_cb () { action_clicked (); var active_window = ((Gtk.Application)GLib.Application.get_default ()).active_window; if (active_window is MainWindow) { var mw = (MainWindow) active_window; var bundle = new Cb.Bundle (); bundle.put_int64 (ProfilePage.KEY_USER_ID, this.user_id); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, this.screen_name); mw.main_widget.switch_page (Page.PROFILE, bundle); } } } cawbird-1.4.2/src/main.vala000066400000000000000000000031611416632607600155250ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ int main (string[] args) { #if X11 X.init_threads(); #endif #if VIDEO Gst.init (ref args); #endif // Setup gettext GLib.Intl.setlocale (GLib.LocaleCategory.ALL, ""); if (Config.LOCALEDIR != null) { GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); } else { GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, GLib.Environment.get_variable("PWD") + "/po/"); } GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); //no initialisation of static fields :( Settings.init (); var cawbird = new Cawbird (); int ret = cawbird.run (args); #if DEBUG var list = Gtk.Window.list_toplevels (); debug ("Toplevels Left: %u", list.length ()); foreach (var w in list) { debug ("Toplevel: %s", __class_name (w)); w.destroy (); } #endif return ret; } cawbird-1.4.2/src/model/000077500000000000000000000000001416632607600150335ustar00rootroot00000000000000cawbird-1.4.2/src/model/DMThreadsModel.vala000066400000000000000000000112611416632607600204750ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class DMThread : GLib.Object { public Cb.UserIdentity user; /* id, name, screen_name */ public int64 last_message_id; public string last_message; public int unread_count = 0; public string? notification_id = null; public Cairo.Surface? avatar_surface = null; public async void load_avatar (Account account, int scale_factor) { assert (this.user.id != 0); if (this.avatar_surface != null) return; this.avatar_surface = yield Twitter.get ().load_avatar_for_user_id (account, user.id, 48 * scale_factor); if (this.avatar_surface != null) Twitter.get ().ref_avatar (this.avatar_surface); } ~DMThread () { if (this.avatar_surface != null) Twitter.get ().unref_avatar (this.avatar_surface); } } /* Let's hope there aren't a lof of threads */ public class DMThreadsModel : GLib.ListModel, GLib.Object { private GLib.GenericArray threads = new GLib.GenericArray (); public GLib.Object? get_item (uint index) { return this.threads.get ((int)index); } public uint get_n_items () { return (uint) this.threads.length; } public GLib.Type get_item_type () { return typeof (DMThread); } public void add (DMThread thread) { bool added = false; for (int i = 0; i < threads.length; i ++) { if (thread.last_message_id > threads.get (i).last_message_id) { this.threads.insert (i, thread); this.items_changed (i, 0, 1); added = true; break; } } if (!added) { this.threads.add (thread); this.items_changed (this.threads.length - 1, 0, 1); } } public void update_last_message (int64 sender_id, int64 message_id, string message_text) { #if DEBUG assert (this.has_thread (sender_id)); #endif int index = 0; for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == sender_id) { if (message_id > thread.last_message_id) { thread.last_message_id = message_id; thread.last_message = message_text; this.threads.remove (thread); this.items_changed (index, 1, 0); this.add (thread); } else { warning ("id %s is < than %s", message_id.to_string (), thread.last_message_id.to_string ()); } /* Don't call items_changed, the caller DMManager has to call thread_changed */ break; } index ++; } } public bool has_thread (int64 user_id) { for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == user_id) return true; } return false; } public int reset_unread_count (int64 user_id) { for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == user_id) { int k = thread.unread_count; thread.unread_count = 0; return k; } } return 0; } public string? reset_notification_id (int64 user_id) { for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == user_id) { string k = thread.notification_id; thread.notification_id = null; return k; } } return null; } public void increase_unread_count (int64 user_id, int amount = 1) { for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == user_id) { thread.unread_count += amount; /* Don't call items_changed, the caller DMManager has to call thread_changed */ break; } } } public DMThread? get_thread (int64 user_id) { for (int i = 0; i < threads.length; i ++) { var thread = threads.get (i); if (thread.user.id == user_id) { return thread; } } return null; } } cawbird-1.4.2/src/sql/000077500000000000000000000000001416632607600145325ustar00rootroot00000000000000cawbird-1.4.2/src/sql/BaseStatement.vala000066400000000000000000000047371416632607600201510ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { public abstract class BaseStatement : GLib.Object { public unowned Sqlite.Database db; protected StringBuilder query_builder = new StringBuilder (); protected GLib.GenericArray bindings = new GLib.GenericArray(); protected bool where_set = false; public STATEMENT_TYPE where (string stmt) { if (!where_set) { query_builder.append (" WHERE "); where_set = true; } query_builder.append (stmt); return this; } public STATEMENT_TYPE where_eq (string field, string val) { where(@"`$field` = ?"); bindings.add(val); return this; } public STATEMENT_TYPE where_eqi (string w, int64 v) { return where_eq (w, v.to_string()); } public STATEMENT_TYPE where_lt (string field, int64 val) { where(@"`$field` < ?"); bindings.add(val.to_string()); return this; } public STATEMENT_TYPE where_prefix (string field, string prefix) { where(@"`$field` LIKE ?"); bindings.add(prefix + "%"); return this; } public STATEMENT_TYPE where_prefix2 (string field, string prefix) { return where_prefix(field, prefix); } public STATEMENT_TYPE or () { query_builder.append (" OR "); return this; } public STATEMENT_TYPE and () { query_builder.append (" AND "); return this; } public STATEMENT_TYPE nocase () { query_builder.append (" COLLATE NOCASE"); return this; } } }cawbird-1.4.2/src/sql/Database.vala000066400000000000000000000067651416632607600171210ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { public const int CAWBIRD_SQL_VERSION = 4; public const string CAWBIRD_INIT_FILE = "/uk/co/ibboard/cawbird/sql/init/Create.%d.sql"; public const int ACCOUNTS_SQL_VERSION = 7; public const string ACCOUNTS_INIT_FILE = "/uk/co/ibboard/cawbird/sql/accounts/Create.%d.sql"; private const int STOP = -1; private const int CONTINUE = 0; public class Database : GLib.Object { private Sqlite.Database db; public Database (string filename, string init_file, int max_version) { int err = Sqlite.Database.open (filename, out db); if (err != 0) { critical ("Error when opening the database '%s': %s", filename, db.errmsg ()); } this.exec ("PRAGMA journal_mode = MEMORY;"); int user_version = 0; this.exec ("pragma user_version;", (n_cols, vals) => {user_version = int.parse(vals[0]); return STOP;}); for (int cur_version = user_version + 1; cur_version <= max_version; cur_version ++) { try { var data = GLib.resources_lookup_data (init_file.printf (cur_version), 0); unowned string sql_str = (string) data.get_data (); debug ("Executing %s for %d", init_file, cur_version); db.exec (sql_str); } catch (GLib.Error e) { critical (e.message); break; } } } public void exec (string sql, Sqlite.Callback? callback = null) { #if DEBUG string err = ""; int val = db.exec (sql, callback, out err); if (val != Sqlite.OK && val != 4) critical ("SQL ERROR(%d): '%s' FOR QUERY '%s'", val, err, sql); #else db.exec (sql, callback); #endif } public Sql.InsertStatement insert (string table_name) { var stmt = new InsertStatement (table_name, InsertType.FAIL); stmt.db = db; return stmt; } public Sql.InsertStatement insert_ignore (string table_name) { var stmt = new InsertStatement (table_name, InsertType.IGNORE); stmt.db = db; return stmt; } public Sql.InsertStatement replace (string table_name) { var stmt = new InsertStatement (table_name, InsertType.REPLACE); stmt.db = db; return stmt; } public Sql.SelectStatement select (string table_name) { var stmt = new SelectStatement (table_name); stmt.db = db; return stmt; } public Sql.UpdateStatement update (string table_name) { var stmt = new UpdateStatement (table_name); stmt.db = db; return stmt; } public Sql.DeleteStatement delete (string table_name) { var stmt = new DeleteStatement (table_name); stmt.db = db; return stmt; } public void begin_transaction () { db.exec ("BEGIN TRANSACTION;"); } public void end_transaction () { db.exec ("END TRANSACTION;"); } public unowned Sqlite.Database get_sqlite_db () { return this.db; } } } cawbird-1.4.2/src/sql/DeleteStatement.vala000066400000000000000000000037671416632607600205030ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { public class DeleteStatement : Sql.BaseStatement { private bool ran = false; public DeleteStatement (string table_name) { query_builder.append ("DELETE FROM `").append (table_name).append ("` "); } public int run () { if (!where_set) { critical ("Bare DELETE statements are not allowed!"); return -1; } Sqlite.Statement stmt; query_builder.append(";"); int ok = db.prepare_v2 (query_builder.str, -1, out stmt); if (ok != Sqlite.OK) { critical (db.errmsg ()); return -1; } for (int i = 0; i < bindings.length; i++) { stmt.bind_text (i + 1, bindings.get (i)); } ok = stmt.step (); if (ok == Sqlite.ERROR) { critical (db.errmsg ()); critical (stmt.sql ()); return -1; } ran = true; return db.changes (); } #if DEBUG ~DeleteStatement () { if (!ran) critical ("UpdateStatement for %s did not run.", query_builder.str); } #endif } }cawbird-1.4.2/src/sql/InsertStatement.vala000066400000000000000000000064531416632607600205400ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { /** * What to do in case of key collision during INSERT - fail, replace or ignore */ public enum InsertType { FAIL, REPLACE, IGNORE } public class InsertStatement : GLib.Object { public unowned Sqlite.Database db; private StringBuilder query_builder = new StringBuilder (); private GLib.GenericArray bindings = new GLib.GenericArray(); private bool ran = false; public InsertStatement (string table_name, InsertType ins_type) { if (ins_type == InsertType.REPLACE) { query_builder.append ("INSERT OR REPLACE INTO `"); } else if (ins_type == InsertType.IGNORE) { query_builder.append ("INSERT OR IGNORE INTO `"); } else { query_builder.append ("INSERT INTO `"); } query_builder.append (table_name).append ("` ("); } public int64 run () { query_builder.append (") VALUES ("); query_builder.append ("?"); for (int i = 0; i < bindings.length -1; i++) query_builder.append (",?"); query_builder.append (");"); Sqlite.Statement stmt; int ok = db.prepare_v2 (query_builder.str, -1, out stmt); if (ok != Sqlite.OK) { critical (db.errmsg ()); return -1; } for (int i = 0; i < bindings.length; i++) { stmt.bind_text (i + 1, bindings.get (i)); } ok = stmt.step (); if (ok != Sqlite.DONE) { critical (db.errmsg ()); StringBuilder err_msg = new StringBuilder (); err_msg.append (stmt.sql ()).append (" --- "); for (int i = 0; i < bindings.length; i++) { err_msg.append (bindings.get (i)).append (", "); } critical (err_msg.str); } ran = true; return db.last_insert_rowid (); } public InsertStatement val (string col_name, string? col_value) { if (bindings.length > 0) query_builder.append (", "); query_builder.append ("`").append (col_name).append ("`"); bindings.add (col_value); return this; } public InsertStatement vali (string col_name, int col_value) { return val (col_name, col_value.to_string ()); } public InsertStatement vali64 (string col_name, int64 col_value) { return val (col_name, col_value.to_string ()); } public InsertStatement valb (string col_name, bool col_value) { return val (col_name, col_value ? "1" : "0"); } #if DEBUG ~InsertStatement () { if (!ran) critical ("InsertStatement for %s did not run.", query_builder.str); } #endif } } cawbird-1.4.2/src/sql/SelectStatement.vala000066400000000000000000000060121416632607600205020ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { public delegate bool SelectCallback (string[] vals); public class SelectStatement : Sql.BaseStatement { private string table_name; public SelectStatement (string table_name) { query_builder.append ("SELECT "); this.table_name = table_name; } public SelectStatement cols (string first, ...) { var arg_list = va_list (); query_builder.append ("`").append (first).append ("`"); for (string? arg = arg_list.arg (); arg != null; arg = arg_list.arg ()) { query_builder.append (", `").append (arg).append ("`"); } query_builder.append (" FROM `").append (table_name).append ("`"); return this; } public SelectStatement count (string col) { query_builder.append ("count(`").append (col).append ("`)"); query_builder.append (" FROM `").append (table_name).append ("`"); return this; } public SelectStatement order (string order_by) { query_builder.append (" ORDER BY ").append (order_by); return this; } public SelectStatement limit (int limit) { query_builder.append (" LIMIT ").append (limit.to_string ()); return this; } public int run (SelectCallback callback) { Sqlite.Statement stmt; int ok = db.prepare_v2 (query_builder.str, -1, out stmt); if (ok != Sqlite.OK) { critical (db.errmsg ()); critical (query_builder.str); return 0; } for (int i = 0; i < bindings.length; i++) { stmt.bind_text (i + 1, bindings.get (i)); } bool next = true; int n_cols = stmt.column_count (); int n_rows = 0; while (stmt.step () == Sqlite.ROW && next) { string[] vals = new string[n_cols]; for (int i = 0; i < n_cols; i++) vals[i] = stmt.column_text (i); next = callback (vals); n_rows ++; } return n_rows; } public int64 once_i64 () { int64 back = -1; this.run ((vals) => { back = int64.parse (vals[0]); return false; }); return back; } public string? once_string () { string? back = null; this.run ((vals) => { back = vals[0]; return false; }); return back; } } } cawbird-1.4.2/src/sql/UpdateStatement.vala000066400000000000000000000056631416632607600205200ustar00rootroot00000000000000 /* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Sql { public class UpdateStatement : GLib.Object { public unowned Sqlite.Database db; private StringBuilder query_builder = new StringBuilder (); private GLib.GenericArray bindings = new GLib.GenericArray (); private bool ran = false; public UpdateStatement (string table_name) { query_builder.append ("UPDATE `").append (table_name).append ("` SET "); } public int64 run () { Sqlite.Statement stmt; query_builder.append(";"); int ok = db.prepare_v2 (query_builder.str, -1, out stmt); if (ok != Sqlite.OK) { critical (db.errmsg ()); return -1; } for (int i = 0; i < bindings.length; i++) { stmt.bind_text (i + 1, bindings.get (i)); } ok = stmt.step (); if (ok == Sqlite.ERROR) { critical (db.errmsg ()); critical (stmt.sql ()); return -1; } ran = true; return db.last_insert_rowid (); } public UpdateStatement where (string where) { query_builder.append (" WHERE ").append (where); return this; } public UpdateStatement where_eq (string col, string value) { query_builder.append (" WHERE `").append (col).append ("`= ?"); bindings.add (value); return this; } public UpdateStatement where_eqi (string col, int64 iv) { return where_eq (col, iv.to_string ()); } public UpdateStatement val (string col_name, string col_value) { if (bindings.length > 0) query_builder.append (", "); query_builder.append ("`").append (col_name).append ("` = ?"); bindings.add (col_value); return this; } public UpdateStatement vali (string col_name, int col_value) { return val (col_name, col_value.to_string ()); } public UpdateStatement vali64 (string col_name, int64 col_value) { return val (col_name, col_value.to_string ()); } public UpdateStatement valb (string col_name, bool col_value) { return val (col_name, col_value ? "1" : "0"); } #if DEBUG ~UpdateStatement () { if (!ran) critical ("UpdateStatement for %s did not run.", query_builder.str); } #endif } } cawbird-1.4.2/src/util/000077500000000000000000000000001416632607600147105ustar00rootroot00000000000000cawbird-1.4.2/src/util/Benchmark.vala000066400000000000000000000023421416632607600174500ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2016 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Benchmark { public class Bench { public string name; public GLib.DateTime first; public void stop () { #if DEBUG var ts = new GLib.DateTime.now_local ().difference (first); int64 ms = (ts / 1000); debug (@"$(this.name) took $ms ms ($ts us)"); #endif } } public Bench start (string name) { var b = new Bench (); #if DEBUG b.name = name; b.first = new GLib.DateTime.now_local (); #endif return b; } } cawbird-1.4.2/src/util/Dirs.vala000066400000000000000000000037711416632607600164660ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace Dirs { static string? config_dir = null; static string? corebird_config_dir = null; public void create_dirs () { create_folder (config ("")); create_folder (config ("accounts/")); create_folder (config ("image-favorites/")); } public string corebird_config (string path) { if (corebird_config_dir == null) { corebird_config_dir = _config ("corebird"); } return corebird_config_dir + path; } public string config (string path) { if (config_dir == null) { config_dir = _config ("cawbird"); } return config_dir + path; } private string _config (string app_name) { string dir = GLib.Environment.get_home_dir () + "/." + app_name + "/"; if (!GLib.FileUtils.test (dir, GLib.FileTest.EXISTS)) { dir = GLib.Environment.get_user_config_dir () + "/" + app_name + "/"; } return dir; } private void create_folder (string path) { if (FileUtils.test (path, FileTest.EXISTS)) return; try { bool success = File.new_for_path (path) .make_directory (); if (!success) { critical("Couldn't create user folder %s", path); } } catch (GLib.Error e) { critical ("%s(%s)", e.message, path); } } } cawbird-1.4.2/src/util/ListUtils.vala000066400000000000000000000030051416632607600175070ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ namespace ListUtils { async void delete_list (Account account, int64 to_rename) throws GLib.Error { var call = account.proxy.new_call (); call.set_method ("POST"); call.set_function ("1.1/lists/subscribers/create.json"); call.add_param ("list_id", to_rename.to_string ()); GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var tmp_err = TweetUtils.failed_request_to_error (call, e); if (tmp_err.domain != TweetUtils.get_error_domain() || tmp_err.code != 34) { err = tmp_err; } } delete_list.callback(); }); yield; if (err != null) { throw err; } } }cawbird-1.4.2/src/util/TweetUtils.vala000066400000000000000000001204631416632607600176740ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ int MAX_CHUNK_SIZE = 5 * 1024 * 1024; namespace TweetUtils { public Quark get_error_domain() { return Quark.from_string("tweet-action"); } /** * Turns Twitter's JSON error messages into a GLib.Error object, unless * the exception was caused by a libsoup error * * Example JSON: * {"errors":[{"message":"Sorry, that page does not exist","code":34}]} */ public GLib.Error failed_request_to_error (Rest.ProxyCall call, GLib.Error e) { if (e.code < 100) { // Special case for _handle_error_from_message in rest-proxy-call.c // All libsoup errors are below the HTTP response code range // so return the error and don't bother with the payload return e; } unowned string json = call.get_payload(); try { // TODO: The Utils function used to have the following to handle multiple errors: /* * if (errors.get_length () == 1) { * var err = errors.get_object_element (0); * sb.append (err.get_int_member ("code").to_string ()).append (": ") * .append (err.get_string_member ("message")) * .append ("(").append (file).append (":").append (line.to_string ()).append (")"); * } else if (errors.get_length () > 1) { * sb.append ("
    "); * errors.foreach_element ((arr, index, node) => { * var obj = node.get_object (); * sb.append ("
  • ").append (obj.get_int_member ("code").to_string ()) * .append (": ") * .append (obj.get_string_member ("message")).append ("
  • "); * }); * sb.append ("
"); * } */ var parser = new Json.Parser(); parser.load_from_data (json); var node = parser.get_root(); if (node == null) { return new GLib.Error (get_error_domain(), 0, "Twitter error is not valid JSON: %s", json); } var obj = node.get_object(); var errors = obj.get_array_member("errors"); // Assume there's always at least one, and we just take the first var error = errors.get_element(0).get_object(); // Twitter's error codes don't go above three digits - https://developer.twitter.com/en/docs/basics/response-codes var code = (int)error.get_int_member("code"); var message = error.get_string_member("message"); return new GLib.Error.literal (get_error_domain(), code, message); } catch (GLib.Error e) { return e; } } /* * Turns a Twitter error code into a translated message for the user, or uses the default message * if we haven't seen the code before */ string code_to_message(int code, string default_message){ // TODO: Remove the magic numbers (including error handling in this class) // Codes and default messages taken from https://developer.twitter.com/en/docs/basics/response-codes // Some lines commented out to avoid work for the translators for strings we aren't likely to hit switch (code) { //case 3: return _("Invalid coordinates."); // We don't do locations //case 13: return _("No location associated with the specified IP address."); // We don't do locations //case 17: return _("No user matches for specified terms."); // We don't use the users/lookup endpoint case 32: return _("Could not authenticate you"); case 34: return _("Sorry, that page does not exist"); //case 36: return _("You cannot report yourself for spam."); // We don't do spam reports //case 38: return _(" parameter is missing."); // We should always be constructing valid queries! //case 44: return _("attachment_url parameter is invalid"); // We control attachment URLs, so they should be valid case 50: return _("User not found."); case 63: return _("User has been suspended."); case 64: return _("Your account is suspended and is not permitted to access this feature"); //case 68: return _("The Twitter REST API v1 is no longer active. Please migrate to API v1.1."); // We're never going to go back to v1.0! //case 87: return _("Client is not permitted to perform this action."); // We shouldn't have UI for the not permitted tasks case 88: return _("Rate limit exceeded"); case 89: return _("Invalid or expired token"); // //case 93: return _("This application is not allowed to access or delete your direct messages"); // We request the DM permission //case 99: return _("Unable to verify your credentials."); // Not relevant to posting case 109: return _("The specified user is not a subscriber of this list."); // Not listed in the docs case 110: return _("The user you are trying to remove from the list is not a member."); // Not listed in the docs case 120: return _("Account update failed: value is too long."); // XXX: This should say how long it can be, and possibly which field ("value") case 130: return _("Over capacity"); case 131: return _("Internal error"); case 135: return _("Could not authenticate you"); case 139: return _("You have already favorited this status."); case 144: return _("No status found with that ID."); case 150: return _("You cannot send messages to users who are not following you."); case 151: return _("There was an error sending your message."); // XXX: There should be a reason here case 160: return _("You've already requested to follow this user."); // XXX: There should be a username here case 161: return _("You are unable to follow more people at this time"); case 179: return _("Sorry, you are not authorized to see this status"); case 185: return _("User is over daily status update limit"); case 186: return _("Tweet needs to be a bit shorter."); case 187: return _("Status is a duplicate"); //case 195: return _("Missing or invalid url parameter"); // We should always be constructing valid queries! //case 205: return _("You are over the limit for spam reports."); // We don't do spam reports case 214: return _("Owner must allow dms from anyone."); case 215: return _("Bad authentication data"); case 220: return _("Your credentials do not allow access to this resource."); case 226: return _("This request looks like it might be automated. To protect our users from spam and other malicious activity, we can’t complete this action right now."); case 231: return _("User must verify login"); case 261: return _("Application cannot perform write actions."); case 271: return _("You can’t mute yourself."); case 272: return _("You are not muting the specified user."); case 323: return _("Animated GIFs are not allowed when uploading multiple images."); case 324: return _("The validation of media ids failed."); case 325: return _("A media id was not found."); case 326: return _("To protect our users from spam and other malicious activity, this account is temporarily locked."); case 327: return _("You have already retweeted this Tweet."); case 349: return _("You cannot send messages to this user."); case 354: return _("The text of your direct message is over the max character limit."); case 355: return _("Subscription already exists."); case 385: return _("You attempted to reply to a Tweet that is deleted or not visible to you."); case 386: return _("The Tweet exceeds the number of allowed attachment types."); case 407: return _("The given URL is invalid."); case 416: return _("Invalid / suspended application"); // Experimental "permissions" feature: https://twittercommunity.com/t/our-experiment-with-new-tweet-settings-for-replies/137953 case 433: return _("The original Tweet author restricted who can reply to this Tweet."); // Else fall back to Twitter's (probably English) message default: return default_message; } } /** * Fetches the given tweet by ID. * * Note: This should not be used frequently as we should (in most situations) * have all of the information that we need already. * * @param account The account to fetch the tweet as * @param tweet_id The ID of the tweet to fetch * @return the tweet object or null if it failed */ async Cb.Tweet? get_tweet (Account account, int64 tweet_id) throws GLib.Error { if (tweet_id <= 0) { return null; } var call = account.proxy.new_call (); call.set_method ("GET"); call.set_function ("1.1/statuses/show.json"); call.add_param ("id", tweet_id.to_string ()); call.add_param ("include_my_retweet", "true"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); Cb.Tweet? tweet = null; GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); unowned string content = call.get_payload(); var parser = new Json.Parser (); debug ("Load tweet got: %s", content); parser.load_from_data (content); var now = new GLib.DateTime.now_local (); tweet = new Cb.Tweet (); tweet.load_from_json (parser.get_root (), account.id, now); get_tweet.callback (); } catch (GLib.Error e) { err = failed_request_to_error (call, e); get_tweet.callback (); return; } }); yield; if (err != null) { throw err; } return tweet; } /** * Posts a new tweet. */ async bool post_tweet (Account account, ComposedTweet tweet) throws GLib.Error { var media_attachments = tweet.get_attachments(); if (media_attachments.length > 0) { GLib.Error err = null; var mutex = GLib.Mutex(); var collect = new CollectById(); var yielded = false; collect.finished.connect((error) => { if (error != null) { err = error; } mutex.lock(); try { if (yielded) { post_tweet.callback(); } } finally { mutex.unlock(); } }); foreach (MediaUpload upload in media_attachments) { collect.add(upload.id); // Connect to progress_complete first so we don't have race conditions // The CollectById makes sure we don't double-count ulong handler_id = 0; handler_id = upload.progress_complete.connect((error) => { if (error != null) { err = error; } collect.emit(upload.id, err); upload.disconnect(handler_id); }); if (upload.is_uploaded()) { upload.disconnect(handler_id); collect.emit(upload.id); } } // XXX: There may be a better way to do this, but this is the best way I can think of to only yield (and call back) when we need to // The other options involve race conditions with potentially bigger windows for the check to say "not uploaded" and the upload to complete before we connect, // or we end up doing a callback when we've not yielded (or we want to do an impossible "connect, yield, and then check if anything had already finished") mutex.lock(); if (!collect.done && !collect.errored) { yielded = true; mutex.unlock(); yield; } if (err != null) { throw err; } } return yield do_post_tweet (account, tweet); } private string map_upload_to_id (MediaUpload upload) { return upload.media_id.to_string(); } private async bool do_post_tweet (Account account, ComposedTweet tweet) throws GLib.Error { var call = account.proxy.new_call(); call.set_method("POST"); call.set_function("1.1/statuses/update.json"); call.add_param("auto_populate_reply_metadata", "true"); call.add_param("tweet_mode", "extended"); call.add_param("include_ext_alt_text", "true"); if (tweet.reply_to_id > 0) { call.add_param("in_reply_to_status_id", tweet.reply_to_id.to_string()); } else if (tweet.has_quote_attachment()) { call.add_param("attachment_url", tweet.get_quoted_url()); } var media_attachments = tweet.get_attachments(); if (media_attachments.length > 0) { // Vala 0.50 can't infer the TARGET type from the return type, so we need to cast to help it var ids = map(media_attachments, (MapFunction)map_upload_to_id); call.add_param("media_ids", string.joinv(",", ids)); } call.add_param("status", tweet.get_text()); try { yield call.invoke_async(null); } catch (GLib.Error e) { throw failed_request_to_error (call, e); } inject_tweet (call.get_payload(), account); return true; } /** * Deletes the given tweet. * * @param account The account to delete the tweet from * @param tweet the tweet to delete * @return True if tweet was successfully deleted, else False */ async bool delete_tweet (Account account, Cb.Tweet tweet) throws GLib.Error { var call = account.proxy.new_call (); call.set_method ("POST"); call.set_function ("1.1/statuses/destroy/"+tweet.id.to_string ()+".json"); call.add_param ("id", tweet.id.to_string ()); var success = false; GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var tmp_error = failed_request_to_error (call, e); if (tmp_error.code != 144) { err = tmp_error; debug("Delete failed: %d", err.code); delete_tweet.callback (); return; } } inject_deletion (tweet.id, account); success = true; delete_tweet.callback (); }); yield; if (err != null) { throw err; } return success; } /** * (Un)favorites the given tweet. * * @param account The account to (un)favorite from * @param tweet The tweet to (un)favorite * @param status %true to favorite the tweet, %false to unfavorite it. * @return True if favourited status was successfully changed, else False */ async bool set_favorite_status (Account account, Cb.Tweet tweet, bool status) throws GLib.Error { if (tweet.is_flag_set (Cb.TweetState.FAVORITED) == status) { // We are already in the right state, so we didn't change it return false; } var call = account.proxy.new_call(); if (status) call.set_function ("1.1/favorites/create.json"); else call.set_function ("1.1/favorites/destroy.json"); call.set_method ("POST"); call.add_param ("id", tweet.id.to_string ()); var success = false; GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var tmp_error = failed_request_to_error (call, e); // If we can handle it cleanly, pretend it worked if ((status && tmp_error.code != 139) // Faving and we didn't get "already Faved" || (!status && tmp_error.code != 144)) { // or un-Faving and didn't get "no such status" err = tmp_error; set_favorite_status.callback (); return; } } if (status) tweet.set_flag (Cb.TweetState.FAVORITED); else tweet.unset_flag (Cb.TweetState.FAVORITED); success = true; set_favorite_status.callback (); }); yield; if (err != null) { throw err; } return success; } /** * (Un)retweets the given tweet. * * @param account The account to (un)retweet from * @param tweet The tweet to (un)retweet * @param status %true to retweet it, false to unretweet it. * @return True if retweet status was successfully changed, else False */ async bool set_retweet_status (Account account, Cb.Tweet tweet, bool status) throws GLib.Error { if (tweet.is_flag_set (Cb.TweetState.RETWEETED) == status) { // We are already in the right state, so we didn't change it return false; } var call = account.proxy.new_call (); call.set_method ("POST"); if (status) call.set_function (@"1.1/statuses/retweet/$(tweet.id).json"); else call.set_function (@"1.1/statuses/unretweet/$(tweet.my_retweet).json"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_my_retweet", "true"); call.add_param ("include_ext_alt_text", "true"); var success = false; GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try{ call.invoke_async.end (res); } catch (GLib.Error e) { var tmp_error = failed_request_to_error (call, e); // If we can handle it cleanly, pretend it worked if ((status && tmp_error.code == 327) // RTing and we got "already RTed" || (!status && tmp_error.code == 144)) { // or un-RTing and got "no such status" // Note: We don't inject the tweet because we don't have it // But we *should* pick it up on the next poll so it will be delayed rather than lost debug("Succeeded by %d", tmp_error.code); if (status) { tweet.set_flag (Cb.TweetState.RETWEETED); } else { tweet.unset_flag (Cb.TweetState.RETWEETED); } success = true; } else { debug("Failed with %d", tmp_error.code); err = tmp_error; } set_retweet_status.callback (); return; } unowned string back = call.get_payload(); var parser = new Json.Parser (); try { parser.load_from_data (back); string message = back; Cb.StreamMessageType message_type; if (status) { int64 new_id = parser.get_root ().get_object ().get_int_member ("id"); tweet.my_retweet = new_id; tweet.set_flag (Cb.TweetState.RETWEETED); message_type = Cb.StreamMessageType.TWEET; } else { inject_deletion(tweet.my_retweet, account); tweet.my_retweet = 0; tweet.unset_flag (Cb.TweetState.RETWEETED); message_type = Cb.StreamMessageType.RT_DELETE; } account.user_stream.inject_tweet(message_type, message); } catch (GLib.Error e) { info (back); err = e; return; } success = true; set_retweet_status.callback (); }); yield; if (err != null) { throw err; } return success; } async List search_for_tweets_json(Account account, string search_query, int64 max_id = -1, int64 since_id = -1, uint count = 35, GLib.Cancellable? cancellable = null) throws GLib.Error { var search_call = account.proxy.new_call (); search_call.set_function ("1.1/search/tweets.json"); search_call.set_method ("GET"); search_call.add_param ("q", search_query); if (max_id > 0) { search_call.add_param ("max_id", (max_id - 1).to_string ()); } else if (since_id > 0) { search_call.add_param ("since_id", since_id.to_string()); } search_call.add_param ("include_entities", "false"); search_call.add_param ("count", count.to_string()); List? statuses = null; GLib.Error? err = null; Cb.Utils.load_threaded_async.begin (search_call, cancellable, (_, res) => { Json.Node? search_root = null; try { search_root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { err = e; search_for_tweets_json.callback(); return; } if (search_root == null) { search_for_tweets_json.callback(); return; } var search_statuses = search_root.get_object().get_array_member("statuses"); if (search_statuses.get_length() == 0) { search_for_tweets_json.callback(); return; } string[] ids = {}; search_statuses.foreach_element ((array, index, node) => { ids += node.get_object().get_string_member("id_str"); }); var call = account.proxy.new_call (); call.set_function ("1.1/statuses/lookup.json"); call.set_method ("POST"); call.add_param ("id", string.joinv(",", ids)); call.add_param ("include_entities", "true"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_ext_alt_text", "true"); Cb.Utils.load_threaded_async.begin (call, cancellable, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { err = e; search_for_tweets_json.callback(); return; } if (root == null) { debug ("load tweets: root is null"); search_for_tweets_json.callback(); return; } statuses = root.get_array().get_elements(); statuses.sort((a, b) => { var a_id = a.get_object().get_string_member("id_str"); var b_id = b.get_object().get_string_member("id_str"); // Return values in the same order as search/tweets.json - newest first return GLib.strcmp (b_id, a_id); }); search_for_tweets_json.callback(); }); }); yield; if (err != null) { throw err; } return statuses == null ? new List() : statuses.copy(); } void set_tweet_hidden_flags(Cb.Tweet t, Account account) { if (account.filter_matches (t)) { t.set_flag (Cb.TweetState.HIDDEN_FILTERED); } if (t.retweeted_tweet != null) { if (account.is_blocked (t.source_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_RETWEETER_BLOCKED); } if (account.is_muted (t.source_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_RETWEETER_MUTED); } if (account.is_blocked (t.retweeted_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_AUTHOR_BLOCKED); } if (account.is_muted (t.retweeted_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_AUTHOR_MUTED); } if (account.disabled_rts_for (t.source_tweet.author.id)) { t.set_flag(Cb.TweetState.HIDDEN_RTS_DISABLED); } } else { if (account.is_blocked (t.source_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_AUTHOR_BLOCKED); } if (account.is_muted (t.source_tweet.author.id)) { t.set_flag (Cb.TweetState.HIDDEN_AUTHOR_MUTED); } } } async Cb.Tweet[] search_for_tweets(Account account, string search_query, int64 max_id = -1, int64 since_id = -1, uint count = 35, GLib.Cancellable? cancellable = null) throws GLib.Error { var statuses = yield search_for_tweets_json(account, search_query, max_id, since_id, count, cancellable); Cb.Tweet[] tweets = {}; var now = new GLib.DateTime.now_local (); statuses.foreach ((node) => { var tweet = new Cb.Tweet (); tweet.load_from_json (node, account.id, now); set_tweet_hidden_flags(tweet, account); tweets += tweet; }); return tweets; } public void inject_tweet (string json, Account account) { account.user_stream.inject_tweet(Cb.StreamMessageType.TWEET, json); } private void inject_user_action_json (string user_json, Account account, Cb.StreamMessageType action) { var message = @"{ \"target\": $(user_json) }"; account.user_stream.inject_tweet(action, message); } private void inject_user_action (int64 user_id, Account account, Cb.StreamMessageType action) { inject_user_action_json(@"{ \"id\":$(user_id) }", account, action); } public void inject_user_mute (string user_json, Account account) { inject_user_action_json(user_json, account, Cb.StreamMessageType.EVENT_MUTE); } public void inject_user_unmute (string user_json, Account account) { inject_user_action_json(user_json, account, Cb.StreamMessageType.EVENT_UNMUTE); } public void inject_user_block (string user_json, Account account) { inject_user_action_json(user_json, account, Cb.StreamMessageType.EVENT_BLOCK); } public void inject_user_unblock (string user_json, Account account) { inject_user_action_json(user_json, account, Cb.StreamMessageType.EVENT_UNBLOCK); } public void inject_user_follow (int64 user_id, Account account) { inject_user_action(user_id, account, Cb.StreamMessageType.EVENT_FOLLOW); } public void inject_user_unfollow (int64 user_id, Account account) { inject_user_action(user_id, account, Cb.StreamMessageType.EVENT_UNFOLLOW); } public void inject_user_hide_rts (int64 user_id, Account account) { inject_user_action(user_id, account, Cb.StreamMessageType.EVENT_HIDE_RTS); } public void inject_user_show_rts (int64 user_id, Account account) { inject_user_action(user_id, account, Cb.StreamMessageType.EVENT_SHOW_RTS); } private void inject_deletion (int64 id, Account account) { var message = @"{ \"delete\":{ \"status\":{ \"id\":$(id), \"id_str\":\"$(id)\", \"user_id\":$(account.id), \"user_id_str\":\"$(account.id)\" } } }"; account.user_stream.inject_tweet(Cb.StreamMessageType.DELETE, message); } private delegate bool UploadMediaCallback(); private class UploadProgress { private MediaUpload _upload; private double _filesize; private double _total_uploaded; private UploadMediaCallback _cb; public UploadProgress(MediaUpload media_upload, int64 filesize, size_t total_uploaded, UploadMediaCallback callback) { _upload = media_upload; _filesize = (double)filesize; _total_uploaded = (double)total_uploaded; _cb = callback; } public void callback(Rest.ProxyCall call, size_t total, size_t uploaded, GLib.Error? error, GLib.Object? weak_object){ if (error != null) { warning("Upload error: %s", error.message); _upload.progress_complete(error); _cb(); return; } _upload.progress = (_total_uploaded + uploaded) / _filesize; if (total == uploaded) { _cb(); return; } } } async bool upload_media(MediaUpload media_upload, Account account, GLib.Cancellable? cancellable = null) { var upload_proxy = new Rest.OAuthProxy(account.proxy.consumer_key, account.proxy.consumer_secret, "https://upload.twitter.com/", false); upload_proxy.set_token(account.proxy.token); upload_proxy.set_token_secret(account.proxy.token_secret); var init_call = upload_proxy.new_call(); init_call.set_function("1.1/media/upload.json"); init_call.set_method("POST"); init_call.add_param("command", "INIT"); init_call.add_param("total_bytes", media_upload.filesize.to_string()); init_call.add_param("media_type", media_upload.filetype); init_call.add_param("media_category", media_upload.media_category); Json.Node root; try { root = yield Cb.Utils.load_threaded_async(init_call, cancellable); } catch (GLib.Error e) { media_upload.progress_complete(TweetUtils.failed_request_to_error (init_call, e)); return false; } if (root == null) { warning("Null response uploading %s", media_upload.filepath); return false; } media_upload.media_id = root.get_object().get_int_member("media_id"); GLib.FileInputStream file_reader = null; try { file_reader = media_upload.read(); } catch (GLib.Error e) { media_upload.progress_complete(e); return false; } var chunk_idx = 0; size_t total_uploaded = 0; int64 filesize = media_upload.filesize; media_upload.progress = 0; while (total_uploaded < filesize) { var append_call = upload_proxy.new_call(); GLib.Bytes chunk; try { chunk = file_reader.read_bytes(MAX_CHUNK_SIZE); } catch (GLib.Error e) { media_upload.progress_complete(e); return false; } append_call.set_function("1.1/media/upload.json"); append_call.set_method("POST"); append_call.add_param("command", "APPEND"); append_call.add_param("media_id", media_upload.media_id.to_string()); append_call.add_param("segment_index", chunk_idx.to_string()); var media_param = new Rest.Param.full("media", Rest.MemoryUse.COPY, chunk.get_data(), "multipart/form-data", media_upload.filepath); append_call.add_param_full(media_param); try { // Use a helper object to work around Vala only expecting a calback to be called once before freeing its closure, // which causes segfaults var upload_progress = new UploadProgress(media_upload, filesize, total_uploaded, upload_media.callback); append_call.upload(upload_progress.callback, cancellable); yield; } catch (GLib.Error e) { media_upload.progress_complete(TweetUtils.failed_request_to_error (append_call, e)); return false; } total_uploaded += chunk.get_size(); media_upload.progress = (double)total_uploaded / (double)filesize; chunk_idx++; } var finalise_call = upload_proxy.new_call(); finalise_call.set_function("1.1/media/upload.json"); finalise_call.set_method("POST"); finalise_call.add_param("command", "FINALIZE"); finalise_call.add_param("media_id", media_upload.media_id.to_string()); try { root = yield Cb.Utils.load_threaded_async(finalise_call, cancellable); } catch (GLib.Error e) { media_upload.progress_complete(TweetUtils.failed_request_to_error (finalise_call, e)); return false; } if (root == null) { warning("Null response finalising %s", media_upload.filepath); return false; } var object = root.get_object(); while (object.has_member("processing_info")) { var processing_info = object.get_object_member("processing_info"); var state = processing_info.get_string_member("state"); if (state == "succeeded") { break; } else if (state == "failed") { var error = processing_info.get_object_member("error"); var error_code = error.get_int_member("code"); string message; if (error_code == 1) { message = _("Invalid media file"); } // TODO: Add error code 3 - seen as "unsupported codec MPEG4" but could be more else { if (error.has_member("message")) { message = error.get_string_member("message"); } else if (error.has_member("name")) { var error_name = error.get_string_member("name"); message = _("Unknown error code %lld during upload: %s").printf(error_code, error_name); } else { message = _("Unknown error code %lld during upload").printf(error_code); } } media_upload.progress_complete(new GLib.Error.literal(TweetUtils.get_error_domain(), 0, message)); return false; } else { var delay = (uint) object.get_object_member("processing_info").get_int_member("check_after_secs"); debug("Media upload processing - check after %u seconds", delay); GLib.Timeout.add (delay * 1000, () => { upload_media.callback (); return false; }, GLib.Priority.DEFAULT); yield; var status_call = upload_proxy.new_call(); status_call.set_function("1.1/media/upload.json"); status_call.set_method("GET"); status_call.add_param("command", "STATUS"); status_call.add_param("media_id", media_upload.media_id.to_string()); try { root = yield Cb.Utils.load_threaded_async(status_call, cancellable); } catch (GLib.Error e) { media_upload.progress_complete(TweetUtils.failed_request_to_error (finalise_call, e)); return false; } if (root == null) { warning("Null response checking status of %s", media_upload.filepath); return false; } object = root.get_object(); } } media_upload.finalize_upload(); return true; } /** * Downloads the avatar from the given url. * * @param avatar_url The avatar url to download * * @return The loaded avatar. */ async Gdk.Pixbuf? download_avatar (string avatar_url, int size = 48, GLib.Cancellable? cancellable = null) throws GLib.Error { Gdk.Pixbuf? avatar = null; var msg = new Soup.Message ("GET", avatar_url); if (cancellable != null) cancellable.cancelled.connect (() => { SOUP_SESSION.cancel_message (msg, Soup.Status.CANCELLED); }); GLib.Error? err = null; SOUP_SESSION.queue_message (msg, (s, _msg) => { if (_msg.status_code != Soup.Status.OK) { avatar = null; download_avatar.callback (); return; } var memory_stream = new MemoryInputStream.from_data(_msg.response_body.data, GLib.g_free); try { avatar = new Gdk.Pixbuf.from_stream_at_scale (memory_stream, size, size, false); } catch (GLib.Error e) { err = e; } download_avatar.callback (); }); yield; if (err != null) { throw err; } return avatar; } bool activate_link (string uri, MainWindow window) { debug ("Activating '%s'", uri); uri = uri._strip (); string term = uri.substring (1); if (uri.has_prefix ("@")) { int slash_index = uri.index_of ("/"); var bundle = new Cb.Bundle (); if (slash_index == -1) { bundle.put_int64 (ProfilePage.KEY_USER_ID, int64.parse (term)); window.main_widget.switch_page (Page.PROFILE, bundle); } else { bundle.put_int64 (ProfilePage.KEY_USER_ID, int64.parse (term.substring (0, slash_index - 1))); bundle.put_string (ProfilePage.KEY_SCREEN_NAME, term.substring (slash_index + 1, term.length - slash_index - 1)); window.main_widget.switch_page (Page.PROFILE, bundle); } return true; } else if (uri.has_prefix ("#")) { var bundle = new Cb.Bundle (); bundle.put_string (SearchPage.KEY_QUERY, uri); window.main_widget.switch_page (Page.SEARCH, bundle); return true; } else if (uri.has_prefix ("https://twitter.com/")) { // https://twitter.com/baedert/status/321423423423 string[] parts = uri.split ("/"); if (parts[4] == "status") { /* Treat it as a tweet link and hope it'll work out */ int64 tweet_id = int64.parse (parts[5]); var bundle = new Cb.Bundle (); bundle.put_int (TweetInfoPage.KEY_MODE, TweetInfoPage.BY_ID); bundle.put_int64 (TweetInfoPage.KEY_TWEET_ID, tweet_id); bundle.put_string (TweetInfoPage.KEY_SCREEN_NAME, parts[3]); window.main_widget.switch_page (Page.TWEET_INFO, bundle); return true; } } else if (uri.has_prefix ("translate-")) { var parts = uri.substring(10).split(":", 2); var src_lang = parts[0]; if (src_lang == "und") { src_lang = "auto"; } var content = parts[1]; var target_lang = Utils.get_user_language(); var url = Settings.get_translation_service_url(); url = url.replace("{SOURCE_LANG}", src_lang).replace("{TARGET_LANG}", target_lang).replace("{CONTENT}", content); try { Gtk.show_uri_on_window(window, url, Gdk.CURRENT_TIME); return true; } catch (Error e) { Utils.show_error_dialog(e, window); } } return false; } void work_array (Json.Array json_array, TweetListBox tweet_list, Account account) { uint n_tweets = json_array.get_length (); /* If the request returned no results at all, we don't need to do all the later stuff */ if (n_tweets == 0) { return; } var now = new GLib.DateTime.now_local (); for (uint i = 0; i < n_tweets; i++) { var tweet = new Cb.Tweet (); tweet.load_from_json (json_array.get_element (i), account.id, now); if (account.user_counter == null || tweet_list == null || !(tweet_list.get_toplevel () is Gtk.Window)) break; account.user_counter.id_seen (ref tweet.source_tweet.author); if (tweet.retweeted_tweet != null) account.user_counter.id_seen (ref tweet.retweeted_tweet.author); set_tweet_hidden_flags(tweet, account); tweet_list.model.add (tweet); } } public void handle_media_click (Cb.Media[] media, MainWindow window, int index) { #if ! VIDEO if (media.length == 1 && media[0].is_video()) { var url = media[0].url; try { Gtk.show_uri_on_window(window, url, Gdk.CURRENT_TIME); } catch (GLib.Error e) { warning ("Unable to open %s: %s", url, e.message); } return; } #endif Gdk.Display default_display = Gdk.Display.get_default(); Gdk.Monitor current_monitor = default_display.get_monitor_at_window(window.get_window()); Gdk.Rectangle workarea = current_monitor.get_workarea(); Gdk.Rectangle max_dimensions = { 0, 0, (int)Math.round(workarea.width * 0.95), (int)Math.round(workarea.height * 0.95) }; MediaDialog media_dialog = new MediaDialog (media, index, max_dimensions); media_dialog.set_transient_for (window); media_dialog.set_modal (true); media_dialog.show (); } public void sort_entities (ref Cb.TextEntity[] entities) { /* Just use bubblesort here. Our n is very small (< 15 maybe?) */ for (int i = 0; i < entities.length; i ++) { for (int k = 0; k < entities.length; k ++) { if (entities[i].from < entities[k].from) { Cb.TextEntity c = entities[i]; entities[i] = entities[k]; entities[k] = c; } } } } public void log_tweet (Cb.Tweet tweet) { #if DEBUG stdout.printf (tweet.json_data+"\n"); #endif message ("Seen : %s", tweet.get_seen ().to_string ()); message ("My retweet: %s", tweet.my_retweet.to_string ()); message ("Retweeted: %s", tweet.is_flag_set (Cb.TweetState.RETWEETED).to_string ()); message ("Favorited: %s", tweet.is_flag_set (Cb.TweetState.FAVORITED).to_string ()); message ("Protected: %s", tweet.is_flag_set (Cb.TweetState.PROTECTED).to_string ()); message ("State : %s", tweet.state.to_string ()); message ("Source tweet author id: %s", tweet.source_tweet.author.id.to_string ()); message ("Source tweet author screen_name: %s", tweet.source_tweet.author.screen_name); if (tweet.retweeted_tweet != null) { message ("Retweet!"); message ("Retweet author id: %s", tweet.retweeted_tweet.author.id.to_string ()); message ("Retweet author screen_name: %s", tweet.retweeted_tweet.author.screen_name); } if (tweet.has_inline_media ()) { foreach (Cb.Media m in tweet.get_medias ()) { message ("Media: %p", m); } } } public int rerun_filters (TweetListBox tweet_list, Account account) { Cb.TweetModel tm = tweet_list.model; // Count how many unseen tweets we've hidden var hidden_unseen = 0; for (uint i = 0; i < tm.get_n_items (); i ++) { var tweet = (Cb.Tweet) tm.get_object (i); if (account.filter_matches (tweet)) { if (tm.set_tweet_flag (tweet, Cb.TweetState.HIDDEN_FILTERED)) { i --; } if (!tweet.get_seen ()) { hidden_unseen++; tweet.set_seen (true); } } else { if (tm.unset_tweet_flag (tweet, Cb.TweetState.HIDDEN_FILTERED)) { i --; } } } // Same thing for invisible tweets... for (uint i = 0; i < tm.hidden_tweets.length; i ++) { var tweet = tm.hidden_tweets.get (i); if (tweet.is_flag_set (Cb.TweetState.HIDDEN_FILTERED)) { if (!account.filter_matches (tweet)) { tm.unset_tweet_flag (tweet, Cb.TweetState.HIDDEN_FILTERED); i --; } } } return hidden_unseen; } } cawbird-1.4.2/src/util/UserCompletion.vala000066400000000000000000000035661416632607600205370ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class UserCompletion : GLib.Object { public signal void start_completion (); public signal void populate_completion (string screen_name, string name); private unowned GLib.Object obj; private unowned Account account; private string name_property_name; private int num_results; public UserCompletion (Account account, int num_results) { this.account = account; this.num_results = num_results; } public void connect_to (GLib.Object obj, string name_property_name) { this.obj = obj; this.name_property_name = name_property_name; obj.notify[name_property_name].connect (prop_changed); } private void prop_changed () { string name; obj.get (name_property_name, out name); if (name.has_prefix ("@")) name = name.substring (1); start_completion (); Cb.UserInfo[] names; account.user_counter.query_by_prefix (account.db.get_sqlite_db (), name, 10, out names); for (int i = 0; i < names.length; i++) populate_completion (names[i].screen_name, names[i].user_name); } } cawbird-1.4.2/src/util/UserUtils.vala000066400000000000000000000224341416632607600175210ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ const uint FRIENDSHIP_FOLLOWED_BY = 1 << 0; const uint FRIENDSHIP_FOLLOWING = 1 << 1; const uint FRIENDSHIP_WANT_RETWEETS = 1 << 2; const uint FRIENDSHIP_BLOCKING = 1 << 3; const uint FRIENDSHIP_MUTING = 1 << 4; const uint FRIENDSHIP_CAN_DM = 1 << 5; struct JsonCursor { int64 next_cursor; bool full; Json.Node? json_object; } namespace UserUtils { async uint load_friendship (Account account, int64 user_id, string screen_name) { var call = account.proxy.new_call (); call.set_function ("1.1/friendships/show.json"); call.set_method ("GET"); call.add_param ("source_id", account.id.to_string ()); if (user_id != 0) call.add_param ("target_id", user_id.to_string ()); else call.add_param ("target_screen_name", screen_name); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return 0; } var relationship = root.get_object ().get_object_member ("relationship"); var target = relationship.get_object_member ("target"); var source = relationship.get_object_member ("source"); uint friendship = 0; if (target.get_boolean_member ("following")) friendship |= FRIENDSHIP_FOLLOWED_BY; if (target.get_boolean_member ("followed_by")) friendship |= FRIENDSHIP_FOLLOWING; if (source.get_boolean_member ("want_retweets")) friendship |= FRIENDSHIP_WANT_RETWEETS; if (source.get_boolean_member ("blocking")) friendship |= FRIENDSHIP_BLOCKING; if (source.get_boolean_member ("muting")) friendship |= FRIENDSHIP_MUTING; if (source.get_boolean_member ("can_dm")) friendship |= FRIENDSHIP_CAN_DM; return friendship; } async JsonCursor? load_followers (Account account, int64 user_id, JsonCursor? old_cursor) { const int requested = 25; var call = account.proxy.new_call (); call.set_function ("1.1/followers/list.json"); call.set_method ("GET"); call.add_param ("user_id", user_id.to_string ()); call.add_param ("count", requested.to_string ()); call.add_param ("skip_status", "true"); call.add_param ("include_user_entities", "false"); if (old_cursor != null) call.add_param ("cursor", old_cursor.next_cursor.to_string ()); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return null; } var root_obj = root.get_object (); var user_array = root_obj.get_array_member ("users"); JsonCursor cursor = JsonCursor (); cursor.next_cursor = root_obj.get_int_member ("next_cursor"); cursor.full = (user_array.get_length () < requested); cursor.json_object = root_obj.get_member ("users"); return cursor; } async JsonCursor? load_following (Account account, int64 user_id, JsonCursor? old_cursor) { const int requested = 25; var call = account.proxy.new_call (); call.set_function ("1.1/friends/list.json"); call.set_method ("GET"); call.add_param ("user_id", user_id.to_string ()); call.add_param ("count", requested.to_string ()); call.add_param ("skip_status", "true"); call.add_param ("include_user_entities", "false"); if (old_cursor != null) call.add_param ("cursor", old_cursor.next_cursor.to_string ()); Json.Node? root = null; try { root = yield Cb.Utils.load_threaded_async (call, null); } catch (GLib.Error e) { warning (e.message); return null; } var root_obj = root.get_object (); var user_array = root_obj.get_array_member ("users"); JsonCursor cursor = JsonCursor (); cursor.next_cursor = root_obj.get_int_member ("next_cursor"); cursor.full = (user_array.get_length () < requested); cursor.json_object = root_obj.get_member ("users"); return cursor; } async void mute_user (Account account, int64 to_block, bool setting) throws GLib.Error { var call = account.proxy.new_call (); call.set_method ("POST"); if (setting) { call.set_function ("1.1/mutes/users/create.json"); call.add_param ("include_entities", "false"); call.add_param ("skip_status", "true"); } else { call.set_function ("1.1/mutes/users/destroy.json"); } call.add_param ("user_id", to_block.to_string ()); GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); if (setting) { TweetUtils.inject_user_mute(call.get_payload(), account); } else { TweetUtils.inject_user_unmute(call.get_payload(), account); } } catch (GLib.Error e) { var tmp_err = TweetUtils.failed_request_to_error (call, e); // Muting muted users fails silently, so errors are important, but code 272 means // we unmuted someone who was already unmuted if (setting || tmp_err.domain != TweetUtils.get_error_domain() || tmp_err.code != 272) { err = tmp_err; } } mute_user.callback(); }); yield; if (err != null) { throw err; } } async void block_user (Account account, int64 to_block, bool setting) throws GLib.Error { var call = account.proxy.new_call (); call.set_method ("POST"); if (setting) { call.set_function ("1.1/blocks/create.json"); } else { call.set_function ("1.1/blocks/destroy.json"); } call.add_param ("user_id", to_block.to_string ()); call.add_param ("include_entities", "false"); call.add_param ("skip_status", "true"); GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); if (setting) { TweetUtils.inject_user_block(call.get_payload(), account); } else { TweetUtils.inject_user_unblock(call.get_payload(), account); } } catch (GLib.Error e) { var tmp_err = TweetUtils.failed_request_to_error (call, e); debug("Error: %s", tmp_err.message); // Muting muted users fails silently, so errors are important, but code 272 means // we unmuted someone who was already unmuted if (setting || tmp_err.domain != TweetUtils.get_error_domain() || tmp_err.code != 272) { err = tmp_err; } } block_user.callback(); }); yield; if (err != null) { throw err; } } async Json.Array load_user_timeline_by_id(Account account, int64 user_id, int tweet_count, int64 since_id = -1, int64 max_id = -1) throws GLib.Error { return yield load_user_timeline(account, "user_id", user_id.to_string(), tweet_count, since_id, max_id); } async Json.Array load_user_timeline_by_screen_name(Account account, string screen_name, int tweet_count, int64 since_id = -1, int64 max_id = -1) throws GLib.Error { return yield load_user_timeline(account, "screen_name", screen_name, tweet_count, since_id, max_id); } private async Json.Array load_user_timeline(Account account, string field, string value, int tweet_count, int64 since_id, int64 max_id) throws GLib.Error { var call = account.proxy.new_call (); call.set_function ("1.1/statuses/user_timeline.json"); call.set_method ("GET"); call.add_param (field, value); if (since_id > 0) { call.add_param ("since_id", since_id.to_string()); } if (max_id > 0) { call.add_param ("max_id", max_id.to_string()); } call.add_param ("count", tweet_count.to_string()); call.add_param ("contributor_details", "true"); call.add_param ("tweet_mode", "extended"); call.add_param ("include_my_retweet", "true"); call.add_param ("include_ext_alt_text", "true"); Json.Node? root = null; Json.Array? array = null; try { root = yield Cb.Utils.load_threaded_async (call, null); if (root != null) { array = root.get_array(); } } catch (GLib.Error e) { if (e.domain == Rest.ProxyError.quark() && e.code == new Rest.ProxyError.SSL ("workaround").code) { debug ("Reloading user timeline on SSL failure"); array = yield load_user_timeline(account, field, value, tweet_count, since_id, max_id); } else { throw e; } } return array ?? new Json.Array(); } }cawbird-1.4.2/src/util/Utils.vala000066400000000000000000000571061416632607600166660ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ enum Page { STREAM = 0, MENTIONS, FAVORITES, DM_THREADS, LISTS, FILTERS, SEARCH, PROFILE, TWEET_INFO, DM, LIST_STATUSES, PREVIOUS = 1024, NEXT = 2048 } public Quark get_error_domain() { return Quark.from_string("cawbird-utils"); } static Soup.Session SOUP_SESSION = null; const int TRANSITION_DURATION = 200 * 1000; public delegate void VadjustOverScroll (Gtk.ScrolledWindow parent, Gtk.ListBox list_box, int over_scroll); #if DEBUG public unowned string __class_name (GLib.Object o) { return GLib.Type.from_instance (o).name (); } #endif void default_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { if (row_before == null) { row.set_header (null); return; } Gtk.Widget? header = row.get_header (); if (header != null) { return; } header = new Gtk.Separator (Gtk.Orientation.HORIZONTAL); header.show (); row.set_header (header); } int twitter_item_sort_func (Gtk.ListBoxRow a, Gtk.ListBoxRow b) { if(((Cb.TwitterItem)a).get_sort_factor () < ((Cb.TwitterItem)b).get_sort_factor ()) return 1; return -1; } int twitter_item_sort_func_inv (Gtk.ListBoxRow a, Gtk.ListBoxRow b) { if(((Cb.TwitterItem)a).get_sort_factor () < ((Cb.TwitterItem)b).get_sort_factor ()) return -1; return 1; } Cairo.Surface? load_surface (string path) { try { var p = new Gdk.Pixbuf.from_file (path); var s = Gdk.cairo_surface_create_from_pixbuf (p, 1, null); return s; } catch (GLib.Error e) { warning (e.message); return null; } } Cairo.Surface create_video_placeholder_surface () { try { Gdk.Pixbuf pixbuf; try { pixbuf = Gtk.IconTheme.get_default().load_icon("video-x-generic", 512, 0); } catch (GLib.Error e) { pixbuf = new Gdk.Pixbuf.from_resource("/uk/co/ibboard/cawbird/data/symbolic/apps/cawbird-video-placeholder.svg"); } return Gdk.cairo_surface_create_from_pixbuf (pixbuf, 1, null); } catch (GLib.Error e) { warning("Failed to load video icon *and* fallback resource!"); return new Cairo.ImageSurface(Cairo.Format.RGB24, 512, 512); } } Cairo.Surface? load_surface_for_video (string path) { #if VIDEO try { // Based on https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/-/blob/master/tests/examples/snapshot/snapshot.c var playbin = Gst.parse_launch("uridecodebin uri=file://%s ! videoconvert ! videoscale ! appsink name=sink caps=\"video/x-raw,format=BGRx,pixel-aspect-ratio=1/1\"".printf(path)); playbin.set_state(Gst.State.PAUSED); // Wait for the state change playbin.get_state(null, null, 5 * Gst.SECOND); playbin.seek_simple(Gst.Format.TIME, Gst.SeekFlags.KEY_UNIT | Gst.SeekFlags.FLUSH, 1000); var sink = ((Gst.Bin)playbin).get_by_name("sink"); Gst.Sample sample; Signal.emit_by_name(sink, "pull-preroll", out sample, null); if(sample == null) { throw new GLib.Error(get_error_domain(), 1, "Null GStreamer sample"); } var sample_caps = sample.get_caps(); if(sample_caps == null) { throw new GLib.Error(get_error_domain(), 2, "Null GStreamer sample caps"); } unowned Gst.Structure structure = sample_caps.get_structure(0); int width = structure.get_value("width").get_int(); int height = structure.get_value("height").get_int(); var buffer = sample.get_buffer(); size_t offset; size_t max_size; var size = buffer.get_sizes(out offset, out max_size); uint8[] data; buffer.extract_dup(offset, size, out data); // Paint one surface on to another so that we don't have to keep the byte array around var screencap_surface = new Cairo.ImageSurface.for_data(data, Cairo.Format.RGB24, width, height, Cairo.Format.RGB24.stride_for_width(width)); var surface = new Cairo.ImageSurface(Cairo.Format.RGB24, width, height); var context = new Cairo.Context(surface); context.set_source_surface(screencap_surface, 0, 0); context.paint(); return surface; } catch (GLib.Error e) { warning("Error creating thumbnail from video for %s: %s", path, e.message); return create_video_placeholder_surface(); } #else // Video support disabled return create_video_placeholder_surface(); #endif } void write_surface (Cairo.Surface surface, string path) { var status = surface.write_to_png (path); if (status != Cairo.Status.SUCCESS) { warning ("Could not write surface to '%s': %s", path, status.to_string ()); } } Cairo.Surface scale_surface (Cairo.ImageSurface input, int output_width, int output_height) { int old_width = input.get_width (); int old_height = input.get_height (); if (old_width == output_width && old_height == output_height) return input; Cairo.Surface new_surface = new Cairo.Surface.similar_image (input, Cairo.Format.ARGB32, output_width, output_height); /* http://lists.cairographics.org/archives/cairo/2006-January/006178.html */ Cairo.Context ct = new Cairo.Context (new_surface); ct.scale ((double)output_width / old_width, (double)output_height / old_height); ct.set_source_surface (input, 0, 0); ct.get_source ().set_extend (Cairo.Extend.PAD); ct.set_operator (Cairo.Operator.SOURCE); ct.paint (); return new_surface; } inline double ease_out_cubic (double t) { double p = t - 1; return p * p * p +1; } public delegate TARGET MapFunction(SOURCE src); TARGET[] map(SOURCE[] arr, MapFunction map_func) { var mapped = new GenericArray(arr.length); foreach (SOURCE src in arr) { mapped.add(map_func(src)); } return mapped.data; } namespace Utils { /** * Removes the retweet flag from a tweet in a model based on a new "un-retweeted" message */ public void unrt_tweet (Json.Node obj, Cb.TweetModel model) { int64 tweet_id = obj.get_object ().get_int_member ("id"); Cb.Tweet? existing_tweet = model.get_for_id (tweet_id, 0); if (existing_tweet != null) { model.unset_tweet_flag (existing_tweet, Cb.TweetState.RETWEETED); } } /** * Sets the retweet flag for a tweet in a model based on a new tweet message */ public void set_rt_from_tweet (Json.Node root, Cb.TweetModel model, Account account) { var obj = root.get_object (); if (!obj.has_member ("retweeted_status")) { return; } var rt_status = obj.get_object_member ("retweeted_status"); var rt_author = rt_status.get_object_member ("user").get_int_member ("id"); if (rt_author != account.id) { return; } int64 tweet_id = rt_status.get_int_member ("id"); Cb.Tweet? existing_tweet = model.get_for_id (tweet_id, 0); if (existing_tweet != null) { model.set_tweet_flag (existing_tweet, Cb.TweetState.RETWEETED); } } /** * Calculates an easily human-readable version of the time difference between * time and now. * Example: "5m" or "3h" or "26m" or "16 Nov" * * Passing `extended` returns "minutes ago" or "hours ago" and full months instead of "h" or "m" * and abbreviated months */ public string get_time_delta (GLib.DateTime time, GLib.DateTime now, bool extended = false) { //diff is the time difference in microseconds GLib.TimeSpan diff = now.difference (time); int minutes = (int)(diff / 1000.0 / 1000.0 / 60.0); if (minutes == 0) { return _("Now"); } else if (minutes < 60) { if (extended) { return ngettext("%d minute ago", "%d minutes ago", minutes).printf (minutes); } else { // TRANSLATORS: short-form "x minutes ago" return _("%dm").printf (minutes); } } int hours = (int)(minutes / 60.0); if (hours < 24) { if (extended) { return ngettext("%d hour ago", "%d hours ago", hours).printf (hours); } else { // TRANSLATORS: short-form "x hours ago" return _("%dh").printf (hours); } } string date_format; if (time.get_year () == now.get_year ()) { if (extended) { // TRANSLATORS: Full-text date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html date_format = _("%e %B"); } else { // TRANSLATORS: Short date format for tweets from this year - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html date_format = _("%e %b"); } } else { if (extended) { // TRANSLATORS: Full-text date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html date_format = _("%e %B %Y"); } else { // TRANSLATORS: Short date format for tweets from previous years - see https://valadoc.org/glib-2.0/GLib.DateTime.format.html date_format = _("%e %b %Y"); } } return time.format(date_format); } /** * Shows an error dialog for a given GLib error * (Unless it is "Operation was cancelled", and then we ignore it) * * @param e The error to show */ void show_error_dialog (GLib.Error e, Gtk.Window? transient_for = null, string? file = null, int line = 0) { if (e is GLib.IOError.CANCELLED) { // It's not really an error, so don't show it return; } string message; if (e.domain == TweetUtils.get_error_domain()) { message = TweetUtils.code_to_message(e.code, e.message); } else { message = e.message; } // Log the original message rather than the translation if (file != null) { warning ("Exception %s:%d: %s in %s:%d", e.domain.to_string(), e.code, e.message, file, line); } else { warning ("Exception %s:%d: %s", e.domain.to_string(), e.code, e.message); } var dialog = new Gtk.MessageDialog (transient_for, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "%s", message); dialog.set_modal (true); /* Hacky way to get the label selectable */ ((Gtk.Label)(((Gtk.Container)dialog.get_message_area ()).get_children ().nth_data (0))).set_selectable (true); dialog.response.connect ((id) => { if (id == Gtk.ResponseType.OK) dialog.destroy (); }); dialog.show (); } async Gdk.Pixbuf? download_pixbuf (string url, GLib.Cancellable? cancellable = null) { Gdk.Pixbuf? result = null; var msg = new Soup.Message ("GET", url); GLib.SourceFunc cb = download_pixbuf.callback; SOUP_SESSION.queue_message (msg, (_s, _msg) => { if (cancellable.is_cancelled ()) { cb (); return; } try { var in_stream = new MemoryInputStream.from_data (_msg.response_body.data, GLib.g_free); result = new Gdk.Pixbuf.from_stream (in_stream, cancellable); } catch (GLib.Error e) { warning (e.message); } finally { cb (); } }); yield; return result; } string unescape_html (string input) { string back = input.replace ("<", "<"); back = back.replace (">", ">"); back = back.replace ("&", "&"); return back; } /** * Gets the word string (and matching begin/end iterators) around the cursor in the buffer (terminated by whitespace) */ public string get_cursor_word (Gtk.TextBuffer buffer, out Gtk.TextIter start_iter, out Gtk.TextIter end_iter) { Gtk.TextMark cursor_mark = buffer.get_insert (); Gtk.TextIter cursor_iter; buffer.get_iter_at_mark (out cursor_iter, cursor_mark); start_iter = end_iter = cursor_iter; for (;;) { Gtk.TextIter left_iter = start_iter; left_iter.backward_char (); unichar c = left_iter.get_char(); if (c.isspace()) { break; } start_iter = left_iter; if (start_iter.is_start()) { break; } } for (;;) { unichar c = end_iter.get_char(); if (c == 0 || c.isspace()) { break; } end_iter.forward_char (); } return buffer.get_text (start_iter, end_iter, false); } /** * Gets the @-mention string (and matching begin/end iterators) around the cursor in the buffer, ignoring preceding and * following punctiation */ public string get_cursor_mention_word (Gtk.TextBuffer buffer, out Gtk.TextIter start_iter, out Gtk.TextIter end_iter) { string cursor_word = get_cursor_word (buffer, out start_iter, out end_iter); bool changed = false; for (;;) { unichar c = start_iter.get_char(); if (c == 0 || c == '@') { break; } else if (c.ispunct()) { changed = true; start_iter.forward_char(); } else { cursor_word = ""; end_iter = start_iter; break; } } for (;;) { if (end_iter.get_offset() <= start_iter.get_offset()) { break; } Gtk.TextIter prev_iter = end_iter; prev_iter.backward_char(); unichar c = prev_iter.get_char(); if (!c.ispunct()) { break; } end_iter = prev_iter; changed = true; if (end_iter.is_start()) { break; } } if (changed) { cursor_word = buffer.get_text(start_iter, end_iter, false); } return cursor_word; } public void load_custom_icons () { var icon_theme = Gtk.IconTheme.get_default (); icon_theme.add_resource_path ("/uk/co/ibboard/cawbird/data/"); } public void load_custom_css () { var provider = new Gtk.CssProvider (); provider.load_from_resource ("/uk/co/ibboard/cawbird/ui/style.css"); Gtk.StyleContext.add_provider_for_screen ((!)Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } public void init_soup_session () { assert (SOUP_SESSION == null); SOUP_SESSION = new Soup.Session (); } /** * Checks if @value is existing in @node and if it is, non-null. * * Returns TRUE if the @value does both exist and is non-null. */ public bool usable_json_value (Json.Object node, string value_name) { if (!node.has_member (value_name)) return false; return !node.get_null_member (value_name); } public void update_startup_account (string old_screen_name, string new_screen_name) { string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); string[] new_startup_accounts = new string[startup_accounts.length]; for (int i = 0; i < startup_accounts.length; i ++) { if (startup_accounts[i] != old_screen_name) new_startup_accounts[i] = startup_accounts[i]; else new_startup_accounts[i] = new_screen_name; } Settings.get ().set_strv ("startup-accounts", new_startup_accounts); } public Cb.Filter create_persistent_filter (string content, Account account) { int id = (int)account.db.insert ("filters") .val ("content", content) .run(); Cb.Filter f = new Cb.Filter (content); f.set_id (id); account.add_filter (f); return f; } public string get_media_display_name (Cb.Media media) { unowned string url = media.target_url ?? media.url; int last_slash_index = url.last_index_of_char ('/'); string filename = url.substring (last_slash_index + 1); filename = filename.replace (":orig", ""); int last_dot_index = filename.last_index_of_char ('.'); if (last_dot_index == -1) { // No file extension, guess! if (media.is_video ()) { filename += ".mp4"; } else { filename += ".jpg"; } } return filename; } public async void download_file (string url, GLib.OutputStream out_stream) { var msg = new Soup.Message ("GET", url); GLib.SourceFunc cb = download_file.callback; SOUP_SESSION.queue_message (msg, (_s, _msg) => { try { var in_stream = new MemoryInputStream.from_data (_msg.response_body.data, GLib.g_free); out_stream.splice (in_stream, GLib.OutputStreamSpliceFlags.CLOSE_SOURCE | GLib.OutputStreamSpliceFlags.CLOSE_TARGET, null); } catch (GLib.Error e) { warning (e.message); } finally { cb (); } }); yield; } public string linkify_screen_name (Cb.UserIdentity user) { return linkify_identity(user, "@%s".printf(user.screen_name)); } public string linkify_user (Cb.UserIdentity user, bool boldify = false) { return linkify_identity (user, GLib.Markup.escape_text(user.user_name), boldify); } private string linkify_identity (Cb.UserIdentity user, string display_text, bool boldify = false) { var buff = new StringBuilder (); if (boldify) { buff.append(""); } buff.append ("") .append (display_text) .append (""); if (boldify) { buff.append(""); } return buff.str; } public void connect_vadjustment (Gtk.ScrolledWindow parent, Gtk.ListBox list_box, VadjustOverScroll? scroll_past_top_func = null) { // Ideally we'd just do: // list_box.set_focus_vadjustment(parent.get_vadjustment()) // but that gives no way of doing an offset, so we've got to // do all of the calculations ourselves. // https://stackoverflow.com/a/8912336/283242 list_box.set_focus_child.connect((focussed_child) => { if (focussed_child == null) { return; } double widget_left, widget_top; focussed_child.translate_coordinates(parent, 0, 0, out widget_left, out widget_top); // The coordinate translation doesn't take into account the existing vadjustment! So add it back. widget_top += parent.vadjustment.value; Gtk.Allocation alloc; focussed_child.get_allocation(out alloc); var widget_bottom = widget_top + alloc.height; var viewport_top = parent.vadjustment.value; var viewport_bottom = viewport_top + parent.vadjustment.page_size; if (parent.vadjustment.value == 0 && widget_top < 0) { if (scroll_past_top_func != null) { scroll_past_top_func (parent, list_box, (int)Math.ceil(-widget_top)); } } else if (widget_top < viewport_top) { parent.vadjustment.value = widget_top; } else if (widget_bottom > viewport_bottom) { parent.vadjustment.value = widget_bottom - parent.vadjustment.page_size; } }); } public bool set_pointer_on_mouseover(Gdk.EventCrossing event){ if (event.type == Gdk.EventType.ENTER_NOTIFY) { event.window.set_cursor(new Gdk.Cursor.from_name(event.window.get_display(), "pointer")); } else if (event.type == Gdk.EventType.LEAVE_NOTIFY) { event.window.set_cursor(null); } return Gdk.EVENT_PROPAGATE; } public bool is_animated_gif(string filepath) { if (filepath.has_suffix(".gif")) { // Be lazy and light-weight and assume GIFs are ".gif" rather than doing proper type checking try { var gif = new Gdk.PixbufAnimation.from_file(filepath); return !gif.is_static_image(); } catch (GLib.Error e) { return false; } } else { return false; } } public string get_user_language() { var langs = GLib.Intl.get_language_names(); var lang = langs[0]; // Remove any country information (e.g. en_GB → en) var user_lang = lang.split("_", 2)[0]; if (user_lang.length < 2) { // User probably had "C", so assume English user_lang = "en"; } return user_lang; } public string create_translate_url(Cb.Tweet tweet) { var text = tweet.get_real_text().replace("<", "<").replace(">", ">").replace("&", "&"); return "translate-%s:%s".printf(tweet.get_language(), GLib.Uri.escape_string(text)); } public bool needs_translating (Cb.Tweet tweet) { var tweet_language = tweet.get_language(); return tweet_language != null && tweet_language != get_user_language(); } public void calculate_draw_offset (int img_width, int img_height, int widget_width, int widget_height, out int draw_x, out int draw_y, out double scale) { if (img_width == -1 && img_height == -1) { draw_x = 0; draw_y = 0; scale = 0.0; return; } scale = double.min(1, widget_width / (double) img_width); var scaled_height = (int)Math.ceil(img_height * scale); if (scaled_height <= widget_height) { draw_y = widget_height - scaled_height; } else { draw_y = (int)Math.floor((widget_height - scaled_height) / 2); } draw_x = int.max(0, (int)Math.round((widget_width - img_width * scale) / 2)); } public string build_reply_to_string(Cb.UserIdentity[] reply_users, int64 author_id, bool and_others = true) { int user_count = reply_users.length; if (user_count == 1) { return _("Replying to %s").printf(linkify_screen_name(reply_users[0])); } else if (user_count > 2 && and_others) { Cb.UserIdentity? user = null; Cb.UserIdentity reply_user; // Find the first user who isn't the author, because that's more interesting for (int i = 0; i < user_count; i++) { reply_user = reply_users[i]; if (reply_user.id != author_id) { user = reply_user; break; } } if (user == null) { // SHOULD NEVER HAPPEN! We've got at least three users, so they shouldn't all be the author! // But be safe, just in case. user = reply_users[0]; } // TRANSLATORS: Note: "…and 1 other" should never be used because it will use the "Replying to X and Y" string return ngettext("Replying to %s and %d other", "Replying to %s and %d others", user_count - 1).printf(linkify_screen_name(user), user_count - 1); } else { int last_idx = user_count - 1; // It would be nice to use our map() function here but we get "not a supported generic type argument" // error: ‘CB_TYPE_USER_IDENTITY’ undeclared (first use in this function); did you mean ‘CB_TYPE_USER_COUNTER’? string[] user_strings = new string[user_count]; for (int i = 0; i < user_count; i ++) { user_strings[i] = linkify_screen_name(reply_users[i]); } var user_list = user_strings[0:last_idx]; // TRANSLATORS: The first %s is a list of one or more comma-separated user names, and the second %s is the last username in the list return _("Replying to %s and %s").printf(string.joinv(", ", user_list), user_strings[last_idx]); } } }cawbird-1.4.2/src/widgets/000077500000000000000000000000001416632607600154015ustar00rootroot00000000000000cawbird-1.4.2/src/widgets/AccountCreateWidget.vala000066400000000000000000000157371416632607600221470ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/account-create-widget.ui")] class AccountCreateWidget : Gtk.Box { [GtkChild] private unowned Gtk.Entry pin_entry; [GtkChild] private unowned Gtk.Label error_label; [GtkChild] private unowned Gtk.Button confirm_button; [GtkChild] private unowned Gtk.Button request_pin_button; [GtkChild] private unowned Gtk.Label info_label; [GtkChild] private unowned Gtk.Stack content_stack; private Rest.OAuthProxy proxy; private unowned Cawbird cawbird; private unowned MainWindow main_window; public signal void account_created (Account acc); public AccountCreateWidget (Cawbird cawbird, MainWindow main_window) { this.cawbird = cawbird; this.main_window = main_window; info_label.label = "%s %s" .printf (_("Don't have a Twitter account yet?"), _("Create one")); pin_entry.buffer.deleted_text.connect (pin_changed_cb); pin_entry.buffer.inserted_text.connect (pin_changed_cb); } private void pin_request_cb (Rest.OAuthProxy proxy, Error? error, Object? weak_object) { if (error != null) { Utils.show_error_dialog (error, this.main_window); critical (error.message); return; } string uri = "http://twitter.com/oauth/authorize?oauth_token=" + proxy.get_token(); debug ("Trying to open %s", uri); try { GLib.AppInfo.launch_default_for_uri (uri, null); } catch (GLib.Error e) { this.show_error (_("Could not open %s").printf ("" + uri + "")); Utils.show_error_dialog (e, this.main_window); critical ("Could not open %s", uri); critical (e.message); } } public void open_pin_request_site () { proxy = new Rest.OAuthProxy(Settings.get_consumer_key(), Settings.get_consumer_secret(), "https://api.twitter.com/", false); try { if (!proxy.request_token_async ("oauth/request_token", "oob", pin_request_cb, this)) { show_error(_("Failed to retrieve request token")); } } catch(GLib.Error e) { Utils.show_error_dialog (e, this.main_window); critical (e.message); } } [GtkCallback] private void request_pin_clicked_cb () { open_pin_request_site (); content_stack.visible_child_name = "pin"; } [GtkCallback] private async void confirm_button_clicked_cb () { pin_entry.sensitive = false; confirm_button.sensitive = false; request_pin_button.sensitive = false; this.do_confirm.begin (); } private void confirm_cb (Rest.OAuthProxy proxy, Error? error, Object? weak_object) { if (error != null) { critical (error.message); // We just assume that it was the wrong code show_error (_("Wrong PIN")); pin_entry.sensitive = true; confirm_button.sensitive = true; request_pin_button.sensitive = true; return; } var call = proxy.new_call (); call.set_function ("1.1/account/settings.json"); call.set_method ("GET"); Cb.Utils.load_threaded_async.begin (call, null, (obj, res) => { Json.Node? root_node; try { root_node = Cb.Utils.load_threaded_async.end(res); } catch (GLib.Error e) { warning ("Could not get json data: %s", e.message); return; } Json.Object root = root_node.get_object (); string screen_name = root.get_string_member ("screen_name"); debug ("Checking for %s", screen_name); Account? acc = Account.query_account (screen_name); if (acc != null) { bool proxies_match = proxy_values_match(proxy, acc.get_proxy_values()); bool override_acct = false; if (!proxies_match) { Gtk.MessageDialog replace_dialog = new Gtk.MessageDialog(main_window, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("The account %s already exists with different keys.\n\nReplace it?"), screen_name); var result = replace_dialog.run(); replace_dialog.destroy(); override_acct = result == Gtk.ResponseType.YES; } if (!proxies_match && override_acct) { Account.update_api_details(acc.id, proxy); acc.proxy = proxy; } else { critical ("Account is already in use"); show_error (_("Account already in use")); pin_entry.sensitive = true; pin_entry.text = ""; request_pin_button.sensitive = true; return; } } Twitter.get().get_own_user_info.begin (proxy, (obj, res) => { UserInfo user_info; try { user_info = Twitter.get().get_own_user_info.end(res); } catch (GLib.Error e) { warning ("Could not get json data: %s", e.message); return; } if (acc == null) { acc = Account.create_account(user_info, proxy); } // else we retrieved and updated an existing account earlier debug ("user info call"); acc.init_database (); acc.init_proxy (); acc.save_info(); acc.suppress_notifications(); cawbird.account_added (acc); account_created (acc); }); }); } private bool proxy_values_match(Rest.OAuthProxy proxy, string[] proxy_values) { return proxy.consumer_key == proxy_values[0] && proxy.consumer_secret == proxy_values[1] && proxy.token == proxy_values[2] && proxy.token_secret == proxy_values[3]; } private async void do_confirm () { try { if (!proxy.access_token_async ("oauth/access_token", pin_entry.get_text (), confirm_cb, this)) { show_error(_("Failed to retrieve access token")); } } catch (GLib.Error e) { Utils.show_error_dialog (e, this.main_window); critical (e.message); } } private void show_error (string err) { info_label.visible = false; error_label.visible = true; error_label.label = err; } private void pin_changed_cb () { string text = pin_entry.get_text (); bool confirm_possible = text.length > 0 && proxy != null; confirm_button.sensitive = confirm_possible; } } cawbird-1.4.2/src/widgets/AddImageButton.vala000066400000000000000000000144461416632607600211060ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class AddImageButton : Gtk.Button { private const int MIN_WIDTH = 40; private const int MAX_HEIGHT = 150; private const int MIN_HEIGHT = 100; private const int ICON_SIZE = 32; private MediaUpload _media_upload; public string? image_path { owned get { return _media_upload.filepath; } } public string uuid { get { return _media_upload.id; } } public int64 media_id { get { return _media_upload.media_id; } } public string description = ""; public Cairo.ImageSurface? surface; public signal void deleted (); private double delete_factor = 1.0; private uint64 delete_transition_start; public AddImageButton(MediaUpload upload) { _media_upload = upload; this.get_accessible().set_description(upload.filepath.substring(upload.filepath.last_index_of_char('/') + 1)); } construct { this.set_has_window (false); } public void get_draw_size (out int width, out int height, out double scale) { if (this.surface == null) { width = 0; height = 0; scale = 0.0; return; } width = this.get_allocated_width (); height = this.get_allocated_height (); double scale_x = (double)width / this.surface.get_width (); double scale_y = (double)height / this.surface.get_height (); scale = double.min (double.min (scale_x, scale_y), 1.0) * delete_factor; width = (int)(this.surface.get_width () * scale); height = (int)(this.surface.get_height () * scale); } public override bool draw (Cairo.Context ct) { int widget_width = get_allocated_width (); int widget_height = get_allocated_height (); var style_context = this.get_style_context (); /* Draw thumbnail */ if (this.surface != null) { ct.save (); ct.rectangle (0, 0, widget_width, widget_height); int draw_width, draw_height; double scale; this.get_draw_size (out draw_width, out draw_height, out scale); if (draw_width > 0 && draw_height > 0) { ct.scale (scale, scale); ct.set_source_surface (this.surface, (widget_width - draw_width) / 2 / scale, (widget_height - draw_height) / 2 / scale); ct.fill (); } ct.restore (); style_context.render_check (ct, (widget_width / 2.0) - (ICON_SIZE / 2.0), (widget_height / 2.0) - (ICON_SIZE / 2.0), ICON_SIZE, ICON_SIZE); } return Gdk.EVENT_PROPAGATE; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void get_preferred_height_for_width (int width, out int minimum, out int natural) { int media_width; int media_height; if (this.surface == null) { media_width = MIN_WIDTH; media_height = MAX_HEIGHT; } else { media_width = this.surface.get_width (); media_height = this.surface.get_height (); } double width_ratio = (double)width / (double) media_width; int height = int.min (media_height, (int)(media_height * width_ratio)); height = int.min (MAX_HEIGHT, height); minimum = MIN_HEIGHT; natural = int.max (minimum, (int)(height * this.delete_factor)); } public override void get_preferred_width_for_height (int height, out int minimum, out int natural) { int media_width; int media_height; if (this.surface == null) { media_width = MIN_WIDTH; media_height = MAX_HEIGHT; } else { media_width = this.surface.get_width (); media_height = this.surface.get_height (); } double height_ratio = (double)height / (double) media_height; int width = int.min (media_width, (int)(media_width * height_ratio)); width = int.max (MIN_WIDTH, width); minimum = natural = (int)(width * this.delete_factor); } public override void get_preferred_width (out int minimum, out int natural) { int media_width; if (this.surface == null) { media_width = 1; } else { media_width = this.surface.get_width (); } minimum = (int)(int.min (media_width, MIN_WIDTH) * delete_factor); natural = (int)(media_width * delete_factor); } public override void get_preferred_height (out int minimum, out int natural) { int media_height; if (this.surface == null) { media_height = 1; } else { media_height = this.surface.get_height (); } minimum = (int)(int.min (media_height, MIN_HEIGHT) * delete_factor); natural = (int)(media_height * delete_factor); } private bool delete_tick_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) { uint64 now = frame_clock.get_frame_time (); double t = (now - this.delete_transition_start) / (double)(TRANSITION_DURATION* 1); t = ease_out_cubic (t); this.delete_factor = 1.0 - t; this.queue_resize (); if (t >= 1.0) { this.delete_factor = 1.0; this.deleted (); return GLib.Source.REMOVE; } return GLib.Source.CONTINUE; } public void start_remove () { if (!this.get_realized ()) return; this.delete_transition_start = this.get_frame_clock ().get_frame_time (); this.add_tick_callback (delete_tick_cb); } } cawbird-1.4.2/src/widgets/AspectImage.vala000066400000000000000000000076151416632607600204410ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class AspectImage : Gtk.Widget { public Gdk.Pixbuf pixbuf { set { if (value != null) { if (value != Twitter.no_banner) { start_animation (); } if (this.pixbuf_surface != null) this.old_surface = this.pixbuf_surface; this.pixbuf_surface = (Cairo.ImageSurface)Gdk.cairo_surface_create_from_pixbuf (value, 1, this.get_window ()); bg_color.alpha = 0.0; } this.queue_draw (); } } public string color_string { set { bg_color.parse (value); } } private Gdk.RGBA bg_color; private Cairo.Surface? old_surface; private Cairo.ImageSurface? pixbuf_surface = null; public AspectImage () {} construct { set_has_window (false); } private void start_animation () { if (!this.get_realized ()) return; alpha = 0.0; in_transition = true; this.start_time = this.get_frame_clock ().get_frame_time (); this.add_tick_callback (fade_in_cb); } private double alpha = 0.0; private int64 start_time; private bool in_transition = false; private bool fade_in_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) { int64 now = frame_clock.get_frame_time (); double t = (double)(now - start_time) / TRANSITION_DURATION; if (t >= 1.0) { t = 1.0; in_transition = false; } this.alpha = ease_out_cubic (t); this.queue_draw (); return t < 1.0; } public override void get_preferred_height_for_width (int width, out int min_height, out int nat_height) { if (pixbuf_surface == null) { min_height = 0; nat_height = 1; return; } min_height = pixbuf_surface.get_height (); nat_height = pixbuf_surface.get_height (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override bool draw (Cairo.Context ct) { int width = get_allocated_width (); int height = get_allocated_height (); double scale_x = 1.0; if (bg_color.alpha == 0.0) scale_x = width / (double)pixbuf_surface.get_width (); scale_x = double.max (scale_x, 1.0); ct.rectangle (0, 0, width, height); ct.scale (scale_x, 1.0); ct.push_group (); if (this.old_surface != null) { ct.set_source_surface (this.old_surface, 0, 0); ct.paint (); } else if (bg_color.alpha > 0.0) { ct.set_source_rgba (bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha); ct.fill (); }else alpha = 1.0; if (bg_color.alpha == 0.0) { int x = (int)(width - (pixbuf_surface.get_width () * scale_x)) / 2; ct.set_source_surface (this.pixbuf_surface, x, 0); } else ct.set_source_rgba (bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha); if (in_transition) ct.paint_with_alpha (alpha); else ct.paint (); ct.pop_group_to_source (); ct.set_operator (Cairo.Operator.OVER); ct.paint (); return Gdk.EVENT_PROPAGATE; } } cawbird-1.4.2/src/widgets/AvatarBannerWidget.vala000066400000000000000000000121261416632607600217600ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class AvatarBannerWidget : Gtk.Container { private const int MIN_HEIGHT = 200; private const int MAX_HEIGHT = 250; private const double BANNER_RATIO = 0.5; /* 320/640 */ private const int AVATAR_SIZE = 48; private unowned Account account; private PixbufButton set_banner_button; private PixbufButton set_avatar_button; public signal void avatar_changed (Gdk.Pixbuf new_avatar); public signal void banner_changed (Gdk.Pixbuf new_banner); public signal void avatar_clicked (); public signal void banner_clicked (); construct { this.set_has_window (false); get_style_context ().add_class ("avatar"); /* set_banner_button */ this.set_banner_button = new PixbufButton (); set_banner_button.show_all (); set_banner_button.clicked.connect (banner_clicked_cb); this.add (set_banner_button); /* set_avatar_button */ this.set_avatar_button = new PixbufButton (); set_avatar_button.show_all (); set_avatar_button.clicked.connect (avatar_clicked_cb); this.add (set_avatar_button); Settings.get ().bind ("round-avatars", set_avatar_button, "round", GLib.SettingsBindFlags.DEFAULT); } public void set_account (Account account) { this.account = account; fetch_banner.begin (); this.queue_draw (); set_avatar_button.set_bg ((Cairo.ImageSurface)account.avatar); } public override bool draw (Cairo.Context ct) { this.propagate_draw (set_banner_button, ct); this.propagate_draw (set_avatar_button, ct); return Gdk.EVENT_PROPAGATE; } private int get_avatar_x () { return (get_allocated_width () / 2) - (AVATAR_SIZE / 2); } private int get_avatar_y () { return get_allocated_height () - AVATAR_SIZE; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void get_preferred_width (out int min, out int nat) { min = AVATAR_SIZE + 40; // 20px margin on either side nat = (int)(MIN_HEIGHT * (1 / BANNER_RATIO)); } public override void get_preferred_height_for_width (int width, out int min, out int nat) { min = (AVATAR_SIZE / 3) + MIN_HEIGHT; nat = int.max (min, int.min (MAX_HEIGHT, (int)(width * BANNER_RATIO) + (AVATAR_SIZE / 3))); } private async void fetch_banner () { if (account.banner_url == null) { set_banner_button.set_pixbuf (Twitter.no_banner); return; } var pixbuf = yield Utils.download_pixbuf (account.banner_url + "/600x200"); this.set_banner_button.set_pixbuf (pixbuf); } public override void size_allocate (Gtk.Allocation allocation) { base.size_allocate (allocation); Gtk.Requisition child_requisition; Gtk.Allocation child_allocation = Gtk.Allocation(); /* set_banner_button */ set_banner_button.get_preferred_size (out child_requisition, null); child_allocation.x = allocation.x; child_allocation.y = allocation.y; child_allocation.width = int.max (allocation.width, child_requisition.width); child_allocation.height = (int)(allocation.width * BANNER_RATIO); child_allocation.height = int.max (allocation.height - (AVATAR_SIZE / 2), child_requisition.height); set_banner_button.size_allocate (child_allocation); /* set_avatar_button */ set_avatar_button.get_preferred_size (out child_requisition, null); child_allocation.x = get_avatar_x () + allocation.x; child_allocation.y = get_avatar_y () + allocation.y; child_allocation.width = AVATAR_SIZE; child_allocation.height = AVATAR_SIZE; set_avatar_button.size_allocate (child_allocation); } public override void add (Gtk.Widget w) { w.set_parent (this); } public override void remove (Gtk.Widget w) { w.unparent (); } public override void forall_internal (bool include_internals, Gtk.Callback cb) { cb (set_banner_button); cb (set_avatar_button); } private void banner_clicked_cb () { this.banner_clicked (); } private void avatar_clicked_cb () { this.avatar_clicked (); } public void set_avatar (Gdk.Pixbuf avatar) { set_avatar_button.set_pixbuf (avatar); } public void set_banner (Gdk.Pixbuf banner) { set_banner_button.set_pixbuf (banner); } } cawbird-1.4.2/src/widgets/AvatarWidget.vala000066400000000000000000000203241416632607600206310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class AvatarWidget : Gtk.Widget { private const int SMALL = 0; private const int LARGE = 1; private const int OVERLAP_DIST = 40; private bool _round = true; public bool make_round { get { return _round; } set { if (value == _round) return; if (value) { this.get_style_context ().add_class ("avatar-round"); } else { this.get_style_context ().remove_class ("avatar-round"); } this._round = value; this.queue_draw (); } } public bool verified { get; set; default = false; } public bool protected_account { get; set; default = false; } public bool overlap { get; set; default = false; } public int size { get; set; default = 48; } private Cairo.ImageSurface _surface; public Cairo.Surface surface { get { return _surface; } set { if (this._surface == value) return; bool animate = false; if (this._surface != null) Twitter.get ().unref_avatar (this._surface); else animate = true; this._surface = (Cairo.ImageSurface)value; if (this._surface != null) { Twitter.get ().ref_avatar (this._surface); if (animate) this.start_animation (); } this.queue_draw (); } } private double alpha = 1.0f; private int64 start_time; static Cairo.Surface[] verified_icons; const int[] VERIFIED_SIZES = {12, 25}; static Cairo.Surface[] protected_account_icons; const int[] PROTECTED_ACCOUNT_SIZES = {16, 32}; static construct { try { verified_icons = { Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/verified-small.png"), 1, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/verified-large.png"), 1, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/verified-small@2.png"), 2, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/verified-large@2.png"), 2, null) }; } catch (GLib.Error e) { critical (e.message); } try { protected_account_icons = { Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/protected-account-small.png"), 1, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/protected-account-large.png"), 1, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/protected-account-small@2.png"), 2, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/protected-account-large@2.png"), 2, null) }; } catch (GLib.Error e) { critical (e.message); } } construct { this.set_has_window (false); Settings.get ().bind ("round-avatars", this, "make_round", GLib.SettingsBindFlags.DEFAULT); this.get_style_context ().add_class ("avatar"); this.get_style_context ().add_class ("avatar-round"); // default is TRUE } ~AvatarWidget () { if (this._surface != null) Twitter.get ().unref_avatar (this._surface); } private void start_animation () { if (!this.get_realized ()) return; alpha = 0.0; this.start_time = this.get_frame_clock ().get_frame_time (); this.add_tick_callback (fade_in_cb); } private bool fade_in_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) { int64 now = frame_clock.get_frame_time (); double t = (now - start_time) / (double) TRANSITION_DURATION; if (t >= 1.0) { t = 1.0; } this.alpha = ease_out_cubic (t); this.queue_draw (); return t < 1.0; } public override bool draw (Cairo.Context ctx) { int width = this.size; int height = this.size; if (this._surface == null) { return Gdk.EVENT_PROPAGATE; } double surface_scale; this._surface.get_device_scale (out surface_scale, out surface_scale); if (width != height) { warning ("Avatar with mapped with width %d and height %d", width, height); } var surface = new Cairo.Surface.similar (ctx.get_target (), Cairo.Content.COLOR_ALPHA, width, height); var ct = new Cairo.Context (surface); double scale = (double)this.get_allocated_width () / (double) (this._surface.get_width () / surface_scale); ct.rectangle (0, 0, width, height); ct.scale (scale, scale); ct.set_source_surface (this._surface, 0, 0); ct.fill(); int y; if (overlap) y = - OVERLAP_DIST; else y = 0; if (_round) { ct.scale (1.0/scale, 1.0/scale); ct.set_operator (Cairo.Operator.DEST_IN); ct.arc ((width / 2.0), (height / 2.0), (width / 2.0) - 0.5, // Radius 0, //Angle from 2 * Math.PI); // Angle to ct.fill (); this.get_style_context ().render_frame (ctx, 0, y, width, height); } ctx.set_source_surface (surface, 0, y); ctx.paint_with_alpha (alpha); if (verified) { double verified_scale = 1.0; int index = SMALL; if (width > 48) index = LARGE; if (index == LARGE && this._size < 100) { verified_scale = (double)this._size / 100.0; } int scale_factor = this.get_scale_factor () == 1 ? 0 : 1; Cairo.Surface verified_img = verified_icons[scale_factor * 2 + index]; ctx.scale (verified_scale, verified_scale); ctx.set_source_surface (verified_img, (width - (VERIFIED_SIZES[index] * verified_scale)) / verified_scale, y); ctx.paint_with_alpha (this.alpha); } if (protected_account) { double protected_scale = 1.0; int index = SMALL; if (width > 48) index = LARGE; if (index == LARGE && this._size < 100) { protected_scale = (double)this._size / 100.0; } int scale_factor = this.get_scale_factor () == 1 ? 0 : 1; Cairo.Surface protected_account_img = protected_account_icons[scale_factor * 2 + index]; ctx.scale (protected_scale, protected_scale); double protected_y = (height - (PROTECTED_ACCOUNT_SIZES[index] * protected_scale)) / protected_scale; if (overlap) { protected_y -= OVERLAP_DIST; } ctx.set_source_surface (protected_account_img, (width - (PROTECTED_ACCOUNT_SIZES[index] * protected_scale)) / protected_scale, protected_y); ctx.paint_with_alpha (this.alpha); } return Gdk.EVENT_PROPAGATE; } public override void size_allocate (Gtk.Allocation alloc) { base.size_allocate (alloc); if (overlap) { alloc.y -= OVERLAP_DIST; alloc.height += OVERLAP_DIST; this.set_clip (alloc); } } public override void get_preferred_width (out int min, out int nat) { min = size; nat = size; } public override void get_preferred_height (out int min, out int nat) { if (overlap) { min = size - OVERLAP_DIST; nat = size - OVERLAP_DIST; } else { min = size; nat = size; } } } cawbird-1.4.2/src/widgets/BadgeRadioButton.vala000066400000000000000000000046231416632607600214300ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class BadgeRadioButton : Gtk.RadioButton { private const int BADGE_SIZE = 10; private bool _show_badge = false; public bool show_badge { set { debug ("New show_badge value: %s", value ? "true" : "false"); if (value != this._show_badge) { this._show_badge = value; this.queue_draw (); } } get { return this._show_badge; } } public BadgeRadioButton (Gtk.RadioButton group, string icon_name, string text="") { GLib.Object (group: group); var ctx = this.get_style_context (); ctx.add_class ("image-button"); ctx.add_class ("flat"); var i = new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.BUTTON); this.add (i); this.set_mode (false); this.focus_on_click = false; this.hexpand = true; if (text != "") { this.tooltip_text = text; Atk.Object accessible = this.get_accessible (); accessible.set_name (text); } } public override bool draw (Cairo.Context ct) { base.draw (ct); if (!show_badge || this.get_child () == null) return Gdk.EVENT_PROPAGATE; Gtk.Allocation child_allocation; Gtk.Allocation allocation; this.get_child ().get_allocation (out child_allocation); this.get_allocation (out allocation); var context = this.get_style_context (); int x = allocation.x - child_allocation.x + child_allocation.width - BADGE_SIZE; int y = 5; context.save (); context.add_class ("badge"); context.render_background (ct, x, y, BADGE_SIZE, BADGE_SIZE); context.render_frame (ct, x, y, BADGE_SIZE, BADGE_SIZE); context.restore (); return Gdk.EVENT_PROPAGATE; } } cawbird-1.4.2/src/widgets/ChildSizedScroller.vala000066400000000000000000000051431416632607600220010ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2018 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ChildSizedScroller : Gtk.ScrolledWindow { public override void get_preferred_height_for_width (int width, out int minimum, out int natural) { int min = 0; int nat = 0; foreach (Gtk.Widget w in get_children()) { int m, n; w.get_preferred_height_for_width (width, out m, out n); min = int.max (m, min); nat = int.max (n, nat); } int max = this.get_max_content_height(); if (max > 0) { minimum = int.min(max, min); natural = int.min(max, nat); } else { minimum = min; natural = nat; } } public override void get_preferred_height (out int minimum, out int natural) { int min = 0; int nat = 0; foreach (Gtk.Widget w in get_children()) { int m, n; w.get_preferred_height (out m, out n); min = int.max (m, min); nat = int.max (n, nat); } int max = this.get_max_content_height(); if (max > 0) { minimum = int.min(max, min); natural = int.min(max, nat); } else { minimum = min; natural = nat; } } // Don't override get_preferred_width because we specifically *don't* want // the child's idea of width to stretch everything! public override unowned Atk.Object get_accessible() { unowned Atk.Object accessible = base.get_accessible(); foreach (Gtk.Widget child in get_children()) { if (child.visible) { accessible = child.get_accessible(); break; } } return accessible; } }cawbird-1.4.2/src/widgets/CompletionTextView.vala000066400000000000000000000420201416632607600220550ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class CompletionTextView : Gtk.TextView { private const string NO_SPELL_CHECK = "gtksourceview:context-classes:no-spell-check"; private const string[] TEXT_TAGS = { "link", "mention", "hashtag", "snippet" }; private Gtk.Window completion_window; private string? current_word = null; private GLib.Cancellable? completion_cancellable = null; private bool _default_listbox = true; public Gtk.ListBox completion_listbox { set { _default_listbox = false; Cb.Utils.unbind_non_gobject_model (completion_list, completion_model); completion_list.row_activated.disconnect(insert_user_completion); this.completion_list = value; Cb.Utils.bind_non_gobject_model (completion_list, completion_model, create_completion_row); completion_list.row_activated.connect(insert_user_completion); } } private Gtk.ListBox completion_list; private Cb.UserCompletionModel completion_model; public signal void show_completion (); public signal void hide_completion (); public signal void update_completion (string query); private unowned Account account; construct { completion_window = new Gtk.Window (Gtk.WindowType.POPUP); completion_window.set_type_hint (Gdk.WindowTypeHint.COMBO); completion_window.focus_out_event.connect (completion_window_focus_out_cb); completion_window.set_screen (this.get_screen ()); completion_list = new Gtk.ListBox (); completion_list.row_activated.connect(insert_user_completion); completion_model = new Cb.UserCompletionModel (); Cb.Utils.bind_non_gobject_model (completion_list, completion_model, create_completion_row); var placeholder_label = new Gtk.Label (_("No users found")); placeholder_label.get_style_context ().add_class ("dim-label"); placeholder_label.show (); completion_list.set_placeholder (placeholder_label); var scroller = new Gtk.ScrolledWindow (null, null); scroller.add (completion_list); var frame = new Gtk.Frame (null); frame.add (scroller); completion_window.add (frame); this.focus_out_event.connect (completion_window_focus_out_cb); /* Your theme uses a wildcard for :link, right? */ var style_context = this.get_style_context (); style_context.save (); style_context.set_state (Gtk.StateFlags.LINK); Gdk.RGBA link_color = style_context.get_color (style_context.get_state ()); style_context.restore (); if (link_color.red == 1.0 && link_color.green == 1.0 && link_color.blue == 1.0 && link_color.alpha == 1.0) { /* Unset, fall back to Adwaita's default */ link_color = { 0.16470, 0.462735, 0.77647, 1.0 }; } Gdk.RGBA snippet_color = { 0.0, 0.65, 0.0627, 1.0}; this.buffer.create_tag (TEXT_TAGS[0], "foreground_rgba", link_color, null); this.buffer.create_tag (TEXT_TAGS[1], "foreground_rgba", link_color, null); this.buffer.create_tag (TEXT_TAGS[2], "foreground_rgba", link_color, null); this.buffer.create_tag (TEXT_TAGS[3], "foreground_rgba", snippet_color, null); /* gspell marker */ this.buffer.create_tag (NO_SPELL_CHECK, null); this.buffer.notify["cursor-position"].connect (update_completion_listbox); this.buffer.changed.connect (buffer_changed_cb); this.key_press_event.connect (key_press_event_cb); /* Set them here so they are consistent everywhere */ this.right_margin = 6; this.left_margin = 6; this.top_margin = 6; this.bottom_margin = 6; #if SPELLCHECK var gspell_view = Gspell.TextView.get_from_gtk_text_view (this); gspell_view.set_inline_spell_checking (true); gspell_view.set_enable_language_menu (true); var gspell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer (this.buffer); var checker = new Gspell.Checker (Gspell.Language.get_default ()); gspell_buffer.set_spell_checker (checker); #endif } public void set_account (Account account) { this.account = account; } private bool insert_snippet () { Gtk.TextIter cursor_word_start; Gtk.TextIter cursor_word_end; string cursor_word = Utils.get_cursor_word (this.buffer, out cursor_word_start, out cursor_word_end); string? snippet = Cawbird.snippet_manager.get_snippet (cursor_word.strip ()); if (snippet == null) { debug ("No snippet for cursor_word '%s' found.", cursor_word); return false; } Gtk.TextIter start_word_iter; this.buffer.freeze_notify (); this.buffer.delete_range (cursor_word_start, cursor_word_end); Gtk.TextMark cursor_mark = this.buffer.get_insert (); this.buffer.get_iter_at_mark (out start_word_iter, cursor_mark); this.buffer.insert_text (ref start_word_iter, snippet, snippet.length); this.buffer.thaw_notify (); return true; } private inline bool snippets_configured () { return Cawbird.snippet_manager.n_snippets () > 0; } private void select_completion_row (Gtk.ListBoxRow? row) { if (row == null) return; assert (row.get_parent () == completion_list); Gtk.Allocation alloc; row.get_allocation (out alloc); completion_list.select_row (row); Gtk.Viewport viewport = completion_list.get_parent () as Gtk.Viewport; if (viewport == null) return; Gtk.ScrolledWindow scroller = viewport.get_parent () as Gtk.ScrolledWindow; if (scroller != null) { Gtk.Adjustment adjustment = scroller.get_vadjustment (); adjustment.clamp_page (alloc.y, alloc.y + alloc.height); } } private bool key_press_event_cb (Gdk.EventKey evt) { if (evt.keyval == Gdk.Key.Tab && snippets_configured ()) { return insert_snippet (); } /* If we are not in 'completion mode' atm, just back out. */ if (!completion_list.get_mapped ()) return Gdk.EVENT_PROPAGATE; int n_results = (int)completion_model.get_n_items (); switch (evt.keyval) { case Gdk.Key.Down: if (n_results == 0) return Gdk.EVENT_PROPAGATE; var current_match = completion_list.get_selected_row().get_index(); current_match = (current_match + 1) % n_results; var row = completion_list.get_row_at_index (current_match); if (_default_listbox) { row.grab_focus (); } select_completion_row (row); return Gdk.EVENT_STOP; case Gdk.Key.Up: var current_match = completion_list.get_selected_row().get_index(); current_match --; if (current_match < 0) current_match = n_results - 1; var row = completion_list.get_row_at_index (current_match); if (_default_listbox) { row.grab_focus (); } select_completion_row (row); return Gdk.EVENT_STOP; case Gdk.Key.Return: if (n_results == 0) return Gdk.EVENT_PROPAGATE; insert_user_completion(); return Gdk.EVENT_STOP; case Gdk.Key.Escape: hide_completion_window (); return Gdk.EVENT_STOP; default: return Gdk.EVENT_PROPAGATE; } } private void insert_user_completion () { var row = completion_list.get_selected_row(); var list_had_focus = row.has_focus; assert (row is UserCompletionRow); string compl = ((UserCompletionRow)row).get_screen_name (); this.buffer.freeze_notify (); Gtk.TextIter start_word_iter; Gtk.TextIter end_word_iter; Utils.get_cursor_mention_word (this.buffer, out start_word_iter, out end_word_iter); var start_offset = start_word_iter.get_offset(); this.buffer.delete_range (start_word_iter, end_word_iter); this.buffer.get_iter_at_offset(out start_word_iter, start_offset); var next_char = start_word_iter.get_char(); if (next_char == 0 || !next_char.ispunct()) { this.buffer.insert_text (ref start_word_iter, compl + " ", compl.length + 1); } else { this.buffer.insert_text (ref start_word_iter, compl, compl.length); } this.buffer.thaw_notify (); hide_completion_window (); if (list_had_focus) { this.grab_focus(); } } private void buffer_changed_cb () { Gtk.TextIter? start_iter; Gtk.TextIter? end_iter; this.buffer.get_start_iter (out start_iter); this.buffer.get_end_iter (out end_iter); var tag_table = this.buffer.get_tag_table (); /* We can't use gtk_text_buffer_remove_all_tags because that will also remove the ones added by gspell */ for (int i = 0; i < TEXT_TAGS.length; i ++) this.buffer.remove_tag (tag_table.lookup (TEXT_TAGS[i]), start_iter, end_iter); string text = this.buffer.get_text (start_iter, end_iter, true); size_t text_length; var entities = Tl.extract_entities_and_text (text, out text_length); foreach (unowned Tl.Entity e in entities) { Gtk.TextIter? e_start_iter; Gtk.TextIter? e_end_iter; this.buffer.get_iter_at_offset (out e_start_iter, (int)e.start_character_index); this.buffer.get_iter_at_offset (out e_end_iter, (int)(e.start_character_index + e.length_in_characters)); switch (e.type) { case Tl.EntityType.HASHTAG: buffer.apply_tag_by_name (NO_SPELL_CHECK, e_start_iter, e_end_iter); buffer.apply_tag_by_name ("hashtag", e_start_iter, e_end_iter); break; case Tl.EntityType.MENTION: buffer.apply_tag_by_name (NO_SPELL_CHECK, e_start_iter, e_end_iter); buffer.apply_tag_by_name ("mention", e_start_iter, e_end_iter); break; case Tl.EntityType.LINK: buffer.apply_tag_by_name (NO_SPELL_CHECK, e_start_iter, e_end_iter); buffer.apply_tag_by_name ("link", e_start_iter, e_end_iter); break; case Tl.EntityType.TEXT: if (Cawbird.snippet_manager.has_snippet_n (e.start, e.length_in_bytes)) { buffer.apply_tag_by_name (NO_SPELL_CHECK, e_start_iter, e_end_iter); buffer.apply_tag_by_name ("snippet", e_start_iter, e_end_iter); } break; default: break; } } if (buffer.text.length == 0) hide_completion_window (); } private void show_completion_window () { if (!this.get_mapped ()) return; completion_model.clear (); if (!_default_listbox) { this.show_completion (); return; } int x, y; Gtk.Allocation alloc; this.get_allocation (out alloc); var window = this.get_window (Gtk.TextWindowType.WIDGET); window.get_origin (out x, out y); Gdk.Display default_display = Gdk.Display.get_default(); Gdk.Monitor current_monitor = default_display.get_monitor_at_window(window); Gdk.Rectangle workarea = current_monitor.get_workarea(); int completion_height = 100; // If it will fit below the text box then put it below, else put it above. // This stops the completion being shown off-screen. if (y + alloc.height + completion_height <= workarea.height) { y += alloc.height; } else { y -= completion_height; } /* +2 for the size and -1 for x since we account for the frame size around the text view */ completion_window.set_attached_to (this); completion_window.set_transient_for ((Gtk.Window) this.get_toplevel ()); completion_window.move (x - 1, y); completion_window.resize (alloc.width + 2, completion_height); completion_window.show_all (); } private void hide_completion_window () { this.current_word = null; if (!_default_listbox) { hide_completion (); return; } completion_window.hide (); } private bool completion_window_focus_out_cb () { if (_default_listbox) { hide_completion_window (); } return false; } private void update_completion_listbox () { string cur_word = Utils.get_cursor_mention_word (this.buffer, null, null); int n_chars = cur_word.char_count (); if (n_chars < 2 || cur_word[0] != '@' || this.buffer.has_selection) { hide_completion_window (); return; } // Strip off the @ cur_word = cur_word.substring (1); if (cur_word != this.current_word) { if (this.completion_cancellable != null) { debug ("Cancelling earlier completion call..."); this.completion_cancellable.cancel (); } /* Clears the model */ show_completion_window (); /* Query users from local cache */ Cb.UserInfo[] corpus; account.user_counter.query_by_prefix (account.db.get_sqlite_db (), cur_word, 10, out corpus); completion_model.insert_infos (corpus); bool corpus_was_empty = (corpus.length == 0); if (corpus.length > 0) { select_completion_row (completion_list.get_row_at_index (0)); } corpus = null; /* Make sure we won't use it again */ bool has_alnum = false; unichar c; for (int i = 0; cur_word.get_next_char (ref i, out c);) { if (c.isalnum()) { debug("Found alnum at %d: %s\n", i, c.to_string ()); has_alnum = true; break; } } // Only query the API if there are alphanumeric characters, because Twitter won't search "@_" if (has_alnum) { /* Now also query users from the Twitter server, in case our local cache doesn't have anything worthwhile */ this.completion_cancellable = new GLib.Cancellable (); var cur_word_query = "\"%s\"".printf(cur_word.replace("\\", "\\\\").replace("\"", "\\\"")); Cb.Utils.query_users_async.begin (account.proxy, cur_word_query, completion_cancellable, (obj, res) => { Cb.UserIdentity[] users; try { users = Cb.Utils.query_users_async.end (res); } catch (GLib.Error e) { if (!(e is GLib.IOError.CANCELLED)) warning ("User completion call error: %s", e.message); return; } completion_model.insert_items (users); if (users.length > 0 && corpus_was_empty) { select_completion_row (completion_list.get_row_at_index (0)); } }); } this.current_word = cur_word; completion_list.show_all (); } } private Gtk.Widget create_completion_row (void *id_ptr) { // *shrug* Cb.UserIdentity *id = (Cb.UserIdentity*) id_ptr; var row = new UserCompletionRow (id->id, id->user_name, id->screen_name, id->verified, id->protected_account); row.show (); return row; } ~CompletionTextView () { Cb.Utils.unbind_non_gobject_model (completion_list, completion_model); } } class UserCompletionRow : Gtk.ListBoxRow { private static Cairo.Surface verified_surface; private static Cairo.Surface protected_account_surface; private Gtk.Label user_name_label; private Gtk.Label screen_name_label; static construct { try { verified_surface = Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/verified-small.png"), 1, null); protected_account_surface = Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/protected-account-small.png"), 1, null); } catch (GLib.Error e) { error (e.message); } } public UserCompletionRow (int64 id, string user_name, string screen_name, bool verified, bool protected_account) { user_name_label = new Gtk.Label (user_name); screen_name_label = new Gtk.Label ("@" + screen_name); this.activatable = true; var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); user_name_label.set_valign (Gtk.Align.BASELINE); user_name_label.set_ellipsize (Pango.EllipsizeMode.END); box.add (user_name_label); screen_name_label.set_valign (Gtk.Align.BASELINE); screen_name_label.get_style_context ().add_class ("dim-label"); box.add (screen_name_label); if (verified) { var verified_image= new Gtk.Image.from_surface (verified_surface); box.add (verified_image); } if (protected_account) { var protected_account_image = new Gtk.Image.from_surface (protected_account_surface); box.add (protected_account_image); } box.margin = 2; this.add (box); this.show_all (); } public string get_screen_name () { return screen_name_label.get_label (); } } cawbird-1.4.2/src/widgets/ComposeImageManager.vala000066400000000000000000000361031416632607600221140ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ComposeImageManager : Gtk.Container { private const int BUTTON_DELTA = 10; private const int BUTTON_SPACING = 12; private GLib.GenericArray buttons; private GLib.GenericArray uploads; private GLib.GenericArray close_buttons; private GLib.GenericArray desc_buttons; private GLib.GenericArray progress_bars; public Rest.OAuthProxy proxy; public int n_images { get { return this.buttons.length; } } public int max_images { get; set; default = Twitter.max_media_per_upload; } // Is there an *animated* GIF? public bool has_gif { get { for (int i = 0; i < uploads.length; i ++) { if (uploads.get (i).media_category.has_suffix("gif")) { return true; } } return false; } } public bool has_video { get { for (int i = 0; i < uploads.length; i ++) { if (uploads.get (i).media_category.has_suffix("video")) { return true; } } return false; } } public bool full { get { return this.buttons.length == max_images || this.has_gif || this.has_video; } } public signal void image_removed (MediaUpload upload); public signal void image_reloaded (MediaUpload upload); public signal void image_uploaded (MediaUpload upload); construct { this.buttons = new GLib.GenericArray (); this.uploads = new GLib.GenericArray (); this.close_buttons = new GLib.GenericArray (); this.desc_buttons = new GLib.GenericArray (); this.progress_bars = new GLib.GenericArray (); this.set_has_window (false); } public MediaUpload[] get_uploads() { return uploads.data; } public void clear() { for (int i = this.buttons.length - 1; i >= 0; i--) { remove_index(i, false); } } private void remove_index (int index, bool animate) { this.close_buttons.get (index).hide (); this.desc_buttons.get (index).hide (); this.progress_bars.get (index).hide (); AddImageButton aib = (AddImageButton) this.buttons.get (index); aib.deleted.connect (() => { this.buttons.remove_index (index); var upload = this.uploads.get(index); this.uploads.remove_index (index); this.close_buttons.remove_index (index); this.desc_buttons.remove_index (index); this.progress_bars.remove_index (index); this.queue_draw (); this.image_removed (upload); }); this.uploads.get(index).cancellable.cancel(); if (animate) { aib.start_remove (); } else { aib.deleted(); } } private void remove_clicked_cb (Gtk.Button source) { int index = -1; for (int i = 0; i < this.close_buttons.length; i ++) { if (close_buttons.get (i) == source) { index = i; break; } } assert (index >= 0); remove_index(index, true); } private void image_description_button_clicked(Gtk.Button source) { int index = -1; for (int i = 0; i < this.desc_buttons.length; i ++) { if (desc_buttons.get (i) == source) { index = i; break; } } assert (index >= 0); var image_button = this.buttons.get(index); assert (image_button.surface != null); ImageDescriptionWindow description_window = new ImageDescriptionWindow((Gtk.Window)this.get_toplevel(), proxy, image_button.media_id, image_button.description, image_button.surface); description_window.description_updated.connect((media_id, description) => { image_button.description = description; }); description_window.hide.connect(() => { description_window.destroy(); }); description_window.show(); } private void reupload_image_cb (Gtk.Button source) { AddImageButton aib = (AddImageButton) source; if (!aib.get_style_context ().has_class ("image-error")) { return; } int index = -1; for (int i = 0; i < this.desc_buttons.length; i ++) { if (desc_buttons.get (i) == source) { index = i; break; } } assert (index >= 0); aib.clicked.disconnect (reupload_image_cb); aib.tooltip_text = null; this.image_reloaded (uploads.get(index)); } // GtkContainer API {{{ public override void forall_internal (bool include_internals, Gtk.Callback cb) { assert (buttons.length == close_buttons.length); assert (buttons.length == desc_buttons.length); assert (buttons.length == progress_bars.length); for (int i = 0; i < this.close_buttons.length;) { int size_before = this.close_buttons.length; cb (close_buttons.get (i)); i += this.close_buttons.length - size_before + 1; } for (int i = 0; i < this.desc_buttons.length;) { int size_before = this.desc_buttons.length; cb (desc_buttons.get (i)); i += this.desc_buttons.length - size_before + 1; } for (int i = 0; i < this.progress_bars.length;) { int size_before = this.progress_bars.length; cb (progress_bars.get (i)); i += this.progress_bars.length - size_before + 1; } for (int i = 0; i < this.buttons.length;) { int size_before = this.buttons.length; cb (buttons.get (i)); i += this.buttons.length - size_before + 1; } } public override void add (Gtk.Widget widget) { widget.set_parent (this); this.buttons.add ((AddImageButton)widget); var btn = new Gtk.Button.from_icon_name ("window-close-symbolic"); btn.set_parent (this); btn.get_style_context ().add_class ("image-button"); btn.get_style_context ().add_class ("close-button"); btn.get_accessible().set_name(_("Remove image")); btn.clicked.connect (remove_clicked_cb); btn.show (); this.close_buttons.add (btn); var bar = new Gtk.ProgressBar (); bar.set_parent (this); bar.get_accessible().set_name(_("Image upload progress")); bar.show_all (); this.progress_bars.add (bar); var desc_btn = new Gtk.Button.from_icon_name("cawbird-compose-symbolic"); desc_btn.set_parent(this); desc_btn.get_style_context ().add_class ("image-button"); desc_btn.get_accessible().set_name(_("Describe image")); desc_btn.clicked.connect(image_description_button_clicked); desc_btn.sensitive = false; desc_btn.show(); this.desc_buttons.add(desc_btn); } public override void remove (Gtk.Widget widget) { widget.unparent (); if (widget is AddImageButton) this.buttons.remove ((AddImageButton)widget); else if (widget is Gtk.Button) { // We only have up to four widgets, so be lazy and try removing from both lists. this.close_buttons.remove ((Gtk.Button)widget); this.desc_buttons.remove((Gtk.Button) widget); } else this.progress_bars.remove ((Gtk.ProgressBar)widget); } // }}} // GtkWidget API {{{ public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void size_allocate (Gtk.Allocation allocation) { base.size_allocate (allocation); Gtk.Allocation child_allocation = {}; if (this.buttons.length == 0) return; int default_button_width = (allocation.width - (buttons.length * BUTTON_SPACING)) / buttons.length; child_allocation.x = allocation.x; child_allocation.y = allocation.y + BUTTON_DELTA; child_allocation.height = int.max (allocation.height - 2 * BUTTON_DELTA, 0); Gtk.Allocation close_allocation = {}; close_allocation.y = allocation.y; Gtk.Allocation desc_allocation = {}; for (int i = 0, p = this.buttons.length; i < p; i ++) { int min, nat; /* Actual image button */ AddImageButton aib = this.buttons.get (i); aib.get_preferred_width_for_height (child_allocation.height, out min, out nat); child_allocation.width = int.min (default_button_width, nat); aib.size_allocate (child_allocation); /* Remove button */ int n; Gtk.Widget btn = this.close_buttons.get (i); btn.get_preferred_width (out close_allocation.width, out n); btn.get_preferred_height (out close_allocation.height, out n); close_allocation.x = child_allocation.x + child_allocation.width - close_allocation.width + BUTTON_DELTA; btn.size_allocate (close_allocation); Gtk.Widget desc_btn = this.desc_buttons.get (i); desc_btn.get_preferred_width (out desc_allocation.width, out n); desc_btn.get_preferred_height (out desc_allocation.height, out n); desc_allocation.x = child_allocation.x + child_allocation.width - desc_allocation.width + BUTTON_DELTA; desc_allocation.y = allocation.y + allocation.height - desc_allocation.height; desc_btn.size_allocate (desc_allocation); /* Progress bar */ int button_width, button_height; double scale; aib.get_draw_size (out button_width, out button_height, out scale); Gtk.Widget bar = this.progress_bars.get (i); Gtk.Allocation bar_allocation = {0}; bar_allocation.x = child_allocation.x + 6; bar.get_preferred_width (out bar_allocation.width, out n); bar_allocation.width = int.max (button_width - 12, bar_allocation.width); bar.get_preferred_height (out bar_allocation.height, out n); bar_allocation.y = child_allocation.y + (child_allocation.height + bar_allocation.height) / 2; bar.size_allocate (bar_allocation); child_allocation.x += child_allocation.width + BUTTON_SPACING; } } public override void get_preferred_height_for_width (int width, out int minimum, out int natural) { int min = 0; int nat = 0; for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); int m, n; btn.get_preferred_height_for_width (width, out m, out n); min = int.max (m, min); nat = int.max (n, nat); } /* We subtract BUTTON_DELTA in size_allocate again */ minimum = min + 2 * BUTTON_DELTA; natural = nat + 2 * BUTTON_DELTA; } public override void get_preferred_height (out int minimum, out int natural) { int min = 0; int nat = 0; for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); int m, n; btn.get_preferred_height (out m, out n); min = int.max (m, min); nat = int.max (n, nat); } /* We subtract BUTTON_DELTA in size_allocate again */ minimum = min + 2 * BUTTON_DELTA; natural = nat + 2 * BUTTON_DELTA; } public override void get_preferred_width (out int minimum, out int natural) { int min = 0; int nat = 0; for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); int m, n; btn.get_preferred_width (out m, out n); min += m; nat += n; } minimum = min + (buttons.length * BUTTON_SPACING); natural = nat + (buttons.length * BUTTON_SPACING); } public override bool draw (Cairo.Context ct) { for (int i = 0, p = this.buttons.length; i < p; i ++) { Gtk.Widget btn = this.buttons.get (i); this.propagate_draw (btn, ct); } for (int i = 0, p = this.close_buttons.length; i < p; i ++) { var btn = this.close_buttons.get (i); this.propagate_draw (btn, ct); } for (int i = 0, p = this.desc_buttons.length; i < p; i ++) { var btn = this.desc_buttons.get (i); this.propagate_draw (btn, ct); } for (int i = 0, p = this.progress_bars.length; i < p; i ++) { var bar = this.progress_bars.get (i); this.propagate_draw (bar, ct); } return Gdk.EVENT_PROPAGATE; } // }}} public string load_media (MediaUpload upload) { #if DEBUG assert (!this.full); #endif upload.progress_updated.connect ((progress) => { set_image_progress (upload.id, progress); }); upload.progress_complete.connect ((error) => { end_progress (upload.id, error); }); upload.media_id_assigned.connect(() => { set_media_id(upload.id); }); this.uploads.add(upload); Cairo.ImageSurface surface; if (upload.media_category.has_suffix("video")) { surface = (Cairo.ImageSurface)load_surface_for_video (upload.filepath); } else { surface = (Cairo.ImageSurface)load_surface (upload.filepath); } var button = new AddImageButton (upload); button.surface = surface; button.hexpand = false; button.halign = Gtk.Align.START; button.show (); this.add (button); return button.uuid; } private void set_image_progress (string uuid, double progress) { for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); if (btn.uuid == uuid) { var progress_bar = progress_bars.get (i); progress_bar.set_fraction (progress); break; } } } private void end_progress (string uuid, GLib.Error? error) { for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); if (btn.uuid == uuid) { image_uploaded (uploads[i]); progress_bars.get(i).hide(); var style_context = btn.get_style_context (); style_context.remove_class ("image-progress"); if (error == null) { style_context.add_class ("image-success"); style_context.remove_class ("image-error"); } else { warning ("%s: %s", btn.image_path, error.message); style_context.add_class ("image-error"); style_context.remove_class ("image-success"); btn.clicked.connect (reupload_image_cb); btn.tooltip_text = error.message; } break; } } } public bool is_ready () { for (int i = 0; i < uploads.length; i++) { if (!uploads[i].is_uploaded()) { return false; } } return true; } public void set_media_id(string uuid) { for (int i = 0; i < buttons.length; i ++) { var btn = buttons.get (i); if (btn.uuid == uuid) { desc_buttons.get(i).sensitive = true; break; } } } public void insensitivize_buttons () { for (int i = 0; i < close_buttons.length; i ++) { close_buttons.get (i).set_sensitive (false); } } public void sensitivize_buttons () { for (int i = 0; i < close_buttons.length; i ++) { close_buttons.get (i).set_sensitive (true); } } } cawbird-1.4.2/src/widgets/CropWidget.vala000066400000000000000000000314401416632607600203170ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class CropWidget : Gtk.DrawingArea { private const int MIN_SIZE = 48; private Gtk.GestureDrag drag_gesture; private Gdk.Pixbuf? image; private Gdk.Rectangle selection_rect; private Gdk.Rectangle image_rect; private Gdk.Cursor drag_cursor; private Gdk.Cursor default_cursor; private Gdk.Cursor resize_cursor; private bool selection_grabbed = false; private bool resize_area_grabbed = false; private int drag_diff_x = 0; private int drag_diff_y = 0; private int resize_diff_x = 0; private int resize_diff_y = 0; private bool resize_area_hovered = false; private double current_scale = 1.0; private int min_width = MIN_SIZE; private double drag_start_x; private double drag_start_y; /** * Ratio of the width to the height, i.e. (width/height) * => values >1.0 for landscape pictures */ public double desired_aspect_ratio { get; set; default = 0.8; } construct { this.set_events (this.get_events () | Gdk.EventMask.POINTER_MOTION_MASK | Gdk.EventMask.BUTTON1_MOTION_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK); this.motion_notify_event.connect (mouse_motion_cb); this.drag_cursor = new Gdk.Cursor.for_display (this.get_display (), Gdk.CursorType.FLEUR); this.default_cursor = new Gdk.Cursor.for_display (this.get_display (), Gdk.CursorType.ARROW); this.resize_cursor = new Gdk.Cursor.for_display (this.get_display (), Gdk.CursorType.BOTTOM_RIGHT_CORNER); this.image_rect = Gdk.Rectangle (); this.selection_rect = Gdk.Rectangle (); this.drag_gesture = new Gtk.GestureDrag (this); this.drag_gesture.set_button (Gdk.BUTTON_PRIMARY); this.drag_gesture.drag_begin.connect (drag_gesture_begin_cb); this.drag_gesture.drag_end.connect (drag_gesture_end_cb); this.drag_gesture.drag_update.connect (drag_gesture_update_cb); } private bool mouse_motion_cb (Gdk.EventMotion event) { /* Just check whether the cursor is over the drag or resize area (or not) and change the cursor accordingly */ if (over_resize_area (event.x, event.y)) { set_cursor (resize_cursor); resize_area_hovered = true; queue_draw (); return false; /* Don't check resize area */ } else if (resize_area_hovered) { resize_area_hovered = false; set_cursor (default_cursor); queue_draw (); } if (cursor_in_selection (event.x, event.y)) { set_cursor (drag_cursor); } else { set_cursor (default_cursor); } return false; } private void drag_gesture_update_cb (Gtk.GestureDrag gesture, double offset_x, double offset_y) { double x = drag_start_x + offset_x; // XXX start_x + offset_x ? double y = drag_start_y + offset_y; /* Resizing */ if (resize_area_grabbed) { resize_selection_rect (x, y); } /* Dragging the selection */ if (selection_grabbed) { selection_rect.x = (int) x - drag_diff_x; selection_rect.y = (int) y - drag_diff_y; /* Limit to image boundaries */ if (selection_rect.x < image_rect.x) selection_rect.x = image_rect.x; if (selection_rect.y < image_rect.y) selection_rect.y = image_rect.y; if (selection_rect.x + selection_rect.width > image_rect.x + image_rect.width) selection_rect.x = image_rect.x + image_rect.width - selection_rect.width; if (selection_rect.y + selection_rect.height > image_rect.y + image_rect.height) selection_rect.y = image_rect.y + image_rect.height - selection_rect.height; this.queue_draw (); return; } } private void drag_gesture_begin_cb (Gtk.GestureDrag gesture, double x, double y) { this.drag_start_x = x; this.drag_start_y = y; /* Check for the resize area(s) first */ if (over_resize_area (x, y)) { resize_area_grabbed = true; resize_diff_x = (int)x - (selection_rect.x + selection_rect.width); resize_diff_y = (int)y - (selection_rect.y + selection_rect.height); gesture.set_state (Gtk.EventSequenceState.CLAIMED); set_cursor (resize_cursor); return; } /* Now the selection rect */ if (cursor_in_selection (x, y)) { selection_grabbed = true; drag_diff_x = (int)(x - selection_rect.x); drag_diff_y = (int)(y - selection_rect.y); gesture.set_state (Gtk.EventSequenceState.CLAIMED); set_cursor (drag_cursor); return; } gesture.set_state (Gtk.EventSequenceState.DENIED); } private void drag_gesture_end_cb (Gtk.GestureDrag gesture, double offset_x, double offset_y) { if (selection_grabbed) { selection_grabbed = false; set_cursor (default_cursor); return; } if (resize_area_grabbed) { resize_area_grabbed = false; set_cursor (default_cursor); return; } } private inline void restrict_selection_size () { if (selection_rect.width > image_rect.width) selection_rect.width = image_rect.width; if (selection_rect.height > image_rect.height) selection_rect.height = image_rect.height; if (selection_rect.width < (min_width * current_scale)) { selection_rect.width = (int)(min_width * current_scale); selection_rect.height = (int)(min_width * current_scale / desired_aspect_ratio); } if (selection_rect.x < image_rect.x) selection_rect.x = image_rect.x; if (selection_rect.y < image_rect.y) selection_rect.y = image_rect.y; if (selection_rect.x + selection_rect.width > image_rect.x + image_rect.width) selection_rect.x = image_rect.x + image_rect.width - selection_rect.width; if (selection_rect.y + selection_rect.height > image_rect.y + image_rect.height) selection_rect.y = image_rect.y + image_rect.height - selection_rect.height; } private void resize_selection_rect (double x, double y) { if (!resize_area_grabbed) return; int max_width = int.min (image_rect.width, int.min ((int)(image_rect.width), (int)(image_rect.height * desired_aspect_ratio))); int new_width = (int)x - selection_rect.x - resize_diff_x; int new_height = (int)(new_width / desired_aspect_ratio); if (new_width <= max_width) { selection_rect.width = new_width; selection_rect.height = new_height; } else { selection_rect.width = max_width; //message ("%d", selection_rect.width); //message ("%f", image_rect.width / desired_aspect_ratio); selection_rect.height = (int)(max_width / desired_aspect_ratio); } restrict_selection_size (); this.queue_draw (); } public void set_image (Gdk.Pixbuf? image) { this.image = image; calculate_image_rect (); /* Place the selection rect initially, using the maximum size given the desired_aspect_ratio */ selection_rect.width = image_rect.width; selection_rect.height = (int)(selection_rect.width / desired_aspect_ratio); if (selection_rect.height > image_rect.height) { selection_rect.height = image_rect.height; selection_rect.width = (int)(selection_rect.height * desired_aspect_ratio); } selection_rect.x = image_rect.x + ((image_rect.width - selection_rect.width) / 2); selection_rect.y = image_rect.y + ((image_rect.height - selection_rect.height) / 2); restrict_selection_size (); this.queue_draw (); } public override bool draw (Cairo.Context ct) { if (image == null) return Gdk.EVENT_PROPAGATE; int widget_width = get_allocated_width (); int widget_height = get_allocated_height (); ct.set_line_width (1.0); /* Draw dark background */ ct.rectangle (0, 0, widget_width, widget_height); ct.set_source_rgba (0.3, 0.3, 0.3, 1.0); ct.fill (); /* Draw image */ ct.save (); ct.rectangle (image_rect.x, image_rect.y, image_rect.width, image_rect.height); ct.scale (current_scale, current_scale); Gdk.cairo_set_source_pixbuf (ct, image, image_rect.x / current_scale, image_rect.y / current_scale); ct.fill (); ct.restore (); /* Draw selection rectangle border */ ct.rectangle (selection_rect.x, selection_rect.y, selection_rect.width, selection_rect.height); ct.set_source_rgba (1.0, 1.0, 1.0, 1.0); ct.stroke (); /* Draw resize quad */ ct.rectangle (selection_rect.x + selection_rect.width - 15, selection_rect.y + selection_rect.height - 15, 14.5, 14.5); if (resize_area_hovered || resize_area_grabbed) ct.set_source_rgba (0.0, 0.0, 0.6, 0.7); else ct.set_source_rgba (1.0, 1.0, 1.0, 0.7); ct.fill (); return Gdk.EVENT_PROPAGATE; } private inline void set_cursor (Gdk.Cursor cursor) { this.get_window ().set_cursor (cursor); } private bool cursor_in_selection (double x, double y) { return x >= selection_rect.x && x <= selection_rect.x + selection_rect.width && y >= selection_rect.y && y <= selection_rect.y + selection_rect.height; } public override void size_allocate (Gtk.Allocation alloc) { base.size_allocate (alloc); calculate_image_rect (); restrict_selection_size (); } private void calculate_image_rect () { int widget_width = this.get_allocated_width (); int widget_height = this.get_allocated_height (); if (this.image == null) { return; } /* current_scale the image down */ if (image.get_width () > image.get_height ()) { current_scale = (double) widget_width / image.get_width (); } else { current_scale = (double) widget_height / image.get_height (); } if (image.get_width () * current_scale > widget_width) current_scale = (double) widget_width / image.get_width (); if (image.get_height () * current_scale > widget_height) current_scale = (double) widget_height / image.get_height (); /* Cap at 1.0 */ if (current_scale > 1.0) current_scale = 1.0; this.image_rect.width = (int)(this.image.get_width () * current_scale); this.image_rect.height = (int)(this.image.get_height () * current_scale); this.image_rect.x = (widget_width - image_rect.width) / 2; this.image_rect.y = (widget_height - image_rect.height) / 2; } private bool over_resize_area (double x, double y) { if (x > selection_rect.x + selection_rect.width - 15 && x < selection_rect.x + selection_rect.width + 5 && y > selection_rect.y + selection_rect.height - 15 && y < selection_rect.y + selection_rect.height + 5) { return true; } return false; } public Gdk.Pixbuf get_cropped_image () { int absolute_x = (int)((selection_rect.x - image_rect.x) / current_scale); int absolute_y = (int)((selection_rect.y - image_rect.y) / current_scale); int absolute_w = (int)(selection_rect.width / current_scale); int absolute_h = (int)(selection_rect.height / current_scale); Gdk.Pixbuf final_image = new Gdk.Pixbuf (Gdk.Colorspace.RGB, this.image.get_has_alpha (), 8, absolute_w, absolute_h); this.image.copy_area (absolute_x, absolute_y, absolute_w, absolute_h, final_image, 0, 0); return final_image; } public void set_min_size (int min_width) { this.min_width = min_width; } } cawbird-1.4.2/src/widgets/DMPlaceholderBox.vala000066400000000000000000000037751416632607600213760ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class DMPlaceholderBox : Gtk.Box { private AvatarWidget avatar_image; private Gtk.Label name_label; private Gtk.Label screen_name_label; public int64 user_id; public new string name { set { name_label.label = value; name_label.tooltip_text = value; } } public string screen_name { set { screen_name_label.label = "@" + value; screen_name_label.tooltip_text = "@" + value; } } public string avatar_url; public void load_avatar () { Twitter.get ().get_avatar.begin (user_id, avatar_url, avatar_image); } construct { this.set_opacity (0.8); this.set_margin_top (60); this.set_orientation (Gtk.Orientation.VERTICAL); this.set_spacing (4); this.avatar_image = new AvatarWidget (); avatar_image.size = 48; avatar_image.set_halign (Gtk.Align.CENTER); this.add (avatar_image); this.name_label = new Gtk.Label (""); var attrs = new Pango.AttrList (); attrs.insert (Pango.attr_weight_new (Pango.Weight.BOLD)); name_label.set_attributes (attrs); this.add (name_label); this.screen_name_label = new Gtk.Label (""); screen_name_label.get_style_context ().add_class ("dim-label"); this.add (screen_name_label); this.show_all (); } } cawbird-1.4.2/src/widgets/DoubleTapButton.vala000066400000000000000000000020531416632607600213210ustar00rootroot00000000000000 /* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class DoubleTapButton : Gtk.ToggleButton { private bool first_step = false; public void reset () { first_step = false; } public void tap () { if (!first_step) { first_step = true; return; } this.active = !this.active; first_step = false; } } cawbird-1.4.2/src/widgets/FavImageView.vala000066400000000000000000000170151416632607600205640ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2017 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class FavImageView : Gtk.Box { private Gtk.ScrolledWindow scrolled_window; private Gtk.FlowBox fav_image_list; private Gtk.Button new_fav_image_button; private bool gifs_enabled = true; public signal void image_selected (string image_path); construct { this.orientation = Gtk.Orientation.VERTICAL; this.scrolled_window = new Gtk.ScrolledWindow (null, null); this.fav_image_list = new Gtk.FlowBox (); fav_image_list.homogeneous = true; scrolled_window.set_vexpand (true); scrolled_window.add (this.fav_image_list); this.add (scrolled_window); this.add (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); this.new_fav_image_button = new Gtk.Button.from_icon_name ("list-add-symbolic", Gtk.IconSize.BUTTON); new_fav_image_button.set_halign (Gtk.Align.START); new_fav_image_button.set_margin_start (6); new_fav_image_button.set_margin_top (6); new_fav_image_button.set_margin_bottom (6); new_fav_image_button.clicked.connect (new_fav_image_button_clicked_cb); this.add (new_fav_image_button); fav_image_list.set_selection_mode (Gtk.SelectionMode.NONE); fav_image_list.get_style_context ().add_class ("view"); fav_image_list.get_style_context ().add_class ("fav-image-box"); fav_image_list.child_activated.connect (fav_image_list_child_activated_cb); fav_image_list.drag_data_received.connect (fav_image_list_drag_data_received_cb); var image_target_list = new Gtk.TargetList (null); image_target_list.add_text_targets (0); Gtk.drag_dest_set (this, Gtk.DestDefaults.ALL, new Gtk.TargetEntry[0], Gdk.DragAction.COPY); Gtk.drag_dest_set_target_list (this, image_target_list); /* Fuck it */ this.show_all (); } public void load_images () { if (fav_image_list.get_children ().length () > 0) return; const int MAX_IMAGES = 50; string fav_image_dir = Dirs.config ("image-favorites/"); try { var dir = File.new_for_path (fav_image_dir); var iter = dir.enumerate_children ("standard::display-name,standard::content-type", GLib.FileQueryInfoFlags.NONE); int i = 0; FileInfo? info = null; while ((info = iter.next_file ()) != null) { var content_type = info.get_content_type (); if (content_type == "image/jpeg" || content_type == "image/png" || content_type == "image/gif") { var file = dir.get_child (info.get_name ()); var row = new FavImageRow (file.get_path ()); if (content_type == "image/gif") { row.is_gif = true; row.set_sensitive (gifs_enabled); } row.show_all (); fav_image_list.add (row); i ++; if (i >= MAX_IMAGES) break; } } } catch (GLib.Error e) { warning (e.message); } } private void fav_image_list_child_activated_cb (Gtk.FlowBoxChild _child) { FavImageRow child = (FavImageRow) _child; this.image_selected (child.get_image_path ()); } private void fav_image_list_drag_data_received_cb (Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { if (info == 0) { /* Text */ string?text = selection_data.get_text ().strip (); if (text.has_prefix ("file://")) { var file = GLib.File.new_for_uri (text); if (!file.query_exists ()) { debug ("File '%s' does not exist.", text); return; } try { var file_info = file.query_info ("standard::content-type", GLib.FileQueryInfoFlags.NONE); var row = new FavImageRow (GLib.File.new_for_uri (text).get_path ()); if (file_info.get_content_type () == "image/gif") { row.is_gif = true; row.set_sensitive (gifs_enabled); } row.show_all (); fav_image_list.add (row); } catch (GLib.Error e) { warning (e.message); } } else { debug ("Can't handle '%s'", text); } } else { warning ("Unknown drag data info %u", info); } } private void new_fav_image_button_clicked_cb () { var filechooser = new Gtk.FileChooserNative (_("Select Image"), this.get_toplevel () as Gtk.Window, Gtk.FileChooserAction.OPEN, _("Open"), _("Cancel")); var filter = new Gtk.FileFilter (); filter.add_mime_type ("image/png"); filter.add_mime_type ("image/jpeg"); filter.add_mime_type ("image/gif"); filechooser.set_filter (filter); if (filechooser.run () == Gtk.ResponseType.ACCEPT) { try { // First, take the selected file and copy it into the image-favorites folder var file = GLib.File.new_for_path (filechooser.get_filename ()); var file_info = file.query_info ("standard::name,standard::content-type", GLib.FileQueryInfoFlags.NONE); var dest_dir = GLib.File.new_for_path (Dirs.config ("image-favorites")); // Explicitly check whether the destination file already exists, and rename // it if it does */ var dest_file = dest_dir.get_child (file_info.get_name ()); if (GLib.FileUtils.test (dest_file.get_path (), GLib.FileTest.EXISTS)) { debug ("File '%s' already exists", dest_file.get_path ()); dest_file = dest_dir.get_child ("%s_%s".printf (GLib.get_monotonic_time ().to_string (), file_info.get_name ())); debug ("New name: '%s'", dest_file.get_path ()); } file.copy (dest_file, GLib.FileCopyFlags.NONE); var row = new FavImageRow (dest_file.get_path ()); if (file_info.get_content_type () == "image/gif") { row.is_gif = true; row.set_sensitive (gifs_enabled); } row.show_all (); fav_image_list.add (row); } catch (GLib.Error e) { warning (e.message); } } } public void set_gifs_enabled (bool enabled) { if (enabled == this.gifs_enabled) return; this.gifs_enabled = enabled; foreach (Gtk.Widget w in fav_image_list.get_children ()) { var child = (FavImageRow)w; if (child.get_image_path ().down ().has_suffix (".gif")) { child.set_sensitive (this.gifs_enabled); } } } } cawbird-1.4.2/src/widgets/FollowButton.vala000066400000000000000000000050051416632607600207040ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class FollowButton : Gtk.Button { private bool _following = false; public bool following { get { return _following; } set { var sc = this.get_style_context (); if (value) { sc.remove_class ("suggested-action"); sc.add_class ("destructive-action"); this.stack.visible_child = unfollow_label; get_accessible().set_name(_("Unfollow")); } else { sc.remove_class ("destructive-action"); sc.add_class ("suggested-action"); this.stack.visible_child = follow_label; get_accessible().set_name(_("Follow")); } this._following = value; } } private Gtk.Stack stack; private Gtk.Label follow_label; private Gtk.Label unfollow_label; private bool _compact = false; public bool compact { get { return _compact; } set { _compact = value; if (_compact) { this.follow_label.label = "+"; this.unfollow_label.label = "-"; } else { this.follow_label.label = _("Follow"); this.unfollow_label.label = _("Unfollow"); } } } construct { this.stack = new Gtk.Stack (); this.follow_label = new Gtk.Label (_("Follow")); this.unfollow_label = new Gtk.Label (_("Unfollow")); this.follow_label.get_accessible().set_name(_("Follow")); this.unfollow_label.get_accessible().set_name(_("Unfollow")); stack.add (follow_label); stack.add (unfollow_label); stack.set_interpolate_size (true); stack.transition_type = Gtk.StackTransitionType.CROSSFADE; stack.hhomogeneous = false; stack.vhomogeneous = true; this.add (stack); this.get_style_context ().add_class ("text-button"); this.get_style_context ().add_class ("suggested-action"); /* Default is false */ } } cawbird-1.4.2/src/widgets/ImpostorWidget.vala000066400000000000000000000032561416632607600212340ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ImpostorWidget : Gtk.Image { private new Cairo.Surface? surface = null; public ImpostorWidget () { this.halign = Gtk.Align.FILL; this.valign = Gtk.Align.FILL; } public override bool draw (Cairo.Context ct) { if (this.surface == null) return false; ct.set_source_surface (this.surface, 0, 0); ct.rectangle (0, 0, this.get_allocated_width (), this.get_allocated_height ()); ct.fill (); return false; } public void clone (Gtk.Widget widget) { int widget_width = widget.get_allocated_width (); int widget_height = widget.get_allocated_height (); this.surface = widget.get_window ().create_similar_surface (Cairo.Content.COLOR_ALPHA, widget_width, widget_height); var ct = new Cairo.Context (surface); widget.draw (ct); } } cawbird-1.4.2/src/widgets/LazyMenuButton.vala000066400000000000000000000020311416632607600212020ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class LazyMenuButton : Gtk.Button { public GLib.Menu menu_model { get; set; } public override void clicked () { var popover = new Gtk.Popover.from_model (this, this.menu_model); popover.position = Gtk.PositionType.BOTTOM; popover.popup (); } } cawbird-1.4.2/src/widgets/ListBox.vala000066400000000000000000000075701416632607600176430ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class ListBox : Gtk.ListBox { private Gtk.Stack? placeholder = null; private Gtk.Label no_entries_label; private Gtk.Box error_box; private Gtk.Label error_label; private Gtk.Button retry_button; public signal void retry_button_clicked (); public ListBox () { } construct { add_placeholder (); this.set_selection_mode (Gtk.SelectionMode.NONE); Settings.get ().bind ("double-click-activation", this, "activate-on-single-click", GLib.SettingsBindFlags.INVERT_BOOLEAN); } private void add_placeholder () { placeholder = new Gtk.Stack (); placeholder.transition_type = Gtk.StackTransitionType.CROSSFADE; var loading_label = new Gtk.Label (_("Loading…")); loading_label.get_style_context ().add_class ("dim-label"); placeholder.add_named (loading_label, "spinner"); no_entries_label = new Gtk.Label (_("No entries found")); no_entries_label.get_style_context ().add_class ("dim-label"); no_entries_label.wrap_mode = Pango.WrapMode.WORD_CHAR; placeholder.add_named (no_entries_label, "no-entries"); error_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12); error_label = new Gtk.Label (""); error_label.get_style_context ().add_class ("dim-label"); error_label.margin = 12; error_label.selectable = true; error_label.wrap = true; retry_button = new Gtk.Button.with_label (_("Retry")); retry_button.set_halign (Gtk.Align.CENTER); retry_button.clicked.connect (() => { placeholder.visible_child_name = "spinner"; retry_button_clicked (); }); error_box.add (error_label); error_box.add (retry_button); placeholder.add_named (error_box, "error"); placeholder.visible_child_name = "spinner"; placeholder.show_all (); placeholder.set_valign (Gtk.Align.CENTER); placeholder.set_halign (Gtk.Align.CENTER); this.set_placeholder (placeholder); } public void set_empty () { placeholder.visible_child_name = "no-entries"; } public void set_unempty () { placeholder.visible_child_name = "spinner"; } public void set_error (string err_msg) { error_label.label = err_msg; placeholder.visible_child_name = "error"; } public Gtk.Stack? get_placeholder () { return placeholder; } public void set_placeholder_text (string text) { no_entries_label.label = text; } public void reset_placeholder_text () { no_entries_label.label = _("No entries found"); } public void remove_all () { this.foreach ((w) => { remove (w); }); } public Gtk.Widget? get_first_visible_row () { int i = 0; Gtk.Widget? row = this.get_row_at_index (0); while (row != null && !row.visible) { i ++; row = this.get_row_at_index (i); } return row; } public Gtk.Widget? get_last_visible_row () { int i = (int)get_children().length() - 1; // We're in trouble if we get more than int.max (but less than uint.max) entries! Gtk.Widget? row = this.get_row_at_index (i); while (row != null && !row.visible) { i--; row = this.get_row_at_index (i); } return row; } } cawbird-1.4.2/src/widgets/MediaButton.vala000066400000000000000000000443311416632607600204660ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ private class MediaButton : Gtk.Bin { private const int PLAY_ICON_SIZE = 32; private const int MAX_HEIGHT = 200; /* We use MIN_ constants in case the media has not yet been loaded */ private const int MIN_HEIGHT = 40; private const int MIN_WIDTH = 40; private Gdk.Window? event_window = null; private Cb.Media? _media = null; private static Cairo.Surface[] play_icons; private bool is_m3u8 { get { // Some URLs have query strings, so we can't just suffix return _media.url.contains(".m3u8"); } } public Cb.Media? media { get { return _media; } set { if (_media != null) { _media.progress.disconnect (media_progress_cb); } _media = value; if (value != null) { if (!media.loaded) { _media.progress.connect (media_progress_cb); } else { this.media_alpha = 1.0; } set_save_as_sensitivity(); set_image_description(); if (!is_m3u8 && !_media.requires_authentication()) { menu_model.append (_("Copy URL"), "media.copy-url"); } } } } public unowned Gtk.Window window; private GLib.Menu menu_model; private Gtk.Menu? menu = null; private GLib.SimpleActionGroup actions; private const GLib.ActionEntry[] action_entries = { {"copy-url", copy_url_activated}, {"open-in-browser", open_in_browser_activated}, {"save-as", save_as_activated}, }; private Pango.Layout layout; private Gtk.GestureMultiPress press_gesture; private bool restrict_height = false; private int64 fade_start_time; private double media_alpha = 0.0; public signal void clicked (MediaButton source, double px, double py); static construct { try { play_icons = { Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/play.png"), 1, null), Gdk.cairo_surface_create_from_pixbuf ( new Gdk.Pixbuf.from_resource ("/uk/co/ibboard/cawbird/data/play@2.png"), 2, null), }; } catch (GLib.Error e) { critical (e.message); } } construct { this.set_has_window (false); this.set_can_focus (true); } ~MediaButton () { if (_media != null) { _media.progress.disconnect (media_progress_cb); } } public MediaButton (Cb.Media? media, bool restrict_height = false) { this.restrict_height = restrict_height; Gtk.Button reload_button = new Gtk.Button(); reload_button.label = _("Reload image"); reload_button.halign = Gtk.Align.CENTER; reload_button.valign = Gtk.Align.CENTER; this.add(reload_button); reload_button.show(); this.get_style_context ().add_class ("inline-media"); actions = new GLib.SimpleActionGroup (); actions.add_action_entries (action_entries, this); this.insert_action_group ("media", actions); this.menu_model = new GLib.Menu (); menu_model.append (_("Open in Browser"), "media.open-in-browser"); menu_model.append (_("Save as…"), "media.save-as"); this.media = media; this.layout = this.create_pango_layout ("0%"); this.press_gesture = new Gtk.GestureMultiPress (this); this.press_gesture.set_exclusive (true); this.press_gesture.set_button (0); this.press_gesture.set_propagation_phase (Gtk.PropagationPhase.CAPTURE); this.press_gesture.released.connect (gesture_released_cb); this.press_gesture.pressed.connect (gesture_pressed_cb); this.enter_notify_event.connect(Utils.set_pointer_on_mouseover); this.leave_notify_event.connect(Utils.set_pointer_on_mouseover); set_image_description(); } private void reload_image() { Cb.MediaDownloader.get_default().reload_async.begin(media); } private void media_progress_cb () { this.queue_draw (); if (this._media.loaded) { if (!_media.invalid && _media.surface != null) { this.start_fade (); } set_save_as_sensitivity(); this.queue_resize (); } } private bool fade_in_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) { if (!this.get_mapped ()) { this.media_alpha = 1.0; return GLib.Source.REMOVE; } int64 now = frame_clock.get_frame_time (); double t = 1.0; if (now < this.fade_start_time + TRANSITION_DURATION) t = (now - fade_start_time) / (double)(TRANSITION_DURATION ); t = ease_out_cubic (t); this.media_alpha = t; this.queue_draw (); if (t >= 1.0) { this.media_alpha = 1.0; return GLib.Source.REMOVE; } return GLib.Source.CONTINUE; } private void start_fade () { assert (this.media != null); assert (this.media.surface != null); if (!this.get_realized () || !this.get_mapped () || !Gtk.Settings.get_default ().gtk_enable_animations) { this.media_alpha = 1.0; return; } this.fade_start_time = this.get_frame_clock ().get_frame_time (); this.add_tick_callback (fade_in_cb); } public override bool draw (Cairo.Context ct) { int widget_width = get_allocated_width (); int widget_height = get_allocated_height (); if (_media != null && _media.invalid) { base.draw(ct); } else if (_media != null && _media.surface != null && _media.loaded) { /* Draw thumbnail */ int draw_x, draw_y; double scale; Utils.calculate_draw_offset (_media.thumb_width, _media.thumb_height, get_allocated_width(), get_allocated_height(), out draw_x, out draw_y, out scale); var draw_width = get_allocated_width() - draw_x; var draw_height = get_allocated_height() - draw_y; ct.save (); ct.rectangle (0, 0, widget_width, widget_height); ct.scale (scale, scale); ct.set_source_surface (media.surface, draw_x / scale, draw_y / scale); ct.paint_with_alpha (this.media_alpha); ct.restore (); ct.new_path (); /* * If image got moved off the top, we cropped it. Indicate that. * Currently trying a gradient overlay top and bottom */ if (draw_y < 0) { Cairo.Pattern pattern = new Cairo.Pattern.linear (0.0, 0.0, 0, widget_height); pattern.add_color_stop_rgba (0.01, 0.3, 0.3, 0.3, 1); pattern.add_color_stop_rgba (0.1, 0.7, 0.7, 0.7, 0); pattern.add_color_stop_rgba (0.9, 0.7, 0.7, 0.7, 0); pattern.add_color_stop_rgba (0.99, 0.3, 0.3, 0.3, 1); ct.rectangle (0, 0, widget_width, widget_height); ct.set_source (pattern); ct.fill (); } /* Draw play indicator */ if (_media.is_video ()) { int x = (widget_width / 2) - (PLAY_ICON_SIZE / 2); int y = (widget_height / 2) - (PLAY_ICON_SIZE / 2); ct.save (); ct.rectangle (x, y, PLAY_ICON_SIZE, PLAY_ICON_SIZE); ct.set_source_surface (play_icons[this.get_scale_factor () == 1 ? 0 : 1], x, y); ct.paint_with_alpha (this.media_alpha); ct.restore (); ct.new_path (); } if (media.alt_text != null && media.alt_text != "") { Gtk.IconTheme icon_theme = Gtk.IconTheme.get_default(); int icon_size = 24; try { Gdk.Pixbuf pixbuf = icon_theme.load_icon_for_scale ("dialog-information", icon_size, this.get_scale_factor(), Gtk.IconLookupFlags.USE_BUILTIN); var icon = Gdk.cairo_surface_create_from_pixbuf (pixbuf, this.get_scale_factor(), null); ct.set_source_surface (icon, draw_x / scale, widget_height - icon_size * this.get_scale_factor()); ct.paint(); } catch (GLib.Error e) { warning(e.message); } } var sc = this.get_style_context (); sc.render_background (ct, draw_x, 0, draw_width, draw_height); sc.render_frame (ct, draw_x, 0, draw_width, draw_height); if (this.has_visible_focus ()) { sc.render_focus (ct, draw_x + 2, 2, draw_width - 4, draw_height - 4); } } else { var sc = this.get_style_context (); double layout_x, layout_y; int layout_w, layout_h; layout.set_text ("%d%%".printf ((int)(_media.percent_loaded * 100)), -1); layout.get_size (out layout_w, out layout_h); layout_x = (widget_width / 2.0) - (layout_w / Pango.SCALE / 2.0); layout_y = (widget_height / 2.0) - (layout_h / Pango.SCALE / 2.0); sc.render_layout (ct, layout_x, layout_y, layout); } return Gdk.EVENT_PROPAGATE; } private void copy_url_activated (GLib.SimpleAction a, GLib.Variant? v) { Gtk.Clipboard clipboard = Gtk.Clipboard.get_for_display (Gdk.Display.get_default (), Gdk.SELECTION_CLIPBOARD); clipboard.set_text (media.url, -1); } private void open_in_browser_activated (GLib.SimpleAction a, GLib.Variant? v) { try { Gtk.show_uri_on_window ((Gtk.Window)get_toplevel(), media.target_url ?? media.url, Gtk.get_current_event_time ()); } catch (GLib.Error e) { critical (e.message); } } private void save_as_activated (GLib.SimpleAction a, GLib.Variant? v) { string title; if (_media.is_video ()) title = _("Save Video"); else title = _("Save Image"); var filechooser = new Gtk.FileChooserNative (title, this.window, Gtk.FileChooserAction.SAVE, _("Save"), _("Cancel")); filechooser.set_current_name (Utils.get_media_display_name (_media)); if (filechooser.run () == Gtk.ResponseType.ACCEPT) { var file = GLib.File.new_for_path (filechooser.get_filename ()); // Download the file string url = _media.target_url ?? _media.url; debug ("Downloading %s to %s", url, filechooser.get_filename ()); GLib.OutputStream? out_stream = null; try { out_stream = file.create (0, null); } catch (GLib.Error e) { Utils.show_error_dialog (e, this.window); warning (e.message); } if (out_stream != null) { Utils.download_file.begin (url, out_stream, () => { debug ("Download of %s finished", url); }); } } } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void get_preferred_height (out int minimum, out int natural) { int media_height; if (this._media == null || this._media.thumb_height == -1) { media_height = 1; } else { media_height = this._media.thumb_height; } if (restrict_height) { minimum = int.min (media_height, MAX_HEIGHT); } else { minimum = media_height; } natural = media_height; } public override void get_preferred_height_for_width (int width, out int minimum, out int natural) { int media_width; int media_height; if (this._media == null || this._media.thumb_width == -1 || this._media.thumb_height == -1) { media_width = MIN_WIDTH; media_height = MAX_HEIGHT; } else { media_width = this._media.thumb_width; media_height = this._media.thumb_height; } double scale = width / (double) media_width; int height = 0; if (scale >= 1) { height = int.min (media_height, (int) Math.floor ((width / 9.0) * 16)); } else { height = (int) Math.floor (double.min (media_height * scale, (media_width * scale / 9.0) * 16)); } if (restrict_height) { height = int.min (height, MAX_HEIGHT); } minimum = natural = height; } public override void get_preferred_width_for_height (int height, out int minimum, out int natural) { int media_width; int media_height; if (this._media == null || this._media.thumb_width == -1 || this._media.thumb_height == -1) { media_width = MIN_WIDTH; media_height = MAX_HEIGHT; } else { media_width = this._media.thumb_width; media_height = this._media.thumb_height; } int max_width = (int) Math.floor ((height / 16.0) * 9); int width = int.min (media_width, max_width); minimum = MIN_WIDTH; natural = int.max (width, minimum); } public override void get_preferred_width (out int minimum, out int natural) { int media_width; if (this._media == null || this._media.thumb_width == -1) { media_width = 1; } else { media_width = this._media.thumb_width; } minimum = int.min (media_width, MIN_WIDTH); natural = media_width; } public override void realize () { this.set_realized (true); Gdk.WindowAttr attr = {}; attr.x = 0; attr.y = 0; attr.width = get_allocated_width(); attr.height = get_allocated_height(); attr.window_type = Gdk.WindowType.CHILD; attr.visual = this.get_visual (); attr.wclass = Gdk.WindowWindowClass.INPUT_ONLY; attr.event_mask = this.get_events () | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK | Gdk.EventMask.TOUCH_MASK | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK; Gdk.WindowAttributesType attr_mask = Gdk.WindowAttributesType.X | Gdk.WindowAttributesType.Y; Gdk.Window window = this.get_parent_window (); this.set_window (window); window.ref (); this.event_window = new Gdk.Window (window, attr, attr_mask); this.register_window (this.event_window); } public override void unrealize () { if (this.event_window != null) { this.unregister_window (this.event_window); this.event_window.destroy (); this.event_window = null; } base.unrealize (); } public override void map () { base.map (); if (this.event_window != null) this.event_window.show (); } public override void unmap () { if (this.event_window != null) this.event_window.hide (); base.unmap (); } public override void size_allocate (Gtk.Allocation alloc) { base.size_allocate (alloc); if (this.get_realized ()) { int draw_x, draw_y; double scale; Utils.calculate_draw_offset (_media.thumb_width, _media.thumb_height, get_allocated_width(), get_allocated_height(), out draw_x, out draw_y, out scale); var draw_width = alloc.width - draw_x * 2; var draw_height = alloc.height - draw_y; this.event_window.move_resize (alloc.x + draw_x, alloc.y + int.max(0, draw_y), draw_width, draw_height); } } public override bool enter_notify_event (Gdk.EventCrossing evt) { if (evt.window == this.event_window && evt.detail != Gdk.NotifyType.INFERIOR) { this.set_state_flags (this.get_state_flags () | Gtk.StateFlags.PRELIGHT, true); } return Gdk.EVENT_PROPAGATE; } public override bool leave_notify_event (Gdk.EventCrossing evt) { if (evt.window == this.event_window && evt.detail != Gdk.NotifyType.INFERIOR) { this.set_state_flags (this.get_state_flags () & ~Gtk.StateFlags.PRELIGHT, true); } return Gdk.EVENT_PROPAGATE; } private void gesture_pressed_cb (int n_press, double x, double y) { Gdk.EventSequence sequence = this.press_gesture.get_current_sequence (); Gdk.Event event = this.press_gesture.get_last_event (sequence); if (this._media == null) return; if (event.triggers_context_menu ()) { this.press_gesture.set_state (Gtk.EventSequenceState.CLAIMED); if (this.menu == null) { this.menu = new Gtk.Menu.from_model (menu_model); this.menu.attach_to_widget (this, null); } menu.show_all (); menu.popup_at_pointer (event); } } private void gesture_released_cb (int n_press, double x, double y) { Gdk.EventSequence sequence = this.press_gesture.get_current_sequence (); Gdk.Event event = this.press_gesture.get_last_event (sequence); uint button = this.press_gesture.get_current_button (); if (this._media == null || event == null) return; if (button == Gdk.BUTTON_PRIMARY) { if (_media.invalid) { reload_image(); } else { this.press_gesture.set_state (Gtk.EventSequenceState.CLAIMED); double px = x / (double)this.get_allocated_width (); double py = y / (double)this.get_allocated_height (); this.clicked (this, px, py); } } } public override bool key_press_event (Gdk.EventKey event) { if (event.keyval == Gdk.Key.Return || event.keyval == Gdk.Key.KP_Enter) { if (_media.invalid) { reload_image(); } else { this.clicked (this, 0.5, 0.5); } return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } private void set_image_description() { if (media != null) { this.set_tooltip_text(media.alt_text); this.get_accessible().set_description(media.alt_text ?? ""); } } private void set_save_as_sensitivity() { ((GLib.SimpleAction)actions.lookup_action ("save-as")).set_enabled (!_media.invalid && !is_m3u8); } } cawbird-1.4.2/src/widgets/MultiMediaWidget.vala000066400000000000000000000073671416632607600214610ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class MultiMediaWidget : Gtk.Box { public const int MAX_HEIGHT = 180; private bool __restrict_height = false; public bool restrict_height{ get { return this.__restrict_height; } set { this.__restrict_height = value; this.set_size_request(-1, value ? MAX_HEIGHT : -1); } } public unowned Gtk.Window window; private MediaButton[] media_buttons; private int media_count = 0; public signal void media_clicked (Cb.Media m, int index, double px, double py); private bool media_invalid_fired = false; public signal void media_invalid (); construct { this.orientation = Gtk.Orientation.HORIZONTAL; this.homogeneous = true; this.spacing = 2; } public MultiMediaWidget () { this.notify["visible"].connect(() => { if (!this.visible) { return; } for (int i = 0; i < media_count; i ++) { if (media_buttons[i] != null) { var media = media_buttons[i].media; if (!media.loaded && !media.loading) { Cb.MediaDownloader.get_default().load_async.begin (media); } } } }); } public void set_all_media (Cb.Media[] medias) { this.remove_all (); this.media_buttons = new MediaButton[medias.length]; this.media_count = medias.length; for (int i = 0; i < medias.length; i++) { assert (medias[i] != null); set_media (i, medias[i]); } } private void remove_all () { this.get_children ().foreach ((w) => { this.remove (w); }); } public void set_media (int index, Cb.Media media) { assert (index < media_count); if (media.loaded && media.invalid) return; var button = new MediaButton (null, this.restrict_height); button.set_data ("pos", index); button.window = this.window; button.halign = Gtk.Align.CENTER; button.valign = Gtk.Align.END; media_buttons[index] = button; if (media.loaded) { media_buttons[index].media = media; } else { media_buttons[index].media = media; media.progress.connect (media_loaded_cb); if (!media.loading && this.visible) { Cb.MediaDownloader.get_default().load_async.begin (media); } } button.visible = true; button.clicked.connect (button_clicked_cb); this.pack_start (button, true, true); this.queue_draw (); } private void button_clicked_cb (MediaButton source, double px, double py) { if (source.media != null && source.media.loaded) { int index = source.get_data ("pos"); media_clicked (source.media, index, px, py); } } private void media_loaded_cb (Cb.Media source) { if (source.percent_loaded < 1) return; if (source.invalid) { if (!media_invalid_fired) { media_invalid (); media_invalid_fired = true; } return; } for (int i = 0; i < media_count; i ++) { if (media_buttons[i] != null && media_buttons[i].media == source) { media_buttons[i].queue_draw (); break; } } } } cawbird-1.4.2/src/widgets/PixbufButton.vala000066400000000000000000000056641416632607600207120ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class PixbufButton : Gtk.Button { private Cairo.ImageSurface bg; private bool _round = false; public bool round { get { return _round; } set { if (value) { this.get_style_context ().add_class ("pixbuf-button-round"); } else { this.get_style_context ().remove_class ("pixbuf-button-round"); } _round = value; } } construct { this.border_width = 0; get_style_context ().add_class ("pixbuf-button"); } public PixbufButton () {} public override bool draw (Cairo.Context ct) { var sc = this.get_style_context (); int widget_width = this.get_allocated_width (); int widget_height = this.get_allocated_height (); if (bg != null) { var surface = new Cairo.Surface.similar (ct.get_target (), Cairo.Content.COLOR_ALPHA, widget_width, widget_height); var ctx = new Cairo.Context (surface); ctx.rectangle (0, 0, widget_width, widget_height); double scale_x = (double)widget_width / bg.get_width (); double scale_y = (double)widget_height / bg.get_height (); ctx.save (); ctx.scale (scale_x, scale_y); ctx.set_source_surface (bg, 0, 0); ctx.fill (); ctx.restore (); if (_round) { // make it round ctx.set_operator (Cairo.Operator.DEST_IN); ctx.translate (widget_width / 2, widget_height / 2); ctx.arc (0, 0, widget_width / 2, 0, 2 * Math.PI); ctx.fill (); // draw outline sc.render_frame (ct, 0, 0, widget_width, widget_height); } ct.rectangle (0, 0, widget_width, widget_height); ct.set_source_surface (surface, 0, 0); ct.fill (); } // The css-styled background should be transparent. base.draw (ct); return GLib.Source.CONTINUE; } public void set_bg (Cairo.ImageSurface bg) { this.bg = bg; this.set_size_request (bg.get_width(), bg.get_height()); this.queue_draw (); } public void set_pixbuf (Gdk.Pixbuf pixbuf) { this.bg = (Cairo.ImageSurface)Gdk.cairo_surface_create_from_pixbuf (pixbuf, 1, null); this.queue_draw (); } } cawbird-1.4.2/src/widgets/ReplyEntry.vala000066400000000000000000000021621416632607600203640ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ class ReplyEntry : Gtk.Entry { [Signal (action = true)] public signal void cancelled (); public ReplyEntry () {} static construct { unowned Gtk.BindingSet binding_set = Gtk.BindingSet.by_class ((GLib.ObjectClass)typeof (ReplyEntry).class_ref ()); Gtk.BindingEntry.add_signal (binding_set, Gdk.Key.Escape, 0, "cancelled", 0, null); } } cawbird-1.4.2/src/widgets/ResizableImage.vala000066400000000000000000000116171416632607600211370ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ private class ResizableImage : Gtk.Image { /* We use MIN_ constants in case the media has not yet been loaded */ private const int MIN_HEIGHT = 120; private const int MIN_WIDTH = 120; public Cairo.ImageSurface? image_surface = null; public ResizableImage () { this.get_style_context ().add_class ("inline-media"); } private void get_draw_size (int allocated_width, int allocated_height, out int width, out int height, out double scale) { if (image_surface != null) { var w = image_surface.get_width () * 1.0; var h = image_surface.get_height () * 1.0; scale = double.min(1.0, double.min(allocated_width / w, allocated_height / h)); width = (int) Math.floor(w * scale); height = (int) Math.floor(h * scale); } else { width = MIN_WIDTH; height = MIN_HEIGHT; scale = 1; } } public override bool draw (Cairo.Context ct) { if (image_surface != null) { int widget_width = get_allocated_width (); int widget_height = get_allocated_height (); int draw_width, draw_height; double scale; this.get_draw_size (widget_width, widget_height, out draw_width, out draw_height, out scale); double draw_x = (widget_width - draw_width) / 2; ct.save (); ct.rectangle (0, 0, widget_width, widget_height); ct.scale (scale, scale); double draw_y = (widget_height - draw_height) / 2; ct.set_source_surface (image_surface, draw_x / scale, draw_y / scale); ct.paint (); ct.restore (); var sc = this.get_style_context (); sc.render_background (ct, draw_x, 0, draw_width, draw_height); sc.render_frame (ct, draw_x, 0, draw_width, draw_height); if (this.has_visible_focus ()) { sc.render_focus (ct, draw_x + 2, 2, draw_width - 4, draw_height - 4); } } return Gdk.EVENT_PROPAGATE; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void get_preferred_height (out int minimum, out int natural) { int media_height = image_surface != null ? image_surface.get_height() : MIN_HEIGHT; minimum = int.min (media_height, MIN_HEIGHT); natural = media_height; } public override void get_preferred_height_for_width (int width, out int minimum, out int natural) { int media_width = image_surface != null ? image_surface.get_width() : MIN_WIDTH; int media_height = image_surface != null ? image_surface.get_height() : MIN_HEIGHT; double scale = width / (double) media_width; if (scale >= 1) { natural = media_height; } else { natural = (int) Math.floor (media_height * scale); } minimum = int.min(media_height, MIN_HEIGHT); } public override void get_preferred_width_for_height (int height, out int minimum, out int natural) { int media_width = image_surface != null ? image_surface.get_width() : MIN_WIDTH; int media_height = image_surface != null ? image_surface.get_height() : MIN_HEIGHT; double scale = height / (double) media_height; if (scale >= 1) { natural = media_width; } else { natural = (int) Math.floor (media_width * scale); } minimum = int.min(media_width, MIN_WIDTH); } public override void get_preferred_width (out int minimum, out int natural) { int media_width = image_surface != null ? image_surface.get_width() : MIN_WIDTH; minimum = int.min (media_width, MIN_WIDTH); natural = media_width; } } cawbird-1.4.2/src/widgets/ScrollWidget.vala000066400000000000000000000150751416632607600206600ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ const int TOP = 1; const int BOTTOM = 2; const int NONE = 0; public class ScrollWidget : Gtk.ScrolledWindow { public signal void scrolled_to_start(double value); public signal void scrolled_to_end(); private double upper_cache; private double value_cache; private int balance = NONE; public double end_diff {get; set; default = 200;} private ulong scroll_down_id; private ulong scroll_up_id; public bool scrolled_down { get { return vadjustment.value >= vadjustment.upper - vadjustment.page_size - 5; } } public bool scrolled_up { get { return vadjustment.value <= 5; } } public bool is_scrollable { get { return vadjustment.upper > vadjustment.page_size; } } //Transition times private int64 start_time; private int64 end_time; private double transition_diff; private double transition_start_value; construct { vadjustment.notify["upper"].connect (keep_upper_func); vadjustment.notify["value"].connect (keep_value_func); } private void keep_upper_func() { // {{{ double upper = vadjustment.upper; if (balance == TOP){ double inc = (upper - upper_cache); this.vadjustment.value += inc; balance = NONE; } this.upper_cache = vadjustment.upper; this.value_cache = vadjustment.value; } // }}} private void keep_value_func () { // {{{ // Call the scrolled_to_top signal if necessary if(vadjustment.value < 10.0) { scrolled_to_start(vadjustment.value); } double max = vadjustment.upper - vadjustment.page_size; if (vadjustment.value >= max - end_diff) scrolled_to_end (); double upper = vadjustment.upper; if (balance == BOTTOM) { double inc = (upper - upper_cache); this.vadjustment.value -= inc; balance = NONE; } this.upper_cache = vadjustment.upper; this.value_cache = vadjustment.value; } // }}} public void balance_next_upper_change (int mode) { balance = mode; } /** * TODO: Update scroll_down_next * Scroll to the very top of the scrolled window once the next * size_allocate occurs. * This will use a transition if the correct Gtk+ settings is set * to true. * * @param animate Whether to animate/transition the change or not (default: true) */ public void scroll_up_next (bool animate = true, bool force_start = false) { // {{{ if (!this.get_mapped ()) { this.vadjustment.value = 0; return; } // TODO: I really can't stand the duplication here if (force_start) { if (Gtk.Settings.get_default ().gtk_enable_animations && animate) { this.start_time = this.get_frame_clock ().get_frame_time (); this.end_time = start_time + TRANSITION_DURATION; this.transition_diff = - this.vadjustment.value; this.transition_start_value = vadjustment.value; this.add_tick_callback (scroll_up_tick_cb); } else { this.vadjustment.value = 0; } } else { if (scroll_up_id != 0) { this.transition_diff = -this.vadjustment.value; this.transition_start_value = this.vadjustment.value; return; } scroll_up_id = this.vadjustment.notify["upper"].connect (() => { if (Gtk.Settings.get_default ().gtk_enable_animations && animate) { this.start_time = this.get_frame_clock ().get_frame_time (); this.end_time = start_time + TRANSITION_DURATION; this.transition_diff = - this.vadjustment.value; this.transition_start_value = vadjustment.value; this.add_tick_callback (scroll_up_tick_cb); } else { this.vadjustment.value = 0; } this.vadjustment.disconnect (scroll_up_id); this.scroll_up_id = 0; }); } } // }}} /** * Scroll to the very end of the scrolled window once the next * size_alocate occurs. * This will use a transition if the correct Gtk+ settings is set * to true * * @param animate Whether to animate/transition the change or not (default: true) * @param force_wait If this is set to true, we will wait for the next size_allocate * event, even if the widget is unmapped (default: false). */ public void scroll_down_next (bool animate = true, bool force_wait = false) { // {{{ if (!this.get_mapped () && !force_wait) { this.vadjustment.value = this.vadjustment.upper - this.vadjustment.page_size; return; } if (this.scroll_down_id != 0) return; scroll_down_id = this.size_allocate.connect (() => { if (Gtk.Settings.get_default ().gtk_enable_animations && animate) { this.start_time = this.get_frame_clock ().get_frame_time (); this.end_time = start_time + TRANSITION_DURATION; this.transition_diff = (vadjustment.upper - vadjustment.page_size - vadjustment.value); this.transition_start_value = this.vadjustment.value; this.add_tick_callback (scroll_up_tick_cb); } else { this.vadjustment.value = this.vadjustment.upper - this.vadjustment.page_size; } this.disconnect (scroll_down_id); this.scroll_down_id = 0; }); } // }}} /* This is essentially a straight-up vala port of the transition code in GtkStack/GtkRevealer */ private bool scroll_up_tick_cb (Gtk.Widget widget, Gdk.FrameClock frame_clock) { if (!this.get_mapped ()) { vadjustment.value = transition_start_value + transition_diff; return false; } int64 now = frame_clock.get_frame_time (); double t = 1.0; if (now < this.end_time) t = (now - start_time) / (double)(end_time - start_time); t = ease_out_cubic (t); this.vadjustment.value = transition_start_value + (t * transition_diff); if (this.vadjustment.value <= 0 || now >= end_time) { this.queue_draw (); return false; } return true; } } cawbird-1.4.2/src/widgets/TextButton.vala000066400000000000000000000034641416632607600203750ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class TextButton : Gtk.Button { public TextButton () {} construct { this.get_style_context ().add_class ("text-only-button"); } /** * Adds a GtkLabel to the Button using the given text as the text. * If the button already contains another child, that will either be replaced if it's * no instance of GtkLabel, or - if it's a GtkLabel already - be reused. * * @param text The markup to use(see pango markup) */ public void set_text (string text) { Gtk.Label label = null; Gtk.Widget child = get_child (); if (child != null) { if (child is Gtk.Label) { label = (Gtk.Label)child; label.set_text (text); } else { this.remove (child); label = new Gtk.Label (text); } } else { label = new Gtk.Label (text); } label.set_justify (Gtk.Justification.CENTER); label.valign = Gtk.Align.BASELINE; label.ellipsize = Pango.EllipsizeMode.END; label.xalign = 0; label.visible = true; if(label.parent == null) this.add (label); } } cawbird-1.4.2/src/widgets/TweetListBox.vala000066400000000000000000000104001416632607600206360ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ public class TweetListBox : ListBox { public Cb.DeltaUpdater delta_updater; public unowned Account account; public Cb.TweetModel model = new Cb.TweetModel (); private Gtk.GestureMultiPress press_gesture; private TweetListEntry? _action_entry; public TweetListEntry? action_entry { get { return _action_entry; } } public MainWindow main_window { private get; set; } public TweetListBox () { } construct { this.press_gesture = new Gtk.GestureMultiPress (this); this.press_gesture.set_button (0); this.press_gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE); this.press_gesture.pressed.connect (gesture_pressed_cb); this.delta_updater = new Cb.DeltaUpdater (this); Cb.Utils.bind_model (this, this.model, widget_create_func); } public void set_thread_mode (bool thread_mode) { this.model.set_thread_mode (thread_mode); } private Gtk.Widget widget_create_func (GLib.Object obj) { assert (obj is Cb.Tweet); var row = new TweetListEntry ((Cb.Tweet) obj, main_window, this.account); row.fade_in (); return row; } private void gesture_pressed_cb (int n_press, double x, double y) { Gdk.EventSequence sequence = this.press_gesture.get_current_sequence (); Gdk.EventButton event = (Gdk.EventButton)this.press_gesture.get_last_event (sequence); if (event.triggers_context_menu ()) { /* From gtklistbox.c */ Gdk.Window? event_window = event.window; Gdk.Window window = this.get_window (); double relative_y = event.y; double parent_y; while ((event_window != null) && (event_window != window)) { event_window.coords_to_parent (0, relative_y, null, out parent_y); relative_y = parent_y; event_window = event_window.get_effective_parent (); } Gtk.Widget row = this.get_row_at_y ((int)relative_y); if (row is TweetListEntry && row.sensitive) { var tle = (TweetListEntry) row; if (tle != this._action_entry && this._action_entry != null && this._action_entry.shows_actions) { this._action_entry.toggle_mode (); } tle.toggle_mode (); if (tle.shows_actions) set_action_entry (tle); else set_action_entry (null); this.press_gesture.set_state (Gtk.EventSequenceState.CLAIMED); } } } private void set_action_entry (TweetListEntry? entry) { if (this._action_entry != null) { this._action_entry.destroy.disconnect (action_entry_destroyed_cb); this._action_entry = null; } if (entry != null) { this._action_entry = entry; this._action_entry.destroy.connect (action_entry_destroyed_cb); } } private void action_entry_destroyed_cb () { this._action_entry = null; } public new void remove_all () { this.model.clear(); } public void hide_tweets_from (int64 user_id, Cb.TweetState reason) { model.toggle_flag_on_user_tweets (user_id, reason, true); } public void show_tweets_from (int64 user_id, Cb.TweetState reason) { model.toggle_flag_on_user_tweets (user_id, reason, false); } public void hide_retweets_from (int64 user_id, Cb.TweetState reason) { model.toggle_flag_on_user_retweets (user_id, reason, true); } public void show_retweets_from (int64 user_id, Cb.TweetState reason) { model.toggle_flag_on_user_retweets (user_id, reason, false); } } cawbird-1.4.2/src/widgets/UserListsWidget.vala000066400000000000000000000342101416632607600213470ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/user-lists-widget.ui")] class UserListsWidget : Gtk.Box { [GtkChild] private unowned Gtk.Label user_list_label; [GtkChild] private unowned Gtk.ListBox user_list_box; [GtkChild] private unowned Gtk.Frame user_list_frame; [GtkChild] private unowned Gtk.Label subscribed_list_label; [GtkChild] private unowned Gtk.ListBox subscribed_list_box; [GtkChild] private unowned Gtk.Frame subscribed_list_frame; [GtkChild] private unowned NewListEntry new_list_entry; [GtkChild] private unowned Gtk.Revealer user_lists_revealer; [GtkChild] private unowned Gtk.Separator upper_separator; [GtkChild] private unowned Gtk.ListBox new_list_box; public unowned MainWindow main_window { get; set; } public unowned Account account { get; set; } private bool show_create_entry = true; public UserListsWidget() { } construct { user_list_box.set_header_func (default_header_func); user_list_box.set_sort_func (ListListEntry.sort_func); subscribed_list_box.set_header_func (default_header_func); subscribed_list_box.set_sort_func (ListListEntry.sort_func); } public void connect_nav(Gtk.ScrolledWindow parent, Gtk.Widget widget_up) { // FIXME: "Up" key works between lists, but not from top list - no keynav_failed event is fired // (and moving between the lists doesn't seem to trigger it anyway - only moving down from // the bottom list) Utils.connect_vadjustment(parent, user_list_box); Utils.connect_vadjustment(parent, subscribed_list_box); } public void hide_user_list_entry () { new_list_entry.hide (); new_list_entry.no_show_all = true; user_list_label.visible = true; //user_list_frame.margin_top = 24; show_create_entry = false; upper_separator.visible = false; upper_separator.no_show_all = true; } [GtkCallback] private void row_activated (Gtk.ListBoxRow row) { if (row is NewListEntry) { ((NewListEntry)row).reveal (); } else { var entry = (ListListEntry) row; var bundle = new Cb.Bundle (); bundle.put_int64 (ListStatusesPage.KEY_LIST_ID, entry.id); bundle.put_string (ListStatusesPage.KEY_TITLE, entry.title); bundle.put_string (ListStatusesPage.KEY_NAME, entry.name); bundle.put_bool (ListStatusesPage.KEY_USER_LIST, entry.user_list); bundle.put_string (ListStatusesPage.KEY_DESCRIPTION, entry.description); bundle.put_string (ListStatusesPage.KEY_CREATOR, entry.creator_screen_name); bundle.put_int (ListStatusesPage.KEY_N_SUBSCRIBERS, entry.n_subscribers); bundle.put_int (ListStatusesPage.KEY_N_MEMBERS, entry.n_members); bundle.put_int64 (ListStatusesPage.KEY_CREATED_AT, entry.created_at); bundle.put_string (ListStatusesPage.KEY_MODE, entry.mode); main_window.main_widget.switch_page (Page.LIST_STATUSES, bundle); } } public async void load_lists (int64 user_id, string name = "", string screen_name = "") { if (user_id == 0) user_id = account.id; var collect_obj = new Collect (2); collect_obj.finished.connect (() => { load_lists.callback (); }); var call = account.proxy.new_call (); call.set_function ("1.1/lists/subscriptions.json"); call.set_method ("GET"); call.add_param ("count", "200"); call.add_param ("user_id", user_id.to_string ()); Cb.Utils.load_threaded_async.begin (call, null, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { warning (e.message); } uint n_subscribed_list = lists_received_cb (root, subscribed_list_box); if (n_subscribed_list == 0) { subscribed_list_box.hide (); subscribed_list_frame.hide (); subscribed_list_label.hide (); } else { subscribed_list_box.show (); subscribed_list_frame.show (); subscribed_list_label.show (); } collect_obj.emit (); }); var user_call = account.proxy.new_call (); user_call.set_function ("1.1/lists/ownerships.json"); user_call.set_method ("GET"); user_call.add_param ("user_id", user_id.to_string ()); user_call.add_param ("count", "200"); Cb.Utils.load_threaded_async.begin (user_call, null, (_, res) => { Json.Node? root = null; try { root = Cb.Utils.load_threaded_async.end (res); } catch (GLib.Error e) { warning (e.message); } uint n_user_list = lists_received_cb (root, user_list_box); if (n_user_list == 0 && !show_create_entry) { user_list_label.hide (); user_list_box.hide (); user_list_frame.hide (); user_list_frame.margin_top = 0; } else { user_list_label.visible = !show_create_entry; user_list_frame.margin_top = show_create_entry ? 24 : 0; user_list_box.show (); user_list_frame.show (); user_lists_revealer.reveal_child = n_user_list > 0; } collect_obj.emit (); }); // TRANSLATORS: Value is user's name - used for accessibility text in "list of lists that this user has subscribed to" var accessible_name = user_id == account.id ? _("Subscribed lists") : _("%s subscribed lists").printf(name); this.subscribed_list_box.get_accessible().set_name(accessible_name); // TRANSLATORS: Value is user's name - used for accessibility text in "list of lists that this user created" accessible_name = user_id == account.id ? _("Your lists") : _("%s lists").printf(name); this.user_list_box.get_accessible().set_name(accessible_name); yield; } private uint lists_received_cb (Json.Node? root, Gtk.ListBox list_box) { // {{{ if (root == null) return 0; int64[] ids = {}; var arr = root.get_object ().get_array_member ("lists"); arr.foreach_element ((array, index, node) => { var obj = node.get_object (); ids += obj.get_int_member ("id"); var entry = new ListListEntry.from_json_data (obj, account); add_list(entry, list_box); }); var size_after = list_box.get_children().length(); var list_count = arr.get_length (); if (size_after != list_count) { foreach (var entry in list_box.get_children()) { var id = ((ListListEntry)entry).id; if (!(id in ids)) { remove_list(id); } } } return list_count; } // }}} public void remove_list (int64 list_id) { uint n_user_lists = user_list_box.get_children ().length (); user_list_box.foreach ((w) => { if (!(w is ListListEntry)) return; if (((ListListEntry)w).id == list_id) { user_list_box.remove (w); if (n_user_lists - 1 == 0) user_lists_revealer.reveal_child = false; } }); subscribed_list_box.foreach ((w) => { if (!(w is ListListEntry)) return; if (((ListListEntry)w).id == list_id) { subscribed_list_box.remove (w); } }); if (subscribed_list_box.get_children ().length () == 0) { subscribed_list_label.hide (); subscribed_list_frame.hide (); } } public void add_list (ListListEntry entry, Gtk.ListBox? list_box = null) { if (list_box == null) { if (entry.user_list) { list_box = user_list_box; } else { list_box = subscribed_list_box; } } var updated = false; // Avoid duplicates var user_lists = list_box.get_children (); foreach (Gtk.Widget w in user_lists) { if (!(w is ListListEntry)) continue; var list_entry = (ListListEntry)w; if (list_entry.id == entry.id) { update_list_entry_from_entry(list_entry, entry); updated = true; break; } } if (!updated) { list_box.add (entry); } if (entry.user_list) { user_lists_revealer.reveal_child = true; } else if (list_box.get_children().length() > 0) { if (list_box == user_list_box) { user_list_frame.show (); user_list_box.show (); user_list_label.show (); } else { subscribed_list_frame.show (); subscribed_list_box.show (); subscribed_list_label.show (); } } } public void update_list (int64 list_id, string title, string name, string description, string mode) { user_list_box.foreach ((w) => { if (!(w is ListListEntry)) return; var lle = (ListListEntry) w; if (lle.id == list_id) { update_list_entry(lle, title, name, description, mode); } }); } private void update_list_entry_from_entry (ListListEntry list_entry, ListListEntry src_entry) { update_list_entry(list_entry, src_entry.title, src_entry.name, src_entry.description, src_entry.mode); } private void update_list_entry (ListListEntry list_entry, string title, string name, string description, string mode) { list_entry.title = title; list_entry.name = name; list_entry.description = description; list_entry.mode = mode; list_entry.queue_draw (); } public void update_member_count (int64 list_id, int increase) { var lists = user_list_box.get_children (); foreach (var list in lists) { if (!(list is ListListEntry)) continue; var lle = (ListListEntry) list; if (lle.id == list_id) { lle.n_members += increase; break; } } } public TwitterList[] get_user_lists () { GLib.List children = user_list_box.get_children (); TwitterList[] lists = new TwitterList[children.length ()]; int i = 0; foreach (Gtk.Widget w in children) { assert (w is ListListEntry); var lle = (ListListEntry) w; lists[i].id = lle.id; lists[i].name = lle.name; lists[i].description = lle.description; lists[i].mode = lle.mode; lists[i].n_members = lle.n_members; i ++; } return lists; } public void clear_lists () { user_list_box.foreach ((w) => { user_list_box.remove (w);}); subscribed_list_box.foreach ((w) => {subscribed_list_box.remove (w);}); } [GtkCallback] private void new_list_create_activated_cb (string list_name) { // {{{ if (list_name.strip ().length <= 0) return; new_list_entry.sensitive = false; var call = account.proxy.new_call (); call.set_function ("1.1/lists/create.json"); call.set_method ("POST"); call.add_param ("name", list_name); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this.main_window); new_list_entry.sensitive = true; return; } var parser = new Json.Parser (); try { parser.load_from_data (call.get_payload ()); } catch (GLib.Error e) { critical (e.message); return; } var root = parser.get_root ().get_object (); var entry = new ListListEntry.from_json_data (root, account); add_list (entry); var bundle = new Cb.Bundle (); bundle.put_int64 (ListStatusesPage.KEY_LIST_ID, entry.id); bundle.put_string (ListStatusesPage.KEY_TITLE, entry.title); bundle.put_string (ListStatusesPage.KEY_NAME, entry.name); bundle.put_bool (ListStatusesPage.KEY_USER_LIST, true); bundle.put_string (ListStatusesPage.KEY_DESCRIPTION, entry.description); bundle.put_string (ListStatusesPage.KEY_CREATOR, entry.creator_screen_name); bundle.put_int (ListStatusesPage.KEY_N_SUBSCRIBERS, entry.n_subscribers); bundle.put_int (ListStatusesPage.KEY_N_MEMBERS, entry.n_members); bundle.put_int64 (ListStatusesPage.KEY_CREATED_AT, entry.created_at); bundle.put_string (ListStatusesPage.KEY_MODE, entry.mode); main_window.main_widget.switch_page (Page.LIST_STATUSES, bundle); new_list_entry.sensitive = true; }); } // }}} public void unreveal () { new_list_entry.unreveal (); } [GtkCallback] private bool new_list_box_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.DOWN) { if (user_list_box.visible) { user_list_box.child_focus (direction); return Gdk.EVENT_STOP; } else if (subscribed_list_box.visible) { subscribed_list_box.child_focus (direction); return Gdk.EVENT_STOP; } } return Gdk.EVENT_PROPAGATE; } [GtkCallback] private bool user_list_box_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.UP) { if (new_list_box.visible) { new_list_box.child_focus (direction); return Gdk.EVENT_STOP; } } else if (direction == Gtk.DirectionType.DOWN) { if (subscribed_list_box.visible) { subscribed_list_box.child_focus (direction); return Gdk.EVENT_STOP; } } return Gdk.EVENT_PROPAGATE; } [GtkCallback] private bool subscribed_list_box_keynav_failed_cb (Gtk.DirectionType direction) { if (direction == Gtk.DirectionType.UP) { if (user_list_box.visible) { user_list_box.child_focus (direction); return Gdk.EVENT_STOP; } else if (new_list_box.visible) { new_list_box.child_focus (direction); return Gdk.EVENT_STOP; } } return Gdk.EVENT_PROPAGATE; } [GtkCallback] private void revealer_child_revealed_cb (GLib.Object source, GLib.ParamSpec spec) { Gtk.Revealer revealer = (Gtk.Revealer) source; if (revealer.child_revealed) revealer.show (); else revealer.hide (); } } cawbird-1.4.2/src/window/000077500000000000000000000000001416632607600152425ustar00rootroot00000000000000cawbird-1.4.2/src/window/AboutDialog.vala000066400000000000000000000020541416632607600203020ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/about-dialog.ui")] class AboutDialog : Gtk.AboutDialog { [GtkCallback] private void response_cb (int id) { if (id == Gtk.ResponseType.DELETE_EVENT) { this.close (); } else warning ("Unhandled response: %d", id); } } cawbird-1.4.2/src/window/AccountDialog.vala000066400000000000000000000364341416632607600206350ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/account-dialog.ui")] public class AccountDialog : Gtk.Window { private const int MAX_DESCRIPTION_LENGTH = 160; private const string PAGE_NORMAL = "normal"; private const string PAGE_DELETE = "delete"; [GtkChild] private unowned Gtk.Entry name_entry; [GtkChild] private unowned AvatarBannerWidget avatar_banner_widget; [GtkChild] private unowned Gtk.Stack delete_stack; [GtkChild] private unowned Gtk.Switch autostart_switch; [GtkChild] private unowned Gtk.Entry website_entry; [GtkChild] private unowned CompletionTextView description_text_view; [GtkChild] private unowned CropWidget crop_widget; [GtkChild] private unowned Gtk.Stack content_stack; [GtkChild] private unowned Gtk.Box info_box; [GtkChild] private unowned Gtk.Label error_label; [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Label description_length_label; private unowned Account account; private string old_user_name; private string old_description; private string old_website; private Gdk.Pixbuf? new_avatar = null; private Gdk.Pixbuf? new_banner = null; private int old_width = 0; private int old_height = 0; private bool account_was_not_initied = false; public AccountDialog (Account account) { this.account = account; name_entry.text = account.name; avatar_banner_widget.set_account (account); description_text_view.set_account (account); set_transient_data (account.website, account.description); autostart_switch.freeze_notify (); string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); foreach (unowned string acc in startup_accounts) { if (acc == this.account.screen_name) { autostart_switch.active = true; break; } } autostart_switch.thaw_notify (); avatar_banner_widget.avatar_changed.connect ((p) => { new_avatar = p; }); avatar_banner_widget.banner_changed.connect ((b) => { new_banner = b; }); if (account.proxy == null) { account_was_not_initied = true; account.init_proxy (); account.query_user_info.begin ((obj, res) => { set_transient_data (account.website, account.description); }); } Gtk.AccelGroup ag = new Gtk.AccelGroup (); ag.connect (Gdk.Key.Escape, 0, Gtk.AccelFlags.LOCKED, escape_pressed_cb); description_text_view.buffer.notify["text"].connect (update_description_length); this.add_accel_group (ag); this.update_description_length (); } private void update_description_length () { int length = description_text_view.buffer.text.length; description_length_label.label = "%d/160".printf (length); if (length > MAX_DESCRIPTION_LENGTH) { save_button.sensitive = false; } else { save_button.sensitive = true; } } public override void destroy () { if (account != null) { if (account_was_not_initied) { account.uninit (); } account = null; } base.destroy (); } private bool escape_pressed_cb () { this.destroy (); return Gdk.EVENT_STOP; } private void set_transient_data (string? website, string? description) { website_entry.text = account.website ?? ""; old_user_name = account.name; old_website = account.website ?? ""; old_description = account.description ?? ""; description_text_view.get_buffer ().set_text (account.description ?? ""); } [GtkCallback] private void delete_button_clicked_cb () { delete_stack.visible_child_name = PAGE_DELETE; } private void save_data () { bool needs_save = (old_user_name != name_entry.text) || (old_description != description_text_view.buffer.text) || (old_website != website_entry.text); bool needs_init = needs_save || (new_avatar != null) || (new_banner != null); if (needs_init && account.proxy == null) { account.init_proxy (); } if (needs_save) { debug ("Saving data..."); var call = account.proxy.new_call (); call.set_function ("1.1/account/update_profile.json"); call.set_method ("POST"); call.add_param ("url", website_entry.text); call.add_param ("name", name_entry.text); call.add_param ("description", description_text_view.buffer.text); call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); debug ("Profile successfully updated"); } catch (GLib.Error e) { warning (e.message); Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this); } }); /* Update local user data */ account.name = name_entry.text; account.description = description_text_view.buffer.text; account.website = website_entry.text; } if (new_avatar != null) { debug ("Updating avatar..."); uint8[] buffer; try { new_avatar.save_to_buffer (out buffer, "png", null); } catch (GLib.Error e) { warning (e.message); return; } string b64 = GLib.Base64.encode (buffer); var call = account.proxy.new_call (); call.set_function ("1.1/account/update_profile_image.json"); call.set_method ("POST"); call.add_param ("skip_status", "true"); call.add_param ("include_entities", "false"); call.add_param ("image", b64); call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); debug ("Avatar successfully updated"); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this); return; } /* Locally set new avatar */ var s = Gdk.cairo_surface_create_from_pixbuf (new_avatar, 1, null); account.set_new_avatar (s); }); } if (new_banner != null) { debug ("Updating banner..."); uint8[] buffer; // XXX With large banners, this can be too slow... try { new_banner.save_to_buffer (out buffer, "png", null); } catch (GLib.Error e) { warning (e.message); return; } string b64 = GLib.Base64.encode (buffer); var call = account.proxy.new_call (); call.set_function ("1.1/account/update_profile_banner.json"); call.set_method ("POST"); call.add_param ("banner", b64); call.invoke_async.begin (null, (obj, res) => { try { call.invoke_async.end (res); debug ("Banner successfully updated"); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this); } }); } } [GtkCallback] private void delete_confirm_button_clicked_cb () { /* - Close open window of that account - Remove the account from the db, disk, etc. - Remove the account from the app menu - If this would close the last opened window, set the account of that window to NULL */ int64 acc_id = account.id; FileUtils.remove (Dirs.config (@"accounts/$(acc_id).db")); FileUtils.remove (Dirs.config (@"accounts/$(acc_id).png")); FileUtils.remove (Dirs.config (@"accounts/$(acc_id)_small.png")); Cawbird.db.exec (@"DELETE FROM `accounts` WHERE `id`='$(acc_id)';"); /* Remove account from startup accounts, if it's in there */ string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); for (int i = 0; i < startup_accounts.length; i++) if (startup_accounts[i] == account.screen_name) { string[] sa_new = new string[startup_accounts.length - 1]; for (int x = 0; x < i; i++) sa_new[x] = startup_accounts[x]; for (int x = i+1; x < startup_accounts.length; x++) sa_new[x] = startup_accounts[x]; Settings.get ().set_strv ("startup-accounts", sa_new); } Cawbird cb = (Cawbird) GLib.Application.get_default (); /* Handle windows, i.e. if this MainWindow is the last open one, we want to use it to show the "new account" UI, otherwise we just close it. */ unowned GLib.List windows = cb.get_windows (); Gtk.Window? account_window = null; int n_main_windows = 0; foreach (Gtk.Window win in windows) { if (win is MainWindow) { n_main_windows ++; MainWindow main_win = (MainWindow)win; if (main_win.account != null && main_win.account.id == this.account.id) { account_window = win; } } } debug ("Open main windows: %d", n_main_windows); if (account_window != null) { if (n_main_windows > 1) { account_window.destroy (); } else { Account first_acct = null; Account first_startup_acct = null; startup_accounts = Settings.get ().get_strv ("startup-accounts"); string startup_acct = null; if (startup_accounts.length > 0) { startup_acct = startup_accounts[0]; } for (uint i = 0; i < Account.get_n (); i ++) { var acct = Account.get_nth (i); if (acct.screen_name == account.screen_name) { continue; } else if (acct.screen_name == startup_acct) { first_startup_acct = acct; } else if (first_acct == null) { first_acct = acct; } } ((MainWindow)account_window).change_account (first_startup_acct ?? first_acct); } } /* Remove the account from the global list of accounts */ Account acc_to_remove = Account.query_account_by_id (acc_id); cb.account_removed (acc_to_remove); Account.remove_account (acc_to_remove.screen_name); /* Close this dialog */ this.destroy (); } [GtkCallback] private void delete_cancel_button_clicked_cb () { delete_stack.visible_child_name = PAGE_NORMAL; } [GtkCallback] private void autostart_switch_activate_cb () { bool active = autostart_switch.active; string[] startup_accounts = Settings.get ().get_strv ("startup-accounts"); if (active) { foreach (unowned string acc in startup_accounts) { if (acc == this.account.screen_name) { return; } } string[] new_startup_accounts = new string[startup_accounts.length + 1]; int i = 0; foreach (unowned string s in startup_accounts) { new_startup_accounts[i] = s; i ++; } new_startup_accounts[new_startup_accounts.length - 1] = this.account.screen_name; Settings.get ().set_strv ("startup-accounts", new_startup_accounts); } else { string[] new_startup_accounts = new string[startup_accounts.length - 1]; int i = 0; foreach (unowned string acc in startup_accounts) { if (acc != this.account.screen_name) { new_startup_accounts[i] = acc; i ++; } } Settings.get ().set_strv ("startup-accounts", new_startup_accounts); } } private void show_crop_image_selector () { var filechooser = new Gtk.FileChooserNative (_("Select Banner Image"), this, Gtk.FileChooserAction.OPEN, _("Open"), _("Cancel")); var filter = new Gtk.FileFilter (); filter.add_mime_type ("image/png"); filter.add_mime_type ("image/jpeg"); filechooser.set_filter (filter); if (filechooser.run () == Gtk.ResponseType.ACCEPT) { string selected_file = filechooser.get_filename (); Gdk.Pixbuf? image = null; try { image = new Gdk.Pixbuf.from_file (selected_file); } catch (GLib.Error e) { warning (e.message); return; } /* Values for banner */ int min_width = 200; int min_height = 100; if (crop_widget.desired_aspect_ratio == 1.0) { /* Avatar */ min_width = 48; min_height = 48; } if (image.get_width () >= min_width && image.get_height () >= min_height) { crop_widget.set_image (image); save_button.sensitive = true; } else { string error_str = ""; error_str += _("Image does not meet minimum size requirements:") + "\n"; error_str += ngettext ("Minimum width: %d pixel", "Minimum width: %d pixels", min_width) .printf (min_width) + "\n"; error_str += ngettext ("Minimum height: %d pixel", "Minimum height: %d pixels", min_height) .printf (min_height); error_label.label = error_str; content_stack.visible_child = error_label; save_button.sensitive = false; } } else { content_stack.visible_child = info_box; } } [GtkCallback] private void avatar_clicked_cb () { this.get_size (out old_width, out old_height); this.resize (400, 400); crop_widget.set_image (null); crop_widget.set_size_request (-1, 400); crop_widget.desired_aspect_ratio = 1.0; crop_widget.set_min_size (48); content_stack.visible_child = crop_widget; show_crop_image_selector (); save_button.label = _("Pick"); } [GtkCallback] private void banner_clicked_cb () { this.get_size (out old_width, out old_height); this.resize (700, 350); crop_widget.set_size_request (700, 350); crop_widget.set_image (null); crop_widget.desired_aspect_ratio = 2.0; crop_widget.set_min_size (200); content_stack.visible_child = crop_widget; show_crop_image_selector (); save_button.label = _("Pick"); } [GtkCallback] private void cancel_button_clicked_cb () { if (content_stack.visible_child == crop_widget || content_stack.visible_child == error_label) { this.resize (old_width, old_height); old_width = 0; old_height = 0; /* Just go back */ content_stack.visible_child = info_box; save_button.label = _("Save"); } else { this.destroy (); } } [GtkCallback] private void save_button_clicked_cb () { if (content_stack.visible_child == crop_widget) { Gdk.Pixbuf new_pixbuf = crop_widget.get_cropped_image (); if (crop_widget.desired_aspect_ratio == 1.0) { /* Avatar */ avatar_banner_widget.set_avatar (new_pixbuf); new_avatar = new_pixbuf; } else if (crop_widget.desired_aspect_ratio == 2.0) { /* Banner */ avatar_banner_widget.set_banner (new_pixbuf); new_banner = new_pixbuf; } else { GLib.assert_not_reached (); } save_button.label = _("Save"); content_stack.visible_child = info_box; } else { save_data (); this.destroy (); } } } cawbird-1.4.2/src/window/ComposeTweetWindow.vala000066400000000000000000000535201416632607600217220ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/compose-window.ui")] class ComposeTweetWindow : Gtk.ApplicationWindow { const int DEFAULT_WIDTH = 460; public enum Mode { NORMAL, REPLY, QUOTE } [GtkChild] private unowned AvatarWidget avatar_image; [GtkChild] private unowned Gtk.Grid content_grid; [GtkChild] private unowned CompletionTextView tweet_text; [GtkChild] private unowned Gtk.Label length_label; [GtkChild] private unowned Gtk.Button send_button; [GtkChild] private unowned Gtk.Spinner title_spinner; [GtkChild] private unowned Gtk.Label title_label; [GtkChild] private unowned Gtk.Stack title_stack; [GtkChild] private unowned ComposeImageManager compose_image_manager; [GtkChild] private unowned Gtk.Button add_media_button; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Grid image_error_grid; [GtkChild] private unowned Gtk.Label image_error_label; [GtkChild] private unowned Gtk.Button cancel_button; [GtkChild] private unowned FavImageView fav_image_view; [GtkChild] private unowned Gtk.Button fav_image_button; [GtkChild] private unowned Gtk.Revealer completion_revealer; [GtkChild] private unowned Gtk.ListBox completion_list; [GtkChild] private unowned Gtk.Box add_button_box; private Cb.EmojiChooser? emoji_chooser = null; private Gtk.Button? emoji_button = null; private unowned Account account; private unowned MainWindow main_window; private Cb.Tweet referenced_tweet; private bool referenced_tweet_loaded = false; private Mode mode; private GLib.Cancellable? cancellable; private Gtk.ListBox? reply_list = null; public ComposeTweetWindow (MainWindow? parent, Account acc, Cb.Tweet? referenced_tweet = null, Mode mode = Mode.NORMAL) { this.set_show_menubar (false); this.main_window = parent; this.account = acc; this.referenced_tweet = referenced_tweet; this.mode = mode; this.tweet_text.set_account (acc); this.application = (Gtk.Application)GLib.Application.get_default (); this.cancellable = new GLib.Cancellable (); var upload_proxy = new Rest.OAuthProxy (acc.proxy.consumer_key, acc.proxy.consumer_secret, "https://upload.twitter.com/", false); upload_proxy.token = account.proxy.token; upload_proxy.token_secret = account.proxy.token_secret; this.compose_image_manager.proxy = upload_proxy; length_label.label = Cb.Tweet.MAX_LENGTH.to_string (); load_tweet.begin (); avatar_image.surface = acc.avatar; acc.notify["avatar"].connect (() => { avatar_image.surface = account.avatar; }); GLib.NetworkMonitor.get_default ().notify["network-available"].connect (update_send_button_sensitivity); tweet_text.buffer.changed.connect (update_send_button_sensitivity); if (parent != null) { this.set_transient_for (parent); this.set_modal (true); } /* Let the text view immediately grab the keyboard focus */ tweet_text.grab_focus (); tweet_text.completion_listbox = this.completion_list; tweet_text.show_completion.connect (() => { completion_revealer.reveal_child = true; }); tweet_text.hide_completion.connect (() => { completion_revealer.reveal_child = false; }); Gtk.AccelGroup ag = new Gtk.AccelGroup (); ag.connect (Gdk.Key.Escape, 0, Gtk.AccelFlags.LOCKED, escape_pressed_cb); ag.connect (Gdk.Key.Return, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.LOCKED, () => {start_send_tweet (); return true;}); ag.connect (Gdk.Key.E, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.LOCKED, () => {show_emoji_chooser (); return true;}); this.compose_image_manager.image_removed.connect ((upload) => { if (!this.compose_image_manager.full) { this.add_media_button.sensitive = true; this.fav_image_button.sensitive = true; } var path = upload.filepath; if (path != null && path.down ().has_suffix (".gif")) { fav_image_view.set_gifs_enabled (true); this.add_media_button.sensitive = true; this.fav_image_button.sensitive = true; } if (this.compose_image_manager.n_images == 0) { this.compose_image_manager.hide (); fav_image_view.set_gifs_enabled (true); } update_send_button_sensitivity (); }); this.compose_image_manager.image_reloaded.connect ((media_upload) => { media_upload.cancellable.cancel(); TweetUtils.upload_media.begin (media_upload, account, null); }); this.compose_image_manager.image_uploaded.connect ((media_upload) => { update_send_button_sensitivity(); }); this.add_accel_group (ag); var image_target_list = new Gtk.TargetList (null); image_target_list.add_text_targets (0); setup_emoji_chooser (); this.set_default_size (DEFAULT_WIDTH, (int)(DEFAULT_WIDTH / 2.5)); } public override void show () { base.show(); load_images.begin(); } private async void load_images () { string[] failed_paths = {}; for (uint i = 0; i < Twitter.max_media_per_upload; i++) { string? image_path = account.db.select ("info").cols ("last_tweet_image_%u".printf(i + 1)).once_string (); if (image_path != null && image_path.length > 0){ try { load_image (image_path); } catch (GLib.Error e) { failed_paths += image_path; } } } if (failed_paths.length > 0) { // It would have been nice to do this in one string, but using format positions to // let translations skip the image count results in `*** invalid %N$ use detected ***` stack.visible_child = image_error_grid; var failed_to_log_str = ngettext("Failed to load image", "Failed to load %u images", failed_paths.length).printf(failed_paths.length); // TRANSLATORS: Combine plural "Failed to load image" and list of failed paths // to make "Failed to load image: " or "Failed to load 3 images: , , " image_error_label.label = _("%s: %s").printf(failed_to_log_str, string.joinv(", ", failed_paths)); cancel_button.label = _("Back"); } } private async void load_tweet () { string? last_tweet = account.db.select ("info").cols ("last_tweet").once_string (); if (last_tweet != null && last_tweet.length > 0 && tweet_text.get_buffer ().text.length == 0) { this.tweet_text.get_buffer ().text = last_tweet; } int64 last_reply_id = account.db.select ("info").cols ("last_tweet_reply_id").once_i64 (); int64 last_quote_id = account.db.select ("info").cols ("last_tweet_quote_id").once_i64 (); var candidate_mode = Mode.NORMAL; int64 load_id = -1; if (this.referenced_tweet != null) { this.referenced_tweet_loaded = true; } else if (last_reply_id != 0) { load_id = last_reply_id; candidate_mode = Mode.REPLY; } else if (last_quote_id != 0){ load_id = last_quote_id; candidate_mode = Mode.QUOTE; } // Else it's a new tweet if (this.referenced_tweet == null && load_id > 0) { string error_reason = "Unknown error"; try { this.referenced_tweet = yield TweetUtils.get_tweet (account, load_id); } catch (GLib.Error e) { error_reason = e.message; warning (e.message); } if (this.referenced_tweet == null) { // TRANSLATORS: %s is the error message returned by Twitter (e.g. "Not Found") string message = candidate_mode == Mode.QUOTE ? _("Error fetching quoted tweet: %s\n\nSave unsent tweet?") : _("Error fetching reply tweet: %s\n\nSave unsent tweet?"); var messagedialog = new Gtk.MessageDialog (this, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.YES_NO, message.printf (error_reason)); messagedialog.set_default_response (Gtk.ResponseType.YES); int response = messagedialog.run (); messagedialog.destroy (); if (response == Gtk.ResponseType.NO) { set_text (""); this.referenced_tweet = null; clear_last_tweet (); } else { // We're in an invalid state - all we can do is close and let the user try again later this.close (); } } else { // Don't set the mode until now in case fetching the tweet fails // If we set it earlier then we get segfaults when code assumes this.referenced_tweet is set. this.mode = candidate_mode; this.referenced_tweet_loaded = true; } } if (mode != Mode.NORMAL) { reply_list = new Gtk.ListBox (); reply_list.selection_mode = Gtk.SelectionMode.NONE; TweetListEntry reply_entry = new TweetListEntry (referenced_tweet, main_window, account, true); reply_entry.activatable = false; reply_entry.read_only = true; reply_entry.show (); reply_list.add (reply_entry); reply_list.show (); content_grid.attach (reply_list, 0, 0, 2, 1); } if (mode == Mode.QUOTE) { assert (referenced_tweet != null); this.title_label.label = _("Quote tweet"); } this.update_send_button_sensitivity (); } private void update_send_button_sensitivity () { Gtk.TextIter start, end; tweet_text.buffer.get_bounds (out start, out end); string text = tweet_text.buffer.get_text (start, end, true); int length = (int)Tl.count_weighted_characters (text); if (compose_image_manager.n_images > 0 && mode == Mode.QUOTE) length += 1 + Twitter.short_url_length; length_label.label = (Cb.Tweet.MAX_LENGTH - length).to_string (); if ((length > 0 && length <= Cb.Tweet.MAX_LENGTH || (length == 0 && compose_image_manager.n_images > 0)) && compose_image_manager.is_ready()) { bool network_reachable = GLib.NetworkMonitor.get_default ().network_available; send_button.sensitive = network_reachable; } else { send_button.sensitive = false; } } private void set_sending_state (bool sending) { if (sending) { title_stack.visible_child = title_spinner; title_spinner.start (); compose_image_manager.insensitivize_buttons (); send_button.sensitive = false; } else { title_stack.visible_child = title_label; title_spinner.stop (); compose_image_manager.sensitivize_buttons (); update_send_button_sensitivity (); } tweet_text.sensitive = !sending; fav_image_button.sensitive = !sending; add_media_button.sensitive = !sending; if (emoji_button != null) { emoji_button.sensitive = !sending; } } [GtkCallback] private void start_send_tweet () { if (!send_button.sensitive) return; set_sending_state (true); Gtk.TextIter start, end; tweet_text.buffer.get_start_iter (out start); tweet_text.buffer.get_end_iter (out end); ComposedTweet tweet = new ComposedTweet(tweet_text.buffer.get_text (start, end, true)); if (this.mode == Mode.REPLY) { tweet.reply_to_id = this.referenced_tweet.id; } else if (this.mode == Mode.QUOTE) { tweet.set_quoted_tweet(this.referenced_tweet); } foreach (MediaUpload upload in compose_image_manager.get_uploads()) { tweet.add_attachment(upload); } /* Save the tweet in case sending fails */ this.save_last_tweet (); TweetUtils.post_tweet.begin (account, tweet, (obj, res) => { bool success = false; try { success = TweetUtils.post_tweet.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (e, this); set_sending_state (false); return; } debug ("Tweet sent."); if (success) { this.clear_last_tweet (); this.destroy (); } else { set_sending_state (false); } }); } private void save_last_tweet () { int64 last_reply_id = 0; int64 last_quote_id = 0; if (this.mode == Mode.REPLY) { last_reply_id = this.referenced_tweet.id; } else if (this.mode == Mode.QUOTE) { last_quote_id = this.referenced_tweet.id; } string text = tweet_text.buffer.text; var query = account.db.update ("info").val ("last_tweet", text); var image_count = compose_image_manager.n_images; var i = 0; foreach (MediaUpload upload in compose_image_manager.get_uploads()) { query.val ("last_tweet_image_%u".printf(i + 1), upload.filepath); i++; } for (i = image_count; i < Twitter.max_media_per_upload; i++) { query.val ("last_tweet_image_%u".printf(i + 1), ""); } if (referenced_tweet_loaded) { // Only overwrite the last_tweet_{reply,quote}_id if it loaded properly query.vali64 ("last_tweet_reply_id", last_reply_id) .vali64 ("last_tweet_quote_id", last_quote_id); } query.run(); } private void clear_last_tweet () { account.db.update ("info").val ("last_tweet", "") .vali64 ("last_tweet_reply_id", 0) .vali64 ("last_tweet_quote_id", 0) .val ("last_tweet_image_1", "") .val ("last_tweet_image_2", "") .val ("last_tweet_image_3", "") .val ("last_tweet_image_4", "") .run (); } [GtkCallback] private void cancel_clicked () { if (stack.visible_child == image_error_grid || stack.visible_child == emoji_chooser || stack.visible_child_name == "fav-images") { stack.visible_child = content_grid; cancel_button.label = _("Cancel"); /* Use this instead of just setting send_button.sensitive to true to avoid sending tweets with 0 length */ this.update_send_button_sensitivity (); } else { if (this.cancellable != null) { this.cancellable.cancel (); } Gtk.TextIter start, end; tweet_text.buffer.get_bounds (out start, out end); string text = tweet_text.buffer.get_text (start, end, true); bool save_tweet = false; if (text != "" || compose_image_manager.n_images > 0) { // TRANSLATORS: A dialog message with Yes/No options when the user closes the "compose tweet" window with content still in the dialog var dialog = new Gtk.MessageDialog (this, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("Keep tweet as draft?")); dialog.set_default_response(Gtk.ResponseType.YES); var result = dialog.run(); dialog.destroy(); save_tweet = result != Gtk.ResponseType.NO; } if (save_tweet) { save_last_tweet (); } else { clear_last_tweet (); } destroy (); } } private bool escape_pressed_cb () { this.cancel_clicked (); return Gdk.EVENT_STOP; } public void set_text (string text) { tweet_text.buffer.text = text; } [GtkCallback] private void add_media_clicked_cb (Gtk.Button source) { var filechooser = new Gtk.FileChooserNative (_("Select Media"), this, Gtk.FileChooserAction.OPEN, _("Open"), _("Cancel")); var filter = new Gtk.FileFilter (); filter.add_mime_type ("image/png"); filter.add_mime_type ("image/jpeg"); filter.add_mime_type ("image/webp"); filter.add_mime_type ("image/gif"); if (compose_image_manager.n_images == 0) { filter.add_mime_type ("video/mpeg"); filter.add_mime_type ("video/mp4"); } filechooser.set_filter (filter); if (filechooser.run () == Gtk.ResponseType.ACCEPT) { var filename = filechooser.get_filename (); try { load_image (filename); } catch (GLib.Error e) { // TODO: Proper error checking/reporting // But it shouldn't happen because we only just picked it, so the file info // should just work warning ("%s (%s)", e.message, filename); } } update_send_button_sensitivity (); } private void load_image (string filename) throws GLib.Error { debug ("Loading %s", filename); /* Get file size */ var file = GLib.File.new_for_path (filename); GLib.FileInfo info = file.query_info (GLib.FileAttribute.STANDARD_TYPE + "," + GLib.FileAttribute.STANDARD_CONTENT_TYPE + "," + GLib.FileAttribute.STANDARD_SIZE, 0); var content_type = info.get_content_type(); #if MSWINDOWS // We can't trust Windows with mime types, it only understands extensions, so fudge something and hope for the best var fname = filename.down(); var is_image = fname.has_suffix(".png") || fname.has_suffix(".gif") || fname.has_suffix(".jpg") || fname.has_suffix(".jpeg") || fname.has_suffix(".webp"); var is_video = !is_image; #else var is_video = content_type.has_prefix("video/"); var is_image = content_type.has_prefix("image/"); #endif var is_animated_gif = is_image && Utils.is_animated_gif(filename); var file_size = info.get_size(); if (!is_image && !is_video) { stack.visible_child = image_error_grid; image_error_label.label = _("Selected file is not an image or video."); cancel_button.label = _("Back"); send_button.sensitive = false; } else if (is_video && file_size > Twitter.MAX_BYTES_PER_VIDEO) { stack.visible_child = image_error_grid; image_error_label.label = _("The selected video is too big. The maximum file size per video is %'d MB") .printf (Twitter.MAX_BYTES_PER_VIDEO / 1024 / 1024); cancel_button.label = _("Back"); send_button.sensitive = false; } else if (is_image && !is_animated_gif && file_size > Twitter.MAX_BYTES_PER_IMAGE) { stack.visible_child = image_error_grid; image_error_label.label = _("The selected image is too big. The maximum file size per image is %'d MB") .printf (Twitter.MAX_BYTES_PER_IMAGE / 1024 / 1024); cancel_button.label = _("Back"); send_button.sensitive = false; } else if (is_image && is_animated_gif && file_size > Twitter.MAX_BYTES_PER_GIF) { stack.visible_child = image_error_grid; image_error_label.label = _("The selected GIF is too big. The maximum file size per GIF is %'d MB") .printf (Twitter.MAX_BYTES_PER_GIF / 1024 / 1024); cancel_button.label = _("Back"); send_button.sensitive = false; } else if (is_animated_gif && this.compose_image_manager.n_images > 0) { stack.visible_child = image_error_grid; image_error_label.label = _("Only one animated GIF file per tweet is allowed."); cancel_button.label = _("Back"); send_button.sensitive = false; } else { this.compose_image_manager.show (); var media_upload = new MediaUpload(filename, false); this.compose_image_manager.load_media (media_upload); TweetUtils.upload_media.begin (media_upload, account, cancellable); if (this.compose_image_manager.n_images > 0) { fav_image_view.set_gifs_enabled (false); } if (this.compose_image_manager.full) { this.add_media_button.sensitive = false; this.fav_image_button.sensitive = false; } update_send_button_sensitivity (); } } [GtkCallback] public void fav_image_button_clicked_cb () { cancel_button.label = _("Back"); stack.visible_child_name = "fav-images"; this.fav_image_view.load_images (); } [GtkCallback] public void favorite_image_selected_cb (string path) { cancel_clicked (); try { load_image (path); } catch (GLib.Error e) { // TODO: Proper error checking/reporting // But it shouldn't happen because we only just picked it from the fav list, // so the file info should just work warning ("%s (%s)", e.message, path); } } private void show_emoji_chooser () { if (this.emoji_chooser == null) return; this.emoji_button.clicked (); } private void setup_emoji_chooser () { this.emoji_chooser = new Cb.EmojiChooser (); if (!emoji_chooser.try_init ()) { this.emoji_chooser = null; return; } emoji_chooser.emoji_picked.connect ((text) => { this.tweet_text.insert_at_cursor (text); cancel_clicked (); }); emoji_chooser.show_all (); stack.add (emoji_chooser); this.emoji_button = new Gtk.Button.with_label ("😀"); emoji_button.set_tooltip_text(_("Insert Emoji")); emoji_button.get_accessible().set_name(_("Insert Emoji")); emoji_button.clicked.connect (() => { this.emoji_chooser.populate (); this.stack.visible_child = this.emoji_chooser; cancel_button.label = _("Back"); }); emoji_button.show_all (); add_button_box.add (emoji_button); } } cawbird-1.4.2/src/window/ImageDescriptionWindow.vala000066400000000000000000000134511416632607600225310ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2020 IBBoard * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/image-description-window.ui")] class ImageDescriptionWindow : Gtk.Window { const int DEFAULT_WIDTH = 450; const int MAX_DESCRIPTION_LENGTH = 1000; public signal void description_updated(int64 media_id, string description); [GtkChild] private unowned ResizableImage image; [GtkChild] private unowned Gtk.TextView description_text; [GtkChild] private unowned Gtk.Label length_label; [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Spinner title_spinner; [GtkChild] private unowned Gtk.Label title_label; [GtkChild] private unowned Gtk.Stack title_stack; private Rest.OAuthProxy proxy; private GLib.Cancellable? cancellable; private int64 media_id; public ImageDescriptionWindow (Gtk.Window? parent, Rest.OAuthProxy proxy, int64 media_id, string description, Cairo.ImageSurface image_surface) { this.media_id = media_id; this.proxy = proxy; image.image_surface = image_surface; description_text.buffer.text = description; this.cancellable = new GLib.Cancellable (); length_label.label = MAX_DESCRIPTION_LENGTH.to_string (); GLib.NetworkMonitor.get_default ().notify["network-available"].connect (update_save_button_sensitivity); description_text.buffer.changed.connect (() => { update_save_button_sensitivity(); }); if (parent != null) { this.set_transient_for (parent); this.set_modal (true); } /* Let the text view immediately grab the keyboard focus */ description_text.grab_focus (); Gtk.AccelGroup ag = new Gtk.AccelGroup (); ag.connect (Gdk.Key.Escape, 0, Gtk.AccelFlags.LOCKED, escape_pressed_cb); ag.connect (Gdk.Key.Return, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.LOCKED, () => {save_image_description_clicked (); return true;}); this.add_accel_group (ag); this.set_default_size (DEFAULT_WIDTH, (int)(DEFAULT_WIDTH / 1.5)); this.update_save_button_sensitivity(); } private void update_save_button_sensitivity () { int length = (int)Tl.count_weighted_characters (description_text.buffer.text, false); if (length <= MAX_DESCRIPTION_LENGTH) { save_button.sensitive = GLib.NetworkMonitor.get_default ().network_available; } else { save_button.sensitive = false; } var chars_remaining = MAX_DESCRIPTION_LENGTH - length; length_label.label = chars_remaining.to_string(); length_label.get_accessible().set_description(ngettext("%d character remaining", "%d characters remaining", chars_remaining).printf((int)chars_remaining)); } private void set_sending_state (bool sending) { if (sending) { title_stack.visible_child = title_spinner; title_spinner.start (); save_button.sensitive = false; } else { title_stack.visible_child = title_label; title_spinner.stop (); update_save_button_sensitivity (); } description_text.sensitive = !sending; } [GtkCallback] private void save_image_description_clicked () { if (!save_button.sensitive) return; set_sending_state (true); this.set_image_description.begin((obj, res) => { bool success = false; try { this.set_image_description.end (res); success = true; } catch (GLib.Error e) { debug("Error setting description: %s", e.message); Utils.show_error_dialog (e, this); set_sending_state (false); return; } if (success) { debug("Image description set"); description_updated(media_id, description_text.buffer.text); hide(); debug("Hidden"); } else { debug("Image description failed"); set_sending_state (false); } }); } private async void set_image_description() throws GLib.Error { debug("Creating JSON"); var gen = new Json.Generator(); var root = new Json.Node(Json.NodeType.OBJECT); var object = new Json.Object(); root.set_object(object); gen.set_root(root); object.set_string_member("media_id", media_id.to_string()); var alt_text = new Json.Object(); object.set_object_member("alt_text", alt_text); alt_text.set_string_member("text", description_text.buffer.text); string json_dump = gen.to_data (null); debug("Sending %s", json_dump); var call = new OAuthProxyCallWithBody(proxy, json_dump); call.set_method ("POST"); call.set_function( "1.1/media/metadata/create.json"); GLib.Error? err = null; call.invoke_async.begin (null, (obj, res) => { try { debug("Call completed"); call.invoke_async.end (res); } catch (GLib.Error e) { debug("ERROR! %s", e.message); err = e; } debug("Callback"); set_image_description.callback(); }); debug("Yielding"); yield; if (err != null) { throw err; } } [GtkCallback] private void cancel_clicked () { if (this.cancellable != null) { this.cancellable.cancel (); } hide(); } private bool escape_pressed_cb () { this.cancel_clicked (); return Gdk.EVENT_STOP; } } cawbird-1.4.2/src/window/MediaDialog.vala000066400000000000000000000117061416632607600202530ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/media-dialog.ui")] class MediaDialog : Gtk.Window { [GtkChild] private unowned Gtk.Frame frame; [GtkChild] private unowned Gtk.Revealer next_revealer; [GtkChild] private unowned Gtk.Revealer previous_revealer; [GtkChild] private unowned Gtk.Revealer media_count_revealer; [GtkChild] private unowned Gtk.Label media_count; private unowned Cb.Media[] media; private int cur_index = 0; private Gtk.GestureMultiPress button_gesture; private Gdk.Rectangle max_dimensions; public MediaDialog (Cb.Media[] media, int start_media_index, Gdk.Rectangle max_dimensions) { this.media = media; var downloader = Cb.MediaDownloader.get_default(); foreach (Cb.Media m in media){ if (!m.loaded_hires && !m.loading_hires) { downloader.load_hires_async.begin (m); } } this.cur_index = start_media_index; this.max_dimensions = max_dimensions; this.button_gesture = new Gtk.GestureMultiPress (this); this.button_gesture.set_button (0); this.button_gesture.set_propagation_phase (Gtk.PropagationPhase.BUBBLE); this.button_gesture.released.connect (button_released_cb); if (media.length == 1) { next_revealer.hide (); previous_revealer.hide (); media_count_revealer.hide (); } change_media (start_media_index); } private void button_released_cb (int n_press, double x, double y) { this.destroy (); button_gesture.set_state (Gtk.EventSequenceState.CLAIMED); } private void change_media (int new_index) { /* Remove the current child */ var cur_child = frame.get_child (); int cur_width = 0, cur_height = 0, new_width, new_height; if (frame.get_child () != null) { frame.remove (cur_child); cur_child.get_size_request (out cur_width, out cur_height); } Cb.Media media = this.media[new_index]; Gtk.Widget new_widget = null; if (media.is_video ()) { new_widget = new Cb.MediaVideoWidget (media, max_dimensions); frame.add (new_widget); ((Cb.MediaVideoWidget)new_widget).start (); } else { new_widget = new Cb.MediaImageWidget (media, max_dimensions); frame.add (new_widget); } new_widget.show_all (); new_widget.get_size_request (out new_width, out new_height); if ((new_width != cur_width || new_height != cur_height) && new_width > 0 && new_height > 0) { this.resize (new_width, new_height); } this.queue_resize (); cur_index = new_index; // TRANSLATORS: Values are current image index (1-based) and total image count. Pluralisation is based on total image count. // Should only be seen when image count is two or more. media_count.set_text(ngettext("Image %d of %d", "Image %d of %d", this.media.length).printf(cur_index + 1, this.media.length)); } private void next_media () { change_media ((cur_index + 1) % media.length); } private void previous_media () { change_media (cur_index != 0 ? cur_index - 1 : media.length - 1); } [GtkCallback] private bool key_press_event_cb (Gdk.EventKey evt) { if (evt.keyval == Gdk.Key.Left) previous_media (); else if (evt.keyval == Gdk.Key.Right) next_media (); else this.destroy (); return Gdk.EVENT_PROPAGATE; } [GtkCallback] private void next_button_clicked_cb () { next_media (); } [GtkCallback] private void previous_button_clicked_cb () { previous_media (); } public override bool enter_notify_event (Gdk.EventCrossing event) { if (event.window == this.get_window () && event.detail != Gdk.NotifyType.INFERIOR) { next_revealer.reveal_child = true; previous_revealer.reveal_child = true; media_count_revealer.reveal_child = true; } return false; } public override bool leave_notify_event (Gdk.EventCrossing event) { if (event.window == this.get_window () && event.detail != Gdk.NotifyType.INFERIOR) { next_revealer.reveal_child = false; previous_revealer.reveal_child = false; media_count_revealer.reveal_child = false; } return false; } } cawbird-1.4.2/src/window/ModifyFilterDialog.vala000066400000000000000000000070041416632607600216250ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/modify-filter-dialog.ui")] class ModifyFilterDialog : Gtk.Dialog { [GtkChild] private unowned Gtk.Entry regex_entry; [GtkChild] private unowned Gtk.Label regex_status_label; [GtkChild] private unowned Gtk.TextView regex_test_text; [GtkChild] private unowned Gtk.Button save_button; private GLib.Regex regex; private unowned Account account; private unowned Cb.Filter filter; private unowned MainWindow main_window; /** created will be true if the filter has just been created by the user(i.e. not modified) */ public signal void filter_added (Cb.Filter filter, bool created); public ModifyFilterDialog (MainWindow parent, Account account, Cb.Filter? filter = null) { GLib.Object (use_header_bar: Gtk.Settings.get_default ().gtk_dialogs_use_header ? 1 : 0); this.set_transient_for (parent); this.application = parent.get_application (); this.account = account; if (filter != null) { regex_entry.text = filter.get_contents (); this.title = _("Modify Filter"); } this.filter = filter; this.main_window = parent; } construct { regex_test_text.buffer.changed.connect (regex_entry_changed_cb); } public override void response (int response_id) { if (response_id == Gtk.ResponseType.CANCEL) { this.destroy (); } else if (response_id == Gtk.ResponseType.OK) { save_filter (); this.destroy (); } } [GtkCallback] private void regex_entry_changed_cb () { try { regex = new GLib.Regex (regex_entry.text); } catch (GLib.RegexError e) { regex_status_label.label = e.message; save_button.sensitive = false; return; } bool matches = regex.match (regex_test_text.buffer.text); if (matches) { regex_status_label.label = _("Matches"); } else { regex_status_label.label = _("Doesn't match"); } save_button.sensitive = (regex_entry.text.length != 0); } private void save_filter () { string content = regex_entry.text; if (this.filter == null) { Cb.Filter f = Utils.create_persistent_filter (content, account); filter_added (f, true); } else { /* We update the existing filter */ account.db.update ("filters").val ("content", content) .where_eq ("id", filter.get_id ().to_string ()) .run (); for (int i = 0; i < account.filters.length; i ++) { var f = account.filters.get (i); if (f.get_id () == this.filter.get_id ()) { f.reset (content); filter_added (f, false); break; } } } /* Update timelines */ main_window.rerun_filters (); } } cawbird-1.4.2/src/window/ModifySnippetDialog.vala000066400000000000000000000075441416632607600220330ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/modify-snippet-dialog.ui")] class ModifySnippetDialog : Gtk.Dialog { [GtkChild] private unowned Gtk.Entry key_entry; [GtkChild] private unowned Gtk.Entry value_entry; [GtkChild] private unowned Gtk.Label error_label; [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Button delete_button; private string? old_key = null; public signal void snippet_updated (string? old_key, string? key, string? value); public ModifySnippetDialog (string? key = null, string? value = null) { GLib.Object (use_header_bar: Gtk.Settings.get_default ().gtk_dialogs_use_header ? 1 : 0); if (key != null) { assert (value != null); this.old_key = key; this.key_entry.text = key; this.value_entry.text = value; this.delete_button.show (); this.title = _("Modify Snippet"); } key_entry.buffer.inserted_text.connect (validate_input); key_entry.buffer.deleted_text.connect (validate_input); value_entry.buffer.inserted_text.connect (validate_input); value_entry.buffer.deleted_text.connect (validate_input); } private void validate_input () { string key = key_entry.text.strip (); string value = value_entry.text.strip (); key_entry.get_style_context ().remove_class ("error"); value_entry.get_style_context ().remove_class ("error"); error_label.label = ""; save_button.sensitive = true; if (key == "") { error_label.label = _("Snippet can't be empty"); key_entry.get_style_context ().add_class ("error"); save_button.sensitive = false; return; } if (value == "") { error_label.label = _("Replacement can't be empty"); value_entry.get_style_context ().add_class ("error"); save_button.sensitive = false; return; } if (key.contains (" ") || key.contains ("\t")) { error_label.label = _("Snippet may not contain whitespace"); key_entry.get_style_context ().add_class ("error"); save_button.sensitive = false; return; } if (Cawbird.snippet_manager.get_snippet (key) != null && this.old_key != key) { error_label.label = _("Snippet already exists"); save_button.sensitive = false; return; } } private void save_snippet () { string new_value = this.value_entry.text; string new_key = this.key_entry.text; if (this.old_key != null) { Cawbird.snippet_manager.set_snippet (old_key, new_key, new_value); } else { Cawbird.snippet_manager.insert_snippet (new_key, new_value); } this.snippet_updated (old_key, new_key, new_value); } [GtkCallback] private void delete_button_clicked_cb () { assert (this.old_key != null); Cawbird.snippet_manager.remove_snippet (this.old_key); this.snippet_updated (this.old_key, null, null); this.destroy (); } public override void response (int response_id) { if (response_id == Gtk.ResponseType.CANCEL) { this.destroy (); } else if (response_id == Gtk.ResponseType.OK) { save_snippet (); this.destroy (); } } } cawbird-1.4.2/src/window/SettingsDialog.vala000066400000000000000000000321571416632607600210370ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ [GtkTemplate (ui = "/uk/co/ibboard/cawbird/ui/settings-dialog.ui")] class SettingsDialog : Gtk.Window { [GtkChild] private unowned Gtk.ComboBoxText shortcut_key_combobox; [GtkChild] private unowned Gtk.Switch on_new_mentions_switch; [GtkChild] private unowned Gtk.ComboBoxText tweet_scale_combobox; [GtkChild] private unowned Gtk.Switch round_avatar_switch; [GtkChild] private unowned Gtk.Switch on_new_dms_switch; [GtkChild] private unowned Gtk.ComboBoxText on_new_tweets_combobox; [GtkChild] private unowned Gtk.Switch auto_scroll_on_new_tweets_switch; [GtkChild] private unowned Gtk.Stack main_stack; [GtkChild] private unowned Gtk.Switch use_dark_theme_switch; [GtkChild] private unowned Gtk.Switch double_click_activation_switch; [GtkChild] private unowned Gtk.ListBox sample_tweet_list; [GtkChild] private unowned Gtk.Switch remove_trailing_hashtags_switch; [GtkChild] private unowned Gtk.Switch remove_media_links_switch; [GtkChild] private unowned Gtk.Switch hide_nsfw_content_switch; [GtkChild] private unowned Gtk.ListBox snippet_list_box; [GtkChild] private unowned Gtk.ComboBoxText media_visibility_combobox; [GtkChild] private unowned Gtk.ComboBoxText translation_service_combobox; [GtkChild] private unowned Gtk.Entry custom_translation_entry; private TweetListEntry sample_tweet_entry; private bool block_flag_emission = false; public SettingsDialog (Cawbird application) { this.application = application; // Interface page auto_scroll_on_new_tweets_switch.notify["active"].connect (() => { on_new_tweets_combobox.sensitive = !auto_scroll_on_new_tweets_switch.active; }); Settings.get ().bind ("shortcut-key", shortcut_key_combobox, "active-id", SettingsBindFlags.DEFAULT); Settings.get ().changed["shortcut-key"].connect(() => { ((Cawbird)get_application()).set_window_switching_accels(); }); Settings.get ().bind ("use-dark-theme", use_dark_theme_switch, "active", SettingsBindFlags.DEFAULT); use_dark_theme_switch.notify["active"].connect (() => { Gtk.Settings.get_default ().gtk_application_prefer_dark_theme = Settings.use_dark_theme(); }); Settings.get ().bind ("auto-scroll-on-new-tweets", auto_scroll_on_new_tweets_switch, "active", SettingsBindFlags.DEFAULT); Settings.get ().bind ("double-click-activation", double_click_activation_switch, "active", SettingsBindFlags.DEFAULT); Settings.get ().bind ("media-visibility", media_visibility_combobox, "active-id", SettingsBindFlags.DEFAULT); Settings.get ().changed["media-visibility"].connect(() => { block_flag_emission = true; if (Settings.get_media_visiblity () == MediaVisibility.SHOW) { remove_media_links_switch.sensitive = true; remove_media_links_switch.active = (Cb.TransformFlags.REMOVE_MEDIA_LINKS in Settings.get_text_transform_flags ()); } else { remove_media_links_switch.sensitive = false; remove_media_links_switch.active = false; } block_flag_emission = false; }); Settings.get ().bind ("new-tweets-notify", on_new_tweets_combobox, "active-id", SettingsBindFlags.DEFAULT); Settings.get ().bind ("new-mentions-notify", on_new_mentions_switch, "active", SettingsBindFlags.DEFAULT); Settings.get ().bind ("new-dms-notify", on_new_dms_switch, "active", SettingsBindFlags.DEFAULT); // Tweets page Settings.get ().bind ("tweet-scale", tweet_scale_combobox, "active-id", SettingsBindFlags.DEFAULT); Settings.get ().bind ("round-avatars", round_avatar_switch, "active", SettingsBindFlags.DEFAULT); Settings.get ().bind ("translation-service", translation_service_combobox, "active-id", SettingsBindFlags.DEFAULT); Settings.get ().changed["translation-service"].connect(() => { set_custom_translation_sensitivity(); }); custom_translation_entry.text = Settings.get_custom_translation_service(); custom_translation_entry.changed.connect(() => { var text = custom_translation_entry.text; var style_context = custom_translation_entry.get_style_context(); if (!text.contains("{SOURCE_LANG}") || !text.contains("{TARGET_LANG}") || !text.contains("{CONTENT}")) { style_context.add_class("error"); } else { style_context.remove_class("error"); Settings.set_custom_translation_service (text); } }); custom_translation_entry.focus_out_event.connect(() => { if (custom_translation_entry.get_style_context().has_class("error")) { var dialog = new Gtk.MessageDialog(this, Gtk.DialogFlags.MODAL, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK, _("Translation URL must contain {SOURCE_LANG}, {TARGET_LANG} and {CONTENT} placeholders" + "\n\nThe URL \"%s\" will be used instead").printf(Settings.get_custom_translation_service())); dialog.run(); dialog.destroy(); // Don't replace the text in case the user made a small typo } return Gdk.EVENT_PROPAGATE; }); set_custom_translation_sensitivity(); // Set up sample tweet {{{ var sample_tweet = new Cb.Tweet (); // Not actually the same tweet, but it's our first one! sample_tweet.id = 1158028807959396353; sample_tweet.source_tweet = Cb.MiniTweet(); sample_tweet.source_tweet.created_at = new GLib.DateTime.now_local().add_minutes(-42).to_unix(); sample_tweet.source_tweet.author = Cb.UserIdentity() { id = 12, screen_name = "cawbirdclient", user_name = "Cawbird" }; string sample_text = _("Hey, check out this new #Cawbird version! \\ (•◡•) / #cool #newisalwaysbetter"); Cairo.Surface? avatar_surface = null; try { var a = Gtk.IconTheme.get_default ().load_icon ("uk.co.ibboard.cawbird", 48 * this.get_scale_factor (), Gtk.IconLookupFlags.FORCE_SIZE); avatar_surface = Gdk.cairo_surface_create_from_pixbuf (a, this.get_scale_factor (), this.get_window ()); } catch (GLib.Error e) { warning (e.message); } sample_tweet.source_tweet.text = sample_text; try { var regex = new GLib.Regex ("#\\w+"); GLib.MatchInfo match_info; bool matched = regex.match (sample_text, 0, out match_info); assert (matched); Cb.TextEntity[] hashtags = {}; int i = 0; while (match_info.matches ()) { assert (match_info.get_match_count () == 1); int from, to; match_info.fetch_pos (0, out from, out to); string match = match_info.fetch (0); hashtags += Cb.TextEntity () { from = sample_text.char_count (from), to = sample_text.char_count (to), original_text = match, display_text = match, tooltip_text = match, // This should be null, but that adds a "Block #hashtag" menu item that we don't want // in the settings dialog target = match }; match_info.next (); i ++; } sample_tweet.source_tweet.entities = hashtags; } catch (GLib.RegexError e) { critical (e.message); } // Just to be sure TweetUtils.sort_entities (ref sample_tweet.source_tweet.entities); this.sample_tweet_entry = new TweetListEntry (sample_tweet, null, new Account (10, "", "")); sample_tweet_entry.set_avatar (avatar_surface); sample_tweet_entry.activatable = false; sample_tweet_entry.read_only = true; sample_tweet_entry.show (); this.sample_tweet_list.add (sample_tweet_entry); // }}} var text_transform_flags = Settings.get_text_transform_flags (); block_flag_emission = true; remove_trailing_hashtags_switch.active = (Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS in text_transform_flags); remove_media_links_switch.active = (Cb.TransformFlags.REMOVE_MEDIA_LINKS in text_transform_flags); block_flag_emission = false; Settings.get ().bind ("hide-nsfw-content", hide_nsfw_content_switch, "active", SettingsBindFlags.DEFAULT); // Fill snippet list box Cawbird.snippet_manager.query_snippets ((key, value) => { var e = new SnippetListEntry ((string)key, (string)value); e.show_all (); snippet_list_box.add (e); }); add_accels (); load_geometry (); } [GtkCallback] private bool window_destroy_cb () { save_geometry (); return Gdk.EVENT_PROPAGATE; } [GtkCallback] private void snippet_entry_activated_cb (Gtk.ListBoxRow row) { var snippet_row = (SnippetListEntry) row; var d = new ModifySnippetDialog (snippet_row.key, snippet_row.value); d.snippet_updated.connect (snippet_updated_func); d.set_transient_for (this); d.modal = true; d.show (); } [GtkCallback] private void add_snippet_button_clicked_cb () { var d = new ModifySnippetDialog (); d.snippet_updated.connect (snippet_updated_func); d.set_transient_for (this); d.modal = true; d.show (); } private void set_custom_translation_sensitivity() { custom_translation_entry.sensitive = Settings.get_translation_service() == TranslationService.CUSTOM; } private void snippet_updated_func (string? old_key, string? key, string? value) { if (old_key != null && key == null && value == null) { foreach (var _row in snippet_list_box.get_children ()) { var srow = (SnippetListEntry) _row; if (srow.key == old_key) { srow.reveal (); break; } } return; } if (old_key == null) { var e = new SnippetListEntry (key, value); e.show_all (); snippet_list_box.add (e); } else { foreach (var _row in snippet_list_box.get_children ()) { var srow = (SnippetListEntry) _row; if (srow.key == old_key) { srow.key = key; srow.value = value; break; } } } } private void load_geometry () { GLib.Variant geom = Settings.get ().get_value ("settings-geometry"); int x = 0, y = 0, w = 0, h = 0; x = geom.get_child_value (0).get_int32 (); y = geom.get_child_value (1).get_int32 (); w = geom.get_child_value (2).get_int32 (); h = geom.get_child_value (3).get_int32 (); if (w == 0 || h == 0) return; this.move (x, y); this.set_default_size (w, h); } private void save_geometry () { var builder = new GLib.VariantBuilder (GLib.VariantType.TUPLE); int x = 0, y = 0, w = 0, h = 0; this.get_position (out x, out y); this.get_size (out w, out h); builder.add_value (new GLib.Variant.int32(x)); builder.add_value (new GLib.Variant.int32(y)); builder.add_value (new GLib.Variant.int32(w)); builder.add_value (new GLib.Variant.int32(h)); Settings.get ().set_value ("settings-geometry", builder.end ()); } private void add_accels () { Gtk.AccelGroup ag = new Gtk.AccelGroup(); ag.connect (Gdk.Key.Escape, 0, Gtk.AccelFlags.LOCKED, () => {this.close (); return true;}); ag.connect (Gdk.Key.@1, Gdk.ModifierType.MOD1_MASK, Gtk.AccelFlags.LOCKED, () => {main_stack.visible_child_name = "interface"; return true;}); ag.connect (Gdk.Key.@2, Gdk.ModifierType.MOD1_MASK, Gtk.AccelFlags.LOCKED, () => {main_stack.visible_child_name = "tweet"; return true;}); ag.connect (Gdk.Key.@3, Gdk.ModifierType.MOD1_MASK, Gtk.AccelFlags.LOCKED, () => {main_stack.visible_child_name = "snippets"; return true;}); this.add_accel_group(ag); } [GtkCallback] private void remove_trailing_hashtags_cb () { if (block_flag_emission) return; if (remove_trailing_hashtags_switch.active) { Settings.add_text_transform_flag (Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS); } else { Settings.remove_text_transform_flag (Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS); } } [GtkCallback] private void remove_media_links_cb () { if (block_flag_emission) return; if (remove_media_links_switch.active) { Settings.add_text_transform_flag (Cb.TransformFlags.REMOVE_MEDIA_LINKS); } else { Settings.remove_text_transform_flag (Cb.TransformFlags.REMOVE_MEDIA_LINKS); } } } cawbird-1.4.2/src/window/UserListDialog.vala000066400000000000000000000165711416632607600210130ustar00rootroot00000000000000/* This file is part of Cawbird, a Gtk+ linux Twitter client forked from Corebird. * Copyright (C) 2013 Timm Bäder (Corebird) * * Cawbird 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. * * Cawbird 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 cawbird. If not, see . */ struct TwitterList { int64 id; string name; string description; string mode; uint n_members; } class UserListDialog : Gtk.Dialog { private unowned Account account; private unowned MainWindow main_window; private Gtk.ListBox list_list_box = new Gtk.ListBox (); private Gtk.Label placeholder_label; private int64 user_id; public UserListDialog (MainWindow parent, Account account, int64 user_id) { GLib.Object (use_header_bar: Gtk.Settings.get_default ().gtk_dialogs_use_header ? 1 : 0); this.title = _("Add to or Remove User From List"); this.main_window = parent; this.user_id = user_id; this.account = account; set_modal (true); set_transient_for (parent); set_default_size (250, 200); add_button (_("Cancel"), Gtk.ResponseType.CANCEL); add_button (_("Save"), Gtk.ResponseType.OK); set_default_response (Gtk.ResponseType.OK); var content_box = get_content_area (); content_box.border_width = 0; var scroller = new Gtk.ScrolledWindow (null, null); list_list_box.selection_mode = Gtk.SelectionMode.NONE; list_list_box.set_header_func (default_header_func); list_list_box.row_activated.connect ((row) => { if (!(row is ListUserEntry)) { warning ("Row != ListUserEntry!"); return; } ((ListUserEntry)row).toggle (); }); scroller.add (list_list_box); content_box.pack_start (scroller, true, true); placeholder_label = new Gtk.Label (_("You have no lists.")); placeholder_label.get_style_context ().add_class ("dim-label"); placeholder_label.show (); list_list_box.set_placeholder (placeholder_label); this.set_default_size (400, 200); } public void load_lists () { var lists_page = (ListsPage)main_window.get_page (Page.LISTS); lists_page.get_user_lists.begin ((obj, res) => { TwitterList[] lists = lists_page.get_user_lists.end (res); foreach (unowned TwitterList list in lists) { var l = new ListUserEntry (list.name, list.description); l.id = list.id; if (list.n_members >= 500) l.disable (); list_list_box.add (l); } this.show_all (); }); var call = account.proxy.new_call (); call.set_function ("1.1/lists/memberships.json"); call.add_param ("user_id", user_id.to_string ()); call.add_param ("filter_to_owned_lists", "true"); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this); return; } var parser = new Json.Parser (); try { parser.load_from_data (call.get_payload ()); } catch (GLib.Error e) { critical (e.message); return; } var root = parser.get_root ().get_object (); var list_arr = root.get_array_member ("lists"); list_arr.foreach_element ((arr, index, node) => { int64 id = node.get_object ().get_int_member ("id"); list_list_box.@foreach ((w) => { var lue = (ListUserEntry) w; if (lue.id == id) { lue.check (); lue.enable (); } }); }); }); } public override void response (int response_id) { if (response_id == Gtk.ResponseType.CANCEL) { this.destroy (); } else if (response_id == Gtk.ResponseType.OK) { var list_entries = list_list_box.get_children (); foreach (Gtk.Widget w in list_entries) { var lue = (ListUserEntry) w; if (lue.changed) { debug ("VALUE CHANGED"); if (lue.active) { // Add user to the list add_user (lue.id); } else { // Remove user from the list remove_user (lue.id); } } } this.destroy (); } } private void add_user (int64 list_id) { var call = account.proxy.new_call (); call.set_function ("1.1/lists/members/create.json"); call.set_method ("POST"); call.add_param ("list_id", list_id.to_string ()); call.add_param ("user_id", user_id.to_string ()); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { // Adding an added users doesn't appear to cause errors, // so there's nothing to ignore as "accidental success" Utils.show_error_dialog (TweetUtils.failed_request_to_error (call, e), this); } }); } private void remove_user (int64 list_id) { var call = account.proxy.new_call (); call.set_function ("1.1/lists/members/destroy.json"); call.set_method ("POST"); call.add_param ("list_id", list_id.to_string ()); call.add_param ("user_id", user_id.to_string ()); call.invoke_async.begin (null, (o, res) => { try { call.invoke_async.end (res); } catch (GLib.Error e) { var err = TweetUtils.failed_request_to_error (call, e); if (err.domain != TweetUtils.get_error_domain() || err.code != 110) { // 110 is "user isn't in the list", so assume they were removed by another source Utils.show_error_dialog (err, this); } } }); } } class ListUserEntry : Gtk.ListBoxRow { public int64 id; public new bool changed = false; private Gtk.CheckButton added_checkbox = new Gtk.CheckButton (); public bool active { get { return added_checkbox.active; } } public ListUserEntry (string list_name, string description) { var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); box.margin = 6; added_checkbox.valign = Gtk.Align.CENTER; added_checkbox.margin_start = 6; box.pack_start (added_checkbox, false, false); var box2 = new Gtk.Box (Gtk.Orientation.VERTICAL, 3); var label = new Gtk.Label ("" + list_name + ""); label.use_markup = true; label.halign = Gtk.Align.START; box2.pack_start (label, true, false); var desc_label = new Gtk.Label (description); desc_label.get_style_context ().add_class ("dim-label"); desc_label.halign = Gtk.Align.START; desc_label.ellipsize = Pango.EllipsizeMode.END; box2.pack_start (desc_label, true, false); box.pack_start (box2, true, true); add (box); added_checkbox.toggled.connect (() => { changed = !changed; }); } public void check () { added_checkbox.active = true; changed = false; } public void toggle () { added_checkbox.active = !added_checkbox.active; } public void disable () { this.added_checkbox.sensitive = false; } public void enable () { this.added_checkbox.sensitive = true; } } cawbird-1.4.2/tests/000077500000000000000000000000001416632607600143065ustar00rootroot00000000000000cawbird-1.4.2/tests/avatarcache.vala000066400000000000000000000026511416632607600174210ustar00rootroot00000000000000void simple () { var surface = new Cairo.ImageSurface (Cairo.Format.A8, 2, 2); var cache = new Cb.AvatarCache (); assert (cache.get_n_entries () == 0); cache.add (1337, surface, "some_url"); assert (cache.get_n_entries () == 1); cache.increase_refcount_for_surface (surface); assert (cache.get_n_entries () == 1); cache.decrease_refcount_for_surface (surface); assert (cache.get_n_entries () == 0); } void deferred_surface () { var surface = new Cairo.ImageSurface (Cairo.Format.A8, 2, 2); var cache = new Cb.AvatarCache (); cache.add (1337, null, "some_url"); assert (cache.get_n_entries () == 1); bool found; var cached_surface = cache.get_surface_for_id (1337, out found); assert (cached_surface == null); assert (found); cache.set_avatar (1337, surface, "some_url"); assert (cache.get_n_entries () == 1); cache.increase_refcount_for_surface (surface); cached_surface = cache.get_surface_for_id (1337, out found); assert (cached_surface == surface); assert (found); cache.decrease_refcount_for_surface (surface); assert (cache.get_n_entries () == 0); cached_surface = cache.get_surface_for_id (1337, out found); assert (cached_surface == null); assert (!found); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/avatarcache/simple", simple); GLib.Test.add_func ("/avatarcache/deferred_surface", deferred_surface); return GLib.Test.run (); } cawbird-1.4.2/tests/avatardownload.vala000066400000000000000000000040601416632607600201610ustar00rootroot00000000000000 void simple () { var loop = new GLib.MainLoop (); Twitter.get ().init (); var avatar_widget = new AvatarWidget (); Twitter.get ().get_avatar.begin (10, "http://i.imgur.com/GzdoOMu.jpg", avatar_widget, 48, false, () => { assert (avatar_widget.surface != null); loop.quit (); }); loop.run (); } void cached () { var loop = new GLib.MainLoop (); Twitter.get ().init (); var avatar_widget = new AvatarWidget (); var avatar_widget2 = new AvatarWidget (); Twitter.get ().get_avatar.begin (10, "http://i.imgur.com/GzdoOMu.jpg", avatar_widget, 48, false, () => { assert (avatar_widget.surface != null); Twitter.get ().get_avatar.begin (10, "http://i.imgur.com/GzdoOMu.jpg", avatar_widget2, 48, false, () => { assert (avatar_widget2.surface != null); assert (avatar_widget2.surface == avatar_widget.surface); loop.quit (); }); }); loop.run (); } void double_download () { var loop = new GLib.MainLoop (); Twitter.get ().init (); var avatar_widget = new AvatarWidget (); var avatar_widget2 = new AvatarWidget (); Twitter.get ().get_avatar.begin (10, "http://i.imgur.com/GzdoOMu.jpg", avatar_widget, 48, false, () => { assert (avatar_widget.surface != null); loop.quit (); }); Twitter.get ().get_avatar.begin (10, "http://i.imgur.com/GzdoOMu.jpg", avatar_widget2, 48, false, () => { assert (avatar_widget2.surface != null); }); loop.run (); } int main (string[] args) { GLib.Test.init (ref args); Gtk.init (ref args); Settings.init (); Utils.init_soup_session (); GLib.Test.add_func ("/avatar-download/simple", simple); GLib.Test.add_func ("/avatar-download/cached", cached); GLib.Test.add_func ("/avatar-download/double_download", double_download); /* We can't test load_avatar_for_user_id here since we can't properly use Accounts and their proxies... */ return GLib.Test.run (); } cawbird-1.4.2/tests/bundlehistory.vala000066400000000000000000000063741416632607600200600ustar00rootroot00000000000000 void all () { var history = new Cb.BundleHistory (); history.push (1, null); history.push (2, null); history.push (3, null); history.push (4, null); history.push (5, null); assert (history.get_current () == 5); history.back (); assert (history.get_current () == 4); history.back (); assert (history.get_current () == 3); history.forward (); assert (history.get_current () == 4); history.push (10, null); assert (history.get_current () == 10); //history.forward (); //assert (history.get_current () == 10); //history.forward (); //assert (history.get_current () == 10); } void end () { var history = new Cb.BundleHistory (); assert (history.at_end ()); history.push (1, null); assert (history.at_end ()); history.push (2, null); assert (history.at_end ()); history.back (); assert (!history.at_end ()); } void equals () { var bundle1 = new Cb.Bundle (); bundle1.put_string (0, "1"); bundle1.put_string (1, "3"); var bundle2 = new Cb.Bundle (); bundle2.put_string (1, "3"); bundle2.put_string (0, "1"); assert (bundle1.equals (bundle2)); assert (!bundle1.equals (null)); var bundle3 = new Cb.Bundle (); assert (!bundle3.equals (bundle1)); } void remove_current () { var history = new Cb.BundleHistory (); var bundle1 = new Cb.Bundle (); bundle1.put_string (0, "a"); bundle1.put_string (1, "b"); history.push (1, bundle1); var bundle2 = new Cb.Bundle (); bundle2.put_string (3, "c"); bundle2.put_string (4, "d"); history.push (2, bundle2); // push advances assert (history.at_end()); assert (history.get_current_bundle () == bundle2); assert (history.get_current () == 2); // remove_current deletes the current bundle but doesn't // to back history.remove_current (); assert (history.get_current_bundle () == null); assert (history.get_current () == -1); // This should bring us to bundle1 history.back (); assert (history.get_current_bundle () == bundle1); assert (history.get_current () == 1); history.remove_current (); assert (history.get_current_bundle () == null); assert (history.get_current () == -1); history.back (); assert (history.at_start ()); // Shouldn't do anything significant even if at_start() == true history.remove_current (); history.remove_current (); // Empty! assert (history.at_end ()); assert (history.at_start ()); var bundle3 = new Cb.Bundle (); bundle3.put_string (0, "_"); var bundle4 = new Cb.Bundle (); bundle4.put_string (7, "__"); var bundle5 = new Cb.Bundle (); bundle5.put_string (10, "___"); history.push (3, bundle3); history.push (4, bundle4); history.push (5, bundle5); assert (history.get_current () == 5); history.back (); assert (history.get_current () == 4); history.remove_current (); assert (history.get_current () == 5); // everything after 4 was moved one to the front assert (history.get_current_bundle () == bundle5); assert (history.at_end ()); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/bundlehistory/all", all); GLib.Test.add_func ("/bundlehistory/end", end); GLib.Test.add_func ("/bundlehistory/equals", equals); GLib.Test.add_func ("/bundlehistory/remove-current", remove_current); return GLib.Test.run (); } cawbird-1.4.2/tests/dmmanager.vala000066400000000000000000000461501416632607600171140ustar00rootroot00000000000000 /// These are the original ids with two zeroes appended! const int64 RECIPIENT_ID = 11805587900; const int64 SENDER_ID = 146929708900; // Some utils Json.Object get_dm_object (string json_input) { var parser = new Json.Parser (); try { parser.load_from_data (json_input); } catch (GLib.Error e) { message ("%s", e.message); assert (false); } var root_node = parser.get_root (); var root_object = root_node.get_object (); assert (root_node != null); assert (root_object != null); var dm_object = root_object.get_object_member ("direct_message"); assert (dm_object != null); return dm_object; } void clear_account (Account acc) { FileUtils.remove (Dirs.config ("accounts/%s.db".printf (acc.id.to_string ()))); } void simple () { var account = new Account (10, "baedert", "BAEDERT"); var manager = new DMManager.for_account (account); var threads_model = manager.get_threads_model (); assert (threads_model != null); assert (threads_model.get_n_items () == 0); assert (manager.empty); } void simple_insert () { var account = new Account (RECIPIENT_ID, "baedert", "BAEDERT"); clear_account (account); account.init_database (); var manager = new DMManager.for_account (account); var threads_model = manager.get_threads_model (); var dm_obj = get_dm_object (DM_DATA1); manager.insert_message (dm_obj); // This should create a new thread. assert (!manager.empty); assert (threads_model.get_n_items () == 1); // ...with the correct info var thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == SENDER_ID); assert (thread.user.user_name == "Hans Wurst"); assert (thread.user.screen_name == "wurst_hw"); // Same DM, but with different ID. // Should NOT create a new thread. var dm_obj2 = get_dm_object (DM_DATA2); manager.insert_message (dm_obj2); assert (threads_model.get_n_items () == 1); } void one_thread () { var account = new Account (RECIPIENT_ID, "baedert", "BAEDERT"); clear_account (account); account.init_database (); var manager = new DMManager.for_account (account); var threads_model = manager.get_threads_model (); var dm_obj = get_dm_object (DM_DATA1); manager.insert_message (dm_obj); // This should create a new thread. assert (!manager.empty); assert (threads_model.get_n_items () == 1); // ...with the correct info var thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == SENDER_ID); assert (thread.user.user_name == "Hans Wurst"); assert (thread.user.screen_name == "wurst_hw"); // DM_DATA3 is the same DM, but with sender/recipient interchanged. dm_obj = get_dm_object (DM_DATA3); manager.insert_message (dm_obj); // ... so this should keep using the same thread. assert (threads_model.get_n_items () == 1); thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == SENDER_ID); assert (thread.user.user_name == "Hans Wurst"); assert (thread.user.screen_name == "wurst_hw"); } void cached () { var account = new Account (RECIPIENT_ID, "baedert", "BAEDERT"); clear_account (account); account.init_database (); var manager = new DMManager.for_account (account); var threads_model = manager.get_threads_model (); var dm_obj = get_dm_object (DM_DATA1); manager.insert_message (dm_obj); // This should create a new thread. assert (!manager.empty); assert (threads_model.get_n_items () == 1); // ...with the correct info var thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == SENDER_ID); assert (thread.user.user_name == "Hans Wurst"); assert (thread.user.screen_name == "wurst_hw"); // Get a new DMManager manager = new DMManager.for_account (account); threads_model = manager.get_threads_model (); manager.load_cached_threads (); // Same assertions as above... assert (!manager.empty); assert (threads_model.get_n_items () == 1); thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == SENDER_ID); assert (thread.user.user_name == "Hans Wurst"); assert (thread.user.screen_name == "wurst_hw"); // Now insert a second DM var dm_obj2 = get_dm_object (DM_DATA2); manager.insert_message (dm_obj2); assert (threads_model.get_n_items () == 1); } void self () { var account = new Account (RECIPIENT_ID, "baedert", "BAEDERT"); clear_account (account); account.init_database (); var manager = new DMManager.for_account (account); var threads_model = manager.get_threads_model (); var dm_obj = get_dm_object (DM_DATA4); manager.insert_message (dm_obj); // DM_DATA4 contains a DM to the user themselves. assert (!manager.empty); assert (threads_model.get_n_items () == 1); var thread = threads_model.get_item (0) as DMThread; assert (thread != null); assert (thread.user.id == RECIPIENT_ID); assert (thread.user.screen_name == "baedert"); dm_obj = get_dm_object (DM_DATA1); manager.insert_message (dm_obj); assert (threads_model.get_n_items () == 2); dm_obj = get_dm_object (DM_DATA2); manager.insert_message (dm_obj); assert (threads_model.get_n_items () == 2); manager = new DMManager.for_account (account); threads_model = manager.get_threads_model (); assert (manager.empty); assert (threads_model.get_n_items () == 0); manager.load_cached_threads (); assert (threads_model.get_n_items () == 2); } //FIXME: Disabled DM tests because the format has changed A LOT *and* we now rely on more web requests (e.g. for screen names) // May need to inject cached user JSON void main (string[] args) { //GLib.Test.init (ref args); //GLib.Test.add_func ("/dmmanager/simple", simple); //GLib.Test.add_func ("/dmmanager/simple-insert", simple_insert); //GLib.Test.add_func ("/dmmanager/one_thread", one_thread); //GLib.Test.add_func ("/dmmanager/cached", cached); //GLib.Test.add_func ("/dmmanager/self", self); //GLib.Test.run (); } // {{{ const string DM_DATA1= """ {"direct_message":{"id":905788395095457796,"id_str":"905788395095457796","text":"SUP FEGET","sender":{"id":11805587900,"id_str":"11805587900","name":"Schupp & Wupp","screen_name":"baedert","location":null,"url":"http:\/\/cawbird.baedert.org","description":"Cawbird developer by night, silently judging people on the train by day.","protected":false,"followers_count":221,"friends_count":70,"listed_count":4,"created_at":"Sat Feb 27 13:13:34 +0000 2010","favourites_count":159,"utc_offset":7200,"time_zone":"Bern","geo_enabled":false,"verified":false,"statuses_count":2142,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C6E2EE","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/118055879\/1488791887","profile_link_color":"1F98C7","profile_sidebar_border_color":"C6E2EE","profile_sidebar_fill_color":"DAECF4","profile_text_color":"663B12","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"sender_id":11805587900,"sender_id_str":"11805587900","sender_screen_name":"baedert","recipient":{"id":146929708900,"id_str":"146929708900","name":"Hans Wurst","screen_name":"wurst_hw","location":null,"url":null,"description":null,"protected":false,"followers_count":1,"friends_count":2,"listed_count":0,"created_at":"Thu May 30 08:47:22 +0000 2013","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":31,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_link_color":"1DA1F2","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"recipient_id":146929708900,"recipient_id_str":"146929708900","recipient_screen_name":"wurst_hw","created_at":"Thu Sep 07 13:42:36 +0000 2017","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]} }}"""; const string DM_DATA2= """ {"direct_message":{"id":905788395095457797,"id_str":"905788395095457797","text":"Foo", "sender":{"id":11805587900,"id_str":"11805587900","name":"Schupp & Wupp","screen_name":"baedert","location":null,"url":"http:\/\/cawbird.baedert.org","description":"Cawbird developer by night, silently judging people on the train by day.","protected":false,"followers_count":221,"friends_count":70,"listed_count":4,"created_at":"Sat Feb 27 13:13:34 +0000 2010","favourites_count":159,"utc_offset":7200,"time_zone":"Bern","geo_enabled":false,"verified":false,"statuses_count":2142,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C6E2EE","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/118055879\/1488791887","profile_link_color":"1F98C7","profile_sidebar_border_color":"C6E2EE","profile_sidebar_fill_color":"DAECF4","profile_text_color":"663B12","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"sender_id":11805587900,"sender_id_str":"11805587900","sender_screen_name":"baedert","recipient":{"id":146929708900,"id_str":"146929708900","name":"Hans Wurst","screen_name":"wurst_hw","location":null,"url":null,"description":null,"protected":false,"followers_count":1,"friends_count":2,"listed_count":0,"created_at":"Thu May 30 08:47:22 +0000 2013","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":31,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_link_color":"1DA1F2","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"recipient_id":146929708900,"recipient_id_str":"146929708900","recipient_screen_name":"wurst_hw","created_at":"Thu Sep 07 13:42:36 +0000 2017","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]} }}"""; const string DM_DATA3 = """ {"direct_message":{"id":905788395095457797,"id_str":"905788395095457798","text":"Foo2", "sender":{"id":146929708900,"id_str":"11805587900","name":"Schupp & Wupp","screen_name":"baedert","location":null,"url":"http:\/\/cawbird.baedert.org","description":"Cawbird developer by night, silently judging people on the train by day.","protected":false,"followers_count":221,"friends_count":70,"listed_count":4,"created_at":"Sat Feb 27 13:13:34 +0000 2010","favourites_count":159,"utc_offset":7200,"time_zone":"Bern","geo_enabled":false,"verified":false,"statuses_count":2142,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C6E2EE","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif","profile_background_tile":false,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg","profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/118055879\/1488791887","profile_link_color":"1F98C7","profile_sidebar_border_color":"C6E2EE","profile_sidebar_fill_color":"DAECF4","profile_text_color":"663B12","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"sender_id":146929708900,"sender_id_str":"146929708900","sender_screen_name":"wurst_hw","recipient":{"id":11805587900,"id_str":"146929708900","name":"Hans Wurst","screen_name":"wurst_hw","location":null,"url":null,"description":null,"protected":false,"followers_count":1,"friends_count":2,"listed_count":0,"created_at":"Thu May 30 08:47:22 +0000 2013","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":31,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png","profile_link_color":"1DA1F2","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false,"translator_type":"none"},"recipient_id":11805587900,"recipient_id_str":"11805587900","recipient_screen_name":"baedert","created_at":"Thu Sep 07 13:42:36 +0000 2017","entities":{"hashtags":[],"symbols":[],"user_mentions":[],"urls":[]}} }"""; const string DM_DATA4 = """ { "direct_message":{ "id":905788395095457799, "text":"Foo2", "sender":{ "id":"11805587900", "name":"Schupp & Wupp", "screen_name":"baedert", "location":null, "url":"http:\/\/cawbird.baedert.org", "description":"Cawbird developer by night, silently judging people on the train by day.", "protected":false, "followers_count":221, "friends_count":70, "listed_count":4, "created_at":"Sat Feb 27 13:13:34 +0000 2010", "favourites_count":159, "utc_offset":7200, "time_zone":"Bern", "geo_enabled":false, "verified":false, "statuses_count":2142, "lang":"en", "contributors_enabled":false, "is_translator":false, "is_translation_enabled":false, "profile_background_color":"C6E2EE", "profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif", "profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme2\/bg.gif", "profile_background_tile":false, "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg", "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/810507927941476352\/cSydClet_normal.jpg", "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/118055879\/1488791887", "profile_link_color":"1F98C7", "profile_sidebar_border_color":"C6E2EE", "profile_sidebar_fill_color":"DAECF4", "profile_text_color":"663B12", "profile_use_background_image":true, "default_profile":false, "default_profile_image":false, "following":false, "follow_request_sent":false, "notifications":false, "translator_type":"none" }, "sender_id":11805587900, "sender_id_str":"11805587900", "sender_screen_name":"baedert", "recipient":{ "id":"11805587900", "name":"Schupp & Wupp", "screen_name":"baedert", "location":null, "url":null, "description":null, "protected":false, "followers_count":1, "friends_count":2, "listed_count":0, "created_at":"Thu May 30 08:47:22 +0000 2013", "favourites_count":0, "utc_offset":null, "time_zone":null, "geo_enabled":false, "verified":false, "statuses_count":31, "lang":"en", "contributors_enabled":false, "is_translator":false, "is_translation_enabled":false, "profile_background_color":"C0DEED", "profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", "profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", "profile_background_tile":false, "profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png", "profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_normal.png", "profile_link_color":"1DA1F2", "profile_sidebar_border_color":"C0DEED", "profile_sidebar_fill_color":"DDEEF6", "profile_text_color":"333333", "profile_use_background_image":true, "default_profile":true, "default_profile_image":false, "following":false, "follow_request_sent":false, "notifications":false, "translator_type":"none" }, "recipient_id":11805587900, "recipient_id_str":"11805587900", "recipient_screen_name":"baedert", "created_at":"Thu Sep 07 13:42:36 +0000 2017", "entities":{ "hashtags":[ ], "symbols":[ ], "user_mentions":[ ], "urls":[ ] } } } """; // }}} cawbird-1.4.2/tests/filters.vala000066400000000000000000000230571416632607600166320ustar00rootroot00000000000000 // {{{ const string TD1 = """ { "created_at" : "Mon May 05 06:48:32 +0000 2014", "id" : 463208606784311296, "id_str" : "463208606784311296", "text" : "RT @BlackForestTeam: DIESELSTÖRMERS Kickstarter is live! - Go and check it out right now!... http://t.co/ZVmefc0w5e", "source" : "web", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 62574927, "id_str" : "62574927", "name" : "Frozenbyte", "screen_name" : "Frozenbyte", "location" : "Helsinki, Finland", "description" : "We're an independent game developer. Follow us on Twitter to get the latest news on our games! For support issues please get in touch via email.", "url" : "http://t.co/NlgW9k9ZXj", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/NlgW9k9ZXj", "expanded_url" : "http://www.frozenbyte.com", "display_url" : "frozenbyte.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 5682, "friends_count" : 137, "listed_count" : 242, "created_at" : "Mon Aug 03 17:52:07 +0000 2009", "favourites_count" : 233, "utc_offset" : 10800, "time_zone" : "Helsinki", "geo_enabled" : false, "verified" : false, "statuses_count" : 1042, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/378800000117103508/abe5a14a1f3b0b78e9038e73bf6b812d.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/378800000117103508/abe5a14a1f3b0b78e9038e73bf6b812d.jpeg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/1130292729/fb_newlogo_black480_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1130292729/fb_newlogo_black480_normal.png", "profile_link_color" : "CBA051", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "D4802D", "profile_text_color" : "4A2500", "profile_use_background_image" : true, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Tue Apr 29 11:00:25 +0000 2014", "id" : 461097667775725569, "id_str" : "461097667775725569", "text" : "DIESELSTÖRMERS Kickstarter is live! - Go and check it out right now!... http://t.co/ZVmefc0w5e #foobar", "source" : "Tumblr", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 726763934, "id_str" : "726763934", "name" : "Black Forest Games", "screen_name" : "BlackForestTeam", "location" : "Offenburg", "description" : "South-german team that brought you Giana Sisters: Twisted Dreams", "url" : "http://t.co/BXuCnqlX50", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/BXuCnqlX50", "expanded_url" : "http://gianasisterstwisteddreams.com", "display_url" : "gianasisterstwisteddreams.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 1145, "friends_count" : 308, "listed_count" : 35, "created_at" : "Mon Jul 30 20:11:50 +0000 2012", "favourites_count" : 116, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 1475, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/3694489354/ed399e59260bf71b10235dcd7eb56fe5_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/3694489354/ed399e59260bf71b10235dcd7eb56fe5_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/726763934/1369154494", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "default_profile" : true, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweet_count" : 6, "favorite_count" : 2, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZVmefc0w5e", "expanded_url" : "http://tmblr.co/ZTqD4s1ERcDZg", "display_url" : "tmblr.co/ZTqD4s1ERcDZg", "indices" : [ 72, 94 ] } ], "user_mentions" : [ ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" }, "retweet_count" : 6, "favorite_count" : 0, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZVmefc0w5e", "expanded_url" : "http://tmblr.co/ZTqD4s1ERcDZg", "display_url" : "tmblr.co/ZTqD4s1ERcDZg", "indices" : [ 93, 115 ] } ], "user_mentions" : [ { "screen_name" : "BlackForestTeam", "name" : "Black Forest Games", "id" : 726763934, "id_str" : "726763934", "indices" : [ 3, 19 ] } ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; // """ // }}} void matches () { var f = new Cb.Filter ("a+"); assert (f.matches ("a")); } void matches_tweet () { var acc = new Account (12345, "foobar", "Foo Bar"); var filter = new Cb.Filter ("a+"); acc.add_filter (filter); var tweet = new Cb.Tweet (); var parser = new Json.Parser (); var now = new GLib.DateTime.now_local (); try { parser.load_from_data (TD1); } catch (GLib.Error e) { critical (e.message); return; } tweet.load_from_json (parser.get_root (), 0, now); } void same_user () { var acc = new Account (12345, "foobar", "Foo Bar"); var filter = new Cb.Filter ("a+"); acc.add_filter (filter); var tweet = new Cb.Tweet (); var parser = new Json.Parser (); var now = new GLib.DateTime.now_local (); try { parser.load_from_data (TD1); } catch (GLib.Error e) { critical (e.message); return; } tweet.load_from_json (parser.get_root (), 0, now); tweet.source_tweet.author.id = 12345; // Should always return false even if the filter(s) would match assert (!acc.filter_matches (tweet)); } void links () { var acc = new Account (12345, "foobar", "Foo Bar"); var filter = new Cb.Filter ("t\\.co"); acc.add_filter (filter); var tweet = new Cb.Tweet (); var parser = new Json.Parser (); var now = new GLib.DateTime.now_local (); try { parser.load_from_data (TD1); } catch (GLib.Error e) { critical (e.message); return; } tweet.load_from_json (parser.get_root (), 0, now); // This should never match since we should be using the // 'real' url instead of the t.co shortened one. assert (!acc.filter_matches (tweet)); // ... which is also why it should match now //message (tweet.get_real_text ()); acc.add_filter (new Cb.Filter ("tmblr")); assert (acc.filter_matches (tweet)); } void hashtags () { var acc = new Account (12345, "foobar", "Foo Bar"); var filter = new Cb.Filter ("#foobar"); acc.add_filter (filter); var tweet = new Cb.Tweet (); var parser = new Json.Parser (); var now = new GLib.DateTime.now_local (); try { parser.load_from_data (TD1); } catch (GLib.Error e) { critical (e.message); return; } tweet.load_from_json (parser.get_root (), 0, now); // This should never match since we should be using the // 'real' url instead of the t.co shortened one. assert (acc.filter_matches (tweet)); } int main (string[] args) { GLib.Test.init (ref args); Settings.init (); Gtk.init (ref args); Twitter.get ().init (); Dirs.create_dirs (); Utils.init_soup_session (); GLib.Test.add_func ("/filters/matches", matches); GLib.Test.add_func ("/filters/matches-tweet", matches_tweet); GLib.Test.add_func ("/filters/same-user", same_user); GLib.Test.add_func ("/filters/links", links); GLib.Test.add_func ("/filters/hashtags", hashtags); return GLib.Test.run (); } cawbird-1.4.2/tests/friends.vala000066400000000000000000000030541416632607600166070ustar00rootroot00000000000000 void set () { Account account = new Account (1337, "Name", "Screen Name"); Json.Array friends = new Json.Array (); for (int i = 10; i <= 17; i ++) { friends.add_int_element (i); } account.set_friends (friends); for (int i = 10; i <= 17; i ++) { assert (account.follows_id (i)); } } void add () { Account account = new Account (1337, "Name", "Screen Name"); Json.Array friends = new Json.Array (); for (int i = 10; i <= 17; i ++) { friends.add_int_element (i); } account.set_friends (friends); account.follow_id (1337); assert (account.follows_id (1337)); for (int i = 10; i <= 17; i ++) { assert (account.follows_id (i)); } } void _remove () { Account account = new Account (1337, "Name", "Screen Name"); Json.Array friends = new Json.Array (); for (int i = 10; i <= 17; i ++) { friends.add_int_element (i); } account.set_friends (friends); account.unfollow_id (10); account.unfollow_id (11); account.unfollow_id (12); for (int i = 13; i <= 17; i ++) { assert (account.follows_id (i)); } assert (!account.follows_id (10)); assert (!account.follows_id (11)); assert (!account.follows_id (12)); account.unfollow_id (17); assert (!account.follows_id (17)); account.unfollow_id (17); assert (!account.follows_id (17)); } int main (string[] args) { GLib.Test.init (ref args); Dirs.create_dirs (); GLib.Test.add_func ("/friends/set", set); GLib.Test.add_func ("/friends/add", add); GLib.Test.add_func ("/friends/remove", _remove); return GLib.Test.run (); } cawbird-1.4.2/tests/inlinemediadownloader.vala000066400000000000000000000065241416632607600215170ustar00rootroot00000000000000 void test_normal_download () { var url = "http://pbs.twimg.com/media/BiHRjmFCYAAEKFg.png"; var main_loop = new GLib.MainLoop (); var media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { main_loop.quit (); }); main_loop.run (); } void test_animation_download () { var main_loop = new GLib.MainLoop (); var url = "http://i.imgur.com/rgF0Czu.gif"; var media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { main_loop.quit (); }); main_loop.run (); } void test_download_twice () { var main_loop = new GLib.MainLoop (); var url = "http://pbs.twimg.com/media/BiHRjmFCYAAEKFg.png"; var media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { var media2 = new Cb.Media (); media2.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media2, () => { main_loop.quit (); }); }); main_loop.run (); } void test_double_download () { var main_loop = new GLib.MainLoop (); var url = "http://pbs.twimg.com/media/BiHRjmFCYAAEKFg.png"; var media = new Cb.Media (); media.url = url; var collect_obj = new Collect (5); Cb.MediaDownloader.get_default ().load_async.begin (media, () => { assert (!media.invalid); collect_obj.emit (); }); media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { assert (!media.invalid); collect_obj.emit (); }); media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { assert (!media.invalid); collect_obj.emit (); }); media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { assert (!media.invalid); collect_obj.emit (); }); media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { assert (!media.invalid); collect_obj.emit (); }); media = new Cb.Media (); media.url = url; collect_obj.finished.connect (() => { main_loop.quit (); }); main_loop.run (); assert (collect_obj.done); } void test_shutdown () { var main_loop = new GLib.MainLoop (); var url = "http://pbs.twimg.com/media/BiHRjmFCYAAEKFg.png"; var media = new Cb.Media (); media.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media, () => { var media2 = new Cb.Media (); media2.url = url; Cb.MediaDownloader.get_default ().load_async.begin (media2); Cb.MediaDownloader.get_default ().shutdown (); main_loop.quit (); }); main_loop.run (); } int main (string[] args) { GLib.Test.init (ref args); GLib.Environment.set_variable ("GSETTINGS_BACKEND", "memory", true); Gtk.init (ref args); Settings.init (); Dirs.create_dirs (); Utils.init_soup_session (); GLib.Test.add_func ("/media/normal-download", test_normal_download); GLib.Test.add_func ("/media/animation-download", test_animation_download); GLib.Test.add_func ("/media/download-twice", test_download_twice); GLib.Test.add_func ("/media/double-download", test_double_download); GLib.Test.add_func ("/media/shutdown", test_shutdown); int retval = GLib.Test.run (); Cb.MediaDownloader.get_default ().shutdown (); return retval; } cawbird-1.4.2/tests/meson.build000066400000000000000000000011201416632607600164420ustar00rootroot00000000000000 tests = [ 'avatarcache', 'avatardownload', 'bundlehistory', 'dmmanager', 'filters', 'friends', 'inlinemediadownloader', 'texttransform', 'tweetmodel', 'tweetparsing', 'twitteritem', 'usercompletionmodel', 'usercounter', 'utils', ] foreach test_name : tests testcase = executable( test_name, test_name + '.vala', meson.source_root() + '/vapi/cawbird-internal.vapi', cb_resources, dependencies: cb_dep, vala_args: [ '--gresources=' + meson.source_root() + '/cawbird.gresource.xml', ], ) test(test_name, testcase) endforeach cawbird-1.4.2/tests/texttransform.vala000066400000000000000000001732221416632607600201020ustar00rootroot00000000000000 void normal () { var entities = new Cb.TextEntity[0]; string source_text = "foo bar foo"; string result = Cb.TextTransform.text (source_text, entities, 0, 0, 0); assert (result == source_text); } void simple () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 4, to = 7, original_text = "bar", display_text = "display_text", tooltip_text = "tooltip_text", target = "target_text" }; string source_text = "foo bar foo"; string result = Cb.TextTransform.text (source_text, entities, 0, 0, 0); // Not the best asserts, but oh well assert (result.contains ("display_text")); assert (result.contains ("tooltip_text")); assert (result.contains ("target_text")); } void url_at_end () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 8, to = 11, original_text = "foo", display_text = "display_text", tooltip_text = "tooltip_text", target = "target_text" }; string source_text = "foo bar foo"; string result = Cb.TextTransform.text (source_text, entities, 0, 0, 0); // Not the best asserts, but oh well assert (result.contains ("display_text")); assert (result.contains ("tooltip_text")); assert (result.contains ("target_text")); } void url_with_ampersand () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 8, to = 11, original_text = "foo", display_text = "display_text", // Link entities have the full URL as the tooltip tooltip_text = "https://example.com/?param1&param2", // Targets should already have ampersands escaped for consistency target = "https://example.com/?param1&param2" }; string source_text = "foo bar foo"; string result = Cb.TextTransform.text (source_text, entities, 0, 0, 0); assert(result.contains("href=\"https://example.com/?param1&param2\"")); assert(result.contains("title=\"https://example.com/?param1&amp;param2\"")); } void utf8 () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 2, to = 6, original_text = "#foo", display_text = "#foo", tooltip_text = "#foo", target = null }; string source_text = "× #foo"; string result = Cb.TextTransform.text (source_text, entities, Cb.TransformFlags.REMOVE_MEDIA_LINKS, 0, 0); assert (result.has_prefix ("× ")); } void expand_links () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 2, to = 6, original_text = "#foo", display_text = "displayfoobar", tooltip_text = "#foo", target = "target_url" }; string source_text = "× #foo"; string result = Cb.TextTransform.text (source_text, entities, Cb.TransformFlags.EXPAND_LINKS, 0, 0); assert (result.has_prefix ("× ")); assert (!result.contains ("displayfoobar")); assert (result.contains ("target_url")); } void multiple_links () { var entities = new Cb.TextEntity[4]; entities[0] = Cb.TextEntity () { from = 0, to = 22, original_text = "http://t.co/O5uZwJg31k", display_text = "mirgehendirurlsaus.com", target = "http://mirgehendirurlsaus.com", tooltip_text = "http://mirgehendirurlsaus.com" }; entities[1] = Cb.TextEntity () { from = 26, to = 48, original_text = "http://t.co/BsKkxv8UG4", display_text = "foobar.com", target = "http://foobar.com", tooltip_text = "http://foobar.com" }; entities[2] = Cb.TextEntity () { from = 52, to = 74, original_text = "http://t.co/W8qs846ude", display_text = "hahaaha.com", target = "http://hahaaha.com", tooltip_text = "http://hahaaha.com" }; entities[3] = Cb.TextEntity () { from = 77, to = 99, original_text = "http://t.co/x4bKoCusvQ", display_text = "huehue.org", target = "http://huehue.org", tooltip_text = "http://huehue.org" }; string text = "http://t.co/O5uZwJg31k http://t.co/BsKkxv8UG4 http://t.co/W8qs846ude http://t.co/x4bKoCusvQ"; string result = Cb.TextTransform.text (text, entities, 0, 0, 0); string spec = """mirgehendirurlsaus.com foobar.com hahaaha.com huehue.org"""; assert (result == spec); } void remove_only_trailing_hashtags () { string text = "Hey, #totally inappropriate @baedert! #baedertworship öä #thefeels "; var entities = new Cb.TextEntity[4]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 28, to = 36, original_text = "@baedert", display_text = "@baedert", target = "blubb" }; entities[2] = Cb.TextEntity () { from = 38, to = 53, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[3] = Cb.TextEntity () { from = 57, to = 66, original_text = "#thefeels", display_text = "#thefeels", target = "foobar" }; string result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (result.contains ("#baedertworship")); assert (!result.contains ("#thefeels")); } void remove_multiple_trailing_hashtags () { string text = "Hey, #totally inappropriate @baedert! #baedertworship #thefeels #foobar"; var entities = new Cb.TextEntity[5]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 28, to = 36, original_text = "@baedert", display_text = "@baedert", target = "blubb" }; entities[2] = Cb.TextEntity () { from = 38, to = 53, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[3] = Cb.TextEntity () { from = 54, to = 63, original_text = "#thefeels", display_text = "#thefeels", target = "foobar" }; entities[4] = Cb.TextEntity () { from = 64, to = 71, original_text = "#foobar", display_text = "#foobar", target = "bla" }; string result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (!result.contains ("#baedertworship")); assert (!result.contains ("#thefeels")); assert (!result.contains ("#foobar")); } void trailing_hashtags_mention_before () { string text = "Hey, #totally inappropriate! #baedertworship @baedert #foobar"; var entities = new Cb.TextEntity[4]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 29, to = 44, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[2] = Cb.TextEntity () { from = 45, to = 53, original_text = "@baedert", display_text = "@baedert", target = "foobar" }; entities[3] = Cb.TextEntity () { from = 54, to = 61, original_text = "#foobar", display_text = "#foobar", target = "bla" }; string result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (result.contains (">#baedertworship<")); assert (!result.contains ("#foobar")); } void whitespace_hashtags () { string text = "Hey, #totally inappropriate @baedert! #baedertworship #thefeels #foobar"; var entities = new Cb.TextEntity[5]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 28, to = 36, original_text = "@baedert", display_text = "@baedert", target = "blubb" }; entities[2] = Cb.TextEntity () { from = 38, to = 53, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[3] = Cb.TextEntity () { from = 54, to = 63, original_text = "#thefeels", display_text = "#thefeels", target = "foobar" }; entities[4] = Cb.TextEntity () { from = 64, to = 71, original_text = "#foobar", display_text = "#foobar", target = "bla" }; string result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (!result.contains ("#baedertworship")); assert (!result.contains ("#thefeels")); assert (!result.contains ("#foobar")); assert (!result.contains (" ")); // 3 spaces between the 3 hashtags } void trailing_hashtags_link_after () { string text = "Hey, #totally inappropriate @baedert! #baedertworship https://foobar.com"; var entities = new Cb.TextEntity[4]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 28, to = 36, original_text = "@baedert", display_text = "@baedert", target = "blubb" }; entities[2] = Cb.TextEntity () { from = 38, to = 53, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[3] = Cb.TextEntity () { from = 54, to = 72, original_text = "https://foobar.com", display_text = "BLA BLA BLA", target = "https://foobar.com" }; string result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (!result.contains ("#baedertworship")); } void no_quoted_link () { var t = new Cb.Tweet (); t.quoted_tweet = Cb.MiniTweet (); t.quoted_tweet.id = 1337; t.source_tweet = Cb.MiniTweet (); t.source_tweet.text = "Foobar Some text after."; t.source_tweet.entities = new Cb.TextEntity[1]; t.source_tweet.entities[0] = Cb.TextEntity () { from = 0, to = 6, target = "https://twitter.com/bla/status/1337", original_text = "Foobar", display_text = "sometextwhocares" }; Settings.add_text_transform_flag (Cb.TransformFlags.REMOVE_MEDIA_LINKS); string result = t.get_trimmed_text (Settings.get_text_transform_flags ()); assert (!result.contains ("1337")); assert (result.length > 0); } void new_reply () { /* * This tests a the 'new reply' behavior, see * https://dev.twitter.com/overview/api/upcoming-changes-to-tweets */ var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (REPLY_TWEET_DATA); t.load_from_json (parser.get_root (), 1337, new GLib.DateTime.now_local ()); } catch (GLib.Error e) { assert (false); } assert (t.source_tweet.display_range_start == 115); //message ("Entities:"); //foreach (var e in t.source_tweet.entities) { //message ("'%s': %u, %u", e.display_text, e.from, e.to); //} var text = t.get_trimmed_text (Cb.TransformFlags.EXPAND_LINKS); message (text); /* Should not contain any mention */ assert (!text.contains ("@")); /* One of the entities is a URL, the expanded link should point to * eventbrite.com, not t.co */ assert (!text.contains ("t.co")); } void trailing_new_lines () { var entities = new Cb.TextEntity[1]; entities[0] = Cb.TextEntity () { from = 11, to = 31, original_text = "pic.twitter.com/test", display_text = "pic.twitter.com/test", tooltip_text = "pic.twitter.com/test", target = "https://pic.twitter.com/test" }; string source_text = "foo bar\r\n\r\npic.twitter.com/test"; string result = Cb.TextTransform.text (source_text, entities, Cb.TransformFlags.REMOVE_MEDIA_LINKS, 0, 0); assert(result == "foo bar"); entities[0] = Cb.TextEntity () { from = 8, to = 28, original_text = "pic.twitter.com/test", display_text = "pic.twitter.com/test", tooltip_text = "pic.twitter.com/test", target = "https://pic.twitter.com/test" }; source_text = "foo bar pic.twitter.com/test\r\n\r\n"; result = Cb.TextTransform.text (source_text, entities, Cb.TransformFlags.REMOVE_MEDIA_LINKS, 0, 0); assert(result == "foo bar"); string text = "Hey, #totally inappropriate @baedert!\r\n\r\n #baedertworship #thefeels #foobar"; entities = new Cb.TextEntity[5]; entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#totally", display_text = "#totally", target = "foobar" }; entities[1] = Cb.TextEntity () { from = 28, to = 36, original_text = "@baedert", display_text = "@baedert", target = "blubb" }; entities[2] = Cb.TextEntity () { from = 42, to = 57, original_text = "#baedertworship", display_text = "#baedertworship", target = "bla" }; entities[3] = Cb.TextEntity () { from = 58, to = 67, original_text = "#thefeels", display_text = "#thefeels", target = "foobar" }; entities[4] = Cb.TextEntity () { from = 68, to = 75, original_text = "#foobar", display_text = "#foobar", target = "bla" }; result = Cb.TextTransform.text (text, entities, Cb.TransformFlags.REMOVE_TRAILING_HASHTAGS, 0, 0); assert (result.contains (">@baedert<")); // Mention should still be a link assert (result.contains (">#totally<")); assert (result[result.length - 1] == '!'); assert (!result.contains ("#baedertworship")); assert (!result.contains ("#thefeels")); assert (!result.contains ("#foobar")); assert (!result.contains (" ")); // 3 spaces between the 3 hashtags } void bug1 () { var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (BUG1_DATA); t.load_from_json (parser.get_root (), 1337, new GLib.DateTime.now_local ()); } catch (GLib.Error e) { assert (false); } string filter_text = t.get_filter_text (); assert (filter_text.length > 0); } void bug69_encode_text () { // Test that our encoding function works independently of tweets. // It'd be nice if we didn't have to expose this function, but we're testing // C code from Vala, so I don't think there's much choice // First make sure normal text doesn't change assert(Cb.TextTransform.fix_encoding ("Hello, World!") == "Hello, World!"); assert(Cb.TextTransform.fix_encoding ("Héllö, Wôrld‽") == "Héllö, Wôrld‽"); assert(Cb.TextTransform.fix_encoding ("ã“ã‚“ã«ã¡ã¯ä¸–界ï¼") == "ã“ã‚“ã«ã¡ã¯ä¸–界ï¼"); // Then test it doesn't break correct encoding assert(Cb.TextTransform.fix_encoding ("Hello, World & Others!!!") == "Hello, World & Others!!!"); // Then test it fixes a simple case assert(Cb.TextTransform.fix_encoding ("Hello, World & Others!") == "Hello, World & Others!"); // Then some other positions assert(Cb.TextTransform.fix_encoding ("&Hello, World!") == "&Hello, World!"); assert(Cb.TextTransform.fix_encoding ("Hello, World!&") == "Hello, World!&"); // And then bug 69 - double ampersand assert(Cb.TextTransform.fix_encoding ("Hello, World && Others!") == "Hello, World && Others!"); // Check other odd ampersand setups while we're at it. assert(Cb.TextTransform.fix_encoding ("Hello, World && Others!") == "Hello, World && Others!"); assert(Cb.TextTransform.fix_encoding ("Hello, World && Others!") == "Hello, World && Others!"); assert(Cb.TextTransform.fix_encoding ("Hello, World &hello& Others!") == "Hello, World &hello& Others!"); // When posting https://twitter.com/IBBoard/status/1222590553793159169 it showed "&" instead of "…" but nothing should change assert(Cb.TextTransform.fix_encoding ("The spammers are dead!\n\nOh, wait, no… #sigh https://t.co/j1LEcVQySq") == "The spammers are dead!\n\nOh, wait, no… #sigh https://t.co/j1LEcVQySq"); } void bug69_old_bad_encoding () { // Some really old tweets properly encode some characters but not ampersands. // This causes our rendering to break as it assumes everything is valid HTML. var now = new GLib.DateTime.now_local (); var bad_tweet = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (bug69_bad_tweet); } catch (GLib.Error e) { critical (e.message); } var bad_root = parser.get_root (); bad_tweet.load_from_json (bad_root, 0, now); var good_tweet = new Cb.Tweet (); parser = new Json.Parser (); try { parser.load_from_data (bug69_good_tweet); } catch (GLib.Error e) { critical (e.message); } var good_root = parser.get_root (); good_tweet.load_from_json (good_root, 0, now); var bad_tweet_text = bad_tweet.get_real_text(); assert (good_tweet.get_real_text().substring(0, bad_tweet_text.length) == bad_tweet_text); } void bug70_substring_memory_allocation() { // Twitter once gave us bad data (duplicate entity indices). This caused negative-length substrings. // We can't linkigy an entity that we don't know the position of, so it has to remain as text. // This is the best we can do with the data available. var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (BUG70); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); // This should raise a WARNING level message and we should use expect, but we have to use INFO because WARNING is fatal in debug mode, which tests use, // and the system uses the structured log writer so we can't do simple filtering //GLib.Test.expect_message("cawbird", GLib.LogLevelFlags.LEVEL_WARNING, "Skipping entity - expected https://t.co/30kMXiKMRU but found https://t.co/4Xxq6jHtm0. Likely bad indices (54 to 77)"); t.load_from_json (root, 0, now); assert (t.get_real_text() == "https://t.co/30kMXiKMRU https://twitter.com/chadloder/status/1211804049240031232"); //GLib.Test.assert_expected_messages (); } void bug70_case_insensitivity() { // Apparently Twitter sometimes mismatches the case between the entity content and the text. // Presumably this happens when someone types @ibboard but the canonical format is @IBBoard. // The text shows what was typed, but the entity shows the canonical value. var t = new Cb.Tweet (); t.quoted_tweet = Cb.MiniTweet (); t.quoted_tweet.id = 1337; t.source_tweet = Cb.MiniTweet (); t.source_tweet.text = "Hello @ibboard! #newyearseve #ebertstraße"; t.source_tweet.entities = new Cb.TextEntity[3]; t.source_tweet.entities[0] = Cb.TextEntity () { from = 6, to = 14, original_text = "@IBBoard", display_text = "@IBBoard", target = "blubb" }; t.source_tweet.entities[1] = Cb.TextEntity () { from = 16, to = 28, original_text = "#NewYearsEve", display_text = "#NewYearsEve", target = "#NewYearsEve" }; t.source_tweet.entities[2] = Cb.TextEntity () { from = 29, to = 41, original_text = "#Ebertstraße", display_text = "#Ebertstraße", target = "#Ebertstraße" }; string result = t.get_real_text (); assert (result == "Hello @IBBoard! #NewYearsEve #Ebertstraße"); } void bug70_wide_hash() { // Some character sets (e.g. Japanese) may use U+FF03 FULLWIDTH NUMBER SIGN // instead of U+0023 NUMBER SIGN var t = new Cb.Tweet (); t.quoted_tweet = Cb.MiniTweet (); t.quoted_tweet.id = 1337; t.source_tweet = Cb.MiniTweet (); t.source_tweet.text = "Wide #リーグオーダー"; t.source_tweet.entities = new Cb.TextEntity[1]; t.source_tweet.entities[0] = Cb.TextEntity () { from = 5, to = 13, original_text = "#リーグオーダー", // Use the fact that get_real_text() expands hashtags to their display text to do a translation that we can spot display_text = "#LeagueOrder" }; info("bug70-wide-hash"); string result = t.get_real_text (); assert (result == "Wide #LeagueOrder"); } int main (string[] args) { GLib.Environment.set_variable ("GSETTINGS_BACKEND", "memory", true); Intl.setlocale (LocaleCategory.ALL, ""); GLib.Test.init (ref args); Settings.init (); GLib.Test.add_func ("/tt/normal", normal); GLib.Test.add_func ("/tt/simple", simple); GLib.Test.add_func ("/tt/url-at-end", url_at_end); GLib.Test.add_func ("/tt/url-with-ampersand", url_with_ampersand); GLib.Test.add_func ("/tt/utf8", utf8); GLib.Test.add_func ("/tt/expand-links", expand_links); GLib.Test.add_func ("/tt/multiple-links", multiple_links); GLib.Test.add_func ("/tt/remove-only-trailing-hashtags", remove_only_trailing_hashtags); GLib.Test.add_func ("/tt/remove-multiple-trailing-hashtags", remove_multiple_trailing_hashtags); GLib.Test.add_func ("/tt/trailing-hashtags-mention-before", trailing_hashtags_mention_before); GLib.Test.add_func ("/tt/whitespace-between-trailing-hashtags", whitespace_hashtags); GLib.Test.add_func ("/tt/trailing-hashtags-media-link-after", trailing_hashtags_link_after); GLib.Test.add_func ("/tt/no-quoted-link", no_quoted_link); GLib.Test.add_func ("/tt/new-reply", new_reply); GLib.Test.add_func ("/tt/trailing_newlines", trailing_new_lines); GLib.Test.add_func ("/tt/bug1", bug1); GLib.Test.add_func ("/tt/bug69-encode-text", bug69_encode_text); GLib.Test.add_func ("/tt/bug69-old-bad-encoding", bug69_old_bad_encoding); GLib.Test.add_func ("/tt/bug70-substring-memory-allocation", bug70_substring_memory_allocation); GLib.Test.add_func ("/tt/bug70-case-insensitivity", bug70_case_insensitivity); GLib.Test.add_func ("/tt/bug70-wide-hash", bug70_wide_hash); return GLib.Test.run (); } // {{{ const string REPLY_TWEET_DATA = """ { "created_at" : "Mon Apr 17 15:16:18 +0000 2017", "id" : 853990508326252550, "id_str" : "853990508326252550", "full_text" : "@jjdesmond @_UBRAS_ @franalsworth @4Apes @katy4apes @theAliceRoberts @JaneGoodallUK @Jane_Goodall @JaneGoodallInst And here's the link for tickets again ... https://t.co/a9lOVMouNK", "truncated" : false, "display_text_range" : [ 115, 180 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "jjdesmond", "name" : "Jimmy Jenny Desmond", "id" : 21278482, "id_str" : "21278482", "indices" : [ 0, 10 ] }, { "screen_name" : "_UBRAS_", "name" : "Roots and Shoots UOB", "id" : 803329927974096896, "id_str" : "803329927974096896", "indices" : [ 11, 19 ] }, { "screen_name" : "franalsworth", "name" : "Fran", "id" : 776983919287754752, "id_str" : "776983919287754752", "indices" : [ 20, 33 ] }, { "screen_name" : "4Apes", "name" : "Ian Redmond", "id" : 155889035, "id_str" : "155889035", "indices" : [ 34, 40 ] }, { "screen_name" : "katy4apes", "name" : "Katy Jedamzik", "id" : 159608654, "id_str" : "159608654", "indices" : [ 41, 51 ] }, { "screen_name" : "theAliceRoberts", "name" : "Prof Alice Roberts", "id" : 260211154, "id_str" : "260211154", "indices" : [ 52, 68 ] }, { "screen_name" : "JaneGoodallUK", "name" : "Roots & Shoots UK", "id" : 423423823, "id_str" : "423423823", "indices" : [ 69, 83 ] }, { "screen_name" : "Jane_Goodall", "name" : "Jane Goodall", "id" : 235157216, "id_str" : "235157216", "indices" : [ 84, 97 ] }, { "screen_name" : "JaneGoodallInst", "name" : "JaneGoodallInstitute", "id" : 39822897, "id_str" : "39822897", "indices" : [ 98, 114 ] } ], "urls" : [ { "url" : "https://t.co/a9lOVMouNK", "expanded_url" : "https://www.eventbrite.com/e/working-with-apes-tickets-33089771397", "display_url" : "eventbrite.com/e/working-with…", "indices" : [ 157, 180 ] } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : 853925036696141824, "in_reply_to_status_id_str" : "853925036696141824", "in_reply_to_user_id" : 21278482, "in_reply_to_user_id_str" : "21278482", "in_reply_to_screen_name" : "jjdesmond", "user" : { "id" : 415472140, "id_str" : "415472140", "name" : "Ben Garrod", "screen_name" : "Ben_garrod", "location" : "Bristol&Norfolk", "description" : "Monkey-chaser, TV-talker, bone geek and Teaching Fellow at @AngliaRuskin https://t.co/FXbftdxxTJ", "url" : "https://t.co/1B9SDHfWoF", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/1B9SDHfWoF", "expanded_url" : "http://www.josarsby.com/ben-garrod", "display_url" : "josarsby.com/ben-garrod", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ { "url" : "https://t.co/FXbftdxxTJ", "expanded_url" : "http://www.anglia.ac.uk/science-and-technology/about/life-sciences/our-staff/ben-garrod", "display_url" : "anglia.ac.uk/science-and-te…", "indices" : [ 73, 96 ] } ] } }, "protected" : false, "followers_count" : 6526, "friends_count" : 1016, "listed_count" : 128, "created_at" : "Fri Nov 18 11:30:48 +0000 2011", "favourites_count" : 25292, "utc_offset" : 3600, "time_zone" : "London", "geo_enabled" : true, "verified" : true, "statuses_count" : 17224, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/415472140/1477223840", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 6, "favorite_count" : 7, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; const string BUG1_DATA = """ { "created_at":"Wed Jul 05 19:38:02 +0000 2017", "id":882685018904068105, "id_str":"882685018904068105", "text":"@maljehani10 @sWs8ycsI3krjWrE @bnt_alhofuf @itiihade12 @A_algrni @berota_q8 @fayadhalshamari @OKadour82 @K_ibraheem\u2026 https:\/\/t.co\/9uPkhLBtv4", "display_text_range":[ 117, 140 ], "source":"\u003ca href=\"http:\/\/twitter.com\/download\/iphone\" rel=\"nofollow\"\u003eTwitter for iPhone\u003c\/a\u003e", "truncated":true, "in_reply_to_status_id":882681872479813633, "in_reply_to_status_id_str":"882681872479813633", "in_reply_to_user_id":784249163206815744, "in_reply_to_user_id_str":"784249163206815744", "in_reply_to_screen_name":"maljehani10", "user":{ "id":328900753, "id_str":"328900753", "name":"\u0627\u062a\u062d\u0627\u062f\u064a \u0644\u0644\u0646\u062e\u0627\u0639", "screen_name":"LudKadol", "location":"21.469328,39.268171", "url":null, "description":"\u0627\u0644\u0639\u064a\u0646 \u062a\u0631\u0649 \u0648 \u062a\u0645\u064a\u0644 \u0648 \u0627\u0644\u0642\u0644\u0628 \u064a\u0639\u0634\u0642 \u0643\u0644 \u062c\u0645\u064a\u0644 \u062d\u0628\u0643 \u064a\u0627 \u0627\u062a\u064a \u064a\u062f\u0627\u0648\u064a \u0643\u0644 \u0639\u0644\u064a\u0644 \u0627\u062a\u062d\u0627\u062f\u064a \u060c \u0627\u0631\u0633\u0646\u0627\u0644\u064a \u060c \u0645\u064a\u0644\u0627\u0646\u064a \u060c \u0645\u062f\u0631\u064a\u062f\u064a D2ABFF98", "protected":false, "verified":false, "followers_count":1771, "friends_count":2111, "listed_count":3, "favourites_count":609, "statuses_count":19199, "created_at":"Mon Jul 04 06:57:10 +0000 2011", "utc_offset":10800, "time_zone":"Riyadh", "geo_enabled":false, "lang":"ar", "contributors_enabled":false, "is_translator":false, "profile_background_color":"C0DEED", "profile_background_image_url":"http:\/\/pbs.twimg.com\/profile_background_images\/396412541\/Abstract_3d_8.jpg", "profile_background_image_url_https":"https:\/\/pbs.twimg.com\/profile_background_images\/396412541\/Abstract_3d_8.jpg", "profile_background_tile":false, "profile_link_color":"0084B4", "profile_sidebar_border_color":"C0DEED", "profile_sidebar_fill_color":"DDEEF6", "profile_text_color":"333333", "profile_use_background_image":true, "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/823799368255938560\/HhgWWlCA_normal.jpg", "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/823799368255938560\/HhgWWlCA_normal.jpg", "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/328900753\/1436433030", "default_profile":false, "default_profile_image":false, "following":null, "follow_request_sent":null, "notifications":null }, "geo":null, "coordinates":null, "place":null, "contributors":null, "is_quote_status":false, "extended_tweet":{ "full_text":"@maljehani10 @sWs8ycsI3krjWrE @bnt_alhofuf @itiihade12 @A_algrni @berota_q8 @fayadhalshamari @OKadour82 @K_ibraheem @Adnan_Jas @othmanmali @ADEL_MARDI @battalalgoos \u0628\u0648\u0644\u0648\u0646\u064a \u0628\u064a\u062a\u0648\u0631\u0643\u0627 \u062a\u0631\u0627\u0648\u0633\u064a \u0645\u0627\u0631\u0643\u064a\u0646\u0647\u0648 \u062c\u064a\u0632\u0627\u0648\u064a \u0646\u0627\u062f\u064a \u0647\u062c\u0631 \u062f\u064a\u0627\u0643\u064a\u062a\u064a \u0645\u0648\u0646\u062a\u0627\u0631\u064a \u0627\u0644\u062e ...", "display_text_range":[ 165, 235 ], "entities":{ "hashtags":[ ], "urls":[ ], "user_mentions":[ { "screen_name":"maljehani10", "name":"\u0645\u062d\u0645\u062f \u0623\u0628\u0648 \u0633\u0627\u0631\u064a", "id":784249163206815744, "id_str":"784249163206815744", "indices":[ 0, 12 ] }, { "screen_name":"sWs8ycsI3krjWrE", "name":"mwni6xx6mwni", "id":859152891076018176, "id_str":"859152891076018176", "indices":[ 13, 29 ] }, { "screen_name":"bnt_alhofuf", "name":"\u0627\u0645 \u0631\u064a\u0646\u0627\u062f \u2665 \u0625\u062a\u062d\u0627\u062f\u064a\u0629 \u2665", "id":2214026312, "id_str":"2214026312", "indices":[ 30, 42 ] }, { "screen_name":"itiihade12", "name":"\u0628\u0637\u0644 \u0643\u0623\u0633 \u0648\u0644\u064a \u0627\u0644\u0639\u0647\u062f", "id":806883635634769921, "id_str":"806883635634769921", "indices":[ 43, 54 ] }, { "screen_name":"A_algrni", "name":"#\u0639\u0628\u062f\u0627\u0644\u0631\u062d\u0645\u0646_\u0627\u0644\u0642\u0631\u0646\u064a", "id":370497227, "id_str":"370497227", "indices":[ 55, 64 ] }, { "screen_name":"berota_q8", "name":"\u0639\u0628\u0640\u064a\u0640\u0631~\u0627\u0644\u0625\u062a\u062d\u0627\u062f", "id":396583124, "id_str":"396583124", "indices":[ 65, 75 ] }, { "screen_name":"fayadhalshamari", "name":"\u0641\u064a\u0627\u0636 \u0627\u0644\u0634\u0645\u0631\u064a", "id":377591886, "id_str":"377591886", "indices":[ 76, 92 ] }, { "screen_name":"OKadour82", "name":"\u0639\u0628\u064a\u062f \u0643\u0639\u062f\u0648\u0631", "id":1358991192, "id_str":"1358991192", "indices":[ 93, 103 ] }, { "screen_name":"K_ibraheem", "name":"\u062e\u0644\u064a\u0644 \u0625\u0628\u0631\u0627\u0647\u064a\u0645", "id":271130300, "id_str":"271130300", "indices":[ 104, 115 ] }, { "screen_name":"Adnan_Jas", "name":"\u0639\u062f\u0646\u0627\u0646 \u062c\u0633\u062a\u0646\u064a\u0647", "id":416120500, "id_str":"416120500", "indices":[ 116, 126 ] }, { "screen_name":"othmanmali", "name":"\u0639\u062b\u0645\u0627\u0646 \u0627\u0628\u0648\u0628\u0643\u0631 \u0645\u0627\u0644\u064a", "id":299213308, "id_str":"299213308", "indices":[ 127, 138 ] }, { "screen_name":"ADEL_MARDI", "name":"\u0639\u0627\u062f\u0644 \u0627\u0644\u0645\u0631\u0636\u064a", "id":508105416, "id_str":"508105416", "indices":[ 139, 150 ] }, { "screen_name":"battalalgoos", "name":"\u0628\u062a\u0627\u0644 \u0627\u0644\u0642\u0648\u0633", "id":251600033, "id_str":"251600033", "indices":[ 151, 164 ] } ], "symbols":[ ] } }, "retweet_count":0, "favorite_count":0, "entities":{ "hashtags":[ ], "urls":[ { "url":"https:\/\/t.co\/9uPkhLBtv4", "expanded_url":"https:\/\/twitter.com\/i\/web\/status\/882685018904068105", "display_url":"twitter.com\/i\/web\/status\/8\u2026", "indices":[ 117, 140 ] } ], "user_mentions":[ { "screen_name":"maljehani10", "name":"\u0645\u062d\u0645\u062f \u0623\u0628\u0648 \u0633\u0627\u0631\u064a", "id":784249163206815744, "id_str":"784249163206815744", "indices":[ 0, 12 ] }, { "screen_name":"sWs8ycsI3krjWrE", "name":"mwni6xx6mwni", "id":859152891076018176, "id_str":"859152891076018176", "indices":[ 13, 29 ] }, { "screen_name":"bnt_alhofuf", "name":"\u0627\u0645 \u0631\u064a\u0646\u0627\u062f \u2665 \u0625\u062a\u062d\u0627\u062f\u064a\u0629 \u2665", "id":2214026312, "id_str":"2214026312", "indices":[ 30, 42 ] }, { "screen_name":"itiihade12", "name":"\u0628\u0637\u0644 \u0643\u0623\u0633 \u0648\u0644\u064a \u0627\u0644\u0639\u0647\u062f", "id":806883635634769921, "id_str":"806883635634769921", "indices":[ 43, 54 ] }, { "screen_name":"A_algrni", "name":"#\u0639\u0628\u062f\u0627\u0644\u0631\u062d\u0645\u0646_\u0627\u0644\u0642\u0631\u0646\u064a", "id":370497227, "id_str":"370497227", "indices":[ 55, 64 ] }, { "screen_name":"berota_q8", "name":"\u0639\u0628\u0640\u064a\u0640\u0631~\u0627\u0644\u0625\u062a\u062d\u0627\u062f", "id":396583124, "id_str":"396583124", "indices":[ 65, 75 ] }, { "screen_name":"fayadhalshamari", "name":"\u0641\u064a\u0627\u0636 \u0627\u0644\u0634\u0645\u0631\u064a", "id":377591886, "id_str":"377591886", "indices":[ 76, 92 ] }, { "screen_name":"OKadour82", "name":"\u0639\u0628\u064a\u062f \u0643\u0639\u062f\u0648\u0631", "id":1358991192, "id_str":"1358991192", "indices":[ 93, 103 ] }, { "screen_name":"K_ibraheem", "name":"\u062e\u0644\u064a\u0644 \u0625\u0628\u0631\u0627\u0647\u064a\u0645", "id":271130300, "id_str":"271130300", "indices":[ 104, 115 ] } ], "symbols":[ ] }, "favorited":false, "retweeted":false, "filter_level":"low", "lang":"ar", "timestamp_ms":"1499283482658" } """; const string bug69_good_tweet = """ { "created_at" : "Fri Dec 20 14:17:23 +0000 2019", "id" : 1208028629059420161, "id_str" : "1208028629059420161", "full_text" : "for i in `seq 1 254` ; do ping -W1 -c 1 10.0.0.$i > /dev/null && echo 10.0.0.$i ; done #scan network 10.0.0.0 for active hosts & stuff", "truncated" : false, "display_text_range" : [ 0, 200 ], "entities" : { "hashtags" : [ { "text" : "scan", "indices" : [ 98, 103 ] } ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Cawbird", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 194913600, "id_str" : "194913600", "name" : "Test Account", "screen_name" : "IBBTwtr", "location" : "", "description" : "IBBoard's test account for sending test messages to without disturbing people. THIS ACCOUNT WILL NEVER POST ANYTHING INTERESTING! May be used as a spam trap.", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 2, "friends_count" : 12, "listed_count" : 0, "created_at" : "Sat Sep 25 09:06:35 +0000 2010", "favourites_count" : 1, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 102, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/853335069934669831/k5Y-rjee_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/853335069934669831/k5Y-rjee_normal.jpg", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "can_media_tag" : true, "followed_by" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 6705176552, "quoted_status_id_str" : "6705176552", "quoted_status_permalink" : { "url" : "https://t.co/0hUF8hx8Lj", "expanded" : "https://twitter.com/climagic/status/6705176552", "display" : "twitter.com/climagic/statu…" }, "quoted_status" : { "created_at" : "Tue Dec 15 19:25:44 +0000 2009", "id" : 6705176552, "id_str" : "6705176552", "full_text" : "for i in `seq 1 254` ; do ping -W1 -c 1 10.0.0.$i > /dev/null && echo 10.0.0.$i ; done #scan network 10.0.0.0 for active hosts", "truncated" : false, "display_text_range" : [ 0, 129 ], "entities" : { "hashtags" : [ { "text" : "scan", "indices" : [ 90, 95 ] } ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 91333167, "id_str" : "91333167", "name" : "Command Line Magic", "screen_name" : "climagic", "location" : "BASHLAND", "description" : "Cool Unix/Linux Command Line tricks you can use in $TWITTER_CHAR_LIMIT characters or less. Here mostly to inspire all to try more. Read docs first, run later.", "url" : "https://t.co/eKoQFEZTLs", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/eKoQFEZTLs", "expanded_url" : "http://www.climagic.org/", "display_url" : "climagic.org", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 187515, "friends_count" : 12242, "listed_count" : 4036, "created_at" : "Fri Nov 20 12:49:35 +0000 2009", "favourites_count" : 1498, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 12536, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/535876218/climagic-icon_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/535876218/climagic-icon_normal.png", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "can_media_tag" : true, "followed_by" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 12, "favorite_count" : 44, "favorited" : false, "retweeted" : false, "lang" : "en" }, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; // This is a tweaked version of the good tweet because const string bug69_bad_tweet = """ { "created_at" : "Tue Dec 15 19:25:44 +0000 2009", "id" : 6705176552, "id_str" : "6705176552", "full_text" : "for i in `seq 1 254` ; do ping -W1 -c 1 10.0.0.$i > /dev/null && echo 10.0.0.$i ; done #scan network 10.0.0.0 for active hosts & stuff", "truncated" : false, "display_text_range" : [ 0, 129 ], "entities" : { "hashtags" : [ { "text" : "scan", "indices" : [ 90, 95 ] } ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 91333167, "id_str" : "91333167", "name" : "Command Line Magic", "screen_name" : "climagic", "location" : "BASHLAND", "description" : "Cool Unix/Linux Command Line tricks you can use in $TWITTER_CHAR_LIMIT characters or less. Here mostly to inspire all to try more. Read docs first, run later.", "url" : "https://t.co/eKoQFEZTLs", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/eKoQFEZTLs", "expanded_url" : "http://www.climagic.org/", "display_url" : "climagic.org", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 187515, "friends_count" : 12242, "listed_count" : 4036, "created_at" : "Fri Nov 20 12:49:35 +0000 2009", "favourites_count" : 1498, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 12536, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/535876218/climagic-icon_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/535876218/climagic-icon_normal.png", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "can_media_tag" : true, "followed_by" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 12, "favorite_count" : 44, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string BUG70 = """ { "created_at" : "Tue Dec 31 00:20:42 +0000 2019", "id" : 1211804333978734592, "id_str" : "1211804333978734592", "full_text" : "@FlizzieMcGuire @schmittlauch https://t.co/30kMXiKMRU https://t.co/4Xxq6jHtm0", "truncated" : false, "display_text_range" : [ 30, 53 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "FlizzieMcGuire", "name" : "Bikini Bottom Mafia Stan Account", "id" : 863773010, "id_str" : "863773010", "indices" : [ 0, 15 ] }, { "screen_name" : "schmittlauch", "name" : "Trolli @schmittlauch@toot.matereal.eu 🦥", "id" : 312869558, "id_str" : "312869558", "indices" : [ 16, 29 ] } ], "urls" : [ { "url" : "https://t.co/30kMXiKMRU", "expanded_url" : "https://twitter.com/chadloder/status/1211804049240031232?s=21", "display_url" : "twitter.com/chadloder/stat…", "indices" : [ 54, 77 ] }, { "url" : "https://t.co/4Xxq6jHtm0", "expanded_url" : "https://twitter.com/chadloder/status/1211804049240031232", "display_url" : "twitter.com/chadloder/stat…", "indices" : [ 54, 77 ] } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : 1211803346975240192, "in_reply_to_status_id_str" : "1211803346975240192", "in_reply_to_user_id" : 863773010, "in_reply_to_user_id_str" : "863773010", "in_reply_to_screen_name" : "FlizzieMcGuire", "user" : { "id" : 98575337, "id_str" : "98575337", "name" : "Chad Loder", "screen_name" : "chadloder", "location" : "Los Angeles, CA", "description" : "Founder @Habitu8 • Recovering tech guy, author, investor • Human • Previously: Founder, VP Engineering @Rapid7 • #blacklivesmatter", "url" : "https://t.co/j2ABO3HoJN", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/j2ABO3HoJN", "expanded_url" : "https://www.habitu8.io/", "display_url" : "habitu8.io", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 41256, "friends_count" : 3984, "listed_count" : 392, "created_at" : "Tue Dec 22 07:11:56 +0000 2009", "favourites_count" : 39913, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 13848, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/1213949167870984193/SohzlEa0_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1213949167870984193/SohzlEa0_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/98575337/1578219983", "profile_link_color" : "F0CBCA", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "can_media_tag" : true, "followed_by" : true, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 1211804049240031232, "quoted_status_id_str" : "1211804049240031232", "quoted_status_permalink" : { "url" : "https://t.co/4Xxq6jHtm0", "expanded" : "https://twitter.com/chadloder/status/1211804049240031232", "display" : "twitter.com/chadloder/stat…" }, "quoted_status" : { "created_at" : "Tue Dec 31 00:19:34 +0000 2019", "id" : 1211804049240031232, "id_str" : "1211804049240031232", "full_text" : "The Kiwifarms shit-stains are mad that Nazi-loving backpfeifengesicht¹ Vincent Canfield got booted from #36C3 conference.\n\nLet’s be clear.\n\n1. The hacking scene has ALWAYS had antifascists.\n\n2. Anti-antifascist literally means “fascistâ€.\n\n¹ - loosely translated, “punchable face†https://t.co/2wXVVs9An8", "truncated" : false, "display_text_range" : [ 0, 279 ], "entities" : { "hashtags" : [ { "text" : "36C3", "indices" : [ 104, 109 ] } ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 1211804033226170368, "id_str" : "1211804033226170368", "indices" : [ 280, 303 ], "media_url" : "http://pbs.twimg.com/media/ENExWQnU4AAcv5K.jpg", "media_url_https" : "https://pbs.twimg.com/media/ENExWQnU4AAcv5K.jpg", "url" : "https://t.co/2wXVVs9An8", "display_url" : "pic.twitter.com/2wXVVs9An8", "expanded_url" : "https://twitter.com/chadloder/status/1211804049240031232/photo/1", "type" : "photo", "sizes" : { "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "medium" : { "w" : 1024, "h" : 496, "resize" : "fit" }, "large" : { "w" : 1024, "h" : 496, "resize" : "fit" }, "small" : { "w" : 680, "h" : 329, "resize" : "fit" } }, "features" : { "orig" : { "faces" : [ ] }, "medium" : { "faces" : [ ] }, "large" : { "faces" : [ ] }, "small" : { "faces" : [ ] } } } ] }, "extended_entities" : { "media" : [ { "id" : 1211804033226170368, "id_str" : "1211804033226170368", "indices" : [ 280, 303 ], "media_url" : "http://pbs.twimg.com/media/ENExWQnU4AAcv5K.jpg", "media_url_https" : "https://pbs.twimg.com/media/ENExWQnU4AAcv5K.jpg", "url" : "https://t.co/2wXVVs9An8", "display_url" : "pic.twitter.com/2wXVVs9An8", "expanded_url" : "https://twitter.com/chadloder/status/1211804049240031232/photo/1", "type" : "photo", "sizes" : { "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "medium" : { "w" : 1024, "h" : 496, "resize" : "fit" }, "large" : { "w" : 1024, "h" : 496, "resize" : "fit" }, "small" : { "w" : 680, "h" : 329, "resize" : "fit" } }, "features" : { "orig" : { "faces" : [ ] }, "medium" : { "faces" : [ ] }, "large" : { "faces" : [ ] }, "small" : { "faces" : [ ] } } } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 98575337, "id_str" : "98575337", "name" : "Chad Loder", "screen_name" : "chadloder", "location" : "Los Angeles, CA", "description" : "Founder @Habitu8 • Recovering tech guy, author, investor • Human • Previously: Founder, VP Engineering @Rapid7 • #blacklivesmatter", "url" : "https://t.co/j2ABO3HoJN", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/j2ABO3HoJN", "expanded_url" : "https://www.habitu8.io/", "display_url" : "habitu8.io", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 41256, "friends_count" : 3984, "listed_count" : 392, "created_at" : "Tue Dec 22 07:11:56 +0000 2009", "favourites_count" : 39913, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 13848, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/1213949167870984193/SohzlEa0_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1213949167870984193/SohzlEa0_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/98575337/1578219983", "profile_link_color" : "F0CBCA", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "can_media_tag" : true, "followed_by" : true, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 4, "favorite_count" : 18, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" }, "retweet_count" : 0, "favorite_count" : 1, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" } """; // }}} cawbird-1.4.2/tests/tweetmodel.vala000066400000000000000000000613511416632607600173320ustar00rootroot00000000000000 bool is_desc_sorted (Cb.TweetModel tm) { int64 last_id = ((Cb.Tweet)tm.get_item (0)).id; for (int i = 1; i < tm.get_n_items (); i ++) { Cb.Tweet t = (Cb.Tweet)tm.get_item (i); if (t.id > last_id) return false; last_id = t.id; } return true; } bool is_thread_mode_sorted (Cb.TweetModel tm) { Cb.Tweet first_tweet = ((Cb.Tweet)tm.get_item (0)); int64 last_id = first_tweet.retweeted_tweet == null ? first_tweet.id : first_tweet.retweeted_tweet.id; for (int i = 1; i < tm.get_n_items (); i ++) { Cb.Tweet t = (Cb.Tweet)tm.get_item (i); int64 id = t.retweeted_tweet == null ? t.id : t.retweeted_tweet.id; if (id < last_id) return false; last_id = id; } return true; } int64 get_max_id (Cb.TweetModel tm) { int64 max = -1; for (int i = 0; i < tm.get_n_items (); i ++) { var t = (Cb.Tweet)tm.get_item (i); if (t.id > max) max = t.id; } return max; } void basic_tweet_order () { Cb.TweetModel tm = new Cb.TweetModel (); Cb.Tweet t1 = new Cb.Tweet (); t1.id = 10; Cb.Tweet t2 = new Cb.Tweet (); t2.id = 100; Cb.Tweet t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); // 1000 assert (tm.min_id == 1000); assert (tm.max_id == 1000); tm.add (t1); // 10 assert (tm.min_id == 10); assert (tm.max_id == 1000); tm.add (t2); // 100 assert (tm.min_id == 10); assert (tm.max_id == 1000); assert (tm.get_n_items () == 3); assert (is_desc_sorted (tm)); assert (((Cb.Tweet)tm.get_item (0)).id == 1000); assert (((Cb.Tweet)tm.get_item (1)).id == 100); assert (((Cb.Tweet)tm.get_item (2)).id == 10); } void basic_tweet_order_thread_mode () { Cb.TweetModel tm = new Cb.TweetModel (); tm.set_thread_mode (true); Cb.Tweet t1 = new Cb.Tweet (); t1.id = 10; Cb.Tweet t2 = new Cb.Tweet (); t2.id = 100; Cb.Tweet t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); // 1000 assert (tm.min_id == 1000); assert (tm.max_id == 1000); tm.add (t1); // 10 assert (tm.min_id == 10); assert (tm.max_id == 1000); tm.add (t2); // 100 assert (tm.min_id == 10); assert (tm.max_id == 1000); assert (tm.get_n_items () == 3); assert (is_thread_mode_sorted (tm)); assert (((Cb.Tweet)tm.get_item (0)).id == 10); assert (((Cb.Tweet)tm.get_item (1)).id == 100); assert (((Cb.Tweet)tm.get_item (2)).id == 1000); } void retweet_order () { Cb.TweetModel tm = new Cb.TweetModel (); Cb.Tweet t1 = new Cb.Tweet (); t1.id = 10; Cb.Tweet t2 = new Cb.Tweet (); t2.id = 100; t2.retweeted_tweet = Cb.MiniTweet (); t2.retweeted_tweet.id = 5; Cb.Tweet t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); // 1000 assert (tm.min_id == 1000); assert (tm.max_id == 1000); tm.add (t1); // 10 assert (tm.min_id == 10); assert (tm.max_id == 1000); tm.add (t2); // 100 assert (tm.min_id == 10); assert (tm.max_id == 1000); assert (tm.get_n_items () == 3); assert (is_desc_sorted (tm)); assert (((Cb.Tweet)tm.get_item (0)).id == 1000); assert (((Cb.Tweet)tm.get_item (1)).id == 100); assert (((Cb.Tweet)tm.get_item (2)).id == 10); } void retweet_order_thread_mode () { Cb.TweetModel tm = new Cb.TweetModel (); tm.set_thread_mode (true); Cb.Tweet t1 = new Cb.Tweet (); t1.id = 10; Cb.Tweet t2 = new Cb.Tweet (); t2.id = 100; t2.retweeted_tweet = Cb.MiniTweet (); t2.retweeted_tweet.id = 5; Cb.Tweet t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); // 1000 assert (tm.min_id == 1000); assert (tm.max_id == 1000); tm.add (t1); // 10 assert (tm.min_id == 10); assert (tm.max_id == 1000); tm.add (t2); // 100 RTing 5 assert (tm.min_id == 5); assert (tm.max_id == 1000); assert (tm.get_n_items () == 3); assert (is_thread_mode_sorted (tm)); // ID order looks odd because of the RT being moved to the top assert (((Cb.Tweet)tm.get_item (0)).id == 100); assert (((Cb.Tweet)tm.get_item (1)).id == 10); assert (((Cb.Tweet)tm.get_item (2)).id == 1000); } void retweet_duplicate_insertion () { Cb.TweetModel tm = new Cb.TweetModel (); tm.set_thread_mode (true); Cb.Tweet t1 = new Cb.Tweet (); t1.id = 10; Cb.Tweet t2 = new Cb.Tweet (); t2.id = 100; t2.retweeted_tweet = Cb.MiniTweet (); t2.retweeted_tweet.id = 10; tm.add(t1); tm.add(t2); assert(tm.get_n_items() == 1); } void tweet_removal () { var tm = new Cb.TweetModel (); //add 10 visible tweets for (int i = 0; i < 10; i ++) { var t = new Cb.Tweet (); t.id = 100 - i; tm.add (t); } // now add 2 invisible tweets { var t = new Cb.Tweet (); t.id = 2; t.set_flag (Cb.TweetState.HIDDEN_FORCE); tm.add (t); assert (tm.get_n_items () == 10); t = new Cb.Tweet (); t.id = 1; t.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t); assert (tm.get_n_items () == 10); } // We should have 10 now assert (tm.get_n_items () == 10); // And two hidden tweets assert (tm.hidden_tweets.length == 2); // Now remove the last 5 visible ones. // This should remove 2 invisible tweets as well as 5 visible ones // Leaving the model with 5 remaining tweets tm.remove_oldest_n_visible (5); assert (tm.get_n_items () == 5); assert (tm.hidden_tweets.length == 0); for (int i = 0; i < 5; i++) { assert (((Cb.Tweet)tm.get_item (i)).id == 100 - i); } } void tweet_removal_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); //add 10 visible tweets for (int i = 0; i < 10; i ++) { var t = new Cb.Tweet (); t.id = 100 - i; tm.add (t); } // now add 2 invisible tweets { var t = new Cb.Tweet (); t.id = 2; t.set_flag (Cb.TweetState.HIDDEN_FORCE); tm.add (t); assert (tm.get_n_items () == 10); t = new Cb.Tweet (); t.id = 1; t.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t); assert (tm.get_n_items () == 10); } // We should have 10 now assert (tm.get_n_items () == 10); // And two hidden tweets assert (tm.hidden_tweets.length == 2); // Now remove the last 5 visible ones. // This should remove 2 invisible tweets as well as 5 visible ones // Leaving the model with 5 remaining tweets tm.remove_oldest_n_visible (5); assert (tm.get_n_items () == 5); assert (tm.hidden_tweets.length == 0); for (int i = 0; i < 5; i++) { assert (((Cb.Tweet)tm.get_item (i)).id == 96 + i); } } void tweet_removal_zero_length_handling () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); for (int i = 0; i < 20; i ++) { var t = new Cb.Tweet(); t.id = 10 + (i * 2); // Only even ids tm.add (t); } tm.remove_oldest_n_visible (0); } void tweet_removal_leaves_newer_hidden_tweets() { var tm = new Cb.TweetModel (); //add 10 visible tweets for (int i = 0; i < 10; i ++) { var t = new Cb.Tweet (); t.id = 100 - i; tm.add (t); } // now add 2 invisible tweets { var t = new Cb.Tweet (); t.id = 200; t.set_flag (Cb.TweetState.HIDDEN_FORCE); tm.add (t); assert (tm.get_n_items () == 10); t = new Cb.Tweet (); t.id = 201; t.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t); assert (tm.get_n_items () == 10); } // We should have 10 now assert (tm.get_n_items () == 10); // And two hidden tweets assert (tm.hidden_tweets.length == 2); // Now remove the last 5 visible ones. // This should remove 2 invisible tweets as well as 5 visible ones // Leaving the model with 5 remaining tweets tm.remove_oldest_n_visible (5); assert (tm.get_n_items () == 5); assert (tm.hidden_tweets.length == 2); for (int i = 0; i < 5; i++) { assert (((Cb.Tweet)tm.get_item (i)).id == 100 - i); } } void tweet_model_set_unset_flags () { var tm = new Cb.TweetModel (); var t = new Cb.Tweet(); t.id = 100; tm.add(t); var newer = new Cb.Tweet (); newer.id = 200; tm.add(newer); var older = new Cb.Tweet (); older.id = 10; tm.add(older); assert(tm.get_n_items() == 3); assert(tm.hidden_tweets.length == 0); assert(tm.min_id == 10); assert(tm.max_id == 200); // Older hidden tweets get removed (because we can scroll back to reload them) tm.set_tweet_flag(older, Cb.TweetState.HIDDEN_FILTERED); assert(tm.get_n_items() == 2); assert(tm.hidden_tweets.length == 0); assert(tm.min_id == 100); assert(tm.max_id == 200); // Newer hidden tweets remain tm.set_tweet_flag(newer, Cb.TweetState.HIDDEN_FILTERED); assert(tm.get_n_items() == 1); assert(tm.hidden_tweets.length == 1); assert(tm.min_id == 100); assert(tm.max_id == 100); // And can be put back tm.unset_tweet_flag(newer, Cb.TweetState.HIDDEN_FILTERED); assert(tm.get_n_items() == 2); assert(tm.hidden_tweets.length == 0); assert(tm.min_id == 100); assert(tm.max_id == 200); } void contains () { var tm = new Cb.TweetModel (); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; tm.add (t); } for (int i = 0; i < n; i++) { assert (tm.contains_id (100 + i)); } assert (!tm.contains_id (99)); assert (!tm.contains_id (100 + n)); } void index_of () { var tm = new Cb.TweetModel (); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; tm.add (t); } for (int i = 0; i < n; i++) { assert (tm.index_of (100 + i) == n - i - 1); } assert (tm.index_of (99) == -1); assert (tm.index_of (100 + n) == -1); } void index_of_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; tm.add (t); } for (int i = 0; i < n; i++) { assert (tm.index_of (100 + i) == i); } assert (tm.index_of (99) == -1); assert (tm.index_of (100 + n) == -1); } void index_of_rt () { var tm = new Cb.TweetModel (); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; t.retweeted_tweet = Cb.MiniTweet(); t.retweeted_tweet.id = 10 + i; tm.add (t); } for (int i = 0; i < n; i++) { assert (tm.index_of_retweet (10 + i) == n - i - 1); } assert (tm.index_of_retweet (9) == -1); assert (tm.index_of_retweet (10 + n) == -1); } void index_of_rt_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; t.retweeted_tweet = Cb.MiniTweet(); t.retweeted_tweet.id = 10 + i; tm.add (t); } for (int i = 0; i < n; i++) { assert (tm.index_of_retweet (10 + i) == i); } assert (tm.index_of_retweet (9) == -1); assert (tm.index_of_retweet (10 + n) == -1); } void clear () { var tm = new Cb.TweetModel (); const int n = 10; for (int i = 0; i < n; i++) { var t = new Cb.Tweet (); t.id = 100 + i; tm.add (t); } assert (tm.get_n_items () == n); tm.clear (); assert (tm.get_n_items () == 0); } void clear2 () { var tm1 = new Cb.TweetModel (); var tm2 = new Cb.TweetModel (); var t = new Cb.Tweet (); t.id = 10000; tm1.add (t); var t2 = new Cb.Tweet (); t2.id = 3400; t2.set_flag (Cb.TweetState.HIDDEN_FORCE); tm1.add(t2); tm1.clear (); assert (tm1.get_n_items () == tm2.get_n_items ()); assert (tm1.max_id == tm2.max_id); assert (tm1.min_id == tm2.min_id); assert (tm1.hidden_tweets.length == 0); } void remove_tweet () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 10; tm.add (t1); var t2 = new Cb.Tweet (); t2.id = 100; tm.add (t2); assert (tm.get_n_items () == 2); tm.remove_tweet (t2); assert (tm.get_n_items () == 1); tm.remove_tweet (t1); assert (tm.get_n_items () == 0); } void remove_hidden () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 10; t1.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t1); assert (tm.get_n_items () == 0); assert (tm.hidden_tweets.length == 1); tm.remove_tweet (t1); assert (tm.get_n_items () == 0); assert (tm.hidden_tweets.length == 0); } void remove_older () { var tm = new Cb.TweetModel (); for (int i = 1; i < 51; i ++) { var t = new Cb.Tweet (); t.id = i; tm.add (t); } assert (tm.get_n_items () == 50); tm.remove_tweets_later_than (25); assert (tm.get_n_items () == 24); assert (tm.max_id == 24); assert (tm.min_id == 1); } void remove_older_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); for (int i = 1; i < 51; i ++) { var t = new Cb.Tweet (); t.id = i; tm.add (t); } assert (tm.get_n_items () == 50); tm.remove_tweets_later_than (25); assert (tm.get_n_items () == 24); assert (tm.max_id == 24); assert (tm.min_id == 1); } void remove_own_retweet () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 1337; t1.my_retweet = 500; // <-- t1.set_flag (Cb.TweetState.RETWEETED); tm.add (t1); for (int i = 1; i < 51; i ++) { var t = new Cb.Tweet (); t.id = i; tm.add (t); } assert (tm.get_n_items () == 51); //tm.remove (5); //assert (tm.get_n_items () == 50); // should not actually remove any tweet tm.remove_tweet (t1); assert (tm.get_n_items () == 50); } void hide_rt () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 100; t1.source_tweet = Cb.MiniTweet (); t1.source_tweet.author = Cb.UserIdentity (); t1.source_tweet.author.id = 10; t1.source_tweet.id = 1; t1.retweeted_tweet = Cb.MiniTweet (); t1.retweeted_tweet.id = 100; t1.retweeted_tweet.author = Cb.UserIdentity (); t1.retweeted_tweet.author.id = 100; tm.add (t1); //assert (!t1.is_hidden ()); //tm.toggle_flag_on_user_retweets (10, Cb.TweetState.HIDDEN_FILTERED, true); //assert (t1.is_hidden ()); //assert (tm.get_n_items () == 0); //assert (tm.hidden_tweets.length == 1); //tm.toggle_flag_on_user_retweets (10, Cb.TweetState.HIDDEN_FILTERED, false); //assert (!t1.is_hidden ()); //assert (tm.get_n_items () == 1); //assert (!((Cb.Tweet)tm.get_item (0)).is_hidden ()); } void get_for_id () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 10; var t2 = new Cb.Tweet (); t2.id = 100; tm.add (t1); tm.add (t2); assert (tm.get_n_items () == 2); assert (((Cb.Tweet)tm.get_item (0)).id == 100); assert (((Cb.Tweet)tm.get_item (1)).id == 10); var result = tm.get_for_id (10); assert (result != null); assert (result.id == 10); var t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); result = tm.get_for_id (100, 1); assert (result != null); assert (result.id == 10); result = tm.get_for_id (100, -1); assert (result != null); assert (result.id == 1000); } void get_for_id_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); var t1 = new Cb.Tweet (); t1.id = 10; var t2 = new Cb.Tweet (); t2.id = 100; tm.add (t1); tm.add (t2); assert (tm.get_n_items () == 2); assert (((Cb.Tweet)tm.get_item (0)).id == 10); assert (((Cb.Tweet)tm.get_item (1)).id == 100); var result = tm.get_for_id (10); assert (result != null); assert (result.id == 10); var t3 = new Cb.Tweet (); t3.id = 1000; tm.add (t3); result = tm.get_for_id (100, 1); assert (result != null); assert (result.id == 1000); result = tm.get_for_id (100, -1); assert (result != null); assert (result.id == 10); } void min_max_id () { var tm = new Cb.TweetModel (); var t = new Cb.Tweet (); t.id = 1337; tm.add (t); var t2 = new Cb.Tweet (); t2.id = 30000; t2.set_flag (Cb.TweetState.HIDDEN_FORCE); tm.add (t2); // Hidden tweets shouldn't affect the min/max id values assert (tm.min_id == 1337); assert (tm.max_id == 1337); var t3 = new Cb.Tweet (); t3.id = 1400; tm.add (t3); var t4 = new Cb.Tweet (); t4.id = 1300; tm.add (t4); assert (tm.min_id == 1300); assert (tm.max_id == 1400); } void min_max_id_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); var t = new Cb.Tweet (); t.id = 1337; tm.add (t); var t2 = new Cb.Tweet (); t2.id = 30000; t2.set_flag (Cb.TweetState.HIDDEN_FORCE); tm.add (t2); // Hidden tweets shouldn't affect the min/max id values assert (tm.min_id == 1337); assert (tm.max_id == 1337); var t3 = new Cb.Tweet (); t3.id = 1400; tm.add (t3); var t4 = new Cb.Tweet (); t4.id = 1300; tm.add (t4); assert (tm.min_id == 1300); assert (tm.max_id == 1400); } void sorting () { var tm = new Cb.TweetModel (); for (int i = 0; i < 100; i ++) { var t = new Cb.Tweet (); t.id = GLib.Random.next_int (); tm.add (t); } assert (is_desc_sorted (tm)); tm = new Cb.TweetModel (); tm.set_thread_mode (true); for (int i = 0; i < 100; i ++) { var t = new Cb.Tweet (); t.id = GLib.Random.next_int (); tm.add (t); } assert (is_thread_mode_sorted (tm)); } void min_max_remove () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 10; tm.add (t1); var t2 = new Cb.Tweet (); t2.id = 20; tm.add (t2); var t3 = new Cb.Tweet (); t3.id = 2; tm.add (t3); assert (tm.max_id == 20); assert (tm.min_id == 2); tm.remove_tweet (t1); // Should still be the same assert (tm.max_id == 20); assert (tm.min_id == 2); var t = new Cb.Tweet (); t.id = 10; tm.add (t); // And again... assert (tm.max_id == 20); assert (tm.min_id == 2); // Now it gets interesting tm.remove_tweet (t2); assert (tm.min_id == 2); assert (tm.max_id == 10); assert (tm.max_id == get_max_id (tm)); tm.remove_tweet (t3); assert (tm.min_id == 10); assert (tm.max_id == 10); tm.remove_tweet (t); assert (tm.get_n_items () == 0); assert (tm.min_id == int64.MAX); assert (tm.max_id == int64.MIN); } void min_max_remove_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); var t1 = new Cb.Tweet (); t1.id = 10; tm.add (t1); var t2 = new Cb.Tweet (); t2.id = 20; tm.add (t2); var t3 = new Cb.Tweet (); t3.id = 2; tm.add (t3); assert (tm.max_id == 20); assert (tm.min_id == 2); tm.remove_tweet (t1); // Should still be the same assert (tm.max_id == 20); assert (tm.min_id == 2); var t = new Cb.Tweet (); t.id = 10; tm.add (t); // And again... assert (tm.max_id == 20); assert (tm.min_id == 2); // Now it gets interesting tm.remove_tweet (t2); assert (tm.min_id == 2); assert (tm.max_id == 10); assert (tm.max_id == get_max_id (tm)); tm.remove_tweet (t3); assert (tm.min_id == 10); assert (tm.max_id == 10); tm.remove_tweet (t); assert (tm.get_n_items () == 0); assert (tm.min_id == int64.MAX); assert (tm.max_id == int64.MIN); } void tweet_count () { var tm = new Cb.TweetModel (); var t1 = new Cb.Tweet (); t1.id = 10; t1.source_tweet = Cb.MiniTweet (); t1.source_tweet.author = Cb.UserIdentity (); t1.source_tweet.author.id = 11; t1.retweeted_tweet = Cb.MiniTweet (); t1.retweeted_tweet.id = 100; t1.retweeted_tweet.author = Cb.UserIdentity (); t1.retweeted_tweet.author.id = 111; tm.add (t1); assert (tm.get_n_items () == 1); assert (tm.max_id == t1.id); assert (tm.min_id == t1.id); tm.toggle_flag_on_user_retweets (11, Cb.TweetState.HIDDEN_FILTERED, true); assert (tm.get_n_items () == 0); assert (tm.hidden_tweets.length == 0); t1.unset_flag (Cb.TweetState.HIDDEN_FILTERED); var t2 = new Cb.Tweet (); t2.id = 20; t2.source_tweet = Cb.MiniTweet (); t2.source_tweet.author = Cb.UserIdentity (); t2.source_tweet.author.id = 11; tm.add (t2); tm.add (t1); assert (tm.get_n_items () == 2); assert (tm.max_id == t2.id); assert (tm.min_id == t1.id); tm.toggle_flag_on_user_tweets (11, Cb.TweetState.HIDDEN_FILTERED, true); assert (tm.get_n_items () == 1); tm.toggle_flag_on_user_retweets (11, Cb.TweetState.HIDDEN_FILTERED, true); assert (tm.get_n_items () == 0); assert (tm.hidden_tweets.length == 0); } void hidden_remove_oldest_n_visible () { var tm = new Cb.TweetModel (); for (int i = 0; i < 20; i ++) { var t = new Cb.Tweet(); t.id = 10 + (i * 2); // Only even ids tm.add (t); } var t1 = new Cb.Tweet (); t1.id = 15; t1.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t1); var t2 = new Cb.Tweet (); t2.id = 21; t2.set_flag (Cb.TweetState.HIDDEN_AUTHOR_MUTED); tm.add (t2); assert (tm.get_n_items () == 20); assert (tm.hidden_tweets.length == 2); tm.remove_oldest_n_visible (10); assert (tm.get_n_items () == 10); for (int i = 0; i < 10; i ++) { assert (((Cb.Tweet)tm.get_item (i)).id >= 30); } tm.remove_oldest_n_visible (10); assert (tm.get_n_items () == 0); } void hidden_remove_oldest_n_visible_thread_mode () { var tm = new Cb.TweetModel (); tm.set_thread_mode (true); for (int i = 0; i < 20; i ++) { var t = new Cb.Tweet(); t.id = 10 + (i * 2); // Only even ids tm.add (t); } var t1 = new Cb.Tweet (); t1.id = 15; t1.set_flag (Cb.TweetState.HIDDEN_UNFOLLOWED); tm.add (t1); var t2 = new Cb.Tweet (); t2.id = 21; t2.set_flag (Cb.TweetState.HIDDEN_AUTHOR_MUTED); tm.add (t2); assert (tm.get_n_items () == 20); assert (tm.hidden_tweets.length == 2); tm.remove_oldest_n_visible (10); assert (tm.get_n_items () == 10); for (int i = 0; i < 10; i ++) { assert (((Cb.Tweet)tm.get_item (i)).id >= 30); } tm.remove_oldest_n_visible (10); assert (tm.get_n_items () == 0); } void empty_hidden_tweets () { int n = 100; var tm = new Cb.TweetModel (); for (int i = 1; i <= n; i += 2) { var t1 = new Cb.Tweet (); t1.id = i + 1; tm.add (t1); var t2 = new Cb.Tweet (); t2.id = i; t2.set_flag (Cb.TweetState.HIDDEN_RT_BY_FOLLOWEE); tm.add (t2); tm.remove_tweet (t1); } message ("%u", tm.get_n_items ()); assert (tm.get_n_items () == 0); message ("%u", tm.hidden_tweets.length); assert (tm.hidden_tweets.length == 0); } void same_id () { var tm = new Cb.TweetModel (); var t = new Cb.Tweet (); t.id = 1337; tm.add (t); assert (tm.get_n_items () == 1); assert (tm.min_id == 1337); assert (tm.max_id == 1337); tm.add (t); /* No change */ assert (tm.get_n_items () == 1); assert (tm.min_id == 1337); assert (tm.max_id == 1337); // What if we created it from a different source? var t2 = new Cb.Tweet (); t2.id = t.id; tm.add (t2); assert (tm.get_n_items () == 1); assert (tm.min_id == 1337); assert (tm.max_id == 1337); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/tweetmodel/basic-tweet-order", basic_tweet_order); GLib.Test.add_func ("/tweetmodel/basic-tweet-order-thread-mode", basic_tweet_order_thread_mode); GLib.Test.add_func ("/tweetmodel/retweet-order", retweet_order); GLib.Test.add_func ("/tweetmodel/retweet-order-thread-mode", retweet_order_thread_mode); GLib.Test.add_func ("/tweetmodel/retweet-duplicate-insertion", retweet_duplicate_insertion); GLib.Test.add_func ("/tweetmodel/tweet-removal", tweet_removal); GLib.Test.add_func ("/tweetmodel/tweet-removal-thread-mode", tweet_removal_thread_mode); GLib.Test.add_func ("/tweetmodel/tweet-removal-zero-length", tweet_removal_zero_length_handling); GLib.Test.add_func ("/tweetmodel/tweet-removal-leaves-newer-tweets", tweet_removal_leaves_newer_hidden_tweets); GLib.Test.add_func ("/tweetmodel/set-unset-flags", tweet_model_set_unset_flags); GLib.Test.add_func ("/tweetmodel/contains", contains); GLib.Test.add_func ("/tweetmodel/index-of", index_of); GLib.Test.add_func ("/tweetmodel/index-of-thread-mode", index_of_thread_mode); GLib.Test.add_func ("/tweetmodel/index-of-retweet", index_of_rt); GLib.Test.add_func ("/tweetmodel/index-of-retweet-thread-mode", index_of_rt_thread_mode); GLib.Test.add_func ("/tweetmodel/clear", clear); GLib.Test.add_func ("/tweetmodel/clear2", clear2); GLib.Test.add_func ("/tweetmodel/remove", remove_tweet); GLib.Test.add_func ("/tweetmodel/remove-hidden", remove_hidden); GLib.Test.add_func ("/tweetmodel/remove-own-retweet", remove_own_retweet); GLib.Test.add_func ("/tweetmodel/remove-older", remove_older); GLib.Test.add_func ("/tweetmodel/remove-older-thread-mode", remove_older_thread_mode); GLib.Test.add_func ("/tweetmodel/hide-rt", hide_rt); GLib.Test.add_func ("/tweetmodel/get-for-id", get_for_id); GLib.Test.add_func ("/tweetmodel/get-for-id-thread-mode", get_for_id_thread_mode); GLib.Test.add_func ("/tweetmodel/min-max-id", min_max_id); GLib.Test.add_func ("/tweetmodel/min-max-id-thread-mode", min_max_id_thread_mode); GLib.Test.add_func ("/tweetmodel/sorting", sorting); GLib.Test.add_func ("/tweetmodel/min-max-remove", min_max_remove); GLib.Test.add_func ("/tweetmodel/min-max-remove-thread-mode", min_max_remove_thread_mode); GLib.Test.add_func ("/tweetmodel/tweet-count", tweet_count); GLib.Test.add_func ("/tweetmodel/empty-hidden-tweets", empty_hidden_tweets); GLib.Test.add_func ("/tweetmodel/hidden-remove-last-n-visible", hidden_remove_oldest_n_visible); GLib.Test.add_func ("/tweetmodel/hidden-remove-last-n-visible-thread-mode", hidden_remove_oldest_n_visible_thread_mode); GLib.Test.add_func ("/tweetmodel/same-id", same_id); return GLib.Test.run (); } cawbird-1.4.2/tests/tweetparsing.vala000066400000000000000000003111361416632607600176740ustar00rootroot00000000000000 // {{{ const string TD1 = """ { "created_at" : "Mon May 05 06:48:32 +0000 2014", "id" : 463208606784311296, "id_str" : "463208606784311296", "text" : "RT @BlackForestTeam: DIESELSTÖRMERS Kickstarter is live! - Go and check it out right now!... http://t.co/ZVmefc0w5e", "source" : "web", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 62574927, "id_str" : "62574927", "name" : "Frozenbyte", "screen_name" : "Frozenbyte", "location" : "Helsinki, Finland", "description" : "We're an independent game developer. Follow us on Twitter to get the latest news on our games! For support issues please get in touch via email.", "url" : "http://t.co/NlgW9k9ZXj", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/NlgW9k9ZXj", "expanded_url" : "http://www.frozenbyte.com", "display_url" : "frozenbyte.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 5682, "friends_count" : 137, "listed_count" : 242, "created_at" : "Mon Aug 03 17:52:07 +0000 2009", "favourites_count" : 233, "utc_offset" : 10800, "time_zone" : "Helsinki", "geo_enabled" : false, "verified" : false, "statuses_count" : 1042, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/378800000117103508/abe5a14a1f3b0b78e9038e73bf6b812d.jpeg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/378800000117103508/abe5a14a1f3b0b78e9038e73bf6b812d.jpeg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/1130292729/fb_newlogo_black480_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1130292729/fb_newlogo_black480_normal.png", "profile_link_color" : "CBA051", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "D4802D", "profile_text_color" : "4A2500", "profile_use_background_image" : true, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Tue Apr 29 11:00:25 +0000 2014", "id" : 461097667775725569, "id_str" : "461097667775725569", "text" : "DIESELSTÖRMERS Kickstarter is live! - Go and check it out right now!... http://t.co/ZVmefc0w5e", "source" : "Tumblr", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 726763934, "id_str" : "726763934", "name" : "Black Forest Games", "screen_name" : "BlackForestTeam", "location" : "Offenburg", "description" : "South-german team that brought you Giana Sisters: Twisted Dreams", "url" : "http://t.co/BXuCnqlX50", "entities" : { "url" : { "urls" : [ { "url" : "http://t.co/BXuCnqlX50", "expanded_url" : "http://gianasisterstwisteddreams.com", "display_url" : "gianasisterstwisteddreams.com", "indices" : [ 0, 22 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 1145, "friends_count" : 308, "listed_count" : 35, "created_at" : "Mon Jul 30 20:11:50 +0000 2012", "favourites_count" : 116, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 1475, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/3694489354/ed399e59260bf71b10235dcd7eb56fe5_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/3694489354/ed399e59260bf71b10235dcd7eb56fe5_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/726763934/1369154494", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "default_profile" : true, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweet_count" : 6, "favorite_count" : 2, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZVmefc0w5e", "expanded_url" : "http://tmblr.co/ZTqD4s1ERcDZg", "display_url" : "tmblr.co/ZTqD4s1ERcDZg", "indices" : [ 72, 94 ] } ], "user_mentions" : [ ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" }, "retweet_count" : 6, "favorite_count" : 0, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZVmefc0w5e", "expanded_url" : "http://tmblr.co/ZTqD4s1ERcDZg", "display_url" : "tmblr.co/ZTqD4s1ERcDZg", "indices" : [ 93, 115 ] } ], "user_mentions" : [ { "screen_name" : "BlackForestTeam", "name" : "Black Forest Games", "id" : 726763934, "id_str" : "726763934", "indices" : [ 3, 19 ] } ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" }"""; const string TD2 = """ { "created_at": "Tue Apr 29 00:50:10 +0000 2014", "id": 460944092554227713, "id_str": "460944092554227713", "text": "Combined. http:\/\/t.co\/fFJqqT1A4j", "source": "\u003ca href=\"http:\/\/twitter.com\/geekculturejam\" rel=\"nofollow\"\u003eCultureJam\u003c\/a\u003e", "truncated": false, "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": { "id": 657693, "id_str": "657693", "screen_name": "FOOBAR", "name": "Foo Bar", "profile_image_url" : "http://pbs.twimg.com/profile_images/3694489354/ed399e59260bf71b10235dcd7eb56fe5_normal.png", "verified" : false }, "geo": null, "coordinates": null, "place": null, "contributors": null, "retweet_count": 0, "favorite_count": 0, "entities": { "hashtags": [], "symbols": [], "urls": [], "user_mentions": [], "media": [] }, "extended_entities": { "media": [ { "id": 460938773744717825, "id_str": "460938773744717825", "indices": [ 10, 32 ], "media_url": "http:\/\/pbs.twimg.com\/media\/BmWVX2BCEAEx4MK.jpg", "media_url_https": "https:\/\/pbs.twimg.com\/media\/BmWVX2BCEAEx4MK.jpg", "url": "http:\/\/t.co\/fFJqqT1A4j", "display_url": "pic.twitter.com\/fFJqqT1A4j", "expanded_url": "http:\/\/twitter.com\/froginthevalley\/status\/460944092554227713\/photo\/1", "type": "photo", "sizes": { "medium": { "w": 599, "h": 397, "resize": "fit" }, "thumb": { "w": 150, "h": 150, "resize": "crop" }, "small": { "w": 340, "h": 225, "resize": "fit" }, "large": { "w": 1023, "h": 678, "resize": "fit" } } }, { "id": 460938635315916, "indices": [ 10, 32 ], "media_url": "http:\/\/pbs.twimg.com\/media\/BmWVPyVCMAAeAwI.jpg", "media_url_https": "https:\/\/pbs.twimg.com\/media\/BmWVPyVCMAAeAwI.jpg", "url": "http:\/\/t.co\/fFJqqT1A4j", "display_url": "pic.twitter.com\/fFJqqT1A4j", "expanded_url": "http:\/\/twitter.com\/froginthevalley\/status\/460944092554227713\/photo\/1", "type": "photo", "sizes": { "medium": { "w": 600, "h": 600, "resize": "fit" }, "thumb": { "w": 150, "h": 150, "resize": "crop" }, "large": { "w": 1024, "h": 1024, "resize": "fit" }, "small": { "w": 340, "h": 340, "resize": "fit" } } } ] }, "favorited": false, "retweeted": false, "possibly_sensitive": false, "lang": "en" } """; const string TD3 = """ { "created_at" : "Thu Jun 12 19:34:16 +0000 2014", "id" : 477172048427765760, "id_str" : "477172048427765760", "text" : "http://t.co/ZGX7b9YGiU http://t.co/6hfxg0TPyt", "source" : "Twitter Web Client", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Core Bird", "screen_name" : "cawbirdgtk", "location" : "", "description" : "", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 1, "friends_count" : 6, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 12, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 537, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://abs.twimg.com/sticky/default_profile_images/default_profile_1_normal.png", "profile_image_url_https" : "https://abs.twimg.com/sticky/default_profile_images/default_profile_1_normal.png", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "default_profile" : true, "default_profile_image" : true, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweet_count" : 0, "favorite_count" : 0, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZGX7b9YGiU", "expanded_url" : "http://i.imgur.com/kgrtCf0.png", "display_url" : "i.imgur.com/kgrtCf0.png", "indices" : [ 0, 22 ] }, { "url" : "http://t.co/6hfxg0TPyt", "expanded_url" : "http://i.imgur.com/xqmzPar.gif", "display_url" : "i.imgur.com/xqmzPar.gif", "indices" : [ 23, 45 ] } ], "user_mentions" : [ ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" } """; const string TD4 = """ { "created_at" : "Thu Jun 12 19:34:16 +0000 2014", "id" : 477172048465760, "id_str" : "477172048427765760", "text" : "http://t.co/ZGX7b9YGiU http://t.co/6hfxg0TPyt", "source" : "Twitter Web Client", "truncated" : false, "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Core Bird", "screen_name" : "cawbirdgtk", "location" : "", "description" : "", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 1, "friends_count" : 6, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 12, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 537, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://abs.twimg.com/sticky/default_profile_images/default_profile_1_normal.png", "profile_image_url_https" : "https://abs.twimg.com/sticky/default_profile_images/default_profile_1_normal.png", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "default_profile" : true, "default_profile_image" : true, "following" : true, "follow_request_sent" : false, "notifications" : false }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweet_count" : 0, "favorite_count" : 0, "entities" : { "hashtags" : [ ], "symbols" : [ ], "urls" : [ { "url" : "http://t.co/ZGX7b9YGiU", "expanded_url" : "http://i.imgur.com/kgrtCf0.png", "display_url" : "i.imgur.com/kgrtCf0.png", "indices" : [ 0, 22 ] }, { "url" : "http://t.co/6hfxg0TPyt", "expanded_url" : "http://i.imgur.com/xqmzPar.gif", "display_url" : "i.imgur.com/xqmzPar.gif", "indices" : [ 23, 45 ] } ], "user_mentions" : [ ] }, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und", "extended_entities": { "media": [ { "id": 460938773744717, "media_url": "http:\/\/pbs.twimg.com\/media\/BmWVX2BCEAEx4MK.jpg", "media_url_https": "https:\/\/pbs.twimg.com\/media\/BmWVX2BCEAEx4MK.jpg", "url": "http:\/\/t.co\/fFJqqT1A4j", "display_url": "pic.twitter.com\/fFJqqT1A4j", "expanded_url": "http:\/\/twitter.com\/froginthevalley\/status\/460944092554227713\/photo\/1", "type": "photo" }, { "id": 460938615916800, "media_url": "http:\/\/pbs.twimg.com\/media\/BmWVPyVCMAAeAwI.jpg", "media_url_https": "https:\/\/pbs.twimg.com\/media\/BmWVPyVCMAAeAwI.jpg", "url": "http:\/\/t.co\/fFJqqT1A4j", "display_url": "pic.twitter.com\/fFJqqT1A4j", "expanded_url": "http:\/\/twitter.com\/froginthevalley\/status\/460944092554227713\/photo\/1", "type": "photo" } ] } } """; // """ const string TD5 = """ { "created_at":"Fri Sep 30 16:45:16 +0000 2016", "id":781897715399532544, "id_str":"781897715399532544", "text":"RT @oscaron: Okay folks! I\|ve decided to donate all profits from the #400LBHACKER shirt to @Hak4Kidz and @EFF \n\nPlease RT!\u2026 ", "source":"Cawbird\u003c\/a\u003e", "truncated":false, "in_reply_to_status_id":null, "in_reply_to_status_id_str":null, "in_reply_to_user_id":null, "in_reply_to_user_id_str":null, "in_reply_to_screen_name":null, "user":{ "id":993713617, "id_str":"993713617", "name":"Core & Bird", "screen_name":"cawbirdgtk", "location":null, "url":null, "description":null, "protected":true, "verified":false, "followers_count":3, "friends_count":4, "listed_count":0, "favourites_count":9, "statuses_count":845, "created_at":"Thu Dec 06 19:47:16 +0000 2012", "utc_offset":7200, "time_zone":"Amsterdam", "geo_enabled":false, "lang":"en", "contributors_enabled":false, "is_translator":false, "profile_background_color":"C0DEED", "profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", "profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png", "profile_background_tile":false, "profile_link_color":"0084B4", "profile_sidebar_border_color":"C0DEED", "profile_sidebar_fill_color":"DDEEF6", "profile_text_color":"333333", "profile_use_background_image":true, "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/655270693341417472\/h6BbZKJy_normal.png", "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/655270693341417472\/h6BbZKJy_normal.png", "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/993713617\/1413106147", "default_profile":true, "default_profile_image":false, "following":null, "follow_request_sent":null, "notifications":null }, "geo":null, "coordinates":null, "place":null, "contributors":null, "retweeted_status":{ "created_at":"Wed Sep 28 14:23:49 +0000 2016", "id":781137340962004992, "id_str":"781137340962004992", "text":"Okay folks! I\|ve decided to donate all profits from the #400LBHACKER shirt to @Hak4Kidz and @EFF \n\nPlease RT!\u2026 https:\/\/t.co\/V2vE2q8ol0", "display_text_range":[ 0, 140 ], "source":"\u003ca href=\"https:\/\/about.twitter.com\/products\/tweetdeck\" rel=\"nofollow\"\u003eTweetDeck\u003c\/a\u003e", "truncated":true, "in_reply_to_status_id":null, "in_reply_to_status_id_str":null, "in_reply_to_user_id":null, "in_reply_to_user_id_str":null, "in_reply_to_screen_name":null, "user":{ "id":16156052, "id_str":"16156052", "name":"Oscaron \u2615", "screen_name":"oscaron", "location":"Galactic Sector ZZ9 Plural Z A", "url":"http:\/\/goo.gl\/skGJRQ", "description":"Tech Alchemist, Infosec Enthusiast. NOT a \|Cybersecurity Expert\|. My views = My views. Trigger warnings: ALL. All stories exaggerated.", "protected":false, "verified":false, "followers_count":1391, "friends_count":762, "listed_count":105, "favourites_count":18359, "statuses_count":49369, "created_at":"Sat Sep 06 12:36:08 +0000 2008", "utc_offset":-14400, "time_zone":"Eastern Time (US & Canada)", "geo_enabled":false, "lang":"en", "contributors_enabled":false, "is_translator":false, "profile_background_color":"131516", "profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme14\/bg.gif", "profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme14\/bg.gif", "profile_background_tile":true, "profile_link_color":"5E2F00", "profile_sidebar_border_color":"FFFFFF", "profile_sidebar_fill_color":"EFEFEF", "profile_text_color":"333333", "profile_use_background_image":true, "profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/773567191832821761\/2ndP1v95_normal.jpg", "profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/773567191832821761\/2ndP1v95_normal.jpg", "profile_banner_url":"https:\/\/pbs.twimg.com\/profile_banners\/16156052\/1404228988", "default_profile":false, "default_profile_image":false, "following":null, "follow_request_sent":null, "notifications":null }, "geo":null, "coordinates":null, "place":null, "contributors":null, "is_quote_status":false, "extended_tweet":{ "full_text":"Okay folks! I\|ve decided to donate all profits from the #400LBHACKER shirt to @Hak4Kidz and @EFF \n\nPlease RT!\n\nhttps:\/\/t.co\/AP7qFSdYp0 https:\/\/t.co\/WtprxUumEE", "display_text_range":[ 0, 134 ], "entities":{ "hashtags":[ { "text":"400LBHACKER", "indices":[ 56, 68 ] } ], "urls":[ { "url":"https:\/\/t.co\/AP7qFSdYp0", "expanded_url":"https:\/\/teespring.com\/400lbhacker#pid=2&cid=2397&sid=front", "display_url":"teespring.com\/400lbhacker#pi\u2026", "indices":[ 111, 134 ] } ], "user_mentions":[ { "screen_name":"Hak4Kidz", "name":"Hak4Kidz", "id":2262322964, "id_str":"2262322964", "indices":[ 78, 87 ] }, { "screen_name":"EFF", "name":"EFF", "id":4816, "id_str":"4816", "indices":[ 92, 96 ] } ], "symbols":[ ], "media":[ { "id":781137070484054016, "id_str":"781137070484054016", "indices":[ 135, 158 ], "media_url":"http:\/\/pbs.twimg.com\/media\/CtcoBucXYAAs4rs.jpg", "media_url_https":"https:\/\/pbs.twimg.com\/media\/CtcoBucXYAAs4rs.jpg", "url":"https:\/\/t.co\/WtprxUumEE", "display_url":"pic.twitter.com\/WtprxUumEE", "expanded_url":"https:\/\/twitter.com\/oscaron\/status\/781137340962004992\/photo\/1", "type":"photo", "sizes":{ "medium":{ "w":480, "h":571, "resize":"fit" }, "large":{ "w":480, "h":571, "resize":"fit" }, "thumb":{ "w":150, "h":150, "resize":"crop" }, "small":{ "w":480, "h":571, "resize":"fit" } } }, { "id":781137082697805824, "id_str":"781137082697805824", "indices":[ 135, 158 ], "media_url":"http:\/\/pbs.twimg.com\/media\/CtcoCb8WcAATK4y.jpg", "media_url_https":"https:\/\/pbs.twimg.com\/media\/CtcoCb8WcAATK4y.jpg", "url":"https:\/\/t.co\/WtprxUumEE", "display_url":"pic.twitter.com\/WtprxUumEE", "expanded_url":"https:\/\/twitter.com\/oscaron\/status\/781137340962004992\/photo\/1", "type":"photo", "sizes":{ "medium":{ "w":480, "h":571, "resize":"fit" }, "large":{ "w":480, "h":571, "resize":"fit" }, "thumb":{ "w":150, "h":150, "resize":"crop" }, "small":{ "w":480, "h":571, "resize":"fit" } } } ] }, "extended_entities":{ "media":[ { "id":781137070484054016, "id_str":"781137070484054016", "indices":[ 135, 158 ], "media_url":"http:\/\/pbs.twimg.com\/media\/CtcoBucXYAAs4rs.jpg", "media_url_https":"https:\/\/pbs.twimg.com\/media\/CtcoBucXYAAs4rs.jpg", "url":"https:\/\/t.co\/WtprxUumEE", "display_url":"pic.twitter.com\/WtprxUumEE", "expanded_url":"https:\/\/twitter.com\/oscaron\/status\/781137340962004992\/photo\/1", "type":"photo", "sizes":{ "medium":{ "w":480, "h":571, "resize":"fit" }, "large":{ "w":480, "h":571, "resize":"fit" }, "thumb":{ "w":150, "h":150, "resize":"crop" }, "small":{ "w":480, "h":571, "resize":"fit" } } }, { "id":781137082697805824, "id_str":"781137082697805824", "indices":[ 135, 158 ], "media_url":"http:\/\/pbs.twimg.com\/media\/CtcoCb8WcAATK4y.jpg", "media_url_https":"https:\/\/pbs.twimg.com\/media\/CtcoCb8WcAATK4y.jpg", "url":"https:\/\/t.co\/WtprxUumEE", "display_url":"pic.twitter.com\/WtprxUumEE", "expanded_url":"https:\/\/twitter.com\/oscaron\/status\/781137340962004992\/photo\/1", "type":"photo", "sizes":{ "medium":{ "w":480, "h":571, "resize":"fit" }, "large":{ "w":480, "h":571, "resize":"fit" }, "thumb":{ "w":150, "h":150, "resize":"crop" }, "small":{ "w":480, "h":571, "resize":"fit" } } } ] } }, "retweet_count":355, "favorite_count":308, "entities":{ "hashtags":[ { "text":"400LBHACKER", "indices":[ 56, 68 ] } ], "urls":[ { "url":"https:\/\/t.co\/V2vE2q8ol0", "expanded_url":"https:\/\/twitter.com\/i\/web\/status\/781137340962004992", "display_url":"twitter.com\/i\/web\/status\/7\u2026", "indices":[ 111, 134 ] } ], "user_mentions":[ { "screen_name":"Hak4Kidz", "name":"Hak4Kidz", "id":2262322964, "id_str":"2262322964", "indices":[ 78, 87 ] }, { "screen_name":"EFF", "name":"EFF", "id":4816, "id_str":"4816", "indices":[ 92, 96 ] } ], "symbols":[ ] }, "favorited":false, "retweeted":false, "possibly_sensitive":false, "filter_level":"low", "lang":"en" }, "is_quote_status":false, "retweet_count":0, "favorite_count":0, "entities":{ "hashtags":[ { "text":"400LBHACKER", "indices":[ 69, 81 ] } ], "urls":[ { "url":"", "expanded_url":null, "indices":[ 124, 124 ] } ], "user_mentions":[ { "screen_name":"oscaron", "name":"Oscaron \u2615", "id":16156052, "id_str":"16156052", "indices":[ 3, 11 ] }, { "screen_name":"Hak4Kidz", "name":"Hak4Kidz", "id":2262322964, "id_str":"2262322964", "indices":[ 91, 100 ] }, { "screen_name":"EFF", "name":"EFF", "id":4816, "id_str":"4816", "indices":[ 105, 109 ] } ], "symbols":[ ] }, "favorited":false, "retweeted":false, "filter_level":"low", "lang":"en", "timestamp_ms":"1475253916965" } """; const string TD6 = """ { "created_at" : "Sat Apr 01 16:28:13 +0000 2017", "id" : 848210402995556353, "id_str" : "848210402995556353", "full_text" : "RT @wurst_hw: SOME TWEET https://t.co/ySiXE2r1dV https://t.co/k8Wlmh67nf", "truncated" : false, "display_text_range" : [ 0, 72 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "wurst_hw", "name" : "Hans Wurst", "id" : 1469297089, "id_str" : "1469297089", "indices" : [ 3, 12 ] } ], "urls" : [ { "url" : "https://t.co/ySiXE2r1dV", "expanded_url" : "https://twitter.com/baedert/status/847671175081676801", "display_url" : "twitter.com/baedert/status…", "indices" : [ 25, 48 ] } ], "media" : [ { "id" : 848210277820747776, "id_str" : "848210277820747776", "indices" : [ 49, 72 ], "media_url" : "http://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "media_url_https" : "https://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "url" : "https://t.co/k8Wlmh67nf", "display_url" : "pic.twitter.com/k8Wlmh67nf", "expanded_url" : "https://twitter.com/wurst_hw/status/848210293465468928/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "large" : { "w" : 1920, "h" : 1080, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } }, "source_status_id" : 848210293465468928, "source_status_id_str" : "848210293465468928", "source_user_id" : 1469297089, "source_user_id_str" : "1469297089" } ] }, "extended_entities" : { "media" : [ { "id" : 848210277820747776, "id_str" : "848210277820747776", "indices" : [ 49, 72 ], "media_url" : "http://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "media_url_https" : "https://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "url" : "https://t.co/k8Wlmh67nf", "display_url" : "pic.twitter.com/k8Wlmh67nf", "expanded_url" : "https://twitter.com/wurst_hw/status/848210293465468928/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "large" : { "w" : 1920, "h" : 1080, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } }, "source_status_id" : 848210293465468928, "source_status_id_str" : "848210293465468928", "source_user_id" : 1469297089, "source_user_id_str" : "1469297089" } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Z??!@*(&*²³¤²³¤", "screen_name" : "cawbirdgtk", "location" : "", "description" : "<asdf", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 3, "friends_count" : 4, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 9, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 865, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/993713617/1413106147", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Sat Apr 01 16:27:47 +0000 2017", "id" : 848210293465468928, "id_str" : "848210293465468928", "full_text" : "SOME TWEET https://t.co/ySiXE2r1dV https://t.co/k8Wlmh67nf", "truncated" : false, "display_text_range" : [ 0, 34 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ { "url" : "https://t.co/ySiXE2r1dV", "expanded_url" : "https://twitter.com/baedert/status/847671175081676801", "display_url" : "twitter.com/baedert/status…", "indices" : [ 11, 34 ] } ], "media" : [ { "id" : 848210277820747776, "id_str" : "848210277820747776", "indices" : [ 35, 58 ], "media_url" : "http://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "media_url_https" : "https://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "url" : "https://t.co/k8Wlmh67nf", "display_url" : "pic.twitter.com/k8Wlmh67nf", "expanded_url" : "https://twitter.com/wurst_hw/status/848210293465468928/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "large" : { "w" : 1920, "h" : 1080, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 848210277820747776, "id_str" : "848210277820747776", "indices" : [ 35, 58 ], "media_url" : "http://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "media_url_https" : "https://pbs.twimg.com/media/C8VywPBXUAAgOkn.jpg", "url" : "https://t.co/k8Wlmh67nf", "display_url" : "pic.twitter.com/k8Wlmh67nf", "expanded_url" : "https://twitter.com/wurst_hw/status/848210293465468928/photo/1", "type" : "photo", "sizes" : { "medium" : { "w" : 1200, "h" : 675, "resize" : "fit" }, "large" : { "w" : 1920, "h" : 1080, "resize" : "fit" }, "small" : { "w" : 680, "h" : 383, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 1469297089, "id_str" : "1469297089", "name" : "Hans Wurst", "screen_name" : "wurst_hw", "location" : "", "description" : "", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 1, "friends_count" : 2, "listed_count" : 0, "created_at" : "Thu May 30 08:47:22 +0000 2013", "favourites_count" : 0, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 31, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png", "profile_image_url_https" : "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : true, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 847671175081676801, "quoted_status_id_str" : "847671175081676801", "quoted_status" : { "created_at" : "Fri Mar 31 04:45:31 +0000 2017", "id" : 847671175081676801, "id_str" : "847671175081676801", "full_text" : "Haha yeah. The Reply Madness of 2017. Very funny. It'll ruin my weekend but let's laugh first.", "truncated" : false, "display_text_range" : [ 0, 94 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ] }, "source" : "Twidere for Android #5", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 118055879, "id_str" : "118055879", "name" : "Schupp & Wupp", "screen_name" : "baedert", "location" : "", "description" : "Cawbird developer by night, silently judging people on the train by day.", "url" : "https://t.co/yGvX7Nf6i3", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/yGvX7Nf6i3", "expanded_url" : "http://cawbird.baedert.org", "display_url" : "cawbird.baedert.org", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 193, "friends_count" : 73, "listed_count" : 4, "created_at" : "Sat Feb 27 13:13:34 +0000 2010", "favourites_count" : 152, "utc_offset" : 7200, "time_zone" : "Bern", "geo_enabled" : false, "verified" : false, "statuses_count" : 2079, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C6E2EE", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme2/bg.gif", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme2/bg.gif", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/810507927941476352/cSydClet_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/810507927941476352/cSydClet_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/118055879/1488791887", "profile_link_color" : "1F98C7", "profile_sidebar_border_color" : "C6E2EE", "profile_sidebar_fill_color" : "DAECF4", "profile_text_color" : "663B12", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 1, "favorite_count" : 2, "favorited" : false, "retweeted" : false, "lang" : "en" }, "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" }, "is_quote_status" : true, "quoted_status_id" : 847671175081676801, "quoted_status_id_str" : "847671175081676801", "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : true, "lang" : "en" } """; const string MENTION_TWEET_DATA = """ { "created_at" : "Sat May 09 18:07:54 +0000 2020", "id" : 1259183327229116416, "id_str" : "1259183327229116416", "full_text" : ".@DrJaninaRamirez's #artdetective has become the soundtrack for lockdown. #geek 🤓 #AlwaysAnArtHistoryGraduate https://t.co/nb9kBDSEfO", "truncated" : false, "display_text_range" : [ 0, 109 ], "entities" : { "hashtags" : [ { "text" : "artdetective", "indices" : [ 20, 33 ] }, { "text" : "geek", "indices" : [ 74, 79 ] }, { "text" : "AlwaysAnArtHistoryGraduate", "indices" : [ 82, 109 ] } ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "DrJaninaRamirez", "name" : "Dr Janina Ramirez", "id" : 425339936, "id_str" : "425339936", "indices" : [ 1, 17 ] } ], "urls" : [ ], "media" : [ { "id" : 1259183317749989376, "id_str" : "1259183317749989376", "indices" : [ 110, 133 ], "media_url" : "http://pbs.twimg.com/tweet_video_thumb/EXmEjlmWAAANLYr.jpg", "media_url_https" : "https://pbs.twimg.com/tweet_video_thumb/EXmEjlmWAAANLYr.jpg", "url" : "https://t.co/nb9kBDSEfO", "display_url" : "pic.twitter.com/nb9kBDSEfO", "expanded_url" : "https://twitter.com/HannahECarroll/status/1259183327229116416/photo/1", "type" : "photo", "sizes" : { "small" : { "w" : 320, "h" : 488, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 320, "h" : 488, "resize" : "fit" }, "medium" : { "w" : 320, "h" : 488, "resize" : "fit" } } } ] }, "extended_entities" : { "media" : [ { "id" : 1259183317749989376, "id_str" : "1259183317749989376", "indices" : [ 110, 133 ], "media_url" : "http://pbs.twimg.com/tweet_video_thumb/EXmEjlmWAAANLYr.jpg", "media_url_https" : "https://pbs.twimg.com/tweet_video_thumb/EXmEjlmWAAANLYr.jpg", "url" : "https://t.co/nb9kBDSEfO", "display_url" : "pic.twitter.com/nb9kBDSEfO", "expanded_url" : "https://twitter.com/HannahECarroll/status/1259183327229116416/photo/1", "type" : "animated_gif", "sizes" : { "small" : { "w" : 320, "h" : 488, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "large" : { "w" : 320, "h" : 488, "resize" : "fit" }, "medium" : { "w" : 320, "h" : 488, "resize" : "fit" } }, "video_info" : { "aspect_ratio" : [ 40, 61 ], "variants" : [ { "bitrate" : 0, "content_type" : "video/mp4", "url" : "https://video.twimg.com/tweet_video/EXmEjlmWAAANLYr.mp4" } ] } } ] }, "source" : "Twitter for Android", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 334713923, "id_str" : "334713923", "name" : "Hannah Carroll", "screen_name" : "HannahECarroll", "location" : "Birmingham, England", "description" : "Marketing Executive (Projects) @brumhippodrome Tweets about theatre, books, films, art and MarComms. Not a whole lot about food. All my views.", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 2006, "friends_count" : 4384, "listed_count" : 53, "created_at" : "Wed Jul 13 15:11:06 +0000 2011", "favourites_count" : 2307, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 5059, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "ACDED6", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme18/bg.gif", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme18/bg.gif", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/1231642501657874432/sDgwVzoV_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1231642501657874432/sDgwVzoV_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/334713923/1582481429", "profile_link_color" : "038543", "profile_sidebar_border_color" : "EEEEEE", "profile_sidebar_fill_color" : "F6F6F6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 3, "favorite_count" : 22, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "possibly_sensitive_appealable" : false, "lang" : "en" } """; const string REPLY_TWEET_DATA = """ { "created_at" : "Mon Apr 17 15:16:18 +0000 2017", "id" : 853990508326252550, "id_str" : "853990508326252550", "full_text" : "@jjdesmond @_UBRAS_ @franalsworth @4Apes @katy4apes @theAliceRoberts @JaneGoodallUK @Jane_Goodall @JaneGoodallInst And here's the link for tickets again ... https://t.co/a9lOVMouNK", "truncated" : false, "display_text_range" : [ 115, 180 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "jjdesmond", "name" : "Jimmy Jenny Desmond", "id" : 21278482, "id_str" : "21278482", "indices" : [ 0, 10 ] }, { "screen_name" : "_UBRAS_", "name" : "Roots and Shoots UOB", "id" : 803329927974096896, "id_str" : "803329927974096896", "indices" : [ 11, 19 ] }, { "screen_name" : "franalsworth", "name" : "Fran", "id" : 776983919287754752, "id_str" : "776983919287754752", "indices" : [ 20, 33 ] }, { "screen_name" : "4Apes", "name" : "Ian Redmond", "id" : 155889035, "id_str" : "155889035", "indices" : [ 34, 40 ] }, { "screen_name" : "katy4apes", "name" : "Katy Jedamzik", "id" : 159608654, "id_str" : "159608654", "indices" : [ 41, 51 ] }, { "screen_name" : "theAliceRoberts", "name" : "Prof Alice Roberts", "id" : 260211154, "id_str" : "260211154", "indices" : [ 52, 68 ] }, { "screen_name" : "JaneGoodallUK", "name" : "Roots & Shoots UK", "id" : 423423823, "id_str" : "423423823", "indices" : [ 69, 83 ] }, { "screen_name" : "Jane_Goodall", "name" : "Jane Goodall", "id" : 235157216, "id_str" : "235157216", "indices" : [ 84, 97 ] }, { "screen_name" : "JaneGoodallInst", "name" : "JaneGoodallInstitute", "id" : 39822897, "id_str" : "39822897", "indices" : [ 98, 114 ] } ], "urls" : [ { "url" : "https://t.co/a9lOVMouNK", "expanded_url" : "https://www.eventbrite.com/e/working-with-apes-tickets-33089771397", "display_url" : "eventbrite.com/e/working-with…", "indices" : [ 157, 180 ] } ] }, "source" : "Twitter for iPhone", "in_reply_to_status_id" : 853925036696141824, "in_reply_to_status_id_str" : "853925036696141824", "in_reply_to_user_id" : 21278482, "in_reply_to_user_id_str" : "21278482", "in_reply_to_screen_name" : "jjdesmond", "user" : { "id" : 415472140, "id_str" : "415472140", "name" : "Ben Garrod", "screen_name" : "Ben_garrod", "location" : "Bristol&Norfolk", "description" : "Monkey-chaser, TV-talker, bone geek and Teaching Fellow at @AngliaRuskin https://t.co/FXbftdxxTJ", "url" : "https://t.co/1B9SDHfWoF", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/1B9SDHfWoF", "expanded_url" : "http://www.josarsby.com/ben-garrod", "display_url" : "josarsby.com/ben-garrod", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ { "url" : "https://t.co/FXbftdxxTJ", "expanded_url" : "http://www.anglia.ac.uk/science-and-technology/about/life-sciences/our-staff/ben-garrod", "display_url" : "anglia.ac.uk/science-and-te…", "indices" : [ 73, 96 ] } ] } }, "protected" : false, "followers_count" : 6526, "friends_count" : 1016, "listed_count" : 128, "created_at" : "Fri Nov 18 11:30:48 +0000 2011", "favourites_count" : 25292, "utc_offset" : 3600, "time_zone" : "London", "geo_enabled" : true, "verified" : true, "statuses_count" : 17224, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_image_url_https" : "https://pbs.twimg.com/profile_background_images/590945579024257024/2F1itaGz.jpg", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/615498558385557505/cwSloac3_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/415472140/1477223840", "profile_link_color" : "0084B4", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 6, "favorite_count" : 7, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; const string TD7 = """ { "created_at" : "Mon Feb 13 07:59:35 +0000 2017", "id" : 831050171744149504, "id_str" : "831050171744149504", "full_text" : "@GOP https://t.co/e2fTVgNC3D", "truncated" : false, "display_text_range" : [ 4, 4 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "GOP", "name" : "GOP", "id" : 11134252, "id_str" : "11134252", "indices" : [ 0, 4 ] } ], "urls" : [ ], "media" : [ { "id" : 831050167918923776, "id_str" : "831050167918923776", "indices" : [ 5, 28 ], "media_url" : "http://pbs.twimg.com/media/C4h7uYFWEAA1Af6.jpg", "media_url_https" : "https://pbs.twimg.com/media/C4h7uYFWEAA1Af6.jpg", "url" : "https://t.co/e2fTVgNC3D", "display_url" : "pic.twitter.com/e2fTVgNC3D", "expanded_url" : "https://twitter.com/CBoyForeman/status/831050171744149504/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 576, "h" : 357, "resize" : "fit" }, "small" : { "w" : 576, "h" : 357, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "medium" : { "w" : 576, "h" : 357, "resize" : "fit" } } } ] }, "extended_entities" : { "media" : [ { "id" : 831050167918923776, "id_str" : "831050167918923776", "indices" : [ 5, 28 ], "media_url" : "http://pbs.twimg.com/media/C4h7uYFWEAA1Af6.jpg", "media_url_https" : "https://pbs.twimg.com/media/C4h7uYFWEAA1Af6.jpg", "url" : "https://t.co/e2fTVgNC3D", "display_url" : "pic.twitter.com/e2fTVgNC3D", "expanded_url" : "https://twitter.com/CBoyForeman/status/831050171744149504/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 576, "h" : 357, "resize" : "fit" }, "small" : { "w" : 576, "h" : 357, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "medium" : { "w" : 576, "h" : 357, "resize" : "fit" } } } ] }, "source" : "Twitter for Android", "in_reply_to_status_id" : 830800137291194368, "in_reply_to_status_id_str" : "830800137291194368", "in_reply_to_user_id" : 11134252, "in_reply_to_user_id_str" : "11134252", "in_reply_to_screen_name" : "GOP", "user" : { "id" : 77301204, "id_str" : "77301204", "name" : "Chrissy Boy", "screen_name" : "CBoyForeman", "location" : "Brighton", "description" : "Musician, allegedly.", "url" : "https://t.co/wkTvkNumcF", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/wkTvkNumcF", "expanded_url" : "http://www.madness.co.uk/chris-cupboard/", "display_url" : "madness.co.uk/chris-cupboard/", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 8605, "friends_count" : 42, "listed_count" : 54, "created_at" : "Fri Sep 25 20:19:11 +0000 2009", "favourites_count" : 71, "utc_offset" : 3600, "time_zone" : "London", "geo_enabled" : true, "verified" : false, "statuses_count" : 1079, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "95B6C7", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/542792449907638272/AsxM39Oz_normal.jpeg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/542792449907638272/AsxM39Oz_normal.jpeg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/77301204/1387963265", "profile_link_color" : "89C9FA", "profile_sidebar_border_color" : "EEEEEE", "profile_sidebar_fill_color" : "EFEFEF", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 2, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" } """; const string TD8 = """ { "created_at" : "Mon Apr 24 11:40:53 +0000 2017", "id" : 856473014560591872, "id_str" : "856473014560591872", "full_text" : "RT @cawbirdgtk: @baedert and @cawbirdclient so?", "truncated" : false, "display_text_range" : [ 0, 49 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "cawbirdgtk", "name" : "Z??!@*(&*²³¤²³¤", "id" : 993713617, "id_str" : "993713617", "indices" : [ 3, 15 ] }, { "screen_name" : "baedert", "name" : "Schupp & Wupp", "id" : 118055879, "id_str" : "118055879", "indices" : [ 17, 25 ] }, { "screen_name" : "cawbirdclient", "name" : "Cawbird", "id" : 2877682863, "id_str" : "2877682863", "indices" : [ 30, 45 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Z??!@*(&*²³¤²³¤", "screen_name" : "cawbirdgtk", "location" : "", "description" : "<asdf", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 3, "friends_count" : 4, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 9, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 909, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/993713617/1413106147", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "retweeted_status" : { "created_at" : "Mon Apr 24 11:35:17 +0000 2017", "id" : 856471602883686400, "id_str" : "856471602883686400", "full_text" : "@baedert and @cawbirdclient so?", "truncated" : false, "display_text_range" : [ 9, 32 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "baedert", "name" : "Schupp & Wupp", "id" : 118055879, "id_str" : "118055879", "indices" : [ 0, 8 ] }, { "screen_name" : "cawbirdclient", "name" : "Cawbird", "id" : 2877682863, "id_str" : "2877682863", "indices" : [ 13, 28 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : 853198126106148864, "in_reply_to_status_id_str" : "853198126106148864", "in_reply_to_user_id" : 993713617, "in_reply_to_user_id_str" : "993713617", "in_reply_to_screen_name" : "cawbirdgtk", "user" : { "id" : 993713617, "id_str" : "993713617", "name" : "Z??!@*(&*²³¤²³¤", "screen_name" : "cawbirdgtk", "location" : "", "description" : "<asdf", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : true, "followers_count" : 3, "friends_count" : 4, "listed_count" : 0, "created_at" : "Thu Dec 06 19:47:16 +0000 2012", "favourites_count" : 9, "utc_offset" : 7200, "time_zone" : "Amsterdam", "geo_enabled" : false, "verified" : false, "statuses_count" : 909, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/655270693341417472/h6BbZKJy_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/993713617/1413106147", "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" }, "is_quote_status" : false, "retweet_count" : 1, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string TD9 = """ { "created_at" : "Thu Apr 27 17:00:15 +0000 2017", "id" : 857640546793177088, "id_str" : "857640546793177088", "full_text" : "Hey @zillow, I get why you make it so hard to close my profile, but it's so transparent and just seems scammy. Scammy, scammy, scammy.", "truncated" : false, "display_text_range" : [ 0, 134 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "zillow", "name" : "Zillow", "id" : 19262500, "id_str" : "19262500", "indices" : [ 4, 11 ] } ], "urls" : [ ] }, "source" : "Twitter Web Client", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 14498432, "id_str" : "14498432", "name" : "Kristian Høgsberg", "screen_name" : "hoegsberg", "location" : "Portland OR", "description" : "Casually Defiant, Inadvertently Pretentious", "url" : "https://t.co/z672MnpkDP", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/z672MnpkDP", "expanded_url" : "http://hoegsberg.blogspot.com", "display_url" : "hoegsberg.blogspot.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 689, "friends_count" : 210, "listed_count" : 25, "created_at" : "Wed Apr 23 18:29:05 +0000 2008", "favourites_count" : 717, "utc_offset" : -14400, "time_zone" : "Eastern Time (US & Canada)", "geo_enabled" : true, "verified" : false, "statuses_count" : 2685, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "352726", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme5/bg.gif", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme5/bg.gif", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/679383568016130049/DcwLkGhB_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/679383568016130049/DcwLkGhB_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/14498432/1450812639", "profile_link_color" : "D02B54", "profile_sidebar_border_color" : "829D5E", "profile_sidebar_fill_color" : "99CC33", "profile_text_color" : "3E4415", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "lang" : "en" } """; const string TD10 = """ { "created_at" : "Fri Apr 28 19:40:04 +0000 2017", "id" : 858043155547140096, "id_str" : "858043155547140096", "full_text" : ".@archillect\nhttps://t.co/8sOpx6hIV8\nhttps://t.co/LazmRudPfL\nhttps://t.co/Q2C5FM0OJO\nhttps://t.co/Vq6aJdD766\nhttps://t.co/otr3LIJ15c", "truncated" : false, "display_text_range" : [ 0, 132 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ { "screen_name" : "archillect", "name" : "Archillect", "id" : 2907774137, "id_str" : "2907774137", "indices" : [ 1, 12 ] } ], "urls" : [ { "url" : "https://t.co/8sOpx6hIV8", "expanded_url" : "http://www.pbs.org/newshour/art/photos-dying-art-neon-hong-kong/", "display_url" : "pbs.org/newshour/art/p…", "indices" : [ 13, 36 ] }, { "url" : "https://t.co/LazmRudPfL", "expanded_url" : "http://www.neonsigns.hk/media/upload/2014/05/shya-5.jpg", "display_url" : "neonsigns.hk/media/upload/2…", "indices" : [ 37, 60 ] }, { "url" : "https://t.co/Q2C5FM0OJO", "expanded_url" : "http://t.umblr.com/redirect?z=http%3A%2F%2Fwww.neonsigns.hk%2Fmy-neon-city%2Fwing-shya%2F%3Flang%3Den&t=YmUzYTFmNTM1NmJjODlkYjlkNGJiMzYyNzRjZmU3OTllOGZhZmIwYyw5ODIzODIzNzg5NQ%3D%3D&b=t%3AYGx1bpY-ynhca0q3r41odw&p=http%3A%2F%2Fotakugangsta.com%2Fpost%2F98238237895&m=0", "display_url" : "t.umblr.com/redirect?z=htt…", "indices" : [ 61, 84 ] }, { "url" : "https://t.co/Vq6aJdD766", "expanded_url" : "https://www.pinterest.com/pin/47569339794350377/", "display_url" : "pinterest.com/pin/4756933979…", "indices" : [ 85, 108 ] }, { "url" : "https://t.co/otr3LIJ15c", "expanded_url" : "https://twitter.com/archillect/status/858043152820826113", "display_url" : "twitter.com/archillect/sta…", "indices" : [ 109, 132 ] } ] }, "source" : "archillinks", "in_reply_to_status_id" : 858043152820826113, "in_reply_to_status_id_str" : "858043152820826113", "in_reply_to_user_id" : 2907774137, "in_reply_to_user_id_str" : "2907774137", "in_reply_to_screen_name" : "archillect", "user" : { "id" : 808595043560857600, "id_str" : "808595043560857600", "name" : "Archillect Links", "screen_name" : "archillinks", "location" : "", "description" : "The Ocular Engine // Links", "url" : "https://t.co/U7Oh7xumQp", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/U7Oh7xumQp", "expanded_url" : "http://archillect.com", "display_url" : "archillect.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 9481, "friends_count" : 2, "listed_count" : 107, "created_at" : "Tue Dec 13 08:50:56 +0000 2016", "favourites_count" : 0, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 17980, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "000000", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/808595805942714368/PuH1r_aQ_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/808595805942714368/PuH1r_aQ_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/808595043560857600/1481619199", "profile_link_color" : "111111", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : true, "quoted_status_id" : 858043152820826113, "quoted_status_id_str" : "858043152820826113", "quoted_status" : { "created_at" : "Fri Apr 28 19:40:03 +0000 2017", "id" : 858043152820826113, "id_str" : "858043152820826113", "full_text" : "https://t.co/4Kk2ol4YDf", "truncated" : false, "display_text_range" : [ 0, 0 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 858043148114808833, "id_str" : "858043148114808833", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/C-hhsnVXUAESrBA.jpg", "media_url_https" : "https://pbs.twimg.com/media/C-hhsnVXUAESrBA.jpg", "url" : "https://t.co/4Kk2ol4YDf", "display_url" : "pic.twitter.com/4Kk2ol4YDf", "expanded_url" : "https://twitter.com/archillect/status/858043152820826113/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 1280, "h" : 960, "resize" : "fit" }, "small" : { "w" : 680, "h" : 510, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 900, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 858043148114808833, "id_str" : "858043148114808833", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/C-hhsnVXUAESrBA.jpg", "media_url_https" : "https://pbs.twimg.com/media/C-hhsnVXUAESrBA.jpg", "url" : "https://t.co/4Kk2ol4YDf", "display_url" : "pic.twitter.com/4Kk2ol4YDf", "expanded_url" : "https://twitter.com/archillect/status/858043152820826113/photo/1", "type" : "photo", "sizes" : { "large" : { "w" : 1280, "h" : 960, "resize" : "fit" }, "small" : { "w" : 680, "h" : 510, "resize" : "fit" }, "medium" : { "w" : 1200, "h" : 900, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "source" : "Archillect", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 2907774137, "id_str" : "2907774137", "name" : "Archillect", "screen_name" : "archillect", "location" : "Meta", "description" : "The ocular engine. Sources: @archillinks", "url" : "https://t.co/U7Oh7xumQp", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/U7Oh7xumQp", "expanded_url" : "http://archillect.com", "display_url" : "archillect.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 332778, "friends_count" : 1, "listed_count" : 4168, "created_at" : "Sat Dec 06 14:07:45 +0000 2014", "favourites_count" : 10461, "utc_offset" : 10800, "time_zone" : "Istanbul", "geo_enabled" : false, "verified" : false, "statuses_count" : 119611, "lang" : "en", "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "050505", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme19/bg.gif", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme19/bg.gif", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/542430526057881602/mjWPm56H_normal.png", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/542430526057881602/mjWPm56H_normal.png", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/2907774137/1418102176", "profile_link_color" : "191919", "profile_sidebar_border_color" : "000000", "profile_sidebar_fill_color" : "000000", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 48, "favorite_count" : 68, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" }, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" } """; const string TWEET_MEDIA_ALT_TEXT = """ { "created_at" : "Tue Apr 07 15:49:32 +0000 2020", "id" : 1247552095441629184, "id_str" : "1247552095441629184", "full_text" : "https://t.co/oJfMRChWEl", "truncated" : false, "display_text_range" : [ 0, 0 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 1247552036507443200, "id_str" : "1247552036507443200", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/EVAx96bXYAAayMR.png", "media_url_https" : "https://pbs.twimg.com/media/EVAx96bXYAAayMR.png", "url" : "https://t.co/oJfMRChWEl", "display_url" : "pic.twitter.com/oJfMRChWEl", "expanded_url" : "https://twitter.com/IBBTwtr/status/1247552095441629184/photo/1", "type" : "photo", "sizes" : { "small" : { "w" : 492, "h" : 680, "resize" : "fit" }, "large" : { "w" : 595, "h" : 823, "resize" : "fit" }, "medium" : { "w" : 595, "h" : 823, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } } } ] }, "extended_entities" : { "media" : [ { "id" : 1247552036507443200, "id_str" : "1247552036507443200", "indices" : [ 0, 23 ], "media_url" : "http://pbs.twimg.com/media/EVAx96bXYAAayMR.png", "media_url_https" : "https://pbs.twimg.com/media/EVAx96bXYAAayMR.png", "url" : "https://t.co/oJfMRChWEl", "display_url" : "pic.twitter.com/oJfMRChWEl", "expanded_url" : "https://twitter.com/IBBTwtr/status/1247552095441629184/photo/1", "type" : "photo", "sizes" : { "small" : { "w" : 492, "h" : 680, "resize" : "fit" }, "large" : { "w" : 595, "h" : 823, "resize" : "fit" }, "medium" : { "w" : 595, "h" : 823, "resize" : "fit" }, "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" } }, "ext_alt_text" : "Cawbird in dark theme" } ] }, "source" : "Twitter Web App", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 194913600, "id_str" : "194913600", "name" : "\"Test & Account\"", "screen_name" : "IBBTwtr", "location" : "", "description" : "IBBoard's test account for sending test messages to without disturbing people. THIS ACCOUNT WILL NEVER POST ANYTHING INTERESTING! May be used as a spam trap.", "url" : null, "entities" : { "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 2, "friends_count" : 10, "listed_count" : 0, "created_at" : "Sat Sep 25 09:06:35 +0000 2010", "favourites_count" : 1, "utc_offset" : null, "time_zone" : null, "geo_enabled" : false, "verified" : false, "statuses_count" : 125, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : false, "profile_background_color" : "C0DEED", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : false, "profile_image_url" : "http://pbs.twimg.com/profile_images/853335069934669831/k5Y-rjee_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/853335069934669831/k5Y-rjee_normal.jpg", "profile_image_extensions_alt_text" : null, "profile_link_color" : "1DA1F2", "profile_sidebar_border_color" : "C0DEED", "profile_sidebar_fill_color" : "DDEEF6", "profile_text_color" : "333333", "profile_use_background_image" : true, "has_extended_profile" : false, "default_profile" : true, "default_profile_image" : false, "following" : true, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 0, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "und" } """; const string TWEET_MEDIA_ADDITIONAL_INFO_ALT_TEXT = """ { "created_at" : "Thu Aug 06 22:00:01 +0000 2020", "id" : 1291494261444284416, "id_str" : "1291494261444284416", "full_text" : "African designers are stepping in to turn face masks into a fashion statement. https://t.co/oOM55CLO5c", "truncated" : false, "display_text_range" : [ 0, 78 ], "entities" : { "hashtags" : [ ], "symbols" : [ ], "user_mentions" : [ ], "urls" : [ ], "media" : [ { "id" : 1286328013446152194, "id_str" : "1286328013446152194", "indices" : [ 79, 102 ], "media_url" : "http://pbs.twimg.com/media/Edn1pu-WAAMMHPk.jpg", "media_url_https" : "https://pbs.twimg.com/media/Edn1pu-WAAMMHPk.jpg", "url" : "https://t.co/oOM55CLO5c", "display_url" : "pic.twitter.com/oOM55CLO5c", "expanded_url" : "https://twitter.com/AJEnglish/status/1291494261444284416/video/1", "type" : "photo", "sizes" : { "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 680, "resize" : "fit" }, "medium" : { "w" : 1080, "h" : 1080, "resize" : "fit" }, "large" : { "w" : 1080, "h" : 1080, "resize" : "fit" } } } ] }, "extended_entities" : { "media" : [ { "id" : 1286328013446152194, "id_str" : "1286328013446152194", "indices" : [ 79, 102 ], "media_url" : "http://pbs.twimg.com/media/Edn1pu-WAAMMHPk.jpg", "media_url_https" : "https://pbs.twimg.com/media/Edn1pu-WAAMMHPk.jpg", "url" : "https://t.co/oOM55CLO5c", "display_url" : "pic.twitter.com/oOM55CLO5c", "expanded_url" : "https://twitter.com/AJEnglish/status/1291494261444284416/video/1", "type" : "video", "sizes" : { "thumb" : { "w" : 150, "h" : 150, "resize" : "crop" }, "small" : { "w" : 680, "h" : 680, "resize" : "fit" }, "medium" : { "w" : 1080, "h" : 1080, "resize" : "fit" }, "large" : { "w" : 1080, "h" : 1080, "resize" : "fit" } }, "video_info" : { "aspect_ratio" : [ 1, 1 ], "duration_millis" : 111320, "variants" : [ { "content_type" : "application/x-mpegURL", "url" : "https://video.twimg.com/amplify_video/1286328013446152194/pl/swf_ETvJZHSMKdZR.m3u8?tag=13" }, { "bitrate" : 1280000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/1286328013446152194/vid/720x720/W3q_MM3yoGVCz6bq.mp4?tag=13" }, { "bitrate" : 432000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/1286328013446152194/vid/320x320/UNLrWI0aGWBb1zlf.mp4?tag=13" }, { "bitrate" : 832000, "content_type" : "video/mp4", "url" : "https://video.twimg.com/amplify_video/1286328013446152194/vid/480x480/cPvyysKFbXYMDzMt.mp4?tag=13" } ] }, "ext_alt_text" : null, "additional_media_info" : { "title" : "Who says face masks have to be boring?", "description" : "More on Aljazeera.com", "embeddable" : true, "monetizable" : true } } ] }, "source" : "Twitter Media Studio", "in_reply_to_status_id" : null, "in_reply_to_status_id_str" : null, "in_reply_to_user_id" : null, "in_reply_to_user_id_str" : null, "in_reply_to_screen_name" : null, "user" : { "id" : 4970411, "id_str" : "4970411", "name" : "Al Jazeera English", "screen_name" : "AJEnglish", "location" : "Doha, Qatar", "description" : "Hear the human story and join the discussion. For more news follow @AJENews.", "url" : "https://t.co/D38RZjLnhI", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/D38RZjLnhI", "expanded_url" : "http://aljazeera.com", "display_url" : "aljazeera.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [ ] } }, "protected" : false, "followers_count" : 6330005, "friends_count" : 215, "listed_count" : 51127, "created_at" : "Tue Apr 17 08:23:08 +0000 2007", "favourites_count" : 6271, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : true, "statuses_count" : 257128, "lang" : null, "contributors_enabled" : false, "is_translator" : false, "is_translation_enabled" : true, "profile_background_color" : "FFFFFF", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/1190503555901394944/T4qowO0X_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/1190503555901394944/T4qowO0X_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/4970411/1592932198", "profile_image_extensions_alt_text" : null, "profile_banner_extensions_alt_text" : null, "profile_link_color" : "234D8C", "profile_sidebar_border_color" : "FFFFFF", "profile_sidebar_fill_color" : "DDDDDD", "profile_text_color" : "000000", "profile_use_background_image" : false, "has_extended_profile" : false, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "none" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 139, "favorite_count" : 346, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; const string BUG305_DATA = """ { "created_at" : "Tue Jan 26 13:11:14 +0000 2021", "id" : 1354054288310497280, "id_str" : "1354054288310497280", "full_text" : "... Done: https://t.co/H5aIluvWfC", "truncated" : false, "display_text_range" : [ 0, 33 ], "entities" : { "hashtags" : [], "symbols" : [], "user_mentions" : [], "urls" : [ { "url" : "https://t.co/H5aIluvWfC", "expanded_url" : "https://en.wikipedia.org/w/index.php?title=Ehud_Manor&type=revision&diff=1002880213&oldid=1000831694&diffmode=visual", "display_url" : "en.wikipedia.org/w/index.php?ti…", "indices" : [ 10, 33 ] } ] }, "source" : "Twitter Web App", "in_reply_to_status_id" : 1353947764015689728, "in_reply_to_status_id_str" : "1353947764015689728", "in_reply_to_user_id" : 14172581, "in_reply_to_user_id_str" : "14172581", "in_reply_to_screen_name" : "aharoni", "user" : { "id" : 14172581, "id_str" : "14172581", "name" : "Amir 💉. Aharoni", "screen_name" : "aharoni", "location" : "Jerusalem, Israel", "description" : "Most people don’t know English · ਸਹੀ ਰਸਤਾ ਵਖਰੇ ਲੋਕਾਂ ਲਈ ਵਖਰਾ ਹੈ। ਆਪਾਂ à¨à¨•ਤਾ ਵਿਚ ਰਹਿà¨, ਜੈ ਜੈ। · Moscow, Haifa, Jerusalem, Music, Languages, Wikipedia · בלשן ובשלן", "url" : "https://t.co/kOtJr0nvvs", "entities" : { "url" : { "urls" : [ { "url" : "https://t.co/kOtJr0nvvs", "expanded_url" : "http://aharoni.wordpress.com/", "display_url" : "aharoni.wordpress.com", "indices" : [ 0, 23 ] } ] }, "description" : { "urls" : [] } }, "protected" : false, "followers_count" : 3116, "friends_count" : 4583, "listed_count" : 76, "created_at" : "Tue Mar 18 21:08:31 +0000 2008", "favourites_count" : 9284, "utc_offset" : null, "time_zone" : null, "geo_enabled" : true, "verified" : false, "statuses_count" : 20838, "lang" : null, "contributors_enabled" : false, "is_translator" : true, "is_translation_enabled" : false, "profile_background_color" : "3B2E32", "profile_background_image_url" : "http://abs.twimg.com/images/themes/theme11/bg.gif", "profile_background_image_url_https" : "https://abs.twimg.com/images/themes/theme11/bg.gif", "profile_background_tile" : true, "profile_image_url" : "http://pbs.twimg.com/profile_images/51865575/amir-piano_normal.jpg", "profile_image_url_https" : "https://pbs.twimg.com/profile_images/51865575/amir-piano_normal.jpg", "profile_banner_url" : "https://pbs.twimg.com/profile_banners/14172581/1455356551", "profile_image_extensions_alt_text" : null, "profile_banner_extensions_alt_text" : null, "profile_link_color" : "0D4FB3", "profile_sidebar_border_color" : "2E2428", "profile_sidebar_fill_color" : "261B1E", "profile_text_color" : "FFE4D9", "profile_use_background_image" : true, "has_extended_profile" : true, "default_profile" : false, "default_profile_image" : false, "following" : false, "follow_request_sent" : false, "notifications" : false, "translator_type" : "moderator" }, "geo" : null, "coordinates" : null, "place" : null, "contributors" : null, "is_quote_status" : false, "retweet_count" : 0, "favorite_count" : 4, "favorited" : false, "retweeted" : false, "possibly_sensitive" : false, "lang" : "en" } """; // """ // }}} void retweet () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD1); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert (t.id == 463208606784311296); assert (t.retweeted_tweet != null); assert (t.retweeted_tweet.id == 461097667775725569); assert (t.source_tweet.author.user_name == "Frozenbyte"); assert (t.favorite_count == 0); assert (t.retweet_count == 6); assert (t.source_tweet.author.id == 62574927); assert (t.get_mentions ().length == 0); assert (t.retweeted_tweet.author.user_name == "Black Forest Games"); assert (!t.is_flag_set (Cb.TweetState.FAVORITED)); assert (!t.is_flag_set (Cb.TweetState.RETWEETED)); assert (!t.is_flag_set (Cb.TweetState.VERIFIED)); assert (t.source_tweet.reply_id == 0); assert (t.my_retweet == 0); //assert (!t.has_inline_media); } void media_count () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD3); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert (t.get_screen_name () == "cawbirdgtk"); assert (t.get_medias ().length == 2); } void double_media () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD5); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); // See https://github.com/baedert/corebird/issues/627 // If this was still a problem, the following call would make this test crash t.get_filter_text (); } void special_quote () { /* This is retweet of a tweet that has media attached, but also contains a URL to another tweet. * It's *not* a quote tweet, but the retweeted_status field contains a tweet with a quoted_status * tweet object attached. * * The media is attached to both the outmost tweet object (the retweet) as well as the retweeted_tweet * object (the retweeted tweet). */ var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD6); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); /* The expected output here is that the tweet is still a retweet, of course... */ assert (t.retweeted_tweet != null); /* ... but we now handle media AND quotes, so we should see the quoted tweet as well */ assert (t.quoted_tweet != null); /* ... I repeat: the retweeted tweet has media attached! */ assert (t.retweeted_tweet.medias.length > 0); } void reply_users () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (REPLY_TWEET_DATA); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); var reply_users = t.get_reply_users (); assert (reply_users.length == 9); /* try { parser.load_from_data (MENTION_TWEET_DATA); } catch (GLib.Error e) { critical (e.message); } root = parser.get_root (); t.load_from_json (root, 0, now); // Dr J is mentioned but isn't in the "reply to" list reply_users = t.get_reply_users (); assert (reply_users.length == 0); */ } void mentions () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (REPLY_TWEET_DATA); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); var mentions = t.get_mentions (); assert (mentions.length == 9); string[] names = {"jjdesmond", "_UBRAS_", "franalsworth", "4Apes", "katy4apes", "theAliceRoberts", "JaneGoodallUK", "Jane_Goodall", "JaneGoodallInst"}; foreach (var name in names) { assert (name in mentions); } try { parser.load_from_data (MENTION_TWEET_DATA); } catch (GLib.Error e) { critical (e.message); } root = parser.get_root (); t.load_from_json (root, 0, now); mentions = t.get_mentions (); assert (mentions.length == 1); debug(mentions[0]); assert (mentions[0] == "DrJaninaRamirez"); } void empty_display_range () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD7); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); // This should NOT break. var s = t.get_filter_text (); assert (s.length > 0); } void rt_reply () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD8); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); /* * The loaded tweet is a retweet of a tweet that replies * to 2 users (namely @cawbirdgtk and @baedert). * @baedert is mentioned directly in the tweet text, before the display_range_start * and @cawbirdgtk as the in_reply_to_screen_name embedded in the retweeted tweet. * * The result we want is that the CbTweet is a reply to all the * users the retweeted tweet was a reply to, so both @cawbirdgtk and @baedert. */ assert (t.retweeted_tweet.reply_users.length == 2); /* It's a direct reply to @cawbirdgtk, so that should be first */ assert (t.retweeted_tweet.reply_users[0].screen_name == "cawbirdgtk"); assert (t.retweeted_tweet.reply_users[1].screen_name == "baedert"); /* Consequently, the ones returned by get_reply_users should be the same. */ var reply_users = t.get_reply_users (); assert (reply_users.length == 2); assert (reply_users[0].screen_name == "cawbirdgtk"); assert (reply_users[1].screen_name == "baedert"); } void entity_count () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD9); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert (t.source_tweet.entities.length == 1); } void reply_id () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TD10); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); /* The source tweet is a reply, but the quoted one isn't */ assert (t.source_tweet.reply_id == 858043152820826113); assert (t.quoted_tweet != null); assert (t.quoted_tweet.reply_id == 0); /* source_tweet is a reply, so should definitely have >0 reply_users. * This is interesting for this tweet since the tweet text contains * a user mention, but it's part of the tweet text and not before * the display_range_start. */ assert (t.source_tweet.reply_users.length == 1); /* Sanity check: The source_tweet's user mention should still end up being * a text entity of course. */ assert (t.source_tweet.entities.length > 0); /* It's the first one. I'm sure. */ assert (t.source_tweet.entities[0].display_text == "@archillect"); } void media_alt_text () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TWEET_MEDIA_ALT_TEXT); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert (t.get_medias ().length == 1); assert (t.get_medias ()[0].alt_text == "Cawbird in dark theme"); } void media_additional_info_alt_text () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (TWEET_MEDIA_ADDITIONAL_INFO_ALT_TEXT); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert (t.get_medias ().length == 1); debug(t.get_medias ()[0].alt_text); assert (t.get_medias ()[0].alt_text == "Who says face masks have to be boring?\n\nMore on Aljazeera.com"); } void bug305_double_encoded_url () { var now = new GLib.DateTime.now_local (); var t = new Cb.Tweet (); var parser = new Json.Parser (); try { parser.load_from_data (BUG305_DATA); } catch (GLib.Error e) { critical (e.message); } var root = parser.get_root (); t.load_from_json (root, 0, now); assert(t.source_tweet.entities.length == 1); debug(t.source_tweet.entities[0].target); // Targets should be pre-escaped so that we don't have to remember to keep doing it while creating markup assert(t.source_tweet.entities[0].target == "https://en.wikipedia.org/w/index.php?title=Ehud_Manor&type=revision&diff=1002880213&oldid=1000831694&diffmode=visual"); } int main (string[] args) { GLib.Test.init (ref args); Settings.init (); Gtk.init (ref args); Twitter.get ().init (); Utils.init_soup_session (); // Parsing tweets will otherwise cause media to be downloaded which is an async operation // so it might spuriously make these tests fail. Also, we don't care about media here. Cb.MediaDownloader.get_default ().disable (); GLib.Test.add_func ("/tweet-parsing/retweet", retweet); GLib.Test.add_func ("/tweet-parsing/media-count", media_count); GLib.Test.add_func ("/tweet-parsing/double-media", double_media); GLib.Test.add_func ("/tweet-parsing/special-quote", special_quote); GLib.Test.add_func ("/tweet-parsing/reply-users", reply_users); GLib.Test.add_func ("/tweet-parsing/mentions", mentions); GLib.Test.add_func ("/tweet-parsing/empty-display-range", empty_display_range); GLib.Test.add_func ("/tweet-parsing/rt-reply", rt_reply); GLib.Test.add_func ("/tweet-parsing/entity-count", entity_count); GLib.Test.add_func ("/tweet-parsing/reply-id", reply_id); GLib.Test.add_func ("/tweet-parsing/media-alt-text", media_alt_text); GLib.Test.add_func ("/tweet-parsing/media-additional-info-alt-text", media_additional_info_alt_text); GLib.Test.add_func ("/tweet-parsing/bug305-double-encoded-url", bug305_double_encoded_url); return GLib.Test.run (); } cawbird-1.4.2/tests/twitteritem.vala000066400000000000000000000023471416632607600175420ustar00rootroot00000000000000 class TestItem : GLib.Object, Cb.TwitterItem { int64 timestamp; int64 sort_factor; GLib.TimeSpan last_timediff; public TestItem (int64 a, int64 b) { timestamp = a; sort_factor = b; } public int64 get_sort_factor () { return sort_factor; } public int64 get_timestamp () { return timestamp; } public int update_time_delta (GLib.DateTime? now = null) { return 0; } public void set_last_set_timediff (GLib.TimeSpan span) { this.last_timediff = span; } public GLib.TimeSpan get_last_set_timediff () { return this.last_timediff; } } void simple () { var a = new TestItem (1, 2); var b = new TestItem (10, 20); assert (a.get_timestamp () == 1); assert (a.get_sort_factor () == 2); assert (b.get_timestamp () == 10); assert (b.get_sort_factor () == 20); a.set_last_set_timediff (100); b.set_last_set_timediff (1000); message ("a: %s", a.get_last_set_timediff ().to_string ()); message ("b: %s", b.get_last_set_timediff ().to_string ()); assert (a.get_last_set_timediff () == 100); assert (b.get_last_set_timediff () == 1000); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/tweetitem/simple", simple); return GLib.Test.run (); } cawbird-1.4.2/tests/usercompletionmodel.vala000066400000000000000000000065071416632607600212540ustar00rootroot00000000000000void simple () { var model = new Cb.UserCompletionModel (); assert (model.get_n_items () == 0); model.clear (); assert (model.get_n_items () == 0); } void add_infos () { var model = new Cb.UserCompletionModel (); var infos = new Cb.UserInfo[3]; infos[0].user_id = 1; infos[0].screen_name = "a"; infos[0].user_name = "aa"; infos[1].user_id = 2; infos[1].screen_name = "b"; infos[1].user_name = "bb"; infos[2].user_id = 3; infos[2].screen_name = "c"; infos[2].user_name = "cc"; model.insert_infos (infos); assert (model.get_n_items () == 3); Cb.UserIdentity *id = (Cb.UserIdentity*)model.get_item (0); assert (id->id == 1); assert (id->screen_name == "a"); assert (id->user_name == "aa"); id = (Cb.UserIdentity*)model.get_item (1); assert (id->id == 2); assert (id->screen_name == "b"); assert (id->user_name == "bb"); id = (Cb.UserIdentity*)model.get_item (2); assert (id->id == 3); assert (id->screen_name == "c"); assert (id->user_name == "cc"); // test clear model.clear (); assert (model.get_n_items () == 0); } void add_items () { var model = new Cb.UserCompletionModel (); var ids = new Cb.UserIdentity[3]; ids[0].id = 1; ids[0].screen_name = "a"; ids[0].user_name = "aa"; ids[1].id = 2; ids[1].screen_name = "b"; ids[1].user_name = "bb"; ids[2].id = 3; ids[2].screen_name = "c"; ids[2].user_name = "cc"; model.insert_items (ids); assert (model.get_n_items () == 3); Cb.UserIdentity *id = (Cb.UserIdentity*)model.get_item (0); assert (id->id == 1); assert (id->screen_name == "a"); assert (id->user_name == "aa"); id = (Cb.UserIdentity*)model.get_item (1); assert (id->id == 2); assert (id->screen_name == "b"); assert (id->user_name == "bb"); id = (Cb.UserIdentity*)model.get_item (2); assert (id->id == 3); assert (id->screen_name == "c"); assert (id->user_name == "cc"); // test clear model.clear (); assert (model.get_n_items () == 0); } void id_duplicates () { var model = new Cb.UserCompletionModel (); var ids = new Cb.UserIdentity[3]; ids[0].id = 1; ids[0].screen_name = "a"; ids[0].user_name = "aa"; ids[1].id = 2; ids[1].screen_name = "b"; ids[1].user_name = "bb"; ids[2].id = 3; ids[2].screen_name = "c"; ids[2].user_name = "cc"; model.insert_items (ids); assert (model.get_n_items () == 3); model.insert_items (ids); // Still. assert (model.get_n_items () == 3); } void info_duplicates () { var model = new Cb.UserCompletionModel (); var infos = new Cb.UserInfo[3]; infos[0].user_id = 1; infos[0].screen_name = "a"; infos[0].user_name = "aa"; infos[1].user_id = 2; infos[1].screen_name = "b"; infos[1].user_name = "bb"; infos[2].user_id = 3; infos[2].screen_name = "c"; infos[2].user_name = "cc"; model.insert_infos (infos); assert (model.get_n_items () == 3); model.insert_infos (infos); assert (model.get_n_items () == 3); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/usercompletionmodel/simple", simple); GLib.Test.add_func ("/usercompletionmodel/add-infos", add_infos); GLib.Test.add_func ("/usercompletionmodel/add-items", add_items); GLib.Test.add_func ("/usercompletionmodel/id-duplicates", id_duplicates); GLib.Test.add_func ("/usercompletionmodel/info-duplicates", info_duplicates); return GLib.Test.run (); } cawbird-1.4.2/tests/usercounter.vala000066400000000000000000000124741416632607600175410ustar00rootroot00000000000000void count () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (0, "baedert", "blabla"); int changed = counter.save (db.get_sqlite_db ()); message ("Single change: %d", changed); assert (changed == 1); assert (counter.save (db.get_sqlite_db ()) == 0); counter.user_seen (1, "baedert", ""); counter.user_seen (1, "baedert", ""); changed = counter.save (db.get_sqlite_db ()); message ("Double change: %d", changed); assert (changed == 1); assert (counter.save (db.get_sqlite_db ()) == 0); counter.user_seen (2, "baedert", ""); counter.user_seen (3, "baedert", ""); changed = counter.save (db.get_sqlite_db ()); message ("Two users changed: %d", changed); assert (changed == 2); assert (counter.save (db.get_sqlite_db ()) == 0); } void query_after_save () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (20, "baedert2", "foobar2"); counter.save (db.get_sqlite_db ()); Cb.UserInfo[] infos; counter.query_by_prefix (db.get_sqlite_db (), "b", 10, out infos); assert (infos.length == 2); assert (infos[0].screen_name == "baedert"); assert (infos[1].screen_name == "baedert2"); } void query_no_save () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (10, "baedert", "foobar"); // See this one twice so the order makes sense counter.user_seen (20, "baedert2", "foobar2"); Cb.UserInfo[] infos; counter.query_by_prefix (db.get_sqlite_db (), "b", 10, out infos); assert (infos.length == 2); assert (infos[0].screen_name == "baedert"); assert (infos[1].screen_name == "baedert2"); } void query_mixed () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (10, "baedert", "foobar"); counter.user_seen (20, "baedert2", "foobar2"); counter.save (db.get_sqlite_db ()); /* Make sure nothing's in memory anymore */ db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); counter = new Cb.UserCounter (); counter.user_seen (11, "b_", "__"); counter.user_seen (11, "b_", "__"); counter.user_seen (11, "b_", "__"); counter.user_seen (12, "ba", "bb"); counter.user_seen (12, "ba", "bb"); Cb.UserInfo[] infos; counter.query_by_prefix (db.get_sqlite_db (), "b", 10, out infos); assert (infos.length == 4); assert (infos[0].user_id == 10); assert (infos[1].user_id == 11); assert (infos[2].user_id == 12); assert (infos[3].user_id == 20); } void duplicates () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (10, "baedert", "foobar"); counter.save (db.get_sqlite_db ()); counter.user_seen (10, "baedert", "foobar"); // Now we have the same entry in memory and in the database. Cb.UserInfo[] infos; counter.query_by_prefix (db.get_sqlite_db (), "b", 2, out infos); assert (infos.length == 1); } void ids_64bit () { FileUtils.remove (Dirs.config ("accounts/test-account.db")); var db = new Sql.Database (Dirs.config ("accounts/test-account.db"), Sql.ACCOUNTS_INIT_FILE, Sql.ACCOUNTS_SQL_VERSION); var counter = new Cb.UserCounter (); counter.user_seen (741369463338115072, "baedert", "foobar"); counter.save (db.get_sqlite_db ()); // Now we have the same entry in memory and in the database. Cb.UserInfo[] infos; counter.query_by_prefix (db.get_sqlite_db (), "b", 1, out infos); assert (infos.length == 1); assert (infos[0].user_id == 741369463338115072); } int main (string[] args) { GLib.Test.init (ref args); Dirs.create_dirs (); GLib.Test.add_func ("/usercounter/count", count); GLib.Test.add_func ("/usercounter/query-after-save", query_after_save); GLib.Test.add_func ("/usercounter/query-no-save", query_no_save); GLib.Test.add_func ("/usercounter/query-mixed", query_mixed); GLib.Test.add_func ("/usercounter/duplicates", duplicates); GLib.Test.add_func ("/usercounter/ids-64bit", ids_64bit); return GLib.Test.run (); } cawbird-1.4.2/tests/utils.vala000066400000000000000000000220131416632607600163110ustar00rootroot00000000000000void file_type () { string p = "foobar.png"; assert (Cb.Utils.get_file_type (p) == "png"); p = ".hidden.bar"; assert (Cb.Utils.get_file_type (p) == "bar"); p = "foo"; assert (Cb.Utils.get_file_type (p) == ""); p = "some.pointy.name.txt"; assert (Cb.Utils.get_file_type (p) == "txt"); p = "/foo/bar/zomg.txt"; assert (Cb.Utils.get_file_type (p) == "txt"); } void time_delta () { var now = new GLib.DateTime.now_local (); var then = now.add (-GLib.TimeSpan.MINUTE * 3); string delta = Utils.get_time_delta (then, now); assert (delta == "3m"); then = now; delta = Utils.get_time_delta (then, now); assert (delta == "Now"); then = now.add (-GLib.TimeSpan.HOUR * 20); delta = Utils.get_time_delta (then, now); assert (delta == "20h"); then = now; delta = Utils.get_time_delta (then, now); assert (delta == "Now"); } private Gtk.TextBuffer create_buffer_with_cursor(string text, int pos) { var buffer = new Gtk.TextBuffer(null); buffer.set_text(text); Gtk.TextIter iter; buffer.get_iter_at_offset(out iter, pos); buffer.place_cursor(iter); return buffer; } void get_ascii_cursor_word () { var text = "@cawbirdclient"; Gtk.TextIter start_iter, end_iter; var buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); buffer = create_buffer_with_cursor(text, 5); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); buffer = create_buffer_with_cursor(text, text.char_count()); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); text = "Hello @cawbirdclient test!"; buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == "Hello"); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == 5); buffer = create_buffer_with_cursor(text, text.char_count() / 2); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == "@cawbirdclient"); assert(start_iter.get_offset() == 6); assert(end_iter.get_offset() == 20); buffer = create_buffer_with_cursor(text, text.char_count() - 2); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == "test!"); assert(start_iter.get_offset() == 21); assert(end_iter.get_offset() == text.char_count()); } void get_unicode_cursor_word () { var text = "@攻殻機動隊"; Gtk.TextIter start_iter, end_iter; var buffer = create_buffer_with_cursor(text, 5); assert(Utils.get_cursor_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); } void get_punctuated_name_cursor_word () { var text = "@o'brien"; Gtk.TextIter start_iter, end_iter; assert(Utils.get_cursor_word(create_buffer_with_cursor(text, 5), out start_iter, out end_iter) == text); text = "@ibboard's"; assert(Utils.get_cursor_word(create_buffer_with_cursor(text, 5), out start_iter, out end_iter) == text); } void get_ascii_cursor_mention_word () { var text = "@cawbirdclient"; Gtk.TextIter start_iter, end_iter; var buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); buffer = create_buffer_with_cursor(text, 5); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); buffer = create_buffer_with_cursor(text, text.char_count()); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); text = "Hello @cawbirdclient test!"; buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == ""); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == 0); buffer = create_buffer_with_cursor(text, text.char_count() / 2); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == "@cawbirdclient"); assert(start_iter.get_offset() == 6); assert(end_iter.get_offset() == 20); buffer = create_buffer_with_cursor(text, text.char_count() - 2); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == ""); assert(start_iter.get_offset() == 21); assert(end_iter.get_offset() == 21); text = "noone@example.com"; buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == ""); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == 0); text = "@someone@mastadon"; buffer = create_buffer_with_cursor(text, 0); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == text); assert(start_iter.get_offset() == 0); assert(end_iter.get_offset() == text.char_count()); } void get_punctuated_name_cursor_mention_word () { var text = "@o'briain"; Gtk.TextIter start_iter, end_iter; assert(Utils.get_cursor_mention_word(create_buffer_with_cursor(text, 5), out start_iter, out end_iter) == text); text = "@ibboard's"; assert(Utils.get_cursor_mention_word(create_buffer_with_cursor(text, 5), out start_iter, out end_iter) == text); } void get_unicode_cursor_mention_word () { var text = "@攻殻機動隊"; Gtk.TextIter start_iter, end_iter; assert(Utils.get_cursor_mention_word(create_buffer_with_cursor(text, 5), out start_iter, out end_iter) == text); } void get_surrounded_cursor_mention_word () { var text = "Ghost in the Shell (@攻殻機動隊) aka GitS"; Gtk.TextIter start_iter, end_iter; var buffer = create_buffer_with_cursor(text, 22); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == "@攻殻機動隊"); assert(start_iter.get_offset() == 20); assert(end_iter.get_offset() == 26); text = "(It's *@IBBoard*!)"; buffer = create_buffer_with_cursor(text, 10); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == "@IBBoard"); assert(start_iter.get_offset() == 7); assert(end_iter.get_offset() == 15); buffer = create_buffer_with_cursor(text, text.char_count()); assert(Utils.get_cursor_mention_word(buffer, out start_iter, out end_iter) == "@IBBoard"); assert(start_iter.get_offset() == 7); assert(end_iter.get_offset() == 15); } //680,527 70,55 => 0,1 @ 0.102941 void test_calculate_draw_offset() { int draw_x, draw_y; double scale; Utils.calculate_draw_offset(100, 100, 100, 100, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == 0); assert(scale == 1); Utils.calculate_draw_offset(200, 200, 100, 100, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == 0); assert(scale == 0.5); Utils.calculate_draw_offset(100, 100, 200, 200, out draw_x, out draw_y, out scale); assert(draw_x == 50); assert(draw_y == 100); assert(scale == 1); Utils.calculate_draw_offset(100, 400, 100, 100, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == -150); assert(scale == 1); Utils.calculate_draw_offset(200, 400, 100, 100, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == -50); assert(scale == 0.5); Utils.calculate_draw_offset(400, 100, 100, 100, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == 75); assert(scale == 0.25); // Make sure we round so that we don't get -1px Y offsets Utils.calculate_draw_offset(491, 680, 470, 650, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == 0); assert(scale == (470.0/491.0)); Utils.calculate_draw_offset(680, 527, 70, 55, out draw_x, out draw_y, out scale); assert(draw_x == 0); assert(draw_y == 0); assert(scale == (70.0/680.0)); } int main (string[] args) { GLib.Test.init (ref args); GLib.Test.add_func ("/utils/file-type", file_type); GLib.Test.add_func ("/utils/time-delta", time_delta); GLib.Test.add_func ("/utils/get-ascii-cursor-words", get_ascii_cursor_word); GLib.Test.add_func ("/utils/get-unicode-cursor-words", get_unicode_cursor_word); GLib.Test.add_func ("/utils/get-punctuated-name-cursor-words", get_punctuated_name_cursor_word); GLib.Test.add_func ("/utils/get-ascii-cursor-mention-words", get_ascii_cursor_mention_word); GLib.Test.add_func ("/utils/get-unicode-cursor-mention-words", get_unicode_cursor_mention_word); GLib.Test.add_func ("/utils/get-punctuated-name-cursor-mention-words", get_punctuated_name_cursor_mention_word); GLib.Test.add_func ("/utils/get-surrounded-cursor-mention-words", get_surrounded_cursor_mention_word); GLib.Test.add_func ("/utils/calculate-draw-offset", test_calculate_draw_offset); return GLib.Test.run (); } cawbird-1.4.2/ui/000077500000000000000000000000001416632607600135615ustar00rootroot00000000000000cawbird-1.4.2/ui/about-dialog.ui000066400000000000000000000017571416632607600165010ustar00rootroot00000000000000 cawbird-1.4.2/ui/account-create-widget.ui000066400000000000000000000142731416632607600203050ustar00rootroot00000000000000 cawbird-1.4.2/ui/account-dialog.ui000066400000000000000000000350521416632607600170160ustar00rootroot00000000000000 cawbird-1.4.2/ui/cb-emoji-chooser.ui000066400000000000000000000324101416632607600172450ustar00rootroot00000000000000 cawbird-1.4.2/ui/compose-window.ui000066400000000000000000000247101416632607600170760ustar00rootroot00000000000000 cawbird-1.4.2/ui/dm-page.ui000066400000000000000000000204671416632607600154430ustar00rootroot00000000000000 cawbird-1.4.2/ui/dm-thread-entry.ui000066400000000000000000000074511416632607600171330ustar00rootroot00000000000000 cawbird-1.4.2/ui/filter-list-entry.ui000066400000000000000000000114311416632607600175150ustar00rootroot00000000000000 cawbird-1.4.2/ui/filter-page.ui000066400000000000000000000076741416632607600163350ustar00rootroot00000000000000 1 Users cawbird-1.4.2/ui/image-description-window.ui000066400000000000000000000113021416632607600210250ustar00rootroot00000000000000 cawbird-1.4.2/ui/list-list-entry.ui000066400000000000000000000162231416632607600172070ustar00rootroot00000000000000 cawbird-1.4.2/ui/list-statuses-page.ui000066400000000000000000000371461416632607600176710ustar00rootroot00000000000000 1 1 Confirm 1 cawbird-1.4.2/ui/media-dialog.ui000066400000000000000000000073671416632607600164510ustar00rootroot00000000000000 cawbird-1.4.2/ui/menus.ui000066400000000000000000000014761416632607600152570ustar00rootroot00000000000000
Settings app.show-settings Shortcuts action-disabled app.show-shortcuts About app.show-about-dialog Quit app.quit
cawbird-1.4.2/ui/modify-filter-dialog.ui000066400000000000000000000116031416632607600201300ustar00rootroot00000000000000 cawbird-1.4.2/ui/modify-snippet-dialog.ui000066400000000000000000000117541416632607600203340ustar00rootroot00000000000000 cawbird-1.4.2/ui/new-list-entry.ui000066400000000000000000000110551416632607600170230ustar00rootroot00000000000000 cawbird-1.4.2/ui/profile-page.ui000066400000000000000000000603671416632607600165060ustar00rootroot00000000000000
Write Direct Message user.write-dm Tweet to @foobar user.tweet-to Add to/Remove from List user.add-remove-list
Blocked user.toggle-blocked Muted user.toggle-muted Retweets disabled user.toggle-retweets
cawbird-1.4.2/ui/search-page.ui000066400000000000000000000100461416632607600163000ustar00rootroot00000000000000 1 6 6 1 0 Tweets 1 6 6 1 0 Users cawbird-1.4.2/ui/settings-dialog.ui000066400000000000000000000750671416632607600172340ustar00rootroot00000000000000 20 2 0.1 1 1 main_stack cawbird-1.4.2/ui/shortcuts-window.ui000066400000000000000000000153631416632607600174730ustar00rootroot00000000000000 1 1 shortcuts 1 General 1 <ctrl>t Compose Tweet 1 <Ctrl>p Show Account settings 1 <Ctrl>k Show Accounts Popover 1 <Ctrl><Shift>p Show Application Settings 1 <Ctrl><Shift>s Toggle Topbar 1 Back Go Back 1 Forward Go Forward 1 <Alt>1...7 Go to nth page 1 Tweets 1 t+t Retweet 1 l Like 1 r Reply true q Quote 1 Return Show Details 1 d+d Delete 1 Compose 1 <ctrl>Return Send 1 Escape Cancel 1 <ctrl>e Show Emoji Chooser cawbird-1.4.2/ui/start-conversation-entry.ui000066400000000000000000000121341416632607600211250ustar00rootroot00000000000000 cawbird-1.4.2/ui/style.css000066400000000000000000000116331416632607600154370ustar00rootroot00000000000000* { /* The default for this style property for gtk3 is 150 * so we have to get rid of that. */ -GtkProgressBar-min-horizontal-bar-width: 0; } .avatar-round { border: 1px solid grey; border-radius: 48px; } .account-button { padding: 2px 4px; } .topbar > button.flat { /* Make it really flat */ background-image: none; box-shadow: none; /* And square */ border-radius: 0; } .topbar button .badge, .topbar .button .badge { background-color: @theme_selected_bg_color; background-image: none; text-shadow: 0px 1px 1px #FFF; border-radius: 20px; border: 1px solid @theme_selected_fg_color; padding: 3px 2px; } .topbar button .badge:backdrop, .topbar .button .badge:backdrop { background-color: grey; } .inline-media { background: none; border-radius: 0px; padding: 0px; opacity: 0.9; box-shadow: none; border: 1px solid @borders; } .inline-media:backdrop { border-color: @unfocused_borders; } .inline-media:hover { opacity: 1.0; box-shadow: none; border-top: 2px solid @theme_selected_bg_color; border-bottom: 2px solid @theme_selected_bg_color; } .image-success { box-shadow: none; background-image: none; border-radius: 0px; border: none; background-color: transparent; color: white; -gtk-icon-shadow: 0 0 black; -gtk-icon-source: -gtk-icontheme("object-select-symbolic"); } .image-error { box-shadow: none; background-image: none; border-radius: 0px; border: none; background-color: transparent; color: @error_color; -gtk-icon-shadow: 0 0 black; -gtk-icon-source: -gtk-icontheme("dialog-error-symbolic"); } .image-error:hover { color: white; -gtk-icon-source: -gtk-icontheme("view-refresh-symbolic"); } .retweet-button:hover, .retweet-button:checked { color: shade(green, 1.1); } .favorite-button:hover, .favorite-button:checked { color: red; } /* Don't target :hover:active here */ .favorite-button:checked:not(:hover), .retweet-button:checked:not(:hover) { background-image: none; background-color: transparent; border-color: transparent; box-shadow: none; } .reply-button:hover { color: blue; } .favorite-button:backdrop, .retweet-button:backdrop { background-color: transparent; } .tweet { border-bottom: solid 1px alpha (grey, 0.3); } .text-only-button{ font-weight: bold; background-image: none; background-color: transparent; border:none; text-shadow: none; padding: 0px; box-shadow: none; border-radius: 0px; border-bottom: 1px solid transparent; min-height: 0px; margin-bottom: -1px; outline-offset: 0; } .text-only-button:hover { border-bottom: 1px solid currentColor; } .text-only-button:active { color: inherit; background-color: alpha(currentColor, 0.4); } .text-only-button:backdrop { background-color: transparent; } .pixbuf-button { padding: 0px; background-image: none; background-color: transparent; border: none; border-radius: 0px; box-shadow: none; transition: box-shadow 0.2s ease-in-out; } .pixbuf-button-round { border: 1px solid grey; border-radius: 48px; } .pixbuf-button:hover { border: 1px solid @theme_selected_bg_color; } .pixbuf-button:active { box-shadow: inset 0px 0px 3px alpha(black, 0.95); } .pixbuf-button:backdrop { background-color: transparent; background-image: none; } replyindicator { background-color: @theme_selected_bg_color; } .profile-button { border-radius: 0px; border-left-width: 0px; border-right-width: 0px; border-top-width: 0px; border-bottom: 2px solid transparent; box-shadow: none; } .profile-button:checked { border: none; box-shadow: none; background-image: none; background-color: transparent; color: inherit; border-bottom: 2px solid @theme_selected_bg_color; } .profile-button:hover { background-image: none; background-color: alpha(white, 0.3); border-bottom: 2px solid #aaa; } .profile-button:active { color: inherit; border-bottom: 2px solid grey; } .quote { border-left: 3px solid alpha(currentColor, 0.4); } .close-button { padding: 3px; border-radius: 50%; -gtk-outline-radius: 50%; min-height: 0px; min-width: 0px; box-shadow: none; } .avatar:disabled, .inline-media:disabled { opacity: 0.4; } label.name link { color: @theme_fg_color; } /* Override the earlier rule to read-only * inline media are still full-opacity */ .read-only .inline-media:disabled { opacity: 1.0; } progressbar.horizontal.embedded-progress, progressbar.horizontal.embedded-progress>trough, progressbar.horizontal.embedded-progress>trough>progress { margin: 0px; border-radius: 0px; border-left-width: 0px; border-right-width: 0px; } .fav-image-box { padding: 6px; } /* GtkFlowBox doesn't handle the hover state of its children so... */ .fav-image-item:hover { background-color: alpha(black, 0.1); } .fav-image-item:active { background-color: alpha(black, 0.3); } .fav-image-item:disabled { opacity:0.5 } .invisible-links link:link { color:inherit; } .tweet-info-grid { padding: 12px; } cawbird-1.4.2/ui/tweet-info-page.ui000066400000000000000000000614001416632607600171140ustar00rootroot00000000000000
Quote tweet.quote Delete tweet.delete
cawbird-1.4.2/ui/tweet-list-entry.ui000066400000000000000000000343711416632607600173700ustar00rootroot00000000000000
Quote tweet.quote Translate tweet.translate
Delete destructive-actions.delete
cawbird-1.4.2/ui/user-filter-entry.ui000066400000000000000000000143361416632607600175270ustar00rootroot00000000000000 cawbird-1.4.2/ui/user-list-entry.ui000066400000000000000000000131671416632607600172160ustar00rootroot00000000000000 cawbird-1.4.2/ui/user-lists-widget.ui000066400000000000000000000116061416632607600175170ustar00rootroot00000000000000 cawbird-1.4.2/vapi/000077500000000000000000000000001416632607600141035ustar00rootroot00000000000000cawbird-1.4.2/vapi/cawbird-internal.vapi000066400000000000000000000414451416632607600202210ustar00rootroot00000000000000 [CCode (cprefix = "Cb", lower_case_cprefix = "cb_")] namespace Cb { [CCode (cprefix = "CB_MEDIA_TYPE_", cheader_filename = "CbMedia.h")] public enum MediaType { IMAGE, GIF, ANIMATED_GIF, TWITTER_VIDEO, INSTAGRAM_VIDEO, UNKNOWN } [CCode (cprefix = "CB_STREAM_MESSAGE_", cheader_filename = "CbUserStream.h")] public enum StreamMessageType { UNSUPPORTED, DELETE, DM_DELETE, RT_DELETE, SCRUB_GEO, LIMIT, DISCONNECT, FRIENDS, EVENT, WARNING, DIRECT_MESSAGE, TWEET, MENTION, EVENT_LIST_CREATED, EVENT_LIST_DESTROYED, EVENT_LIST_UPDATED, EVENT_LIST_UNSUBSCRIBED, EVENT_LIST_SUBSCRIBED, EVENT_LIST_MEMBER_ADDED, EVENT_LIST_MEMBER_REMOVED, EVENT_FAVORITE, EVENT_UNFAVORITE, EVENT_FOLLOW, EVENT_UNFOLLOW, EVENT_BLOCK, EVENT_UNBLOCK, EVENT_MUTE, EVENT_UNMUTE, EVENT_USER_UPDATE, EVENT_QUOTED_TWEET, EVENT_HIDE_RTS, EVENT_SHOW_RTS, TIMELINE_LOADED, MENTIONS_LOADED, FAVORITES_LOADED, DIRECT_MESSAGES_LOADED } [CCode (cprefix = "CbMedia_", lower_case_cprefix = "cb_media_", cheader_filename = "CbMedia.h")] public class Media : GLib.Object { public int64 length; public bool loading; public bool loaded; public bool loading_hires; public bool loaded_hires; public bool invalid; public string consumer_key; public string consumer_secret; public string token; public string token_secret; public string url; public string thumb_url; public string target_url; public string alt_text; public MediaType type; public int width; public int height; public int thumb_width; public int thumb_height; public double percent_loaded; public double percent_loaded_hires; public Cairo.ImageSurface? surface; public Cairo.ImageSurface? surface_hires; public Gdk.PixbufAnimation? animation; public signal void progress(); public Media(); public bool is_video (); public bool requires_authentication(); } [CCode (cprefix = "CbUserIdentity_", lower_case_cprefix = "cb_user_identity_", cheader_filename = "CbTypes.h", destroy_function = "cb_user_identity_free")] public struct UserIdentity { public int64 id; public string screen_name; public string user_name; public bool verified; public bool protected_account; public void parse (Json.Object object); } /* Needed for unit tests */ [CCode (cprefix = "CbMediaDownloader", lower_case_cprefix = "cb_media_downloader_", cheader_filename = "CbMediaDownloader.h")] public class MediaDownloader : GLib.Object { public static unowned MediaDownloader get_default (); public async void load_async (Media media); public async void reload_async (Media media); public async void load_hires_async (Media media); public void disable (); public void shutdown (); } [CCode (cprefix = "CbTextEntity", lower_case_cprefix = "cb_text_entity_", cheader_filename = "CbTypes.h", destroy_function = "cb_text_entity_free")] public struct TextEntity { public uint from; public uint to; public string original_text; public string display_text; public string tooltip_text; public string? target; // If target is null, use display_text as target! public uint info; } [CCode (cprefix = "CbMiniTweet", lower_case_cprefix = "cb_mini_tweet_", cheader_filename = "CbTypes.h", destroy_function = "cb_mini_tweet_free")] public struct MiniTweet { public int64 id; public int64 created_at; public uint display_range_start; public int64 reply_id; public Cb.UserIdentity author; public string text; [CCode (array_length_cname = "n_entities", array_length_type = "size_t")] public Cb.TextEntity[] entities; [CCode (array_length_cname = "n_medias", array_length_type = "size_t")] public Cb.Media[] medias; [CCode (array_length_cname = "n_reply_users", array_length_type = "size_t")] public Cb.UserIdentity[] reply_users; [CCode (cname = "cb_mini_tweet_init")] public MiniTweet(); public void parse (Json.Object obj); public void parse_entities (Json.Object status); } [CCode (cprefix = "CbTweet", lower_case_cprefix = "cb_tweet_", cheader_filename = "CbTweet.h")] public class Tweet : GLib.Object { [CCode (cname = "CB_TWEET_MAX_LENGTH")] public static int MAX_LENGTH; public Cb.MiniTweet source_tweet; public Cb.MiniTweet? retweeted_tweet; public Cb.MiniTweet? quoted_tweet; public int64 id; public int64 my_retweet; public int favorite_count; public int retweet_count; public string avatar_url; public Tweet(); public int64 get_user_id (); public unowned string get_screen_name (); public unowned string get_user_name (); public unowned string get_language (); public bool has_inline_media (); public bool has_quoted_inline_media (); public void load_from_json (Json.Node node, int64 account_id, GLib.DateTime now); public bool is_reply (); public bool is_flag_set (uint flag); public void set_flag (uint flag); public void unset_flag (uint flag); public bool is_quoted_flag_set (uint flag); public void set_quoted_flag (uint flag); public void unset_quoted_flag (uint flag); public string get_formatted_text (); public string get_trimmed_text (uint transform_flags); public string get_real_text (); public string get_filter_text (); public unowned Cb.UserIdentity[] get_reply_users (); public unowned Cb.Media[] get_medias(); public unowned Cb.Media[] get_quoted_medias(); public string[] get_mentions(); public signal void state_changed(); public bool is_hidden (); public string? notification_id; public bool get_seen (); public void set_seen (bool val); public uint state; #if DEBUG public string json_data; #endif } [CCode (cprefix = "CB_TWEET_STATE_", lower_case_cprefix = "CbTweetState", cheader_filename = "CbTweet.h")] [Flags] public enum TweetState { HIDDEN_FORCE, HIDDEN_UNFOLLOWED, HIDDEN_FILTERED, HIDDEN_RTS_DISABLED, HIDDEN_RT_BY_USER, HIDDEN_RT_BY_FOLLOWEE, HIDDEN_AUTHOR_BLOCKED, HIDDEN_RETWEETER_BLOCKED, HIDDEN_AUTHOR_MUTED, HIDDEN_RETWEETER_MUTED, RETWEETED, FAVORITED, DELETED, VERIFIED, PROTECTED, NSFW } [CCode (cprefix = "CB_TEXT_TRANSFORM_", lower_case_cprefix = "CbTextTransformFlags", cheader_filename = "CbTextTransform.h")] public enum TransformFlags { REMOVE_TRAILING_HASHTAGS, EXPAND_LINKS, REMOVE_MEDIA_LINKS } [CCode (cprefix = "cb_text_transform_", cheader_filename = "CbTextTransform.h")] namespace TextTransform { string tweet (ref MiniTweet tweet, uint flags, int64 quote_id); string text (string text, TextEntity[] entities, uint flags, size_t n_medias, int64 quote_id, uint display_range_start = 0); string fix_encoding (string text); } [CCode (cprefix = "cb_filter_", cheader_filename = "CbFilter.h")] public class Filter : GLib.Object { public Filter(string expr); public bool matches (string text); public void reset (string expr); public unowned string get_contents(); public int get_id (); public void set_id (int id); } [CCode (cprefix = "cb_avatar_cache_", cheader_filename = "CbAvatarCache.h")] class AvatarCache : GLib.Object { public AvatarCache (); public void add (int64 user_id, Cairo.Surface? surface, string? avatar_url); public void increase_refcount_for_surface (Cairo.Surface surface); public void decrease_refcount_for_surface (Cairo.Surface surface); public void set_url (int64 user_id, string url); public void set_avatar (int64 user_id, Cairo.Surface? surface, string url); public unowned Cairo.Surface? get_surface_for_id (int64 user_id, out bool found); public unowned string? get_url_for_id (int64 user_id); public uint get_n_entries (); } [CCode (cprefix = "cb_user_counter_", cheader_filename = "CbUserCounter.h")] public class UserCounter : GLib.Object { public UserCounter (); public void id_seen (ref Cb.UserIdentity id); public void user_seen (int64 id, string screen_name, string user_name); public void user_seen_full (int64 id, string screen_name, string user_name, bool verified, bool protected_account); public int save (Sqlite.Database db); public void query_by_prefix (Sqlite.Database db, string prefix, int max_results, out Cb.UserInfo[] infos); } [CCode (cheader_filename = "CbUserCounter.h")] public struct UserInfo { int64 user_id; string screen_name; string user_name; int score; bool changed; bool verified; bool protected_account; } [CCode (cprefix = "CbMediaImageWidget_", lower_case_cprefix = "cb_media_image_widget_", cheader_filename = "CbMediaImageWidget.h")] public class MediaImageWidget : Gtk.ScrolledWindow { public MediaImageWidget (Media media, Gdk.Rectangle max_dimensions); public void scroll_to (double x, double y); } [CCode (cprefix = "CbTweetModel_", lower_case_cprefix = "cb_tweet_model_", cheader_filename = "CbTweetModel.h")] public class TweetModel : GLib.Object, GLib.ListModel { public int64 min_id; public int64 max_id; public GLib.GenericArray hidden_tweets; public TweetModel (); public void set_thread_mode (bool thread_mode); public bool contains_id (int64 id); public int index_of (int64 id); public int index_of_retweet (int64 id); public void clear (); public unowned Tweet? get_for_id (int64 id, int diff = 0); public void add (Tweet t); public void remove_oldest_n_visible (uint amount); public bool delete_id (int64 id, out bool seen); public bool set_tweet_flag (Tweet t, TweetState flag); public bool unset_tweet_flag (Tweet t, TweetState flag); public void remove_tweet (Tweet t); public void remove_tweets_later_than (int64 id); public void toggle_flag_on_user_tweets (int64 user_id, TweetState flag, bool active); public void toggle_flag_on_user_retweets (int64 user_id, TweetState flag, bool active); } [CCode (cprefix = "CbTwitterItemInterface_", lower_case_cprefix = "cb_twitter_item_", cheader_filename = "CbTwitterItem.h", type_cname = "CbTwitterItemInterface")] public interface TwitterItem : GLib.Object { public abstract int64 get_sort_factor(); public abstract int64 get_timestamp(); public abstract int update_time_delta (GLib.DateTime? now = null); public abstract void set_last_set_timediff (GLib.TimeSpan span); public abstract GLib.TimeSpan get_last_set_timediff (); } [CCode (cprefix = "CbMessageReceiverInterface_", lower_case_cprefix = "cb_message_receiver_", cheader_filename = "CbMessageReceiver.h", type_cname = "CbMessageReceiverInterface")] public interface MessageReceiver : GLib.Object { public abstract void stream_message_received (Cb.StreamMessageType type, Json.Node node); } [CCode (cprefix = "CbDeltaUpdater_", lower_case_cprefix = "cb_delta_updater_", cheader_filename = "CbDeltaUpdater.h")] public class DeltaUpdater : GLib.Object { public DeltaUpdater (Gtk.Widget listbox); } [CCode (cprefix = "CbUtils_", lower_case_cprefix = "cb_utils_", cheader_filename = "CbUtils.h")] namespace Utils { public void init_gui ([CCode (array_length_pos = 0.1)] ref unowned string[] args); public delegate Gtk.Widget CreateWidgetFunc (void *item); public void bind_model (Gtk.Widget listbox, GLib.ListModel model, Gtk.ListBoxCreateWidgetFunc func); public void bind_non_gobject_model (Gtk.Widget listbox, GLib.ListModel model, Cb.Utils.CreateWidgetFunc func); public void unbind_non_gobject_model (Gtk.Widget *listbox, GLib.ListModel model); public void linkify_user (ref Cb.UserIdentity id, GLib.StringBuilder str); public GLib.DateTime parse_date (string _in); public string get_file_type (string url); public string rest_proxy_call_to_string (Rest.ProxyCall c); public async Json.Node? load_threaded_async (Rest.ProxyCall call, GLib.Cancellable? cancellable) throws GLib.Error; public async UserIdentity[] query_users_async (Rest.Proxy p, string q, GLib.Cancellable? cancellable) throws GLib.Error; } [CCode (cprefix = "CbBundle_", lower_case_cprefix = "cb_bundle_", cheader_filename = "CbBundle.h")] public class Bundle : GLib.Object { public Bundle (); public void put_string (int key, string val); public unowned string get_string (int key); public void put_int (int key, int val); public int get_int (int key); public void put_int64 (int key, int64 val); public int64 get_int64 (int key); public void put_bool (int key, bool val); public bool get_bool (int key); public void put_object (int key, GLib.Object val); public unowned GLib.Object get_object (int key); public bool equals (Bundle? other); } [CCode (cprefix = "CbBundleHistory_", lower_case_cprefix = "cb_bundle_history_", cheader_filename = "CbBundleHistory.h")] public class BundleHistory : GLib.Object { public BundleHistory (); public void push (int v, Cb.Bundle? b); public int forward (); public int back (); public bool at_start (); public bool at_end (); public void remove_current (); public int get_current (); public unowned Cb.Bundle? get_current_bundle (); } [CCode (cprefix = "CbSnippetManager_", lower_case_cprefix = "cb_snippet_manager_", cheader_filename = "CbSnippetManager.h")] public class SnippetManager : GLib.Object { public SnippetManager (Sqlite.Database db); public unowned string get_snippet (string key); public bool has_snippet_n (string key, size_t key_len); public uint n_snippets (); public void query_snippets (GLib.HFunc func); public void set_snippet (string old_key, string key, string value); public void remove_snippet (string key); public void insert_snippet (string key, string value); } [CCode (cprefix = "CbMediaVideoWidget_", lower_case_cprefix = "cb_media_video_widget_", cheader_filename = "CbMediaVideoWidget.h")] public class MediaVideoWidget : Gtk.Stack { public MediaVideoWidget (Media media, Gdk.Rectangle max_dimensions); public void start (); } [CCode (cprefix = "CbUserStream_", lower_case_cprefix = "cb_user_stream_", cheader_filename = "CbUserStream.h")] public class UserStream : GLib.Object { public UserStream (string name, Rest.OAuthProxy proxy); public void register (MessageReceiver r); public void unregister (MessageReceiver r); public void push_data (string data); public void start (); public void stop (); public void set_proxy_data (string a, string b); public void inject_tweet (Cb.StreamMessageType message_type, string content); public signal void interrupted (); public signal void resumed (); } [CCode (cprefix = "CbComposeJob_", lower_case_cprefix = "cb_compose_job_", cheader_filename = "CbComposeJob.h")] public class ComposeJob : GLib.Object { public string response_payload; [CCode (cname="MAX_UPLOADS")] public static int MAX_UPLOADS; public signal void image_upload_progress (string a, double d); public signal void image_upload_finished (string a, string? b); public signal void image_upload_id_assigned (string file_path, int64 media_id); public ComposeJob (Cb.UserStream user_stream, Rest.Proxy proxy, Rest.Proxy proxy2, GLib.Cancellable cancellable); public void set_reply_id (int64 id); public void set_quoted_tweet (Cb.Tweet t); public void set_text (string s); public void upload_image_async (string path, string uuid); public void abort_image_upload (string uuid); public async bool send_async (GLib.Cancellable c) throws GLib.Error; // https://wiki.gnome.org/Projects/Vala/ManualBindings#Array_Lengths public uint get_n_filepaths(); public unowned string? get_filepath (uint pos); } [CCode (cprefix = "CbUserCompletionModel_", lower_case_cprefix = "cb_user_completion_model_", cheader_filename = "CbUserCompletionModel.h")] public class UserCompletionModel : GLib.Object, GLib.ListModel { public UserCompletionModel (); public void insert_infos (Cb.UserInfo[] infos); public void insert_items (Cb.UserIdentity[] ids); public void clear (); } [CCode (cprefix = "CbEmojiChooser_", lower_case_cprefix = "cb_emoji_chooser_", cheader_filename = "CbEmojiChooser.h")] public class EmojiChooser : Gtk.Box { public EmojiChooser (); public void populate (); public bool try_init (); public signal void emoji_picked (string emoji); } } cawbird-1.4.2/vapi/config.vapi000066400000000000000000000003631416632607600162330ustar00rootroot00000000000000[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")] namespace Config { public const string GETTEXT_PACKAGE; public const string LOCALEDIR; public const string CONSUMER_KEY; public const string CONSUMER_SECRET; } cawbird-1.4.2/vapi/libtl.vapi000066400000000000000000000023261416632607600160750ustar00rootroot00000000000000namespace Tl { [CCode (cprefix = "TL_ENT_", cheader_filename = "libtl/libtweetlength.h")] enum EntityType { TEXT = 1, HASHTAG, LINK, MENTION, WHITESPACE } [CCode (cprefix = "TlEntity_", lower_case_cprefix = "tl_entity_", cheader_filename = "libtl/libtweetlength.h")] struct Entity { EntityType type; string *start; size_t start_character_index; size_t length_in_characters; size_t length_in_bytes; size_t length_in_weighted_characters; } [CCode (cprefix = "tl_", lower_case_cprefix = "tl_", cheader_filename = "libtl/libtweetlength.h")] size_t count_characters (string input); [CCode (cprefix = "tl_", lower_case_cprefix = "tl_", cheader_filename = "libtl/libtweetlength.h")] size_t count_weighted_characters (string input, bool use_short_link = true); [CCode (cprefix = "tl_", lower_case_cprefix = "tl_", cheader_filename = "libtl/libtweetlength.h", array_length_pos = 1)] Entity[]? extract_entities (string input, out size_t text_length); [CCode (cprefix = "tl_", lower_case_cprefix = "tl_", cheader_filename = "libtl/libtweetlength.h", array_length_pos = 1)] Entity[]? extract_entities_and_text (string input, out size_t text_length); }