pax_global_header00006660000000000000000000000064141535020460014512gustar00rootroot0000000000000052 comment=97103210e8d8ad4a35fe5ba8aa5d498acec50949 liferea-1.13.7/000077500000000000000000000000001415350204600132125ustar00rootroot00000000000000liferea-1.13.7/.github/000077500000000000000000000000001415350204600145525ustar00rootroot00000000000000liferea-1.13.7/.github/FUNDING.yml000066400000000000000000000000551415350204600163670ustar00rootroot00000000000000custom: ["https://paypal.me/32799746569265"] liferea-1.13.7/.github/workflows/000077500000000000000000000000001415350204600166075ustar00rootroot00000000000000liferea-1.13.7/.github/workflows/cb.yml000066400000000000000000000017641415350204600177260ustar00rootroot00000000000000name: "Build Check" on: push: branches: [master, ] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 21 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-18.04 steps: - name: Checkout repository uses: actions/checkout@v2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 - run: | sudo apt-get update -qq sudo apt-get install -y -qq libxml2-dev libxslt1-dev libsqlite3-dev libwebkit2gtk-4.0-dev libjson-glib-dev libgirepository1.0-dev libpeas-dev gsettings-desktop-schemas-dev python3 libtool intltool valgrind mkdir inst - run: | sh autogen.sh ./configure --prefix=$(pwd)/inst - run: make -C po check - run: make && make install - run: cd src/tests && make test - run: cd src/tests && ./memcheck.sh parse_xml parse_date liferea-1.13.7/.gitignore000066400000000000000000000014271415350204600152060ustar00rootroot00000000000000.deps *.a *.gmo *.o *.Po *.la *.lo *.lai *.so *.swp *.swo Makefile Makefile.in Makecache aclocal.m4 ar-lib autom4te.cache compile config.guess config.h config.h.in config.log config.status config.sub configure depcomp install-sh libtool liferea.appdata.xml ltmain.sh missing net.sf.liferea.gschema.valid net.sf.liferea.gschema.xml net.sourceforge.liferea.appdata.xml net.sourceforge.liferea.desktop net.sourceforge.liferea.service m4 glade/*.ui~ po/.intltool-merge-cache po/Makefile.in.in po/POTFILES po/liferea.pot po/stamp-it src/js.c src/js.h src/dbus_wrap.c src/liferea src/liferea-add-feed src/Liferea-3.0.gir src/Liferea-3.0.typelib src/tests/favicon src/tests/html_auto src/tests/parse_html src/tests/parse_date src/tests/parse_xml xslt/*.xml org.gnome.liferea.gschema.valid stamp-h1 liferea-1.13.7/.travis.yml000066400000000000000000000002201415350204600153150ustar00rootroot00000000000000language: c sudo: required services: - docker before_install: - docker pull ubuntu:20.04 script: - docker build -f Dockerfile.travis . liferea-1.13.7/AUTHORS000066400000000000000000000163461415350204600142740ustar00rootroot00000000000000The following sections list people who have contributed to Liferea. Coding ====== People that spent a lot of time on the Liferea code base: Lars Windolf Nathan J. Conrad Arnold Noronha Emilio Pozuelo Monfort Adrian Bunk Laetitia Berthelot Code Contributors ================= Loosely ordered by time of contribution: James Doherty dialog design Jeremy Messenger search dialog, icons... John McKnight Makefile help Tomasz Maka window position saving Karl Soderstrom heavy debugging, patching memory leak fixing, mini popup support Christophe Barbe patch to count folder items and Debian bug management Juho Snellman CSS support Roshan Revankar patch for selection handling Johannes Schlueter pipe as feed source Niklas Morberg Debugging and patching Pierre Phaneuf RPM spec update ahmed el-helw feed link autodiscovery James Bowes tray icon popup menu Marc Deslauriers patch for window state saving Amit D. Chaudhary patch to update all favicons Christoph Hohmann patch for IPv6 Raphael Slinckx New subscriptions over DBUS Bjorn Monnens Hiding read items from folders Patch for itemlist updating Thomas de Grenier de Latour Online state in tray icon Aristotle Pagaltzis Atom 1.0 support, XSLT rendering Norman Jonas libnotify notification Sebastian Droege Transparent tray icon Daniel Gryniewicz AMD64 fixes Remi Cardona Build improvements keizue On/offline switching DBUS method Frederic Peters Unread/new counter in status bar Don Malcolm Offline Mozilla rendering Ed Catmur RTL item list improvements Chris Pirillo Cool new icon for OPML Eric Anderson Use icons from gtk theme Main menu improval Daniel Gryniewicz Network Manager support mooonz improved DBUS support Ori Avtalion Preference for toolbar style improved tray icon menu Stephan Maka gwget download support goyko Menu enabling/disabling Support for enclosure URL open Alexander Sack XulRunner 1.9 build support Gustavo Chain Starting minimized to tray Lars Strojny Statusbar improvements WebKit Rendering Support Jon Forsberg Enclosure context menu patch Mathie Leplatre Option to filter feed list Mikel Olasagasti Uranga GeoRSS support, Gsettings port Rupert Swarbrick OPML duplicate id fix Yaron Sheffer Bidi support for LTR and RTL locales Ricardo Cruz Many GUI improvements Maia Kozheva Optional libindicate support Peter Oliver Google Reader Label Support Sergey Snitsaruk Google Reader Label Support Sebastian Keller Gedit-like tab close buttons Simon Kågedal Reimer Seekable GStreamer media player Daneil Aleksandersen Partial RFC3229 support saving bandwidth Security fixes, network improvements Maintainer Credits ================== David Michael Smith Debian Emilio Pozuelo Monfort Debian Paul Gevers Debian Emilio Pozuelo Monfort Ubuntu Phillip Compton Fedora Dag Wieers Fedora Documentation and Translations ============================== Polish Bart Kreska Polish Wojciech Myrda Polish Piotr Sokół Italian Dario Conigliaro Italian Emanuele Grande Italian Gianvito Cavasoli Italian Lorenzo L. Ancora Brazilian Portuguese Fernando Ike de Oliveira Brazilian Portuguese Og Maciel Brazilian Portuguese Leon Nardella Arabic Khaled Hosny Spanish Sargate Kanogan Spanish Rodrigo Gallardo Japanese Takeshi AIHANA Japanese Takeshi Hamasaki Japanese IWAI, Masaharu French Vincent Lefèvre French Guillaume Bernard Swedish Daniel Nylander Swedish Andreas Ronnquist Turkish Mehmet Atif Ergun Turkish Eren Türkay Belorusian Latin Ihar Hrachyshka European Portuguese António Lima European Portuguese Bruno Miguel Czech Martin Picek Russian Justin Forest Russian Oleg Maloglovets Russian Sergey Rudchenko Russian Leonid Selivanov Hungarian Máté Őry Hungarian Gabor Kelemen Slovakian Pavol Klačanský Greek Evgenia Petoumenou Albanian Besnik Bleta German Christian Dywan German Robin Stocker German Paul Seyfert Catalan Gil Forcada Basque Iñaki Larrañaga Murgoitio Basque Mikel Olasagasti Uranga Romanian Adi Roiban Dutch Erwin Poeze Chinese/Simplified Aron Xu Galician Anxo Outeiral Danish Joe Hansen Asturian Marquinos Asturian Iñigo Varela Hebrew Yaron Sheffer Ukrainian Yuri Chornoivan Latvian Rūdolfs Mazurs Latvian Rihards Priedītis Lithuanian Mindaugas Baranauskas Vietnamese Trần Ngọc Quân Finnish Pauli Virtanen Finnish Jorma Karvonen Indonesian Samsul Ma'arif Code included from other projects ================================= Evolution Team AM/PM time formatting If we forgot to list you here, leave us a message on the mailing list and we'll include your name and email! liferea-1.13.7/COPYING000066400000000000000000000432551415350204600142560ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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. liferea-1.13.7/COPYING.LIB000066400000000000000000000167451415350204600146670ustar00rootroot00000000000000 GNU LESSER 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. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. liferea-1.13.7/ChangeLog000066400000000000000000001622431415350204600147740ustar00rootroot000000000000002021-12-06 Lars Windolf Version 1.13.7 * Allow converting TinyTinyRSS subscriptions to local subscriptions (Lars Windolf) * #1045: Further integration of Reader mode with internal browser (Matthew Moran) * #1044: Support for user-agent string customization and anonymization (Lorenzo Ancora) * #1037: Fix deprecation of g_time_zone_new (sunwire) * #1027: Add option to show news bins in reduced feed list (Alexandre Erwin Ittner) * #1023: Execute feed pipe/filter commands asynchronously (Alexandre Erwin Ittner) * #1018: Fix deprecation of pango_find_base_dir (sunwire) * #1017: Fix deprecation of g_memdup (sunwire) * #1016: Type casting to silence compiler warnings (sunwire) * #1014: Fixes some compiler warnings (sunwire) * #947: Rework of Reader mode feature toggling: toggle is now a context menu toggle of the HTML view context menu. * Fixes #1013: Certificate problems with some URLs (sunwire) * Fixes 1005: aria2 download manager triggered by wrong command (zorlaski) * Fixes #981: Left-clicking an external link in the headline view caused the Javascript disabled setting to be ignored. (Lars Windolf) * Fixes #993: Duplicate registration of libsoup content decoder (Yanko Kaneti) * Fixes #955: Blog post truncated after a YouTube embed (reported by Jeff Fortin, fix by Rich Coe) * Fixes #952: Number of unread items is 2 times the correct number (reported by GaryGate15) * Fixes #950: Multiple license issues: - several header files with LGPLv2+ license -> fixed to GPLv2+ - several plugins with outdated LGPLv2+ -> updated to LGPLv3+ - missing LGPL license file COPYING.LIB -> added (reported by Paul Gevers) * #1040: Updated all documentation for WCAG and HTML5 semantics improves accessibility for screen readers (Lorenzo Ancora) * #1049: Update and extend man page (Lorenzo Ancora) * #477: Updated documentation for enclosure download (Lars Windolf) * Updated documentation for preference dialog (Lars Windolf) * Added Italian documentation translation (Lorenzo Ancora) * Updated Brazilian Portuguese translation (Fúlvio Alves) * Updated Czech translation (RadimNo) * Updated Hungarian translation (Balázs Úr) * Updated Polish translation (sunwire) * Updated Simplified Chinese translation (Sefler Zhou) 2021-05-07 Lars Windolf Version 1.13.6 * Change reader mode toggle from item view icon to toggle entry in item view context menu. (Lars Windolf) * #343: UX redesign of the update message in the status bar. Now shows a update counter of the feeds being in update. (Lars Windolf) * #964: Added pane position fix plugin providing a workaround for pane position issues when using the headerbar (sunwire) * #983: Use GtkFileChooserNative to allow selecting local files when run in sandbox. (Leiaz) * Fixes #780: Improve date formatting when in locale en_GB (sunwire) * Fixes #944: Broken RTL text direction in feed content view (Lars Windolf) * Fixes #949: Regression in notification plugin that caused it to show non-new items (Lars Windolf) * Fixes #963: Checking for git command in plugin installer -> warn user (sunwire) * Fixes #973: Broken CSS in Webkit2Gtk >= 2.32 (Leiaz) * Updated Albanian translation (Besnik Bleta) * Updated French translation (guilieb) * Updated Italian translation (Janvitus) * Updated German translation (Lars Windolf) 2021-01-10 Lars Windolf Version 1.13.5 * #907: Add new search folder property that allows hiding read items (Lars Windolf, suggested by Jeff Fortin) * Now always shows the unread count of a search folder (instead of the item count) in the feed list (Lars Windolf) * Now search folders are automatically rebuild when rules are changed (Lars Windolf) * Adds a new simple focus plugin that adds transparency on the feed list when it is not focussed. (sunwire) * Add F10 hotkey to headerbar plugin to allow triggering the hamburger menu (sunwire) * Update Flatpak AppData with release dates (Mikel Olasagasti) * Make several plugins support gettext (sunwire) * Fixes #192: wrong button order of media player in RTL locales (patch by GreenLunar) * Fixes #343: Usability of update infos in status bar. Now a total count is displayed on mass-updates and a per-feed info on single feed updates (Lars Windolf) * Fixes #809: Usability of feed fetching errors. (Lars Windolf) * Updated Polish translation (sunwire) * Updated German translation (Lars Windolf) 2020-12-24 Lars Windolf Version 1.13.4 * Prevent endless loop on favicon discovery (Lars Windolf) * #903: Prevent endless loop in feed autodiscovery (reported by Leiaz) * #912: Allow mutiple feed in same libnotify notification (Tasos Sahanidis) * Further favicon discovery improvements: now detects all types of Apple Touch Icons, MS Tile Images and Safari Mask Icons Fixes #440 (Lars Windolf) * Add reader mode toggle in the item view (Lars Windolf) * #876 Add reader mode preference (Lars Windolf) * Implement support for subscribing to LD+Json metadata listings e.g. concert or theater event listings (Lars Windolf) * Provide the default icon for feeds with no favicon as SVG for nicer rendering in wide view. Drop some unused icons. Replace tray icon with scalable version. (Lars Windolf) * Fixes CRITICAL on trayicon plugin init (Lars Windolf) * Fixes #884: Content of wrong feed is shown (Leiaz, reported by Michael F. Schönitzer) * Fixes #900: Flatpak needs icon named after appid (Mikel Olasagasti) * Fixes #908, #332: Search folder are not case-insensitive as documented (reported by Heinz Peter Hippenstiel and Jeff Fortin) * Fixes #899: Truncated articles (on 'Extrat full content') (reported by Leiaz) * #902: Updated Polish translation (sunwire) 2020-10-09 Lars Windolf Version 1.13.3 * #893: Update of bookmarking sites (Mikel Olasagasti) * #888: Changes required for Flathub (Mikel Olasagasti) * #882: Implement support for Webkits Intelligent Tracking Protection (Leiaz) * #875: Add -0 hotkey to reset zoom (Leiaz) * #874: Add debug printing of SAX parser errors (Leiaz) * #846: Remove deprecated usage of gdk_screen_* (ghost) * #844: Update metainfo path (appdata target directory) (Yuri Konotopov) * #776: CSS color update on theme change without restart (sillyslux) * Fixes #883: Feed with comments: last comment replaces all content (Leiaz) * Fixes #866: Bad encoding in doc/html/reference_de.html (reported by Paul Gevers) * Fixes #865: Fixes view mode switch (Leiaz) * Fixes #841: Now shows progress bar when loading websites (Leiaz) * Fixes #828: Crashing with segfault after opening feed articles (chronoscz) * Fixes XLST exception on mediarss feeds with description (Lars Windolf) * Drop blogChannel namespace support (Lars Windolf) * Support multi-feed discovery (Lars Windolf) * Default to https:// instead of http:// when user doesn't provide protocol on subscribing feed (Lars Windolf) * Drop CDF channel legacy support (Lars Windolf) * Drop Atom 0.2/0.3 (aka Pie) legacy support (Lars Windolf) * #893: Added Persion default feed list (Kevin Scruff) * #890: Update of Indionesian translation (Samsul Ma'arif) * #890: Update of French translation (Yannick A.) 2020-08-29 Lars Windolf Version 1.13.2 * #846, #864, #735: Fix main menu/toolbar not being translated (reported by nesfla, Qik000, SingleMalt2104) * #854: Adding new search folder options to match subscription source ULR and parent folder name. (suggested by muhlinux) * #851: Add accessibility check to code tests (Lars Windolf) * #851: Fix accessibility annotations for several combo boxes. (Lars Windolf) * #850: Fix embedded youtube video bug. (reported by sblondon) * #765: Embed YouTube videos from MediaRSS feeds (Mikel Olasagasti) * #749: Add Readability.js library (Apache 2.0 licensed) and auto-apply Readability.js to all headlines (Lars Windolf) * Drop unused glade/google_source.ui (Lars Windolf) * Drop support of combined view mode, this is necessary to add rich content support which relies on DOM transformation and Readability.js (Lars Windolf) * Drop ns_photo support, as it is rarely used and allows us to get rid of XSLT extra handling (Lars Windolf) * #747: Font improvements (Azhar Mithani) * #250 Update some dialog labels (GreenLunar) * #803: Fix legacy links to mailing list and IRC in documentation. (reported by Bill Dietrich) * Updated German translation (Lars Windolf) * #861: Updated Spanish translation (vosian) 2020-06-11 Lars Windolf Version 1.13.1 * Fixes #840: OPML source subfolders not working (fixed by Tomáš Janoušek) * #837: Removing GTimeVal references (Tom Perez) * #827: Respect global update refresh interval for TheOldReader (Matthew Horan) * #826: Fix save/restore position issues with tray icon (Matthew Horan) * #822: Improve performance of item list loading (Rich Coe) * Fixes #821: Skip current item when finding next unread item (Tom Perez) * #815: Add support for subscribing to HTML5 websites without RSS/Atom feeds by extracting article titles,links and descriptions (Lars Windolf) * #816: Increase size of stored favicons to 128x128px to improve icon quality in 3-pane wide view. Also add favicon URL discovery tests. * Fixes #821: Skip current selected unread item on 'next unread item' (Tom Perez) * #800: Expose remove-item action for plugins (mozbugbox) * Fixes #799: plugins/headerbar.py translations not active (reported by Paul Gevers) * Fixes #783: Update IS_STATE when update item in itemlistview (mozbugbox) * #752: Trayicon plugin has now a configuration option to change the behaviour when closing Liferea. (BurnhamG) * Fixes #693: Add trayicon plugin option to disable minimizing to tray (BurnhamG) 2020-03-04 Lars Windolf Version 1.13.0 * #764: Add MediaRSS support (e.g. Youtube feeds) to display descriptions and thumbnails (Mikel Olasagasti) * #773: Add WebKit Inspector menu option to HTML view when run with --debug-html (by sillyslux) * #714: Replaces deprecated gtk_menu_popup (Leiaz) * #705: New hotkey Ctrl-O to open enclosures (Leiaz) * #680: Add xdg-email command to item list popup menu (poetsmeniet) * Drop unused glade/google_source.ui * #699: Refactoring of duplicate check handlingg (dymoksc) * #746: Fix accessibility fatals reported by gla11y (reported by Paul Gevers) * Fixes #730: "Update all subscriptions at startup" overrides "Don't update this feed automatically" (Dymtro Kyrychuk, reported by Paul Wise) * Fixes #639: Right clicking on article no longer selects it (Leiaz, reported by Bruce Guenter) * #737: Update of Italian translation (madmurphy) * #745: URL fix for French example feeds (sblondon) * Update inline docs on headline view modes 2019-12-04 Lars Windolf Version 1.12.6 * #658: Add confirmation dialog when adding duplicate subscription (dymoksc) * Fixes #689: When resuming from sleep feeds were being fetched before Wifi cam up (ghost) * Fixes #685: Headerbar plugin "Mark All Read" button is not feed-specific (Robbie Cooper) * Fixes #549: Scrollbars not always present in the headline area (Leiaz) * Fixes #543: Next update overrides HTML5 enrichment (Lars Windolf) * Fixes overly wide label in enclosure preferences dialog (Lars Windolf) * Dropped unencrypted warning from auth dialog (not true anymore) (Lars Windolf) * #692: Update of Czech translation (RadimNo) * #688: Adding translatable tooltips for the headerbar plugin (Robbie Cooper) 2018-09-07 Lars Windolf Version 1.12.5 * #665: Webkit browser now supplies 'Liferea' component in user agent * #664: Added "Mark All As Read" button to headerbar plugin * #620: Added flatpak JSON (glitsj16) * #579: Added item list column drag and drop reordering (Yanko Kaneti) * #436, #662: Move from GnomeKeyring to libsecret (bgermann) * Fixes #663: Correct instapaper sharing link (Daniel Alexandersen) * Fixes #661: Update sharing links (Daniel Alexandersen) * Fixes #271: Consistent over usage of CPU (trigger by "Next Unread" loop) (reported by GreenLunar) * #472, #632: Dropping Inoreader support (API broke) 2018-07-23 Lars Windolf Version 1.12.4 * Fixes #660: Added installable plugin to change accels (Lars Windolf) * Fixes #654: Segfault on date out of range (Leaiz) * Fixes #651: Fixes Free Music Archive link in default OPMLs (reported by benjbrandall) * Fixes #649: Switch from persistent to session-only cookies (Daniel Alexandersen) * Fixes #645, #646: unread count of vfolder (Leaiz) * Fixes #637: Extra keywords in .desktop file (syndication; rss; atom) (Daniel Alexandersen) * Fixes #557: Updating counters for remote sources (Leiaz) * Updated cookie usage hint in FAQ 2018-05-02 Lars Windolf Version 1.12.3 * #634: Added setting for custom download commands (Leiaz) * #614: GTK Headerbar support via plugin (Lars Windolf) * #608: Refactoring UI code to switch to GAction and GtkBuilder Note: this implies not having icons in the main menu anymore which were still there for all non-GNOME users (see #626). (Leiaz) * #589: Item list view column order rework as a preparation for possible real column drag&drop. Introduces a new DConf setting for the column order. (Yanko Kaneti) * Fixes #280: Mark read toolbar button always disabled for search folders (Lars Windolf, reported by dvahalev) * Fixes #591: Please add a safety question when "marking all read" (Leiaz, reported by Nudin) * Fixes #625: Avoid exception in trayicon.py (Lars Windolf) * Fixes #627: GnomeKeyring plugin fails to activate when keyring doesn't exist (Lars Windolf) * Fixes #630: Fix feed list selection after DnD (Peter Zaitev) * Fixes #633: Big Memory leak in date code (Leiaz) * Update of Turkish translation (emintufan) * Update of French translation (guilieb) 2018-03-07 Lars Windolf Version 1.12.2 * Adding a plugin installer plugin that allows discovering and automatically installing 3rd party plugins * #585: Drop language from user agent to increase privacy (Daniel Aleksandersen) * #583: Add transmission-gtk and aria2 as download tool options (Daniel Aleksandersen) * #495: New command line option --disable-plugins (-p) to start with all plugins disabled. * Fixes #610: Liferea not showing up in GNOME Software (Yanko Kaneti) * Fixes #604: Correctly print error message when failing to unlock GNOME keyring (ghost) * Fixes #602: CSS style for GTK link colors not used (reported by pupyc) * Fixes #581: Redirect location updates and adds HTTP 308 (RFC 7538) support (Daniel Aleksandersen) * Fixes #578: Unable to set unread items in bold (Leiaz, reported by EverEve) * #612: Update of French translation (Guillaume Bernard) * #596: Update of Swedish translation (jony0008) * #594: Update of Polish default feed list (wmyrda) * #584: Fixes broken OPML feed list entries (Daniel Aleksandersen) * #584: Added Norwegian feed list (Daniel Aleksandersen) * #577: Fixes newsbin doc typo (Daniel Aleksandersen) 2017-12-27 Lars Windolf Version 1.12.1 * Fixes #562: Lintian spelling errors (reported by Paul Gevers) * Fixes #563: Appstream data has new format (patch by Paul Gevers) * Fixes #572: Doesn't remember some sort orders (reported by geplus) * Fixes #504: Fix assertions/crashes on changing view layouts (Leiaz) * Fixes #573: Workaround to avoid GtkPaned shrinking (Leiaz) * #566: Update of Italian translation (Gianvito Cavasoli) * #566: Update of Italian default feed list (Gianvito Cavasoli) * #514: Update of Indonesian translation (Samsul Ma'arif) * #514: Added Indonesian default feed list (Samsul Ma'arif) * Update of German translation 2017-11-30 Lars Windolf Version 1.12.0 * Fixes unhiding from tray icon when activated via GApplication (when starting Liferea a 2nd time) * #399: Reorder columns in 'Normal' email-like view to have the date column always at the end (Mikel Olasagasti) * #532: Add plugin to make unread feeds titles bold (Yanko Kaneti) * Workaround for #503: Liferea deanonymize Tor (Leiaz) * Fixes #450: #546 Resize both panes in normal and wide view (Leiaz) * Fixes #538: toggle_visibility() does not make a minimized window visible again (reported by Balló György) * Fixes #522: Segfault when switching feed in combined view (patch by jonmstone) * Fixes #419, #457: Handling of relative URLs in Atom parser (Leiaz) * Added 'View Image' context menu option in HTML view * Dropped del.icio.us from social bookmarking options as it is a read-only service now. * Redesign of the wide view mode: larger titles with small text teasers * Added optional AMP/HTML5 content enrichment feature 2017-03-26 Lars Windolf Version 1.12-rc3 * Fixes #459: Fixes GtkDoc warnings (Leiaz) * Fixes #415: Filter commands are not asynchronous (Rich Coe) * Fixes #363: Missing space above internal browser address bar (reported by nekohayo, patch by Mikel Olasagasti) * Fixes #208: All "Unread" search folder items marked read at once (Leiaz) * Fixes #251: Liferea does not always use theme icons when it is launched on system startup (reported by GreenLunar, fix by Leiaz) * Change headline column sorting in wide view to time sorted * Updated Finnish translation (Jorma Karvonen) * Updated Latvian translation (Rihards Prieditis) * Updated Albanian translation (Bensik Bleta) * Updated Hungarian translation (Balázs Úr) * Updated Brazlian translation (Rafael Ferreira) * Updated French translation (Guillaume Bernard) 2016-11-11 Lars Windolf Version 1.12-rc2 * Change all g_warnings() to g_print() for remote source to avoid "crashing" on errors. * Reorganized all UI definitions in separate files to simplify GtkBuilder handling. * Github #425: Add GeoRSS info and map link in item header (Mikel Olasagasti) * Github #407: Replacing deprecated elements in preferences (Leiaz) * Github #396: Create LifereaApplication type (Leiaz) * Github #434: Partial RFC3229+feed support for bandwidth savings (Daniel Aleksandersen) * Fixes Github #208: gtk_tree_store_get_path: assertion 'iter->stamp == priv->stamp' (reported by Mno-hime) * Fixes Github #403: Leftover OSM XSLT in item view (reported by Paul Gevers) * Fixes Github #423: Internal browser shows files system on go-back (Leiaz, reported by Paul Gevers) * Updated German translation * Github #441: Updated French translation (Surfoo) 2016-09-20 Lars Windolf Version 1.12-rc1a * Fixing missing header files 2016-09-19 Lars Windolf Version 1.12-rc1 * Github #348: Added support for downloading content that cannot be displayed by HTML widget (e.g. PDFs) (Leiaz) * Github #355: Migrate to Python3 libpeas loader (patch by picsel2) * Github #311: Upgrade to WebKit2 (patch by Leiaz) * Github #292: Show new item count in tray icon (patch by mozbugbox) * Github #297: Minimize to systray on window close (patch by Hugo Arregui) * Github #325: Auto-fitting, translated license (patches by GreenLunar and Adolfo Jayme-Barrientos) * Fixes Github #73: Problem with favicon update (reported by asl97) * Fixes Github #177, #350: Tray icon not scaled properly (patch by mozbugbox) * Removes GeoIP rendering via OSM to avoid exposing users to remote JS library resources. (reported by Paul Gevers) * Fixes Github #337: Case sensitive sorting (reported by Pi03k) * Fixes Github #361: Show all enclosuers (Leiaz) * Fixes Github #368: Segfault on liferea-feed-add (Leiaz) * Fixes Github #382: Broken Auto-Detect/No Proxy setting (Leiaz) * Fixes Github #383: Per feed don't use proxy setting is broken (reported by Leiaz) * Github #309: Update of Japanese translation (IWAI, Masaharu) * Github #329: Update of Hebrew translation (GreenLunar) * Github #330: Update of Spanish translation (Adolfo Jayme-Barrientos) * Update of Swedish translation (Andreas Ronnquist) 2016-01-30 Lars Windolf Version 1.11.7 * Github #287: Add support for media:group. (patch by Leiaz) * Github #287: Fixes issues with media:content. (patch by Leiaz) * Fixes Github #283: Bad .desktop categories definition (reported by Wuzzy2) * Fixes Github #279: Fixes rules no visible in searchdialog (patch by Leiaz) * Fixes Github #278: No "Download" tab in Tools/Preferences (docs error, reported by Anders Jonsson) * Fixes Github #83: Segfault when sorting feeds in folder (patch by Leiaz) * Fixes French translation (patch by polo2ro) * Github #300: Updated manpage (patch by GreenLunar) 2015-10-12 Lars Windolf Version 1.11.6 * Added "Do Not Track" support (enabled per default) * Github #193: Added x-scheme-handler/feed to desktop file (suggested by GreenLunar) * Github #209: Add image icons to plugins (by GreenLunar) * Github #210: Enable tests for parsing RFC822 dates with 2 digit year (patch by arunanbala) * Fixes Github #78: Shaky text in feed list (reported by GreenLunar) * Fixes Github #195: Out-dated documentation on enclose download (reported by brian-in-crawford) * Fixes Github #198: Traceback on popup notifications (reported by GreenLunar) * Fixes Github #216: Untranslatable strings (reported by GreenLunar) * Fixes Github #256: PyGIWarnings on loading plugins (patch by glitjs16) 2015-06-19 Lars Windolf Version 1.11.5 * Github #178: Implementing full screen mode for videos (mozbugbox) * Fixes Github #32: Prevent erroneous "Mark all as read" (reported by Mno-hime) * Improves Github #36, #113: UI lock up during refresh (suggested by mozbugbox) * Fixes Github #180: Removing item from (v)folder marks all read (reported by GreenLunar) * Fixes Github #140, #158: Vertical pane placement is forgotten. (patch by foresto) * Fixes Github #182: Missing config.h include in date.c (reported by Paul Gevers) * Update of Russian translation (bboa) 2015-04-14 Lars Windolf Version 1.11.4 * Fixes Github #154: Crashes while starting (corrupt icon) (reported by jcamposz) * Github #149: Fixes a random crash on startup (patch by mozbugbox) * Fixes Github #79: RTL ordering of Back/Forward icons (reported by GreenLunar) * Fixes Github #30: Segfault after updating from 1.8 to 1.10 (reported by vakuum) * Fixes Github #87: URL resolving wrong if base tag involved (reported by DanMan, fixed by mozbugbox) * Fixes all defects reported by Coverity scan * Simplied external browser handling. Now Liferea only supports the gtk_show_uri() launch mechanism for the system default browser and a user specified browser command. * Update of Albanian translation (Besnik Bleta) * Update of Hebrew translation (Genghis Khan) * Update of Spanish translation (Juan Campos Zambrana) * Fixes typo in Italian translation 2015-02-11 Lars Windolf Version 1.11.3 * Fixes Github #134: Broken default news feed. (reported by pvdl) * Fixes Github #133: Subscribe into TheOldReader categories * Fixes Github #122: Crashes at launch, "segmentation fault" (reported by geoffm) * Fixes some memory leaks (patch by Rich Coe) * Fixes Github #145: Incorrect method triggered for 'Launch External' (patch by mozbugbox) * Fixes Github #48: Window stays hidden on next start after Ctrl+W (reported by Jeff Fortin) * Expose LifereaHtmlView to GObject Introspection (patch by mozbugbox) * Improves Google Reader API error handling * Now using HTTPS only when accessing TheOldReader * Added LifereaNodeSourceActivatable interface to allow plugins implementing new node source types. * Downgrade enclosure drop warning from Glib warning to debug trace. 2015-01-06 Lars Windolf Version 1.11.2 * Fixes Github #132: Broken documentation link (reported by kallus) * Fixes Github #121: Wrapping issue in folder display (reported by Jeff Forting) * Fixes Github #114: Avoid termination on UTF-8 validation error * Fixes Github #90: Libnotify plugin not working (reported by asl97) * Fixes Github #86: Support HTTP content negotiation (suggested by DanMan) * Black-list some categories used by Google Reader clones that should not be visible. * Allowing browser history to go back to previously shown headline when browsing inside the item view. * Dropping offline option as this is duplicated with desktop environment in GNOME/network manager. * Fixes Github #100: Problems with dark Adwaita theme in GTK 3.14 (reported by majutsushi) * Fixes for preferences dialog width. (patch by Jeff Fortin) * Update of Arabic translation (Khaled Hosny) 2014-08-31 Lars Windolf Version 1.11.1 * Fixes Github #81: Inability to add subscriptions (reported by GreenLunar) * Fixes Javascript links not opening in new browser tabs * Updated Hebrew translation (Genghis Khan) * Fixes Github #88: Minor DE translation mistake (moraxy) 2014-08-12 Lars Windolf Version 1.11.0 * Added experimental InoReader support * Added experimental Reedah support * Fixes SF #1123: Mistakenly claims "TinyTinyRSS source is not self-updating" (reported by Dominik Grafenhoher) * Fixes SF #1119: Crash on font resize at startup. (reported by David Smith) * Fixes #1056, #1089, #1098: Honor preferences when opening links (patch by Daniel Seither) * Fixes #1117: Selecting last unread item in reduced feed list jumps to next feed (reported by Bruce Guenter) * Fixes missing "Via" metadata type (patch by Rich Coe) * Fixes incorrect new count reset handling in item_state.c and some of the node source implementations. * Fixes SF #1096: missing installation of liferea.convert file (reported by stqn) * Fixes SF #1135: liferea-add-feed doesn't process feed:https// (patch by Kevin Walke) * Fixes SF #1137, #1142: startup race with LifereaHtmlView (reported by Yanko Kaneti) * Fixes Github #13: Parsing errors not visible with dark themes (reported by Steve Kelly) * Fixes Github #29: Do not use bold text for feeds/folders with unread items in the leftmost treeview (repored by Jeff Fortin) * Fixes SF #1141: Liferea does not update feeds with TinyTinyRSS (reported by Dominik Grafenhofer, denk_mal, Fabian Henze) * Fixes SF #1150: subscription prop/source: not all fields and buttons visible (reported by David Smith) * Fixes Github #26: RTL comments appear incorrectly (reported by yaconf) * Fixes Github #27: Images do not autosize to fit the available space (reported by Jeff Fortin) * Fixes Github #34: Add TinyTinyRSS Enclosure Support (reported by Adrixan) * Fixes Github #43: "Any of the following" search condition doesn't work (reported by Jeff Fortin) * Fixes Github #49: Some dialogs scrolling areas do not request enough height (reported by Jeff Fortin) * Fixes Github #53: Doesn't automatically update feed name and favicon for new feed (reported by asl97) * Patch SF #224: Update to new libxml2 buffer API (Simon Kagedal Reimer) * Patch SF #209: Avoid copying list in itemset_merge_items (kaloyan) * Make Liferea use ETags and send If-None-Match (patch by Chris Siebenmann) * Support NOCONFIGURE for RPM builds (Charles A Edwards) * Rename README to README.md * Removing libindicate support (to be added as plugin maybe) * Removing libnotify support (to be added as plugin maybe) * Removing build in tray icon support * Added tray icon plugin * Added category/folder support for TheOldReader * Added folder auto-removal for TinyTinyRSS & TheOldReader * Updated README on plugin contribution * Updated Arabic translation (Khaled Hosny) 2013-10-08 Lars Windolf Version 1.10.3 * Asking for credentials again if TinyTinyRSS login fails * Asking for TinyTinyRSS credentials only 3 times * Checking wether TinyTinyRSS base URL is lost * Added warning on TinyTinyRSS login when source is not self-updating * "--debug-net --debug-verbose" now traces POST data * Patch #230 Add GNOME AppData XML (Mikel Olasagasti) * Updated Italian translation (Gianvito Cavasoli) * Updated Italian localized feed list (Gianvito Cavasoli) 2013-09-05 Lars Windolf Version 1.10.2 * Patch SF #222: Make media player seekable (Simon Kågedal Reimer) * Fixes SF #1102: Spelling error in man page (David Smith) * Fixes SF #1104: liferea.desktop missing keywords (David Smith) * Fixes SF #1105: Start Minimized to Tray Does Not Work (reported by bitlord) * Fixes SF #1114: Crashes opening browser on item without link via popup (reported by Rich Coe, David Smith) * Improved handling of broken Atom author information. (Lars Windolf) * Removed dead Google Reader code to avoid doing requests to Google. Replaced with dummy source that even allows normal feed updates. (Lars Windolf) * Added hint to FAQ on how to workaround broken Flash support (Lars Windolf) * Dumping feedlist.opml with indentation for readability. (suggested by Christoph Temmel and Simon Kågedal Reimer) 2013-07-28 Lars Windolf Version 1.10.1a * Fixes SF #1102: Liferea does not show a window (reported by genodeftest) 2013-07-28 Lars Windolf Version 1.10.1 * Fixes SF #1059: Liferea crashes with system proxy enabled (reported by genodeftest) * Fixes SF #1095: Theme color detection bug / white fonts. (reported by David Smith and others) * Fixes SF #1097: Default feed refresh interval cannot be set to 0 (reported by stqn) * Fixes SF #1100: --debug-gui crashes with segmentation fault (reported by genodeftest) * Fixes SF #1101: Outdated manpage (reported by genodeftest) * Patch SF #225: Make media player work with GStreamer 1.0 (Simon Kågedal Reimer) * Patch SF #226: Add trailing semi-colon to MimeType so that the desktop file validates (Yanko Kaneti) * Patch SF #227: Remove letfover square bracket configure.ac (Yanko Kaneti) * Patch SF #228: Add net.sf.liferea.gschema.xml to AC_CONFIG_FILES (Yanko Kaneti) 2013-07-10 Lars Windolf Version 1.10.0 * Added experimental sync support for TheOldReader (Lars Windolf) * Removed 'Update' link in comments display as it is pretty useless (Lars Windolf) * Removed 'No Comments' display as it is rather useless (Lars Windolf) * Prevent re-rendering item display on setting item flagged (Lars Windolf) * Changed unread number rendering to be right bound and non-ellipsized (Lars Windolf) * Fixes g_strstr_len assertions caused by search folder item matching (Rich Coe) * Updated documentation to reflect Google Reader, TheOldReader changes (Lars Windolf) * Removed welcome text, restoring last feed/item selection instead (Lars Windolf) * autogen.sh now reports errors on missing autoconf or intltool (suggested by Scott Kostyshak) * Correctly check for gobject-introspection build dependency (suggested by Scott Kostyshak) * Updated Basque translation (Mikel Olasagasti Uranga) * Updated Danish translation (Joe Hansen) * Updated Dutch translation (Erwin Poeze) * Updated Finnish translation (Jorma Karvonen) * Updated Russian translation (Leonid Selivanov) * Updated Ukrainian translation (Yuri Chornoivan) * Updated Vietnamese translation (Trần Ngọc Quân) * Updated German translation (Lars Windolf) 2013-05-22 Lars Windolf Version 1.10-RC4 * Added an option to convert Google Reader subscriptions to local feeds (Lars Windolf) * Fixes SF #1080: segfault opening attachment due to incorrect g_free() (reported by Adam Nielsen) * Fixes SF #1075: GLib warnings of "string != NULL" assertion failure (reported by Simon Kågedal Reimer) * Fixes missing shading in 2-pane mode rendering (reported by Zoho Vignochi) * Fixes search folders including comment items (reported by David Willmore) 2013-05-22 Lars Windolf Version 1.10-RC3 * Fixes SF #1069: broken rendering in tt-rss feeds (patch by Simon Kågedal Reimer) * Merged SF #219: View *.xml files along with *.opml files in file chooser (patch by Simon Kågedal Reimer) * Merged SF #233: Show feed name in item view when in merged views. (patch by Simon Kågedal Reimer) * Merged SF #193: Use GtkInfoBar for note in preferences window (patch by Fred Morcos) * Require intltool >= 0.40.4 (Adrian Bunk) * Updated Catalan translation (Gil Forcada) * Updated Danish translation (Joe Hansen) * Updated Polish translation (Piotr Sokół) 2013-05-12 Lars Windolf Version 1.10-RC2 * Extended user agent by "AppleWebKit (KHTML, like Gecko)" to solve incorrect mobile redirect with zdf.de * Added social bookmarking support for Mister Wong * Added social bookmarking support for Google Bookmarks * Update of German FAQ * Update of English FAQ * Added MimeType to .desktop file (Craig Barnes) * Fixes SF #1063: Can't open preferences twice (Emilio Pozuelo Monfort, reported by David Smith) * Fixes SF #1040: In feed entries, spaces are replaced with "+" (reported by Emmanuel Seyman) * Fixes SF #1051: Issues in RTL GUI of Liferea (reported by phixy) * Fixes SF #1038, #1074: Updates ttrss feeds over and over (reported by many users) * Fix several memory leaks (Emilio Pozuelo Monfort) * Require glib >= 2.28 for GApplication (Adrian Bunk) * Use the GTK+ 3 version, not wrongly the GTK+ 2 version, of the libindicate GTK+ bindings (Adrian Bunk) * Updated the default feedlists (Adrian Bunk) * Removed support for libnotify < 0.7 (Adrian Bunk) * Added Vietnamese translation (Trần Ngọc Quân) * Updated Albanian translation (Besnik Bleta) * Updated Asturian translation (Iñigo Varela) * Updated Basque translation (Mikel Olasagasti Uranga) * Updated Danish translation (Joe Hansen) * Updated Finnish translation (Jorma Karvonen) * Updated German translation (Christian Stadelmann) * Updated Hungarian translation (Gabor Kelemen) * Updated Japanese translation (Takeshi Hamasaki) * Updated Latvian translation (Rihards Priedītis) * Updated Ukrainian translation (Yuri Chornoivan) 2013-01-30 Lars Windolf Version 1.10-RC1 Please note that due to the SourceForge upgrade bug ticket numbering did change. This might be confusing... Old numbers are 7 figures, newer ones only 4! * Patch SF #3407290: Migrate to GSettings (by Mikel Olasagasti) * Patch SF #3579177: Change .desktop category to News;Feed; (by Stanislav Brabec) * Fix for Debian #668197: x-www-browser preference not working (David Smith) * Added slider and time display to media player plugin. * Added Google Plus to social bookmarking options. * Removing deprecated g_thread_init() call * Auto-enable plugins on migration * Added missing -a option to manpage * Updated manpage to reflect XDG path migration * Changing GSettings path from /apps/liferea to /org/gnome/liferea * Changes default download thread concurrency from 2 to 3 * Fixes regression about using the GNOME default font * Improves all item/link launching menus to consistently provide three options: Tab, Browser and External Browser * Fixes SF #1037: Incorrect notifications for Google Reader (patch by David Smith) * Fixes SF #1048: Removed all feedvalidator.org references from FAQ and XSLT as it was reported to host malware. (reported by bkat) * Fixes SF #1041: Some GPLv2 license headers were outdated (reported by Emmanuel Seyman) * Fixes SF #1044: tt-rss API changed (we now support only 1.6 API) (patch by Sebastian Noel) * Fixes assertion when creating new tt-rss subscriptions * Fixes XHTML errors caused by extra tags returned by tt-rss * Fixes missing item list update when browsing item URLs in Liferea 2012-10-28 Lars Windolf Version 1.9.7 * Added new preference for default viewing mode. * Changing toolbar button order to prevent accidental clicks on "Mark All Read" when clicking on more frequent buttons like "Next Unread". * Added Google Chrome as a browser choice to preferences. * Roughly reordered browser choices after browser market share. * Removed shading behaviour for unread items in combined view as it doesn't match GTK theming well * Removed auto-hide Javascript menu from combined view to simplify rendering in 3-pane modes. * Fixes items not removed from search folder count when feed is removed. * Fixes search folder rebuilding (do not include comment items). * Fixes SELECT offset handling when rebuilding search folders. * Now gives feedback when rebuilding search folders in feed list. * Update of German translation 2012-10-09 Lars Windolf Version 1.9.6 * Removed "pass URL" check box from MIME type dialog. * Removed "Save In" entry from "Download" tab in preferences. * Removed "curl" choice in download tool preferences. * Removed "wget" choice in download tool preferences. * Added "steadyflow" choice in download tool preferences. * Patch SF #3569056: Use symbolic close buttons and spacing on tabs like gedit (Sebastian Keller) * Fixes reloading item when browsing the web inside the item view. * Fixes preferences dialog not opening up a second time. * Fixes padding/alignments in preferences dialog. * Fixes SF #1418701: Remote server pounded into dirt on auto-download (reported by anonymous) * Fixes SF #3567827: Double border around webview (reported by borschty) * Fixes SF #3572660: crash in google_source_remove_node (reported by Yanko Kaneti) * Prevents adding folders/search folders/newsbins to Google Reader * Prevents sorting subscriptions in Google Reader * Updated Polish translation (Wojciech Myrda) 2012-09-14 Lars Windolf Version 1.9.5 * GIR dependencies are now mandatory * Migration to XDG directory layout in $HOME * Migrate from X session manager to GtkApplication * Raising GTK dependency to 3.4 for GtkApplication * Storing last window state in GConf now instead in the session command * Added Instapaper.com to social bookmarking sites (SF #3564393) (patch by prurigro) * Use hint label for manual browser command preference (SF #3129429) (patch by Fred Morcos) * Fixes comments_deinit() never being called * Fixes search folder counter update on feed removal * Fixes SF #3567715: Crash on network online status changes (patch by Yanko Kaneti) 2012-08-24 Lars Windolf Version 1.9.4 * Changes (c) name "Lars Lindner" -> "Lars Windolf" due to marriage * Removed compilation support for GTK2 * Added GIR plugin system with libpeas * Added GnomeKeyring plugin that stores password in a keyring instead of in the exported OPML. * Added simple media player plugin to play audio and video enclosures. * Only present enclosures of audio and video MIME type * Raise libindicate minimum dependency to 0.6 * Patch SF #3515882: Also support libindicate 0.7 (Chow Loong Jin) * Dropping SIGSEGV signal handler to allow distro crash report tools to work (as found in Ubuntu) * Ensure node ids are in DB node relation on startup. * Adding AM_PROG_AR to configure.ac to work with automake 1.12 * Moved tab close button from the URL bar to the right of the tab label. * Smarter browser toolbar: appears now also in the item view when browsing external content. * Don't ask for Google Reader authentication more than three times with auto-update to avoid annoying the user. * Fixes SF Trac #10: Crash on empty search folders within folders (reported by phyxi) * Fixes SF Trac #19: Auto-load-link doesn't work with feeds with comments (reported by wonk0) * Fixes SF #2855990: Crash when dragging Google Reader feeds outside Google Reader. This is now prevented. (reported by algnod) * Fixes SF #3515880: missing include when compiling with libindicate (patch by Chow Loong Jin) * Fixes search folders being invisible in reduced mode. * Fixes ever growing temporary DB files. (patch by Sven Hartge) * Fixes visibility of enclosure list view for Ubuntu. * Fixes crashes on enclosure list context menu. * Fixes SF #3557513: Fixes crash on empty links in auto-load-link mode. (patch by msquared84) * Fixes unknown metadata types reported in trace when loading Google Reader subscriptions from DB. 2012-03-30 Lars Windolf Version 1.9.3 * Added a new item history feature that allows navigating through recently viewed items. * Added new "Fullscreen" toggle menu option. * For GTK+3: request dark theme variant for better contrast between GUI and content. (Jeff Fortin) * Change schema defaults for folder display. Now unread items are loaded per-default when clicking a folder. * Patch SF #3473743: GTK2 dependency has to be 2.24 (bento) * Improve DB item counting statements. (patch by Regis Floret) * Change OpenStreetMap rendering from osmarender to mapnik. (patch by Mikel Olasagasti) * Patch SF #3127016: Automatic scrollbars on enclosure actions view (patch by Fred Morcos) * SF Trac #7: Removing icon from "Cancel All" in update dialog so that .gtkrc "gtk-button-images=0" does have correct effect. (reported by phixy) * Fixes SF #3480238: crashes when double clicking find (reported by joeserneem) * Fixes Debian #660602: Item pane may be reset during feed update (reported by Ben Hutchings) * Reimplemented search folder rule for item with enclosures. * Reimplemented search folder rule for item categories. * Reimplemented feed title matching rule for search folders. (patch by John Levon) * Updated Catalan translation (Gil Forcada) 2012-03-23 Lars Windolf Version 1.9.2 * Fixes another migration issue left from 1.9.1 * Increasing sqlite3 dependency to 3.7+ for WAL journaling. * Removed sqliteasync code in favour of WAL journaling. This significantly improves performance for ext4. * Added indices for parent_item_id and parent_node_id to avoid slow item removal. (suggested by Paulo Anes) 2012-03-18 Lars Windolf Version 1.9.1 * Disabled migration to ~/.liferea_1.9 * Revert ISO 8601 parsing using Glib due to Debian #653196 This fixes SF #3465106 (reported by Vincent Lefevre) * Fixes SF #3477582: welcome screen not using theme colors. (reported by stqn) * Do not update DB node and subscription info on startup for performance reasons. * Perform VACCUM only when page fragmentation ratio < 10%. (suggested by adriatic) * Removed tooltip on the "Next Unread Item" button to avoid having it flashing each time it is clicked when skimming through items. 2011-12-23 Lars Windolf Version 1.9.0 * Add configure switch to compile against GTK2 or GTK3. (Emilio Pozuelo Monfort, Adrian Bunk) * Raise dependencies and updated code to compile against GTK3. (Emilio Pozuelo Monfort, Adrian Bunk) * Fixes proxy preference not affecting the HTML widget. (reported by Chris Siebenmann) * Fixes SF #3363481: Feeds fail to update properly when entries ordered "wrong" (patch by Robert Trace) * Fixes writing subscriptions into DB when importing from OPML (reported by Dennis Nezic) 2011-12-10 Lars Windolf Version 1.8.0 * Fixes SF #3441643: Deleting a feed also removes items copied to news bins (reported by Jan Larres) * Updated French translation (Vincent Lefevre) * Added Hungarian default feed list (Gabor Kelemen) * Removed broken feeds from all default feed lists. (suggested by Gabor Kelemen) 2011-11-16 Lars Windolf Version 1.8 RC2 * Fixes Basque feed list. (Mikel Olasagasti) * Added user template CSS that helps users changing the CSS definitions and is installed into the cache directory per default. (suggested by Jeff Fortin) * Fixes SF #3349330: Segfault when dropping folders into Google Reader subscription. (reported by no|disc) * Fixes SF #3046762: DB contains old comment items of deleted feed entries. (reported by FuturePilot) * Added Latvian translation. (Rihards Priedītis, Rūdolfs Mazurs) * Added Lithuanian translation. (Mindaugas Baranauskas) * Updated Basque translation. (Mikel Olasagasti Uranga) * Updated Chinese/Simplified translation. (Aron Xu) * Updated Dutch translation. (Erwin Poeze) * Updated Hungarian translation. (Gabor Kelemen) * Updated Russian translation. (Leonid Selivanov) * Updated Swedish translation. (Daniel Nylander) * Updated Ukrainian translation. (Yuri Chornoivan) 2011-09-14 Lars Windolf Version 1.8 RC1 * Migrate cache directory to ~/.liferea_1.8 * Merges SF #3367900: Fixes memory allocation issues. (patch by doomkopf) * Merges new default feedlist for European Portuguese (provided by Bruno Miguel) * Fixes SF #3398789: Keeps marking read items as unread. (reported by naoliv) * Updated manual page 2011-06-21 Lars Windolf Version 1.7.6 (Unstable) * Fixes SF #3102116: MIME type definitions not saved. (reported by Alexander Gnodtke) * Merges SF #3273050: Added diggio bookmarking option. (patch by Daniel Noffsinger) * Merges SF #3273213: Open Social bookmarking in tab. (patch by Daniel Noffsinger) * Allow reordering browser tabs. * Support popup menu key in feed/item/enclosure tree view. * Raise tt-rss dependency to 1.5 (patch by Paulo Schreiner) * Update tt-rss subscriptions when source node is updated. (patch by Paulo Schreiner) * Fixes 2 way item state sync for tt-rss subscriptions. (patch by Paulo Schreiner) * Fixes SF #3162756: HTML view doesn't use configured font. (reported by nomnex) * Add X-GNOME-FullName to desktop file. (Maia Kozheva) * Added optional libindicate support. (Maia Kozheva) * Added partial Google Reader label support: Labels are now sync'ed as folders from Google into Liferea. (patch by Peter Oliver and Sergey Snitsaruk) * Fixes accidental drag&drop in HTML view. * Updated Polish translation (Myrda Wojciech) 2011-04-14 Lars Windolf Version 1.7.5 (Unstable) * Some more GTK+ GSEAL work. (Emilio Pozuelo Monfort) * Make "Disable Javascript" and "Enable plugins" preferences have an effect without having to restart Liferea. (Emilio Pozuelo Monfort) * Fix NetworkManager support. (Emilio Pozuelo Monfort) * Code cleanups. (Fabian Keil) * Fixes SF #2883678: Shorter notification messages (Ted Gould) * Fixes SF #2965158: Enclosures URLs with spaces do not work. (reported by Michael Novak) * Replace the deprecated GTK_WIDGET_TYPE with G_OBJECT_TYPE. (Adrian Bunk) * Fixes SF #2981879: Unknown metadata type itemTitle (reported by stk1) * Removing red title bar for flagged headlines. This is to avoid suggesting an error situation. (suggested by Felipe Ignacio Canas Sabat) * Rewrite of the search folder code. We do not use SQlite views for rule based matching anymore. (Lars Windolf) * Added identi.ca bookmarking support. (Adrian Bunk) * Added copy text selection to clipboard option. (patch by Ricardo Cruz) * Fixes SF #2994622: Atom author URIs not markup escaped. (reported by Ricardo Cruz) * Fixes SF #2829961: spaces are no urlencoded on copy link (reported by Winston Weinert) * Fixes SF #2901447: comma in link prevents launching browser (reported by Rafal Ochmanski) * Fixes SF #3002400: tags makes text invisible (patch by Sergey Snitsaruk) * Improve the UI responsiveness by using sqlite3async. (patch by Wictor Lund) * Improved localization handling of filenames. (Adrian Bunk) * Added new DBUS method to trigger a global feed updated. (patch by Matthew Bauer) * Removing feed update state from DB and simplifying startup feed update options. (Lars Windolf) * Fixes SF #3019939, Debian #586926: Broken Google Reader authentication. (Arnold Noronha) * Don't ship autogen.sh in release tarballs. (Adrian Bunk) * Added --debug-vfolder option. (Lars Windolf) * Added feature to sort feeds in a folder alphabetically. (Lars Windolf) * Require WebKitGTK+ >= 1.2.2 to avoid bugs in older versions. (Adrian Bunk) * Better error messages when essential files are missing under /usr/share/liferea. (Adrian Bunk) * Fixes a crash with feeds with relative item links and empty channel link. (Adrian Bunk) * Fixes SF #3039421: Useless trailing spaces in litereals. (reported by Joe Hansen) * Adapt on_tab_switched() to GTK+ API change. (Adrian Bunk) * Fixes SF #3067801: crash in enclosure handling (patch by Peter Oliver) * Fixes SF #3060658: Save As Enclosure doesn't set directory. (Lars Windolf) * Added "Copy Link/Image Location" to HTML view (Lars Windolf) * Added "Save Link/Image As" to HTML view (Lars Windolf) * Fixes Debian #593415: Seems to misidentify MIME type of some podcast enclosures (reported by Celejar) * Removed the obsolete Bloglines support. (Adrian Bunk) * Also print the stacktrace when debugging is not enabled. (Adrian Bunk) * Removed the outdated .spec file. (Adrian Bunk) * Merged patch SF #3089150: Bidi support (Yaron Sheffer) * Fixes second search not clearing item list. (Lars Windolf) * Fixes SF #3019444 and #2978831: incorrect handling of HTTP 301 (patch by Solomon Peachy) * Added support for the new libnotify API. (Emilio Pozuelo Monfort) * Fixes Debian #600638: Wrong number of unread items in mouse over tray icon with zh_TW locale. (Adrian Bunk) * Port from dbus-glib to GDBus. (Emilio Pozuelo Monfort) * Port from libnm-glib and dbus-glib to direct DBus calls using GDBus. This way we always build the NetworkManager support, and it's only used if it's running on the user's system. (Emilio Pozuelo Monfort) * Removed bookmarking support for the dead Backflip. (Adrian Bunk, reported by Kenneth Lakin) * SF #3127001: Fix Toolbar Settings label wrong xalign in preferences dialog (patch by Fred Morcos) * SF #3177676: Cannot handle gzipped HTTP responses. (patch by hyperair) * SF #3132354: Show tooltip when column to small to show text (patch by Ricardo Cruz) * SF #3203121: Add support for NM 0.9 (patch by Dan Williams) * SF #3019505: Fixed drag and drop in the feed list. (Emilio Pozuelo Monfort) * Fixes Debian #538250: Revert item_set_description() to the 1.4 code since the HTML detection created too many false negatives. (Adrian Bunk, reported by Wouter Verhelst) * Added Asturian translation. (Marquinos) * Added Danish translation. (Joe Hansen) * Added Galician translation. (Anxo Outeiral) * Added Hebrew translation. (Yaron Sheffer) * Added Ukrainian translation. (Yuri Chornoivan) * Updated Albanian translation. (Besnik Bleta) * Updated Arabic translation. (Khaled Hosny) * Updated Chinese/Simplified translation. (Aron Xu) * Updated German translation. (Lars Windolf) * Updated Italian translation. (Gianvito Cavasoli) * Updated Russian translation. (Leonid Selivanov) * Updated Slovak translation. (Pavol Klacansk) 2010-02-16 Adrian Bunk Version 1.7.4 (Unstable) * Merged patch that fixes LP #238958: always present window on current workspace if tray icon is clicked. (patch by Fabien Tassin and Sasa Bodiroza) * Cleanup of default stylesheet. Removed used styles. (Lars Windolf) * Permanently removed LUA scripting support. (Lars Windolf) * Added more default feeds to be not so open source specific. (Lars Windolf) * Require libsoup >= 2.28.2 to avoid bugs in older versions. (Adrian Bunk) * Updated Spanish translation. (Rodrigo Gallardo) 2010-01-24 Lars Windolf Version 1.7.3 (Unstable) * Added patch SF #2883678: Support for notification append (patch by Ted Gould) * Implemented folder re-expansion when switching from reduced feed list mode back to full hierarchic mode. (Lars Windolf) * Updated proxy settings are now forwarded to the internal renderer again. (Lars Windolf) * Fixes SF #2872001: Allowing compilation against NetworkManager 0.8 which decided to force 100000 packages out there to support both "libnm-glib" and "libnm_glib". (reported by Michal Nowak) * Fixes an RSS 1.1 parsing bug that caused items not to be parsed at all (reported by Roberto Guido) * Fixes SF #2883971: proxy authentication doesn't work (reported by Louis White and others) * Improves handling of feeds with relative item links. (patch by Rafael Kitover) * Fixes SF #2928927: Remember sort column when sorting by item state (patch by Andy Kittner) * Readded workaround for zoom setting of zero (which prevents zooming). (patch by Rafael Kitover) * Use gtk_show_uri() instead of relying on "gnome-open". (Lars Windolf) * Using g_time_val_from_iso8601 instead of propietary solution we had. (Lars Windolf) * Simplified pixmaps resolving as we used only one source directory anyway. (Lars Windolf) * Updated Catalan translation. (Gil Forcada) 2009-11-19 Adrian Bunk Version 1.7.2 (Unstable) * Fixes SF #2827075: Migrate from libglade to GtkBuilder. (Hubert Figuiere and Emilio Pozuelo Monfort) * Fixes SF #2831121: Require WebKitGTK+ >= 1.1.11 since older versions crash frequently when built with gcc 4.4. (Adrian Bunk) * Go back to using libtool for getting a proper rpath with libraries in unusual locations found through pkg-config. (Adrian Bunk, reported by Leon Nardella) * Fixes SF #2831007: Opening enclosures by URL fails intermitantly (Lars Windolf, reported by Eric Drechsel) * Fixes a unique node id check that could have caused duplicate node ids. (Lars Windolf) * Removes the GConf option for a user defined date format. (Lars Windolf) * We do not update comment feeds returning HTTP 410 anymore. (Lars Windolf) * Fixes SF #2897668: Crash when adding Bloglines subscriptions. (Lars Windolf) * Fixes a crash when deleting items. (Lars Windolf) * Fixes SF #2823526: Increase the width of the left column in the Script Manager. (Adrian Bunk, reported by Pavol Klačanský) * Fixes Debian #539857: Hide link cosmos for items that don't have a valid url, instead of displaying it and crashing when clicked. (Emilio Pozuelo Monfort, reported by Nelson A. de Oliveira) * Use silent automake rules. (Adrian Bunk) * Patch #2843340: Fixes handling of duplicate ids in OPML files. (Ruper Swarbrick) * Added a timeout for downloads. (Arnold Noronha) * Fixes SF #2861203: Crash when creating new search folder. (reported by Andreas Kern and larslj) * Fixes SF #2873588: Error in welcome message. (reported by adiroiban) * Use soup_message_disable_feature() to disable proxy and cookies when needed, instead of creating multiple SoupSessions, one for each use case. Require libsoup >= 2.28.0 for it. (Emilio Pozuelo Monfort) * Stop accessing GTK+ structures directly, use accessors instead in preparation for GTK+ 3.0. Require gtk+ >= 2.18.0 for it. (Emilio Pozuelo Monfort) * Simplified DB schema to avoid costly cleanup. (Lars Windolf) * Patch SF #2894307: Fixes key cycling in items list. (Simon Lipp) * Put the next selected item on top when scrolling in the item list during Next-Unread. (patch by JustFillBug) * Updated Dutch translation. (Erwin Poeze) * Updated French translation. (Vincent Lefevre) * Updated Hungarian translation. (Gabor Kelemen) * Updated Romanian translation. (Adi Roiban) * Updated Slovak translation. (Pavol Klacansk) 2009-08-10 Adrian Bunk Version 1.7.1 (Unstable) * Re-fix item being unselected when opening preferences for the first time, or when (un)hiding read items from folders. (Emilio Pozuelo Monfort) * Fixes a crash when adding subscriptions that needs feed auto discovery. (Arnold Noronha) * Add a --add-feed option that adds a new subscription. It can also be used while Liferea is running, and it will add the feed to the running process. (Emilio Pozuelo Monfort) * Let liferea-add-feed work when there is no DBus support by using --add-feed. (Emilio Pozuelo Monfort) * Changed the update queue logic which previously restricted the number of updates to 1 per 500ms under peak loads. (Arnold Noronha) * Removed a not-useful-enough interface to save/load GoogleSource edits to disk across Liferea sessions. (Arnold Noronha) * Link directly with libgthread-2.0 and libICE, fixes linking with gold. (Adrian Bunk) * Re-add grayflag.png since it is actually used. (Adrian Bunk, reported by Maik Zumstrull) * Let notification.h be shipped in tarballs. (Emilio Pozuelo Monfort, reported by George Sherwood) * Don't ship Google and Bloglines icons, download them as with any other subscription instead. (Emilio Pozuelo Monfort) * Fix advanced new subscription dialog to set the filter filechooser bar insensitive if the filter checkbox is unset. Also make the feed properties dialog not completely hide the filter filechooser, only set it insensitive if the filter checkbox is unset. (Emilio Pozuelo Monfort) [truncated] liferea-1.13.7/Dockerfile.travis000066400000000000000000000007771415350204600165260ustar00rootroot00000000000000FROM ubuntu:20.04 MAINTAINER Lars Windolf ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install --no-install-recommends -y libtool intltool gcc automake autoconf gla11y libxml2-dev libxslt1-dev libgtk-3-dev libwebkit2gtk-4.0-dev libpeas-dev libsqlite3-dev libjson-glib-dev libgirepository1.0-dev gsettings-desktop-schemas-dev RUN mkdir -p /src/ WORKDIR /src/ COPY . /src/ RUN ./autogen.sh RUN ["/bin/bash", "-c", "set -o pipefail && ./configure && make && cd src/tests && make test"] liferea-1.13.7/INSTALL000066400000000000000000000227701415350204600142530ustar00rootroot00000000000000Installation Instructions ************************* Copyright (C) 1994, 1995, 1996, 1999, 2000, 2001, 2002, 2004, 2005 Free Software Foundation, Inc. This file is free documentation; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. Basic Installation ================== These are generic installation instructions. The `configure' shell script attempts to guess correct values for various system-dependent variables used during compilation. It uses those values to create a `Makefile' in each directory of the package. It may also create one or more `.h' files containing system-dependent definitions. Finally, it creates a shell script `config.status' that you can run in the future to recreate the current configuration, and a file `config.log' containing compiler output (useful mainly for debugging `configure'). It can also use an optional file (typically called `config.cache' and enabled with `--cache-file=config.cache' or simply `-C') that saves the results of its tests to speed up reconfiguring. (Caching is disabled by default to prevent problems with accidental use of stale cache files.) If you need to do unusual things to compile the package, please try to figure out how `configure' could check whether to do them, and mail diffs or instructions to the address given in the `README' so they can be considered for the next release. If you are using the cache, and at some point `config.cache' contains results you don't want to keep, you may remove or edit it. The file `configure.ac' (or `configure.in') is used to create `configure' by a program called `autoconf'. You only need `configure.ac' if you want to change it or regenerate `configure' using a newer version of `autoconf'. The simplest way to compile this package is: 1.A. Compiling from a Tarball release `cd' to the directory containing the package's source code and type `./configure' to configure the package for your system. If you're using `csh' on an old version of System V, you might need to type `sh ./configure' instead to prevent `csh' from trying to execute `configure' itself. 1.B. Or compiling from Git `cd' to the directory containing the package's source code and type `./autogen.sh' to configure the package for your system. Running `configure' or 'autogen.sh' may take a while. While running, it prints some messages telling which features it is checking for. 2. Type `make' to compile the package. 3. Optionally, type `make check' to run any self-tests that come with the package. 4. Type `make install' to install the programs and any data files and documentation. 5. You can remove the program binaries and object files from the source code directory by typing `make clean'. To also remove the files that `configure' created (so you can compile the package for a different kind of computer), type `make distclean'. There is also a `make maintainer-clean' target, but that is intended mainly for the package's developers. If you use it, you may have to get all sorts of other programs in order to regenerate files that came with the distribution. Compilers and Options ===================== Some systems require unusual options for compilation or linking that the `configure' script does not know about. Run `./configure --help' for details on some of the pertinent environment variables. You can give `configure' initial values for configuration parameters by setting variables in the command line or in the environment. Here is an example: ./configure CC=c89 CFLAGS=-O2 LIBS=-lposix *Note Defining Variables::, for more details. Compiling For Multiple Architectures ==================================== You can compile the package for more than one kind of computer at the same time, by placing the object files for each architecture in their own directory. To do this, you must use a version of `make' that supports the `VPATH' variable, such as GNU `make'. `cd' to the directory where you want the object files and executables to go and run the `configure' script. `configure' automatically checks for the source code in the directory that `configure' is in and in `..'. If you have to use a `make' that does not support the `VPATH' variable, you have to compile the package for one architecture at a time in the source code directory. After you have installed the package for one architecture, use `make distclean' before reconfiguring for another architecture. Installation Names ================== By default, `make install' installs the package's commands under `/usr/local/bin', include files under `/usr/local/include', etc. You can specify an installation prefix other than `/usr/local' by giving `configure' the option `--prefix=PREFIX'. You can specify separate installation prefixes for architecture-specific files and architecture-independent files. If you pass the option `--exec-prefix=PREFIX' to `configure', the package uses PREFIX as the prefix for installing programs and libraries. Documentation and other data files still use the regular prefix. In addition, if you use an unusual directory layout you can give options like `--bindir=DIR' to specify different values for particular kinds of files. Run `configure --help' for a list of the directories you can set and what kinds of files go in them. If the package supports it, you can cause programs to be installed with an extra prefix or suffix on their names by giving `configure' the option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. Optional Features ================= Some packages pay attention to `--enable-FEATURE' options to `configure', where FEATURE indicates an optional part of the package. They may also pay attention to `--with-PACKAGE' options, where PACKAGE is something like `gnu-as' or `x' (for the X Window System). The `README' should mention any `--enable-' and `--with-' options that the package recognizes. For packages that use the X Window System, `configure' can usually find the X include and library files automatically, but if it doesn't, you can use the `configure' options `--x-includes=DIR' and `--x-libraries=DIR' to specify their locations. Specifying the System Type ========================== There may be some features `configure' cannot figure out automatically, but needs to determine by the type of machine the package will run on. Usually, assuming the package is built to be run on the _same_ architectures, `configure' can figure that out, but if it prints a message saying it cannot guess the machine type, give it the `--build=TYPE' option. TYPE can either be a short name for the system type, such as `sun4', or a canonical name which has the form: CPU-COMPANY-SYSTEM where SYSTEM can have one of these forms: OS KERNEL-OS See the file `config.sub' for the possible values of each field. If `config.sub' isn't included in this package, then this package doesn't need to know the machine type. If you are _building_ compiler tools for cross-compiling, you should use the option `--target=TYPE' to select the type of system they will produce code for. If you want to _use_ a cross compiler, that generates code for a platform different from the build platform, you should specify the "host" platform (i.e., that on which the generated programs will eventually be run) with `--host=TYPE'. Sharing Defaults ================ If you want to set default values for `configure' scripts to share, you can create a site shell script called `config.site' that gives default values for variables like `CC', `cache_file', and `prefix'. `configure' looks for `PREFIX/share/config.site' if it exists, then `PREFIX/etc/config.site' if it exists. Or, you can set the `CONFIG_SITE' environment variable to the location of the site script. A warning: not all `configure' scripts look for a site script. Defining Variables ================== Variables not defined in a site shell script can be set in the environment passed to `configure'. However, some packages may run configure again during the build, and the customized values of these variables may be lost. In order to avoid this problem, you should set them in the `configure' command line, using `VAR=value'. For example: ./configure CC=/usr/local2/bin/gcc causes the specified `gcc' to be used as the C compiler (unless it is overridden in the site shell script). Here is a another example: /bin/bash ./configure CONFIG_SHELL=/bin/bash Here the `CONFIG_SHELL=/bin/bash' operand causes subsequent configuration-related scripts to be executed by `/bin/bash'. `configure' Invocation ====================== `configure' recognizes the following options to control how it operates. `--help' `-h' Print a summary of the options to `configure', and exit. `--version' `-V' Print the version of Autoconf used to generate the `configure' script, and exit. `--cache-file=FILE' Enable the cache: use and save the results of the tests in FILE, traditionally `config.cache'. FILE defaults to `/dev/null' to disable caching. `--config-cache' `-C' Alias for `--cache-file=config.cache'. `--quiet' `--silent' `-q' Do not print messages saying which checks are being made. To suppress all normal output, redirect it to `/dev/null' (any error messages will still be shown). `--srcdir=DIR' Look for the package's source code in directory DIR. Usually `configure' can determine that directory automatically. `configure' also accepts some other, not widely useful, options. Run `configure --help' for more details. liferea-1.13.7/Makefile.am000066400000000000000000000041711415350204600152510ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in SUBDIRS = doc man opml pixmaps po src xslt glade desktop_in_files = net.sourceforge.liferea.desktop.in desktopdir = $(datadir)/applications desktop_DATA = $(desktop_in_files:.desktop.in=.desktop) @INTLTOOL_DESKTOP_RULE@ dbusservicedir = $(datadir)/dbus-1/services dbusservice_DATA = net.sourceforge.liferea.service net.sourceforge.liferea.service: Makefile $(AM_V_GEN) (echo '[D-BUS Service]'; \ echo 'Name=net.sourceforge.liferea'; \ echo 'Exec=${bindir}/liferea --gapplication-service') > $@.tmp && \ mv $@.tmp $@ appdatadir = $(datadir)/metainfo appdata_in_files = net.sourceforge.liferea.appdata.xml.in appdata_DATA = $(appdata_in_files:.xml.in=.xml) @INTLTOOL_XML_RULE@ cssdir = $(pkgdatadir)/css css_DATA = \ css/liferea.css \ css/user.css jsdir = $(pkgdatadir)/js js_DATA = \ js/gresource.xml \ js/htmlview.js \ js/Readability.js dtddir = $(pkgdatadir)/dtd dtd_DATA = dtd/html.ent plugindir = $(pkglibdir)/plugins plugin_DATA = \ plugins/bold-unread.py \ plugins/bold-unread.plugin \ plugins/getfocus.py \ plugins/getfocus.plugin \ plugins/gnome-keyring.py \ plugins/gnome-keyring.plugin \ plugins/headerbar.py \ plugins/headerbar.plugin \ plugins/libnotify.py \ plugins/libnotify.plugin \ plugins/media-player.py \ plugins/media-player.plugin \ plugins/pane.py \ plugins/pane.plugin \ plugins/plugin-installer.py \ plugins/plugin-installer.plugin \ plugins/trayicon.py \ plugins/trayicon.plugin gsettings_SCHEMAS = net.sf.liferea.gschema.xml @INTLTOOL_XML_NOMERGE_RULE@ @GSETTINGS_RULES@ data_convertdir = $(datadir)/GConf/gsettings dist_data_convert_DATA = liferea.convert EXTRA_DIST = \ net.sf.liferea.gschema.xml.in \ po/liferea.pot \ $(desktop_in_files) \ $(desktop_DATA) \ $(schema_DATA) \ $(css_DATA) \ $(js_DATA) \ $(dtd_DATA) \ $(plugin_DATA) \ $(gsettings_SCHEMAS) \ $(appdata_in_files) \ $(appdata_DATA) DISTCLEANFILES = \ liferea.desktop \ net.sourceforge.liferea.service \ intltool-extract \ intltool-merge \ intltool-update CLEANFILES = \ $(gsettings_SCHEMAS) \ $(appdata_DATA) po/liferea.pot: cd po && $(MAKE) liferea.pot liferea-1.13.7/README.md000066400000000000000000000164631415350204600145030ustar00rootroot00000000000000[![Build Status](https://github.com/lwindolf/liferea/actions/workflows/cb.yml/badge.svg)](https://github.com/lwindolf/liferea/actions/workflows/cb.yml) [![Packages](https://repology.org/badge/latest-versions/liferea.svg)](https://repology.org/metapackage/liferea/versions) [![Packages](https://repology.org/badge/tiny-repos/liferea.svg)](https://repology.org/metapackage/liferea/versions) [![Dependency](https://img.shields.io/librariesio/github/lwindolf/liferea)](https://libraries.io/github/lwindolf/liferea) Introduction ------------ Liferea is a desktop feed reader/news aggregator that brings together all of the content from your favorite subscriptions into a simple interface that makes it easy to organize and browse feeds. Its GUI is similar to a desktop mail/news client, with an embedded web browser. ![screenshot](https://lzone.de/liferea/screenshots/screenshot2.png) Installation from Package ------------------------- For distro specific package installation check out https://lzone.de/liferea/install.htm Building Liferea Yourself ------------------------ This section describes how to compile Liferea yourself. If you have any problems compiling the source file an issue at Github and we will help you asap. ###### _Mandatory Dependencies_ libxml2-dev libxslt1-dev libsqlite3-dev libwebkit2gtk-4.0-dev libjson-glib-dev libgirepository1.0-dev libpeas-dev gsettings-desktop-schemas-dev python3 libtool intltool fribidi-dev ###### _Compiling from Tarball_ Download a tarball from https://github.com/lwindolf/liferea/releases and extract and compile with tar jxvf liferea-1.13.3.tar.bz2 ./configure make sudo make install ###### _Compiling from Git_ Check out the code: git clone https://github.com/lwindolf/liferea.git Then build it with: ./autogen.sh make sudo make install If you compile with a --prefix directory which does not match $XDG_DATA_DIRS you will get a runtime error about the schema not being found. To workaround set $XDG_DATA_DIRS before starting Liferea. For example: my_dir=$HOME/tmp/liferea ./autogen.sh --prefix=$my_dir make sudo make install env XDG_DATA_DIRS="$my_dir/share:$XDG_DATA_DIRS" $my_dir/bin/liferea Contributing ------------ As the project is hosted at Github pull requests and tickets via Github are the best way to contribute to Liferea. ###### _Translating_ Before starting to translate you need a translation editor. We suggest to use poedit or gtranslator. Please edit the translation using such a translation editor and send us the resulting file. Once you have finished your work please send us the resulting file. Please do not send translation patches. Those are a lot of work to merge and the bandwidth saving is not that huge! ###### _New Translations_ To create a new translation you must load the translation template, which you can find in the release tarball as "po/liferea.pot", into the translation editor. After editing it save it under a new name (usually your locales name with the extension ".po"). ###### _Updating Translations_ When updating an existing translation please ensure to respect earlier translators work. If the latest translation is only a few months old please contact the latest translator first asking him to review your changes especially if you change already translated literals. ###### _Localizing Feed Lists_ When Liferea starts for the first time it installs a localized feed list if available. If this is not the case for your locale you might want to provide one. To check if there is one for your country have a look into the "opml" subdirectory in the latest release tarball or GIT. If you want to provide/update a localized feed list please follow these rules: + Keep the English part of the default feed list + Only add neutral content feeds (no sex, no ideologic politics, no illegal stuff) + Provide good and short feed titles + Provide HTML URLs for each feed. ###### _Creating Plugins_ Liferea supports GObject Introspection based plugins using libpeas. The Liferea distribution comes with a set of Python plugin e.g. the media player, libsecret support, a tray icon plugin and maybe others. ###### Why We Use Plugins? The idea behind plugins is to extend Liferea without changing compile time requirements. With the plugin only activating if all its bindings are available Liferea uses plugins to automatically enable features where possible. ###### How Plugins Interact With Liferea You can develop plugins for your private use or contribute them upstream. In any case it makes sense to start by cloning one of the existing plugins and to think about how to hook into Liferea. There are two common ways: + using interfaces, + or by listening to events on Liferea objects, + or not at all by just controlling Liferea from the outside. The media player is an example for 1.) while the tray icon is an example for 3.) If you find you need a new plugin interface (called Activatables) in the code feel free to contact us on the mailing list. In general such a tight coupling should be avoided. About the exposed GIR API: At the moment there is no stable API. Its just some header files fed into g-ir-scanner. Despite this method names of the core functionality in Liferea has proven to be stable during release branches. And if you contribute your plugin upstream it will be updated to match renamed functionality. ###### Testing Plugins To test your new plugin you can use ~/.local/share/liferea/plugins. Create the directory and put the plugin script and the .plugin file there and restart Liferea. Watch out for initialization exceptions on the command line as they will permanently disable the plugin. Each time this happens you need to reenable the plugin from within the plugin tab in the preferences dialog! ###### _How to Help With Testing_ ###### *Bug Reports* If you want to help with testing grab the latest tarball or follow GIT master and write bug reports for any functional problem you experience. If you have time help with bug triaging. Check if you see any of the open bugs that are not yet confirmed. ###### *Debugging Crashes* In case of crashes create gdb backtraces and post them in the bug tracker. To create a backtrace start Liferea using "gdb liferea". At the gdb prompt type "run" to start the execution and "bt" after the crash. Send us the "bt" output! Note: Often people confuses assertions with crashes. Assertions do halt the program because of a totally unexpected situation. Creating a backtrace in this situation will only point to the assertion line, which doesn't help much. In case of an assertion simply post a bug report with the assertion message. ###### *Debugging Memory Leaks* If you see memory leakage please take the time to do a run valgrind --leak-check=full liferea to identify leaks and send in the output. How to Get Support ------------------ ### When using distribution packages Do not post bug reports in the Liferea bug tracker, use the bug reporting system of your distribution instead. We (upstream) cannot fix distribution packages! ### Before raising an issue Install the latest stable release and check if the problem is solved already. Please do not ask for help for older releases! ### Issue Tracker Once you verified the latest stable release still has the problem please raise an issue in the GitHub bug tracker (https://github.com/lwindolf/liferea/issues). liferea-1.13.7/autogen.sh000077500000000000000000000006241415350204600152150ustar00rootroot00000000000000#!/bin/sh tmp=`which autoreconf` if [ "$tmp" = "" ]; then echo "ERROR: You need to install autoconf!" exit 1 fi tmp=`which intltoolize` if [ "$tmp" = "" ]; then echo "ERROR: You need to install intltool!" exit 1 fi tmp=`which libtoolize` if [ "$tmp" = "" ]; then echo "ERROR: You need to install libtool!" exit 1 fi autoreconf -i intltoolize if test -z "$NOCONFIGURE"; then ./configure "$@" fi liferea-1.13.7/configure.ac000066400000000000000000000054441415350204600155070ustar00rootroot00000000000000dnl Process this file with autoconf to produce a configure script. AC_INIT([liferea],[1.13.7],[liferea-devel@lists.sourceforge.net]) AC_CANONICAL_HOST AC_CONFIG_SRCDIR([src/feedlist.c]) AC_CONFIG_HEADERS([config.h]) AM_INIT_AUTOMAKE([1.11 foreign std-options -Wall -Werror]) AM_SILENT_RULES([yes]) dnl Needed for automake 1.12 m4_ifdef([AM_PROG_AR], [AM_PROG_AR]) AC_PREREQ(2.59) LT_INIT IT_PROG_INTLTOOL([0.40.4]) AC_PROG_CC AM_PROG_CC_C_O AC_PROG_MAKE_SET AC_SYS_LARGEFILE GLIB_GSETTINGS AC_CHECK_FUNCS([strsep]) PKG_PROG_PKG_CONFIG() ################################################################################ # Mandatory library dependencies pkg_modules=" gtk+-3.0 >= 3.22.0 glib-2.0 >= 2.50.0 gio-2.0 >= 2.50.0 pango >= 1.4.0 libxml-2.0 >= 2.6.27 libxslt >= 1.1.19 sqlite3 >= 3.7.0 gmodule-2.0 >= 2.0.0 gthread-2.0 libsoup-2.4 >= 2.42 webkit2gtk-4.0 json-glib-1.0 gobject-introspection-1.0 gsettings-desktop-schemas libpeas-1.0 >= 1.0.0 libpeas-gtk-1.0 >= 1.0.0 fribidi >= 0.19.7" ################################################################################ PKG_CHECK_MODULES(PACKAGE, [$pkg_modules]) AC_CHECK_LIB(glib-2.0, g_memdup2, [PACKAGE_CFLAGS="$PACKAGE_CFLAGS -DHAVE_G_MEMDUP2"]) AC_CHECK_LIB(glib-2.0, g_time_zone_new_identifier, [PACKAGE_CFLAGS="$PACKAGE_CFLAGS -DHAVE_G_TIME_ZONE_NEW_IDENTIFIER"]) AC_SUBST(PACKAGE_CFLAGS) AC_SUBST(PACKAGE_LIBS) PKG_CHECK_MODULES([WEB_EXTENSION], [ webkit2gtk-web-extension-4.0 ]) AC_SUBST([WEB_EXTENSION_CFLAGS]) AC_SUBST([WEB_EXTENSION_LIBS]) uname=`uname` AC_DEFINE_UNQUOTED(OS, $uname, [defines a OS version string, used for OS specific code]) AC_DEFINE_UNQUOTED(OSNAME, "$uname", [defines a OS version string, used for the user agent string]) AC_MSG_RESULT(user agent OS = $uname) ################################################################################ # Plugins via Introspection GOBJECT_INTROSPECTION_CHECK([0.9.3]) ################################################################################ AM_GLIB_GNU_GETTEXT GETTEXT_PACKAGE=liferea AC_SUBST(GETTEXT_PACKAGE) AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["${GETTEXT_PACKAGE}"], [gettext domain]) AC_CONFIG_FILES([ Makefile net.sf.liferea.gschema.xml src/Makefile src/webkit/Makefile src/webkit/web_extension/Makefile src/parsers/Makefile src/fl_sources/Makefile src/ui/Makefile src/tests/Makefile doc/Makefile doc/html/Makefile xslt/Makefile man/Makefile pixmaps/Makefile pixmaps/16x16/Makefile pixmaps/22x22/Makefile pixmaps/24x24/Makefile pixmaps/32x32/Makefile pixmaps/48x48/Makefile pixmaps/scalable/Makefile opml/Makefile glade/Makefile po/Makefile.in src/liferea-add-feed ]) AC_OUTPUT echo echo "$PACKAGE $VERSION" echo eval eval echo Liferea will be installed in $bindir. echo echo configure complete, now type \'make\' echo liferea-1.13.7/css/000077500000000000000000000000001415350204600140025ustar00rootroot00000000000000liferea-1.13.7/css/liferea.css000066400000000000000000000114321415350204600161240ustar00rootroot00000000000000/** * @file liferea.css Liferea rendering stylesheet * * Copyright (C) 2004-2020 Lars Windolf * Copyright (C) 2004 delusional * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /* Font Definitions: ================= No style definition should set absolute font sizes, font families or line heigth and spacing. This is to allow a GNOME preference controlled default font size. Color Definitions: ================== To allow using GTK theme colors the following key words will be replaced with the corresponding GTK theme color values: GTK-COLOR-FG GTK-COLOR-BG GTK-COLOR-LIGHT GTK-COLOR-DARK GTK-COLOR-MID GTK-COLOR-BASE GTK-COLOR-TEXT GTK-COLOR-NORMAL-LINK GTK-COLOR-VISITED-LINK */ body { background: #GTK-COLOR-BASE; color: #GTK-COLOR-TEXT; padding:0; margin:0; } blockquote { border-left: 3px solid #GTK-COLOR-DARK; background: #GTK-COLOR-BG; padding: 6px; font-style: italic; margin: 5px 20px; clear:both; } a { color: #GTK-COLOR-NORMAL-LINK; } a:visited { color: #GTK-COLOR-VISITED-LINK; } dd { padding-left: 30px; } section, div.content svg, div.content * img { max-width: 100%; height: auto; } figure, iframe { max-width: 100%; box-sizing: border-box; /* Note: object-fit would be best practice here, but it fails on embedded Youtube videos (pushing the embed iframe far to the right with the controls being not in the place visually shown) */ } div.content * img { border:0; margin:2px; } div.content img.thumbnail { border:0; margin-right:12px; } img.gravatar { /* gravatars have align="left" so we need proper margin */ margin-right:12px; } #map img { /* OpenStreeMap map tiles must have no border */ border:0px; margin:0px; } /* styles for the item description (currently also used for the feed description) */ table.itemhead, table.feedhead { margin:0; width:100%; border:0; border-bottom: 2px solid #GTK-COLOR-DARK; clear:both; color:#GTK-COLOR-TEXT; } .itemhead, .feedhead { background-color: #GTK-COLOR-MID; } table.itemhead * a.itemhead { text-decoration:none; color:#GTK-COLOR-TEXT; font-weight:bold; } table.itemhead * td { padding-top: 4px; } td.head_favicon { width: 20px; padding-left: 10px; vertical-align: middle; } td.head_title { padding:2px 5px; width: 100%; vertical-align: middle; } table.headmeta { background:#GTK-COLOR-BG; width:100%; border:0; border-bottom:#GTK-COLOR-MID solid 1px; margin:0; } table.headmeta td { padding:1px 6px 1px 12px; font-size:0.9em; } .hidden { visibility:hidden; display:none; } .author, .categories, .source { padding:0; margin-left:0; margin-right:12px; } .date { white-space:nowrap; text-align:right; padding-right:5px; } div.content, div.item_comments { padding:0px 6px 0px 12px; } div.comment { border:1px solid #GTK-COLOR-DARK; margin-bottom:5px; } div.comment_body { padding-left:6px; padding-right:6px; } div.comment_title { padding-left:6px; padding-right:6px; font-weight:bold; background:#GTK-COLOR-BG; } a.favicon { text-decoration:none; } a.favicon img { width:16px; border:0; margin:0; padding:0; } /* style for the feed fetch error box at the beginning of the feed description and for item comment feeds */ #errors { border:0; border-bottom:1px solid black; margin:12px 24px; padding:12px; background:#ffa; color:black; border:1px solid #GTK-COLOR-DARK; } #errors ul { list-style-type: none; padding-inline-start: 15px; } #errors * li { padding:1px; } pre.errorOutput { margin:10px 15px; padding:5px; border:2px solid #GTK-COLOR-DARK; background:#GTK-COLOR-LIGHT; overflow:auto; } del { text-decoration: line-through; } ins { text-decoration: underline; } /* namespace specific styles */ .slash { background:#60A080; padding-left:5px; padding-right:5px; } .slashSection, .slashDepartment { padding-right:2px; color:black; } .slashValue { padding-right:6px; color:white; font-weight:bold; } div.photoheader { margin:10px 0; padding-left:10px; padding-right:10px; background-color:#GTK-COLOR-DARK; color:#GTK-COLOR-TEXT; } liferea-1.13.7/css/user.css000066400000000000000000000050561415350204600155000ustar00rootroot00000000000000/** This is a template file which you can use to redefine the Liferea CSS definitions use to render items. Below you find empty class definitions including comments describing what they are used for. Before you start customizing... Reloading: ========== For performance reasons Liferea will read this CSS file only on startup. So when you modify it please restart Liferea for changes to take effect. About Font Definitions: ======================= You should avoid setting absolute font sizes. This allows Liferea to follow the GNOME font and font size. Use relative definitions instead (e.g. "1.2em" or "0.8em"). Color Definitions: ================== Try to reuse GTK theme colors. Liferea uses the following definitions and will be replace them on the fly: GTK-COLOR-FG GTK-COLOR-BG GTK-COLOR-LIGHT GTK-COLOR-DARK GTK-COLOR-MID GTK-COLOR-BASE GTK-COLOR-TEXT GTK-COLOR-NORMAL-LINK GTK-COLOR-VISITED-LINK Inspecting the HTML: ==================== If the definitions below do not help you, run Liferea with the parameter "--debug-html". Then Liferea will dump HTML into ~/.cache/liferea/output.xhtml each time it renders an item or a feed. So you can check for style classes and the layout you want to affect. */ /* Item display rendering header table (with title, categories...) */ // table.itemhead { } /* Feed display rendering header table (with title, categories...) */ // table.feedhead { } /* 1st of feed/item table display containing favicon */ // td.head_favicon { } // a.favicon { } // a.favicon img { } /* 2nd of feed/item table display containing title */ // td.head_title { } /* Metadata display table (inside header table) */ // table.headmeta { } /* Header table fields to different item metadata */ // .author, .categories, .source { } // .date { } /* Image resizing to fit HTML view */ // section, div.content svg, div.content * img { } /* Resizing for video embeds */ // figure, iframe { } /* Item/feed description */ // div.content { } /* Comment rendering */ // div.comment { } // div.comment_body { } // div.comment_title { } /* Styles for the feed fetch error box at the beginning of the feed description and for item comment feeds */ // #errors { } // pre.errorOutput { } /* namespace specific styles */ /* Gravatar embedding */ // img.gravatar { } /* OpenStreeMap embedded map*/ // #map img { } /* Slashdot Header */ // .slash { } // .slashSection, .slashDepartment { } // .slashValue { } liferea-1.13.7/doc/000077500000000000000000000000001415350204600137575ustar00rootroot00000000000000liferea-1.13.7/doc/Doxyfile000066400000000000000000001313671415350204600155000ustar00rootroot00000000000000# Doxyfile 1.3.6-20040222 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project # # All text after a hash (#) is considered a comment and will be ignored # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" ") #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # The PROJECT_NAME tag is a single word (or a sequence of words surrounded # by quotes) that should identify the project. PROJECT_NAME = Liferea # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = 1.1 # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, # Finnish, French, German, Greek, Hungarian, Italian, Japanese, Japanese-en # (Japanese with English messages), Korean, Korean-en, Norwegian, Polish, Portuguese, # Romanian, Russian, Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian. OUTPUT_LANGUAGE = English # This tag can be used to specify the encoding used in the generated output. # The encoding is not always determined by the language that is chosen, # but also whether or not the output is meant for Windows or non-Windows users. # In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES # forces the Windows encoding (this is the default for the Windows binary), # whereas setting the tag to NO uses a Unix-style encoding (the default for # all platforms other than Windows). USE_WINDOWS_ENCODING = NO # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is used # as the annotated text. Otherwise, the brief description is used as-is. If left # blank, the following values are used ("$name" is automatically replaced with the # name of the entity): "The $name class" "The $name widget" "The $name file" # "is" "provides" "specifies" "contains" "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all inherited # members of a class in the documentation of that class as if those members were # ordinary class members. Constructors, destructors and assignment operators of # the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = NO # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. It is allowed to use relative paths in the argument list. # If left blank the directory from which doxygen is run is used as the # path to strip. STRIP_FROM_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful is your file systems # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like the Qt-style comments (thus requiring an # explicit @brief command for a brief description. JAVADOC_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the DETAILS_AT_TOP tag is set to YES then Doxygen # will output the detailed description near the top, like JavaDoc. # If set to NO, the detailed description appears after the member # documentation. DETAILS_AT_TOP = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 8 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = YES # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources # only. Doxygen will then generate output that is more tailored for Java. # For instance, namespaces will be presented as packages, qualified scopes # will look different, etc. OPTIMIZE_OUTPUT_JAVA = NO # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = NO # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = YES # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # users are advised to set this option to NO. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = NO # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or define consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and defines in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = ../src # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp # *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm FILE_PATTERNS = *.h # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = YES # The EXCLUDE tag can be used to specify files and/or directories that should # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. EXCLUDE = ../src/scripting/liferea_wrap.h ../src/ui/eggtrayicon.c ../src/ui/eggtrayicon.h ../src/net ../src/parsers/opml.h ../src/parsers/ocs_ns.h ../src/parsers/ocs_dir.c ../src/parsers/ocs_dir.h ../src/parsers/cdf_channel.h ../src/parsers/cdf_item.h # The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories # that are symbolic links (a Unix filesystem feature) are excluded from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. EXCLUDE_PATTERNS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. INPUT_FILTER = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C and C++ comments will always remain visible. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES (the default) # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = YES # If the REFERENCES_RELATION tag is set to YES (the default) # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = YES # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = NO # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If the tag is left blank doxygen # will generate a default style sheet. Note that doxygen will try to copy # the style sheet file to the HTML output directory, so don't put your own # stylesheet in the HTML output directory as well, or it will be erased! HTML_STYLESHEET = # If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, # files or namespaces will be aligned in HTML using tables. If set to # NO a bullet list will be used. HTML_ALIGN_MEMBERS = YES # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compressed HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # The DISABLE_INDEX tag can be used to turn on/off the condensed index at # top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. DISABLE_INDEX = NO # This tag can be used to set the number of enum values (range [1..20]) # that doxygen will group on one line in the generated HTML documentation. ENUM_VALUES_PER_LINE = 4 # If the GENERATE_TREEVIEW tag is set to YES, a side panel will be # generated containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, # Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are # probably better off using the HTML help feature. GENERATE_TREEVIEW = NO # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, a4wide, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4wide # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = NO # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = NO # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load stylesheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # The XML_SCHEMA tag can be used to specify an XML schema, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_SCHEMA = # The XML_DTD tag can be used to specify an XML DTD, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_DTD = # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. This is useful # if you want to understand what is going on. On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = NO # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_PREDEFINED tags. EXPAND_ONLY_PREDEF = NO # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # in the INCLUDE_PATH (see below) will be search if a #include is found. SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. PREDEFINED = # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all function-like macros that are alone # on a line, have an all uppercase name, and do not end with a semicolon. Such # function macros are typically used for boiler-plate code, and will confuse the # parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. # Optionally an initial location of the external documentation # can be added for each tagfile. The format of a tag file without # this location is as follows: # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths or # URLs. If a location is present for each tag, the installdox tool # does not have to be run to correct the links. # Note that each tag file must have a unique name # (where the name does NOT include the path) # If a tag file is not located in the directory in which doxygen # is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base or # super classes. Setting the tag to NO turns the diagrams off. Note that this # option is superseded by the HAVE_DOT option below. This is only a fallback. It is # recommended to install and use dot, since it yields more powerful graphs. CLASS_DIAGRAMS = NO # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # the CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = NO # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will # generate a call dependency graph for every global function or class method. # Note that enabling this option will significantly increase the time of a run. # So in most cases it will be better to enable call graphs for selected # functions only using the \callgraph command. CALL_GRAPH = YES # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are png, jpg, or gif # If left blank png will be used. DOT_IMAGE_FORMAT = png # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found on the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width # (in pixels) of the graphs generated by dot. If a graph becomes larger than # this value, doxygen will try to truncate the graph, so that it fits within # the specified constraint. Beware that most browsers cannot cope with very # large images. MAX_DOT_GRAPH_WIDTH = 1024 # The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height # (in pixels) of the graphs generated by dot. If a graph becomes larger than # this value, doxygen will try to truncate the graph, so that it fits within # the specified constraint. Beware that most browsers cannot cope with very # large images. MAX_DOT_GRAPH_HEIGHT = 1024 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes that # lay further from the root node will be omitted. Note that setting this option to # 1 or 2 may greatly reduce the computation time needed for large code bases. Also # note that a graph may be further truncated if the graph's image dimensions are # not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH and MAX_DOT_GRAPH_HEIGHT). # If 0 is used for the depth value (the default), the graph is not depth-constrained. MAX_DOT_GRAPH_DEPTH = 0 # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES #--------------------------------------------------------------------------- # Configuration::additions related to the search engine #--------------------------------------------------------------------------- # The SEARCHENGINE tag specifies whether or not a search engine should be # used. If set to NO the values of all tags below this one will be ignored. SEARCHENGINE = NO liferea-1.13.7/doc/Makefile.am000066400000000000000000000000511415350204600160070ustar00rootroot00000000000000EXTRA_DIST = \ Doxyfile SUBDIRS = html liferea-1.13.7/doc/html/000077500000000000000000000000001415350204600147235ustar00rootroot00000000000000liferea-1.13.7/doc/html/Makefile.am000066400000000000000000000032101415350204600167530ustar00rootroot00000000000000html_doc_en = about_en.html \ concepts_en.html \ folders_en.html \ headlines_en.html \ preferences_en.html \ reference_en.html \ searching_en.html \ subscriptions_en.html \ topics_en.html \ updating_en.html \ enclosures_en.html \ newsbin_en.html \ onlineservices_en.html \ faq_en.html html_doc_de = about_de.html \ concepts_de.html \ folders_de.html \ headlines_de.html \ preferences_de.html \ reference_de.html \ searching_de.html \ subscriptions_de.html \ topics_de.html \ updating_de.html \ enclosures_de.html \ newsbin_de.html \ onlineservices_de.html \ faq_de.html html_doc_it = reference_it.html \ topics_it.html \ faq_it.html html_doc_pictures_en = help_feed_default.png \ help_feed_error.png \ help_folder.png \ help_opml.png \ help_feed_prop_downl_1.6.0.png \ help_feed_prop_cache_1.6.0.png \ help_feed_prop_adv_1.6.0.png \ help_feed_prop_general_1.6.0.png \ help_feed_prop_source_1.6.0.png \ help_item_flag.png \ help_item_unread.png \ help_prefs_browser_1.14.0.png \ help_prefs_enclosures_1.14.0.png \ help_prefs_feeds_1.14.0.png \ help_prefs_folders_1.14.0.png \ help_prefs_desktop_1.14.0.png \ help_prefs_headlines_1.14.0.png \ help_prefs_proxy_1.14.0.png \ help_search_1.6.0.png \ help_subscribe_1.6.0.png \ help_subscribe_adv_1.6.0.png \ help_vfolder_1.6.0.png EXTRA_DIST = $(html_doc_en) \ $(html_doc_de) \ $(html_doc_it) \ $(html_doc_pictures_en) \ reference.css htmldoc_DATA = $(html_doc_en) \ $(html_doc_de) \ $(html_doc_it) \ $(html_doc_pictures_en) \ reference.css htmldocdir = $(datadir)/liferea/doc/html liferea-1.13.7/doc/html/about_de.html000066400000000000000000000032051415350204600173730ustar00rootroot00000000000000 Einleitung

Über Liferea

Liferea ist ein News Aggregator für Online News Feeds. Es gibt viele andere Programme, aber diese sind entweder nicht für Linux verfügbar oder benötigen viele Zusatzbibliotheken. Liferea versucht diese Lücke zu schließen und ein möglichst schneller, einfach zu benutzender und einfach installierbarer News Aggregator für GTK/GNOME zu sein.

Bei Interesse an der neuesten Liferea Version, bei Programmfehlern oder dem Wunsch nach neuen Features lohnt sich ein Besuch der GitHub Projekt Seite (Sprache Englisch)! Alternativ lohnt sich auch das Mitlesen der liferea-devel Mailingliste. Hinweise zur Mailinglisste finden sich auf der SourceForge Infoseite.

liferea-1.13.7/doc/html/about_en.html000066400000000000000000000027601415350204600174120ustar00rootroot00000000000000 Introduction

About Liferea

Liferea is a simple desktop news aggregator for online news feeds. There are many other news readers available, but these others are not available for Linux or require many extra libraries to be installed. Liferea tries to fill this gap by creating a fast, easy to use, easy to install news aggregator for GTK/GNOME.

If you are interested in the most recent Liferea version, want to submit a bug or request a feature you may want to visit our GitHub project page! You are also invited to join the liferea-devel mailing list. Details on the list are located at the SourceForge list info page.

liferea-1.13.7/doc/html/concepts_de.html000066400000000000000000000141351415350204600201030ustar00rootroot00000000000000 Basic Concepts

This section tries to clarify some of the technical jargon used along with feed readers.

What does "online/offline desktop" news aggregator mean?

Feed readers or news aggregators can be realised as web applications or as desktop applications. The place of execution (your computer vs. some webserver) implies certain advantages and disadvantages.

A desktop news aggregator runs on your own PC and stores aggregated feeds locally. It accesses each news feed and downloads its newest headlines periodically. Note that not all desktop news aggregators can work offline. Some desktop aggregators retrieve feeds each time you access them instead of caching them for later use. Liferea as an offline capable desktop aggregator caches feeds and is suitable for use with portable devices (laptops, hand helds...) that do not have a permanent internet connection.

  • Disadvantages:
    • No implicit state synchronization when used from different client machines
    • Higher bandwidth usage compared to online aggregators
    • Need to be installed on each client machines
    • OS-dependant
  • Advantages:
    • Your cached feed contents are saved on your own (secure) machine.
    • Your cached feed contents are searchable locally with other tools (e.g. Beagle)
    • Your feed list is not accessible to 3rd parties
    • Each feed access are done in private.

A online news aggregator (for example TheOldReader) is run on a remote webserver which you can access from everywhere as long as you have network access. When you log in to the online news aggregator it usually doesn't need to fetch the most recent headlines of you feed subscriptions because it implements feed caching to save bandwidth.

  • Disadvantages:
    • You rely on some 3rd party vendor for the news aggregator service.
    • Often you have to pay for premium features.
    • No chance to improve/modify/fix the online application.
    • You need a web browser to access it.
  • Advantages:
    • You get the same feed list and feed states no matter from where you access them.
    • Practically no bandwidth usage except for the access of the web interface.
    • Community headline rating systems to categorize content.
    • Sharing headlines with the community (or specific users).

What is a Feed?

For a news aggregator a news feed is a distinct information source. A news aggregator usually retrieves news headlines from many news feed subscriptions. The term "news feed" is not very precise as feeds can also provide weblog postings, podcasts and practically every type of content. The same goes for the term "news headlines": often "article" or "post" would be a better description. Nonetheless the prefix "news" is usually used with the terms "feed", "headline" and "aggregator".

When you add a new subscription then you have to specify the source of the feed so that the news aggregator can initially retrieve it. The feed source is usually a HTTP URL. When updating the news feed the aggregator downloads the document served when accessing this URL. There are several XML-based and XML-like syndication standards (RSS, Atom...) that define how a feed must look like so that the aggregator knows how to interpret the document contents.

As a user you usually do not have to know about the syndication format. You just need to know the source URL of the feed you want to subscribe.

What is the Feed List?

Typical news aggregators present you with a list of your feed subscription. Following the email client interface paradigm the feed list is often presented in the left pane of the news aggregator interface.

Depending on the aggregator you use the feed list might be hierarchically organized using folders or categories.

What is a Headline?

When a news aggregator updates a news feed it download the source document from the source URL of the feed subscription. This source document provides a set of the most recent headlines which then need to be merged against the current feed cache of the aggregator because the set of headlines provided by the feed source changes over time.

Headlines as provided by the feed source are the smallest information unit handled by aggregators.

What is the Item List?

In Liferea headlines are also called items and are presented in the so called item list. Following the email client paradigm the item list is often presented in the upper right pane in the news aggregator interface.

liferea-1.13.7/doc/html/concepts_en.html000066400000000000000000000127771415350204600201270ustar00rootroot00000000000000 Basic Concepts

This section tries to clarify some of the technical jargon used along with feed readers.

What does "online/offline desktop" news aggregator mean?

Feed readers or news aggregators can be realised as web applications or as desktop applications. The place of execution (your computer vs. some webserver) implies certain advantages and disadvantages.

A desktop news aggregator runs on your own PC and stores aggregated feeds locally. It accesses each news feed and downloads its newest headlines periodically. Note that not all desktop news aggregators can work offline. Some desktop aggregators retrieve feeds each time you access them instead of caching them for later use. Liferea as an offline capable desktop aggregator caches feeds and is suitable for use with portable devices (laptops, hand helds...) that do not have a permanent internet connection.

  • Disadvantages:
    • No implicit state synchronization when used from different client machines
    • Higher bandwidth usage compared to online aggregators
    • Need to be installed on each client machines
    • OS-dependant
  • Advantages:
    • Your cached feed contents are saved on your own (secure) machine.
    • Your cached feed contents are searchable locally with other tools (e.g. Beagle)
    • Your feed list is not accessible to 3rd parties
    • Each feed access are done in private.

A online news aggregator (for example TheOldReader) is run on a remote webserver which you can access from everywhere as long as you have network access. When you log in to the online news aggregator it usually doesn't need to fetch the most recent headlines of you feed subscriptions because it implements feed caching to save bandwidth.

  • Disadvantages:
    • You rely on some 3rd party vendor for the news aggregator service.
    • Often you have to pay for premium features.
    • No chance to improve/modify/fix the online application.
    • You need a web browser to access it.
  • Advantages:
    • You get the same feed list and feed states no matter from where you access them.
    • Practically no bandwidth usage except for the access of the web interface.
    • Community headline rating systems to categorize content.
    • Sharing headlines with the community (or specific users).

What is a Feed?

For a news aggregator a news feed is a distinct information source. A news aggregator usually retrieves news headlines from many news feed subscriptions. The term "news feed" is not very precise as feeds can also provide weblog postings, podcasts and practically every type of content. The same goes for the term "news headlines": often "article" or "post" would be a better description. Nonetheless the prefix "news" is usually used with the terms "feed", "headline" and "aggregator".

What is a Subscription?

When you add a new feed then you have to specify the source of the feed, so that the news aggregator can initially retrieve it. The feed source is usually a HTTP URL. This URL with optional authentication and proxy settings is a "subscription" managed by Liferea.

What is the Feed List?

To manage and easily navigate your subscriptions Liferea provides you with a hierarchic tree of your subscriptions. Similar to email clients the "feed list" is located in the left pane. You can organize your subscription after topics in different folders.

What is a Headline?

When a news aggregator updates a news feed it downloads the source document from the source URL of the feed subscription. This source document provides a set of the most recent headlines which then need to be merged against the current feed cache of the aggregator because the set of headlines provided by the feed source changes over time.

Headlines as provided by the feed source are the smallest information unit handled by aggregators.

What is the Item List?

In Liferea headlines are also called items and are presented in the so called item list. Following the email client paradigm the item list is often presented in the upper right pane in the news aggregator interface.

liferea-1.13.7/doc/html/enclosures_de.html000066400000000000000000000070111415350204600204420ustar00rootroot00000000000000 Enclosures/Podcasting

Enclosures/Podcasting

Worum geht es hier eigentlich?

Viele Weblogs und News Feeds wollen mehr als nur ASCII oder HTML formatierte Artikel bieten. Ein Weblog-Autor möchte zum Beispiel seine täglichen Erlebnisse aufnehmen und als Audio Daten (meist MP3) im Weblog und dem zugehörigen News Feed den Lesern anbieten. Das ermöglichen sogenannte Enclosures. Ein Enclosure an einer Schlagzeile hinzuzufügen bedeutet der Schlagzeile eine URL beizugeben, die eine Applikation wie Liferea herunterladen und mit einem passendem Medienplayer abspielen kann.

Feeds mit Audioinhalten werden oft als Podcasts bezeichnet. Das Publizieren solchen Inhalts wird Podcasting genannt. Die Wikipedia Seite über Podcasting ist ein guter Startpunkt um mehr darüber zu erfahren.

Das Anbieten von Audioinhalten ist zwar am weitesten verbreitet aber es sind auch Feeds denkbar die Bilder, Torrents oder Videos einbinden. Praktisch jedes Datenformat ist möglich.

Wie Liferea Enclosures herunterlädt

Standardmäßt lädt Liferea Enclosures nicht automatisch herunter. Es ist jedoch möglich für jedes Feed einzeln das automatische Herunterladen zu aktivieren. Wird ein neues Enclosure erkannt startet Liferea den Download mit dem in den Einstellungen konfigurierten Download Tool. Alle heruntergeladenen Dateien werden im ebenfalls in den Einstellungen konfigurierbaren Download-Verzeichnis abgelegt. Heruntergeladene Dateien werden nach dem Abspielen nicht automatisch gelöscht!

Natürlich können die Enclosures auch manuell heruntergeladen werden. Dazu wählt man die entsprechende Schlagzeilen aus und klickt den Pfeil links des Enclosures an um dessen Kontextmenü zu öffnen. Dieses bietet zwei Menüpunkte. Der erste lädt das Enclosure herunter und lädt es mit dem entsprechenden Medienplayer. Mit dem zweiten Menüpunkt kann die Datei einfach nur in ein angegebenes Verzeichnis gesichert werden.

Wie Liferea Enclosures abspielt

Nach dem Herunterladen eines Enclosures versucht Liferea dieses entsprechend des MIME Typs oder der Dateiendung zu öffnen. Die Anwendung zum Anzeigen oder Abspielen der heruntergeladenen Datei kann in den Einstellungen konfiguriert werden. Hinweis: Es hängt vom Feed ab ob der MIME Typ für das Starten des konfigurierten Abspielprogrammes verfügbar ist oder nur die Dateiendung verwendet werden kann.

liferea-1.13.7/doc/html/enclosures_en.html000066400000000000000000000061101415350204600204530ustar00rootroot00000000000000 Enclosures/Podcasting

Enclosures/Podcasting

What is this all about?

Some weblogs and news feeds want to provide you with more than just ASCII text or HTML formatted articles. A weblog author might want to record his daily experiences and publish them as audio data (often MP3) and of course make this available in the weblog's feed too. This is done by adding enclosures to feed items. Adding an enclosure means adding an URL to an item which the feed reader application can download and play with an appropriate media player.

Feeds with audio content are often called podcasts. The act of publishing such content is called podcasting. The Wikipedia page about podcasting is a good start to learn more about this publishing method.

Including audio content might be the most common use case for enclosures but there are other feeds around containing images, torrents or videos. Every data format is thinkable.

How Liferea downloads enclosures

Per default Liferea does not automatically download enclosures. But you can enable auto-downloading for each feed separately. If a new enclosure is found Liferea will then start a download using the configured download method. All downloaded files will be stored into the configured download directory. After viewing the contents you have to delete them manually.

Of course you can also manually download enclosures. To do so open the item with the enclosure and click the arrow to open the enclosure context menu. There are two options. The first one downloads and launches the file while the second option save the file into a specified directory.

How Liferea opens enclosures

After downloading a file Liferea will try to launch the file according to it's MIME type or file extension. The application used to view or play the downloaded file can be configured in the program preferences. Note that it depends on the feed whether you can use MIME types to associate launcher programs or if you must rely on the file extension to do this.

liferea-1.13.7/doc/html/faq_de.html000066400000000000000000000142561415350204600170400ustar00rootroot00000000000000 Liferea FAQ

Hinweis: Diese FAQ ist auch online verfügbar.

Q: Warum funktioniert Flash nicht (mehr)?
A: Automatische Flash-Unterstützung ging leider mit Liferea 1.10, welches erstmal GTK3 nutzt, kaputt wenn es mit einer WebkitGTK3-Version älter als 2.0 verwendet wird. Eine detaillierte Erklärung findet sich hier (Englisch).

Es gibt die folgenden Workarounds:

  1. WebkitGTK3 auf 2.0 oder neuer aktualisieren
  2. nspluginwrapper und das 32bit Flash-Plugin installieren (Anleitung, Englisch)
Q: Wo finde ich die neueste Liferea-Version?
A: Der einfachste Weg ist es das neueste Binär- oder Quellpaket von der GitHub Projekt Homepage herunterzuladen. Noch besser ist es aber Liferea-Pakete der eigenen Linux-Distribution zu verwenden. Eine Liste der Distributionen mit eigene Liferea-Paketen gibt es hier.
Q: Der Download von Anhängen mit Gwget funktioniert nicht!
A: Eine bekannte Ursache für dieses Problem ist Gwget Version 1.0.0 oder 1.0.1, welche eine defekte DBUS API bereitstellt, die es Liferea nicht erlaubt Anhänge an Gwget weiterzugeben. In diesem Falle muß Gwget auf eine neuere Version aktualisiert werden.
Q: Ich benutze Firefox/Seamonkey, und wenn ich Schlagzeilen mit dem Firefox/Seamonkey öffne wird immer dessen Fenster fokussiert.
A: Das ist ein Verhalten von Firefox/Seamonkey. Um es zu ändern muß mit about:config die Einstellung browser.tabs.loadDivertedInBackground auf "true" gestellt werden.
Q: Ich will Cookies!
A: Liferea unterstützt nur Session Cookies beim Abruf von Feeds. Diese werden in ~/.config/liferea/session_cookies.txt gespeichert.
Q: Ich mag die HTML/CSS Styles von Liferea nicht!
A: Jeder kann die Styles ändern. Dazu kann das Stylesheet ~/.config/liferea/liferea.css editiert werden. Die Datei enthält Beschreibungen der verschiedenen verwendeten Klassen so daß es einfacher ist diese zu modifizieren.
Q: Wie kann ich die friends-only Posts von LiveJournal Abonnements sehen?
A: Dazu muß das LiveJournal RSS Feed mit Authentifikation benutzt werden. Z.B. http://www.livejournal.com/users/pigrew/data/rss?auth=digest. In den Abonnement-Eigenschaften muß zusätzlich noch Username und Paßwort des LiveJournal Accounts angegeben werden.
Q: Warum werden manche Schlagzeilen immer wieder als ungelesen angezeigt?
A: Meistens sind fehlerhafte Feeds mit z.B. nicht eindeutigen oder fehlenden Schlagzeilen-Ids der Grund. Hier lohnt es sich, dass entsprechende Feed im Quelltext zu überprüfen und den Autor/Webmaster zu kontakieren. Ist das Feed in Ordnung lohnt es sich einen Eintrag im Liferea Bug Tracker anzulegen.

Hinweis: Dieses Problem haben viele Planet Feeds, die einfach nur die gesammelten Schlagzeilen der aggregierten Feeds anbieten, ohne für eindeutige Schlagzeilen-Ids zu sorgen. In solchen Fällen hat Liferea keine Möglichkeit die Schlagzeilen korrekt zu unterscheiden.
Q: Liferea stürzt laufend ab.
A: Das hören wir oft. Um wirklich helfen zu können benötigen wir aber detaillierte Beschreibungen über die Absturzursache. Stürzt Liferea reproduzierbar ab wollen wir das natürlich unbedingt wissen. Zum Debugging brauchen wir die Ausgaben eines Aufrufes mit dem Schalter "--debug-all" und wenn möglich einem Backtrace. Um einen Backtrace zu erzeugen einfach ein Terminal öffnen und die folgenden zwei Kommandos absetzen: ulimit -c unlimited und dann liferea. So aufgerufen wird Liferea beim Absturz eine core-Datei erzeugen. Diese kann dann mit gdb liferea core geöffnet werden. Gibt man in der gdb Eingabeaufforderung dann bt ein wird der Backtrace des Absturzes ausgegeben.
Q: Warum fehlt Liferea die Funktion X?
A: Wenn du findest das weitere Funktionen in Liferea eingebaut werden sollten beteilige dich bitte an der Weiterentwicklung des Programms. Um weitere Funktionen zu realisieren benötigen wir mehr freiwillige Entwickler!
Q: Was bedeutet "Es traten Fehler beim Parsen des Feeds auf!" und was kann ich dagegen tun?
A: Der Feed-Parser von Liferea erwartet gültiges XML. Enthält ein Feed ungültiges XML, so kommt es zu diesem Fehler. Die Gültigkeit eines Feeds lässt sich mit http://feedvalidator.org/ und http://validator.w3.org/feed/ prüfen.

Häufig lässt sich der fehlerhafte Feed mit Hilfe von xmllint retten. Dazu muss xmllint beim betreffenden Feed in den "Eigenschaften des Abonnements" im Reiter "Quelle" unter "Benutze einen Filter zum Konvertieren" wie folgt gesetzt werden: xmllint --recover -

Hinweis: Unter Debian-basierten Distributionen gehört xmllint zum Paket libxml2-utils.

liferea-1.13.7/doc/html/faq_en.html000066400000000000000000000140661415350204600170510ustar00rootroot00000000000000 Liferea FAQ

Note: This FAQ is also available online.

Q: Why doesn't Flash work (anymore)?
A: Easy flash support broke when Liferea 1.10 which uses GTK3 is used with WebkitGTK3 versions before 2.0. For a detailed explanation read this post.

There are the following known workarounds:

  1. Upgrade WebkitGTK3 to 2.0 or later
  2. Install nspluginwrapper and the 32bit flash plugin (instructions)
Q: Where can I get it?
A: The easiest way is to download the source package from the GitHub project page. But you should also consider checking if your Linux distribution provides an own package for Liferea. A list of all known prebuilt packages is provided in the installation section.
Q: Enclosures download with Gwget doesn't work
A: Most likely you have Gwget 1.0.0 or 1.0.1 installed, and due to a temporary breakage in the dbus API of Gwget these versions don't work with Liferea. Both older and more recent versions of Gwget do work.
Q: I'm using Firefox/Seamonkey, and when opening items in the browser the focus goes to the browser.
A: That's done by your browser. For changing it, open about:config in your browser and set browser.tabs.loadDivertedInBackground to true.
Q: Liferea is buggy! It does not close when I click on the window manager's close button and the tray icon is activated.
A: This is a pretty disputed topic. We see this as a useful feature, other people don't. If you don't like the behaviour you can change it by right-clicking the tray icon and toggling the checkbox labelled "Minimize to tray on close."
Q: I want cookies!
A: Liferea supports only session cookies while downloading feeds. They are stored in ~/.config/liferea/session_cookies.txt
Q: I don't like the HTML/CSS styles Liferea uses!
A: You can have your own stylesheet. Just edit ~/.config/liferea/liferea.css During startup it will be loaded additionally to the default stylesheets so you can modify some or all style definitions. The file contains comments describing the different style so it should be easy to modify them.
Q: How do I see my LiveJournal friend's friends-only entries?
A: Use the authenticated LiveJournal RSS feed. For example use http://www.livejournal.com/users/pigrew/data/rss?auth=digest. You will need to use the feed properties dialog box to set the username and password of your LiveJournal account.
Q: Why do feed items keep being displayed as new?
A: This is usually due to a bad feed which associated a particular ID to multiple items. If you are experienced with syndication formats please check the feed source code. If you find problems please contact the author/webmaster of the feed. If not please submit a bug report including the URL of the problem feed to the Liferea bug tracker.

Note: If you experience this problem with a planet feed the reason might be that the planet feed does not provide unique item ids for one or all off its source feeds. If this is the case Liferea has no chance to match identical items.
Q: Liferea crashes too much.
A: We hear this complaint a lot, but we rarely do not get information on how it crashed. If you find a way to make Liferea crash, we would love to know about it. Please send us a copy of the output of running Liferea with the "--debug-all" flag, plus a backtrace if possible. To create a backtrace, open up a terminal and type two commands: ulimit -c unlimited and then liferea. This causes Liferea to create a core dump when it crashes. Then, run gdb liferea core and type bt at the gdb prompt. This will display the backtrace of the crash, which should be sent along with the bug report.
Q: Why doesn't Liferea have feature X?
A: If you miss a feature in Liferea please consider contributing code to implement the feature! Please also consider that the Liferea project aims to provide a simple feed reader that might lack more complex functionality.
Q: What does "There were errors while parsing this feed!" mean and how can I fix it?
A: The feed parser of Liferea expects valid XML. So, this error happens when a feed contains invalid XML. You can check the validity of a feed with http://feedvalidator.org/ and http://validator.w3.org/feed/.

To fix this problem, you can try to sanitize the XML with xmllint. For this, open the subscription properties of the affected feed and add xmllint as a conversion filter in the source tab like this: xmllint --recover -

Note: On Debian-based distribution xmllint is part of the libxml2-utils package.

liferea-1.13.7/doc/html/faq_it.html000066400000000000000000000155161415350204600170640ustar00rootroot00000000000000 FAQ di Liferea

Nota: Questa FAQ è anche disponibile online.

Q: Perché Flash non funziona (più)?
A: Il supporto ad Easy flash si rompe quando Liferea 1.10 che dipende da GTK3 viene utilizzato con le versioni di WebkitGTK3 precedenti alla 2.0. Per una spiegazione dettagliata leggi questo post.

Sono conosciute le seguenti soluzioni:

  1. Aggiorna WebkitGTK3 alla versione 2.0 o successiva
  2. Installa nspluginwrapper ed il plugin flash a 32bit (istruzioni)
Q: Dove lo posso ottenere?
A: La via più facile è scaricare il pacchetto sorgente dalla pagina del progetto su GitHub. Ma dovresti anche informarti se la tua distribuzione Linux fornisce un proprio pacchetto per Liferea. Una lista di tutti i pacchetti precompilati è fornita nella sezione di installazione.
Q: Il download degli allegati tramite Gwget non funziona.
A: Molto probabilmente hai Gwget 1.0.0 o 1.0.1 installato, ed a causa di un malfunzionamento temporaneo delle API dbus di Gwget queste versioni non funzionano con Liferea. Sia le versioni più vecchie che più recenti di Gwget funzionano.
Q: Sto usando Firefox/Seamonkey, e quando apro i titoli nel browser il focus passa al browser.
A: Questo è effettuato dal tuo browser. Per cambiarlo, apri about:config nel tuo browser ed imposta browser.tabs.loadDivertedInBackground su vero.
Q: Liferea è buggato! Non si chiude quando clicco sul pulsante di chiusura del gestore delle finestre e l'icona nel vassoio di sistema viene attivata.
A: Questo è un tema abbastanza controverso. Noi la vediamo come una caratteristica utile, altre persone no. Se non ti piace il comportamento lo puoi cambiare cliccando con il tasto destro del mouse sull'icona nel vassoio di sistema e attivando la casella di controllo etichettata "Nascondi nel vassoio di sistema alla chiusura."
Q: Voglio i cookies!
A: Liferea supporta solo i cookie di sessione mentre si scaricano i notiziari. Essi vengono conservati in ~/.config/liferea/session_cookies.txt
Q: Non gradisco gli stili HTML/CSS che Liferea utilizza!
A: Puoi avere i tuoi fogli di stile. Ti basta modificare ~/.config/liferea/liferea.css
Durante l'avvio sarà caricato in aggiunta ai fogli di stile predefiniti così che tu possa modificare alcune o tutte le definizioni di stile. Il file contiene commenti che descrivono i differenti stili, così dovrebbe essere facile cambiarli.
Q: Come vedo gli elementi LiveJournal del mio amico che sono stati riservati solo agli amici?
A: Usa il notiziario RSS LiveJournal autenticato. Ad esempio utilizza http://www.livejournal.com/users/pigrew/data/rss?auth=digest. Avrai bisogno di usare la finestra di dialogo delle proprietà del notiziario per impostare il nome utente e la password del tuo account LiveJournal.
Q: Perché gli elementi di un notiziario continuano a essere mostrati come nuovi?
A: Questo è generalmente causato da un notiziario malformato che ha associato un certo ID a elementi multipli. Se hai esperienza con i formati di pubblicazione controlla il codice sorgente del feed. Se trovi problemi contatta l'autore/webmaster del feed. Altrimenti per piacere invia una segnalazione di bug includendo l'URL del notiziario problematico presso il bug tracker di Liferea.

Nota: Se ti capita questo problema con un notiziario planet la ragione potrebbe essere che il notiziario planet non fornisce id unici degli oggetti per uno o per tutti i suoi notiziari sorgente. Se questo è il caso Liferea non potrà mai distinguere oggetti identici.
Q: Liferea va in crash troppo spesso.
A: Sentiamo molto spesso questa lamentela, ma di rado non otteniamo informazioni su come è andato in crash. Se trovi un modo per mandare in crash Liferea, saremmo molto felici di conoscerlo. Per piacere inviaci una copia dell'output dell'esecuzione di Liferea con il parametro "--debug-all", se possibile in aggiunta ad un backtrace. Per creare un backtrace, apri un terminale e scrivi due comandi: ulimit -c unlimited e poi liferea. Ciò spinge Liferea a creare un core dump quando va in crash. Poi, esegui gdb liferea core e scrivi bt nel prompt di gdb. Questo mostrerà il backtrace del crash, che dovrebbe essere inviato assieme alla segnalazione del bug.
Q: Perché Liferea non include la caratteristica X?
A: Se ti manca una caratteristica di Liferea per favore considera di contribuire al codice per implementare la funzionalità! Per piacere prendi anche in considerazione che il progetto Liferea vorrebbe fornire un semplice lettore di news che potrebbe mancare di funzionalità complesse.
Q: Cosa significa "Ci sono stati errori durate l'analisi di questo notiziario!" e come lo posso risolvere?
A: L'analizzatore di notiziari di Liferea si aspetta XML valido. Quindi, questo errore avviene quando un notiziario contiene XML non valido. Puoi controllare la validità di un notiziario con http://feedvalidator.org/ e http://validator.w3.org/feed/.

Per correggere questo problema, puoi provare a sanificare l'XML con xmllint. Per far questo, apri le proprietà dell'abbonamento del notiziario interessato e aggiungi xmllint come filtro di conversione nella scheda fonti in questo modo: xmllint --recover -

Nota: Nelle distribuzioni basate su Debian, xmllint è parte del pacchetto libxml2-utils.

liferea-1.13.7/doc/html/folders_de.html000066400000000000000000000072211415350204600177210ustar00rootroot00000000000000 Arbeiten mit Ordnern

Arbeiten mit Ordnern

Dieser Abschnitt beschreibt die Organisation von Abonnements mit Ordnern.

Abonnement Icons

Dieses oder ein ähnliches Icon (abhängig vom GTK-Theme) ist das Standardicon für Ordner.
Dies ist das Standardicon für Abonnements. Zusätzlich unterstützt Liferea auch individuelle Icons die viele Webserver mittels als "favicon.ico" Datei anbieten. Liferea versucht jeweils diese Datei herunterzuladen und als Icon für das entsprechende Abonnement zu verwenden.
Dieses oder ein ähnliches Icon (abhängig vom GTK Theme) ist ein Hinweis auf eines der folgenden Probleme:
  • Das Feed wurde initial noch nicht heruntergeladen.
  • Das letzte Herunterladen des Feeds schlug fehl.
  • Der Inhalt des Feed ist ungültig.

Wie aktualisiert man Abonnements

Es gibt 3 Möglichkeiten Abonnements zu aktualisieren:

  • Indem man "Alle aktualisieren" im "Abonnement" Menü auswählt.
  • Indem man "Abonnement aktualisieren" im Kontextmenü auswählt (Rechts-Klick in der Liste der Abonnements).
  • Indem man "Ordner aktualisieren" im Kontextmenü eines Ordners auswählt in dem das Abonnements enthalten ist.
Die beste Art und Weise ist es aber keine der obigen Möglichkeiten zu nutzen und Liferea die Abonnements einfach regelmäßig automatisch aktualisieren zu lassen. Mehr dazu im Abschnitt über das Aktualisieren von Abonnements.

Wie ändert man die Eigenschaften eines Abonnements

Die Eigenschaften eines Abonnements (Titel, Quelle, Aktualisierungsintervall und Caching-Eigenschaften) können mit dem Menüpunkt "Eigenschaften" aus dem Hauptmenü oder dem Kontextmenü des Abonnements aufgerufen werden. Daraufhin erscheint der Eigenschaften-Dialog. Die dort präsentierten Eigenschaften sind im Abschnitt Abonnements verwalten beschrieben.

Wie entfernt man Abonnements oder Ordner

Um ein Abonnements oder einen Order zu entfernen muß der "Löschen" Menupünkt aus dem Hauptmenü oder dem Kontextmenü des Abonnements aufgerufen werden. Beim Entfernen von Abonnements wird automatisch der gesamte Inhalt des Feeds gelöscht.

Wie ändert man die Sortierung von Abonnements

Zum Umsortieren der Abonnements und Folder zieht man diese mit der Mouse an eine neue Zielposition und läßt sich dort fallen. Während es Ziehens wird ein Einfügecursor an der aktuell ausgewählten Einfügeposition angezeigt.

liferea-1.13.7/doc/html/folders_en.html000066400000000000000000000067641415350204600177460ustar00rootroot00000000000000 Managing Folders

Managing Folders

This section describes how to organize subscriptions into folders.

The Feed List Icons

This icon or a similar icon (because it's GTK theme dependant) is the standard icon for folders.
This is the standard icon for subscriptions. Note that Liferea supports individual icons provided by a "favicon.ico" file on the webserver. Liferea always tries to download this file and use it instead of the default icon.
This is the standard icon for OPML subscriptions.
This icon or a similar icon (because it's GTK theme dependant) may indicate on of the following conditions:
  • That the subscription was not yet downloaded.
  • That the last retrieval failed.
  • That the content of the feed is mal-formed.

How to Update Subscriptions

There are three possibilities to update a feed:

  • Select "Update All" from the "Subscriptions" menu.
  • Select "Update Feed" after right-clicking a subscriptions in the feed list.
  • Select "Update Folder" after right-clicking one of the folders the feed is child of.
But the best way might be to use none of the above. Just let Liferea update your subscription periodically. To learn more about updating read the section on Updating Subscriptions.

How to Change the Properties of a Subscription

To change the properties of a subscription (the title, the source URL, the update interval and cache settings) either select "Properties" from the context menu by right clicking the feed in the feed list or selecting "Properties" from the "Feed" menu. The properties dialog will then appear. The settings of the properties dialog are described in the Managing Subscriptions section.

How to Remove a Subscription or Folder

To remove a feed or folder, just select the "Delete" option either from the feed list context menu or the "Feed" menu after selecting the feed list's entry to delete. When removing feeds, this will remove the feed and all of its cached contents.

How to Reorder Folder Contents

To reorder feed list elements simply drag them with the mouse and drop them into their new destination. A little line is shows where the destination will be while dragging a feed or folder.

liferea-1.13.7/doc/html/headlines_de.html000066400000000000000000000135071415350204600202230ustar00rootroot00000000000000 Arbeiten mit Schlagzeilen

Arbeiten mit Schlagzeilen

Liferea kennt zwei Anzeigemodi:

 

   
 
Normale Ansicht (Email Client) - Der rechte obere Bereich enthält eine Liste der Schlagzeilen und der Inhalt der ausgewählten Schlagzeile wird im rechten unteren Bereich angezeigt.

   
Breite Ansicht - In der Mitte findet sich hier die Liste der Schlagzeilen und rechts der Inhalt der ausgewählten Schlagzeile. Diese Ansicht ist gut für breite Bildschirme geeignet.

Zwischen diesen beiden Anzeigemodi kann im Menü "Ansicht" jederzeit umgeschaltet werden. Liferea merkt sich den Anzeigemodus für jedes Feed, so daß beim erneuten Anwählen der alte Anzeigemodus wiederhergestellt wird. Zusätzlich kann der präferierte Modus als Standard in den Einstellungen festgelegt werden.

Laden eines Schlagzeilen-Links

Der Link einer ausgewählten Schlagzeile kann mit einem Doppelklick oder Return im Browser geladen werden. Hinweis: nicht jede Schlagzeile bietet einen Link an! Ob sie einen einen anbietet erkannt man daran, daß der Schlagzeilentitel in der Schlagzeilenanzeige als Hyperlink dargestellt ist oder nicht.

Die Konfiguruation des zu benutzenden Browsers ist im Abschnitt Ändern der Einstellungen beschrieben!

Ändern des Lesestatus einer Schlagzeile

Jede Schlagzeile hat einen Lesestatus. Die Titel ungelesener Schlagzeile werden fett dargestellt. Beim Abwählen einer zuvor ungelesenen Schlagzeile ändert sich der Lesestatus auf gelesen und der Titel wird normal dargestellt.

Manchmal will man nicht alle Schlagzeilen durchlesen. Um sie dennoch als ungelesen zu markieren kann der Menüpunkt "Ausgewähtle als gelesen markieren" aus dem Hauptmenü bzw. "Alle als gelesen markieren" aus dem Kontextmenü benutzt werden.

Außerdem gibt es noch die Möglichkeit den Lesestatus einer Schlagzeile zurückzusetzen. Dazu muß der Menupunkt "Lesestatus ändern" aus dem Hauptmenü oder dem Kontextmenü der Schlagzeile ausgewählt werden. Alternativ hilft auch ein Klick auf den Schlagzeilentitel mit der dritten Mousetaste.

Markieren wichtiger Schlagzeilen

Liferea erlaubt das Markieren wichtiger Schlagzeilen. Dies erleichtert das Auffinden der Schlagzeilen und was viel wichtiger ist es verhindert das automatische Entfernen dieser Schlagzeile wenn sie älter wird und aus dem Cache "herausfallen" würde. Um eine Schlagzeile zu markieren muß der Menupunkt "Flagge setzen oder löschen" aus dem Hauptmenü oder dem Kontextmenü der Schlagzeile ausgewählt werden. Alternativ hilft auch ein Klick auf die Flaggenspalte der Schlagzeilenliste mit der dritten Mousetaste.

Dieses Icon haben alle markierten Schlagzeilen.

Zur nächsten ungelesenen Schlagzeile springen

Durch das Anklicken des "Nächste Ungelesene" Buttons in der Toolbar oder durch die Auswahl des Menüpunktes im Hauptmenü oder durch das Drücken des Hotkeys (Ctrl-N) ist es möglich sehr schnell alle neuen Schlagzeilen anzuwählen. Liferea wird nacheinander die verbleibenden ungelesenen Schlagzeilen des ausgewählten Abonnements anspringen und danach die Schlagzeilen aller weiteren Abonnements auswählen.

Nach Schlagzeilen suchen

Liferea erlaubt das Durchsuchen aller Abonnements nach einer Schlagzeile. Dabei kann nach einem Text der entweder im Titel oder Inhalt der Schlagzeile vorkommt gesucht werden. Mehr dazu im Abschnitt Schlagzeilen suchen!

Schlagzeilen entfernen

Durch die Auswahl des Menüpunktes "Lösche alle Schlagzeilen" aus dem Hauptmenü kann der Schlagzeilen-Cache des ausgewählten Abonnements geleert werden. Mit dem Menüpunkt "Ausgewählte löschen" im Hauptmenü oder Kontextmenü der ausgewählte Schlagzeile können auch einzelne Schlagzeilen entfernt werden.

liferea-1.13.7/doc/html/headlines_en.html000066400000000000000000000122571415350204600202360ustar00rootroot00000000000000 Managing Headlines

This section documents all user interaction with headlines. Note: in Liferea headlines are also referred to as items.

Available Viewing Modes

Liferea knows two display modes:

 

   
 
Normal View - Email client like: the upper right pane contains a list of all headlines of the selected feed and the selected headline is shown in the lower right pane.

     
Wide View - 3 panes: the middle pane contains a list of all headlines of the selected feed and the selected headline is shown in the right pane.

You can change between these modes in the "View" menu. This is a per-feed setting so you can view each feed in the mode that fits best. The default viewing can be changed in the preferences dialog.

Launching an Item Link

To open the link the item refers to double click on the item or for keyboard users select the item and press return. Note that not each item provides a link! If the item provides a link the item title text in the item views header box is a hyperlink.

To learn how to configure the browser that is used to open links refer to the Program Preferences section!

Changing the Read Status

A headline has a read state. Unread items have bold titles. If you select an item the read status changes to read and the item title style becomes regular.

Sometimes you may not want to read the headlines of a subscription. To mark all unread at once select "Mark all as Read" from the "Feed" menu or the tool bar. Alternatively you can press the Ctrl-R hotkey.

There is also the possibility to change back the item read status. Select "Toggle Read Flag" from the items menu or click the headline with the third mouse button to achieve this.

The icon used to indicate unread items.

Flagging Items

Liferea allows you to mark items with a flag. This is useful to easily find these items and to prevent them from being dropped from the cache when aging. To do so select "Toggle Flag" from the item context menu or click the icon column of the headline list with the third mouse button.

The icon used to indicate flagged items.

Jump to Next Unread Item

When using this feature by either clicking the button in the toolbar or using the hotkey (Ctrl-N) you will be able to access all unread items very fast. The program will first jump to the next unread item in the currently displayed item list and when there are no unread items it will select the first unread item in the first feed of the feed list that contains unread items.

Searching Items

There is the possibility to search all items which contain a specific literal either in their title or their content by using the search box. Please refer to the Searching section!

Removing Items

By selecting "Remove All Items" from the "Items" menu you can clear the item cache of the currently selected feed. If you only want to remove a single by choosing the "Remove Selected" option.

liferea-1.13.7/doc/html/help_feed_default.png000066400000000000000000000013741415350204600210550ustar00rootroot00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxtMHTQs;:CDi\QPDH"sljDQ`ȍ-[&[A.EdXZQOBUgwν3҅yac璉dp0z1,`  SD[[7 MgIFd]'2yEH2֓3+ҢSaHm .Ȃ !0oe/\gD8by,Z  l$(/}iX/c?`[56JbߑSh%vK+ <60:wДj6i zv< A7SuǵXrnނg); du o%}&%Qam'29U ⮦(kbe"Ep;of@zMBmX%GO#,ZjWBV<{GHsm,Ec+Q Xag%߼^c;37q".? zIDbcQN}yCfB|f6yef}uYҨ† [%b5q=`6U6uuh%WA͢i0M;`Q5S:9IENDB`liferea-1.13.7/doc/html/help_feed_error.png000066400000000000000000000011721415350204600205560ustar00rootroot00000000000000PNG  IHDRZAIDAT8˭MkQ aMX$R u#-N7Jw] ]T)FD-`4m_.LƎ=ZP]cXf$^2'FIM>:[ kM\Aӏy)5R֨Y{8; ė>e sa Ld5Y~؇? RhЈV5꼾s+ Ŀ|!堕(Jc) P@eQz9Eu T/#&u}.h19^Z[Z@Ӷ*URJRZBHMU[j5ymZAt< Ҫ)QQS(Wh M bhqZDA9d>ͱaohQhfGO${!?iAaaW={f{Rj̬黎c;*ܺR'"#RcbԐS[+ 0#rArC,͵~cf1򄐛)|B+ަMɣK5t5:Lɧ{1_>-**mZ^"9ܘ2^ŶGt0Rj#/UXP|IoEo9\41 q'f,/p;N1 e 7nlzDZQ[nP7T!"R?rPVZ~3n.}E5 &KPM15)(J <˸XX$WPTH?w/V :I|yטYAXⴜOja"tE>qp?Oq2+!;m%}&彬2y*={\zAQlb4GS /ޗ+(}וC&Ynߍ4Z-ԤlVInn Dv]1;rv] ^_)Qz65ŎOױ篦%~W iyeuFU!WQL3 &Sk,'3r#$B,{)h+;h*ZLEٞj5w=|wq&U]o26VfUGTvsU>bQQyB8dB~}Je3cijv؜R>!4!zΘR~GIZ±J:VWBX,V)#ĥӌNWw҄?t:WSWH3wW %d!*K[ZpN*] %g'gEXϰTVj&$[RY񜽐i VSf)[ɮ!U1PWH3w)$YR~t32p^[99itܷ<'8vl%+jZyP5 Vj) v[T g3KKXZ+ _:ŋ,b$9D7n+};yITOV7Qb̍40i>4J51}GH)-Br ˲My)_PTz?6lGs2Kç'oI''0U)TzNŌϟLswJmJ+^%^퍒wU8t7Ĵ⻱u UG> ]%ކ+cnvD.bjRLZק܍*FZjLnJUZZU[CA_!@_%![ߎ]SdlPsWvҋue5ͫ:{f} _=;eq-6o\^f"" fn3TAVd@Vd@Vd@Vd@Vdh:0CCC:d/sN777tmL <{/+_`s8MTU 7Y'Я: !e[;lÒC;]޵%O~0At?9bWq^{׸>}ѻӧOF?WXqeMvVfںփV"pK^y=(GyN7i0qӢ a6X:ZS*wB/h׫iwCU15RWބ^ OofZI^JF^UǠd\7ު[#(\]tìYZ{׍ײkWBHQ׳e^Ny!V=[YkqHI)3tx?̦e7aDXm1BkgOѳe!OD:4%N_U?,>9#!Xv?aҳxz6sYxMJާ@g!Os)>zq8*X.V\$L[[ģգ~{Qk;BȰ{p3.\ta eI++=|ۮCF= k/W3)ޱ[':cQnPdhTvY=2sI};(M ! n|NÍK:*Hׯ5܌BЇWUAIJ !xuɧ BHqΑT=Z:ZjdGl-GD= p}BW%N~+K*宥Gg2\#q͟Oj8損#!N|TYNZFAuGU76t}yO~n}Nd;t~Q=x~ȺH1 >h}9+_6MC=B6ЗR4qf6 f #Vw~?WUq~}c%Y)i\35\{ =EY e-v]8Ҳ.v[']ݴI ;G +g10QJn(Am&҄Zh}.®~H}yZ;FlBH,i\BHZ6~r`_ZJr:us#z;q4"-omau{ᮣ9n&mD%ۚ|[-aa9!w BHIm8l6[K׌[VtzՓKFހW';,b ՚:UnA{ Qc+6 ?s Y-< ŪJBU^5mQ9VތJ(()'q?G%iGB.^6n+*٦ҼaN)dڦfIX)y}/|VwVc(MԻzI)rJ(67ׯ緛ik4DH2ݯ#b3≔~7b8V˙w%ZWb4Z^\\qyΪ-+._*K&iMôW~{y~ViUfB;L/ZzkiA)[K%XM<^^Z 4GVm#]h0rO@.Ohj}۟Of%^:J!2$MmvUE[Ff=!4h;H3XuI5^Si۽VSYQk%"@tL(p5}o D>٫6BTrO5 j($7< ׿&>}Z)UA¯KyerĄ B=tmxZqpA3j L͒Wuٳ5Qj|GÂ7|+}|E)"-ۧSI_B\M)1g-*֊3ߖ:8JGry.dۧ?Խ ^^rl.v \d~)I9)6@cflKY~qAnDϸ9`x2Ń njQy\Ozzv^u<~Ƈ">!Ay wZ>8t?q<>/=aCVgt2>,ɍgs/>~]H{/Sԋ%W5GRrZ+{x1yL(kO- 2Y#O!65gͰ%N7>M$MTeڑas$^kI[xOn?GO^9}.*yZqxaduG=OZL^H琓Sk9 ߙ,\> xQeimѣ .í2"O |(u=aƂ6w}X^| ã . J|~˲cªV7lmm^Ev2Om5{9[{/^e8jޟo|J[RZc-lJNsW.vA?4]]3ڞ ZmȬ6Jj] {'MُǏ4SooJI˵Q44bkUx !O]֣!e­1o2UTsbS j;x#PΝU~;pP_lv— ]6{[? e|;TAVd~ 3 A*ud*&'SnxBCC\l<7n$@Vd@Vd@Vd@Vd@Vd@Vd@Vd@VzXxl_%>-$M-ff"oj VM4?zNc@Cڮ}s Wk< 0,bzUY)ggCCC >: ⼵%G s +6]eKт 焝W=1LXN1epB]dmcn=hDY3ڜf쑘X9]",V9ޤ=GK@DžO*$6$ޥ 9jmP$M=Ք]7Rs,;)V+cgekn=hu-][^1į,iOMr Z_GaWNRœf4er8Acl{ptt-z_//p$a&]u86}/;cפk;N(ŒxT..*GP>{jҧħ’#39l+r e%~{=Qwk6$z`%ݕzuӷ/JBc{&f᫺ʆ=^m*|ṷva%4\wC&}†r }w[oaHqPW-K3R4S O>VF9> ,%^1։W]e&LPk*G%ZuDZAڀFNheZHǮNܤ,"Hx[~X޿{:Ur}#SUöI糘0B !6byd\}52Cq9!ŪZ*, ՚xDE_n(/"@9SiҦjP& ^xM\m_͉ u-w '=j"*/:SBd xyI5@#>*h !JR j 3pCfk2~ګ*034CNt]I3(] gW\|s~W\l,vTzE\{<3;''+E 3ުw=K8`!$>x-P1$o-,~-lrsa9Rm%Tdzř~RZfffVVVvvvNNNvvkz s߅{}?Le1s*`=.dƸ%iiтu%\B[7`K_zb/zXSQ}\z^u^,Rq!IJ۞1&?7Q+5^'1vh)i-)\s~b5:8MTm#E[Ff]W1\6K <"8GfM֩+' [Tk^=a8(] |Y!TI+'-"-ۧSIK}(sK_G]qDTZE@tuBg_I)1gɽNR./q>mŗUcG#i7b7q;o) /i]u qs%6rP k|ɂG2;NѠj4egz ::_ʭ()xqsMGQaqvl~I0rm ӬI}aߨPULzpP4EWRX>7ijlt "L;~O):'. 2u.[|fhj Xleg?ks祇ԫS}O@W`D7$#}N߱J{G"I ^mT^~'?lfŇDǫ7j p!3b.2nѠj4egzD^eQiwS(9b23wQC^kOCfM23U Fj`pmI>u`}`14/3H4/jx:wƅya@K"7e6--t>Y[- IYYYYYYY5?Iy~[J LI:mm IDATۓpB'B>Z;bWT|gjxx8.}x:vÞkov6f_D'[ecL h_3IٹFn >: Kl3j] +VRrW[✰Jw7 )n_H֞5uṱ{ITGyN7i0qӢ ѮP;+3]m]sACn\Jd1=8::?Xسf9GG#1y1G7 ݙñs'(~x,{[k}-NS^qb=ɋ=kC]ڦmEXl6O J|_;G'maiﺶãoc-MM3PBȭՃuuͬ|B֖ıFr +]Q'Ϧnda9!0a 9{98SgeSuBhfE&kc׺.Ss|1\TK_8G4ϑZKo"+ IY*ȷ SqN%BH9)Ғ.{9OkܷGJN{f2p{ns%15e϶ԩ|G, <&%z k?vNr _GFO;)䤗/HN|X|rFBձ W+xyEl޷!B#6 ]zvҺ/Sަp]YN(ŒxN..*GPrZIdx⽞仄U e6Wv{Ru4+X,NK&,y,!m`[9Btn< (l('nː9w&hxUYZFuq6G3-2Y04e\d%,-*⧂j@,U !<u1KR=.Bg_I)1gɝ4 WSXy^)q~C\u qs%6MT`𹎻Er+J DbӜA[ˆyjxmۧ?Խ ^^rlz~Ͽ,=6BUiFJ|BERcv v _EE2C !C]M,e3]lt*??z%giy#Js ]?sT,:Dw 3|!&uՒ #C;7b|J_o7$#}N߱J{5u~9#K:yO:x,7siF54ag>β<`ݺ.*Bި)rޏ9Έr˨gt9޽~N֞eGG/Ԇ ՒDkGu*a2X6& @): bgAoʠ?6=O͌9UtO7߈&>0䛔W%:_.=;i݁wOoSxIC򶝹QQӎ~ 8y;99QbkƜJ5_NTN+c7Fyxig[f*y&#n{H|=+XhtTyekܲˉWB<6z}mT+jTM2}TkP[xx8ZWXai|/T?6.fHqPW%LIbrj 5ᮼgk&}J|/?櫁t1:bV1//5+oÂ׻5N]1(!ꨄ$tVYk4=kHC İȰXUNkRЫO˱#08,2ˁsk1jtLb5Y鷥\QW(^)'FhP5Af[P09p߸=KiO͟Q }v}9&\=>d&wuu.2[QR$\O7~{eumkTNW9WbS+^55'{_R+/\۾5_8&*a>sՁ|n+nc% EjَNkZQ4C'WST= Wv~=vT;BUvNGy0gY3r:MXzxH=%Y-8Kׯ ,E/f1k.m[5U˒um->3[h>D@S9.~'/1xzF*q+v$*+֢Wv4|ctt̴_zu_SPPAtԡfkfOOZ׉ՃukZQ4C-N5gQqNl'xW%f='xeh?q͉E8B sW$/ |+&m@Vd@Vd@Vd@Vd@Vd@VdH:&&D.FFt55]BMT~A⊬k#ݗ{^u2QD!1զјw?h8@sgBs͍?\,RB#_Q{i@uT$j MM+ox$0=sI{n VNk (De=T'P+*PtuNzڜw9 TehB54-Jju 5Bs|䌄cّrmJ@G𥤴K1~SE5tSجlBHvĆKNZweb};7fa7)KtX{_b\- x%3j㸟g(z ".GOEbPic?ѯDY%L;CSy!UOD}݅g^~ɛ/;Qs a>TQMxh9/ᕣFnn.Jé }6Ep#,Qpxfj7F=}BM0ZI }2Rs kz}YV celtJ‡ ,%^UU?T6fC}:] }oT]7U+JOKS9}F!18ut.0R* L旪66^&M4A NuZ/t$'*$*j9䑩aNJ$_^O,Tkp0c7%*XLT5qAqN/B61<*D]b*h%Y=y_P\Nib2tz8ʡQc{w5LGf%L0_ڨ&fu]x;G;H-Jջ, I~RZfffVVVvvvNNNvv-&}QQbإazQ=J؟Nzp️bg>?~eG6c6^V@VSW J=,Hة>NbIJ۞Kx㢭s#I[A DRbOddߖrEJ_`V"bCڵfU")*g>M;4pa^üLWbmTQd&w%]O7~{e ::_ʭ()xqsB\M򹩱Wf;;g<,g:-}um&m5<,* p5Gry.'*OU9ijIW qvl~I0rm2/Sj"hP5suhdIwYvN^7RObIk.m[5U˒D%WׄbI.C̲N'g2Qӎi}MA9KcBBZjxX̜bsn!_ 5TA=?cKs[k}c0Y}̧q']\.{F_%Q \쿲y/UmTA#4gnyT} TW8&fay + + + + + + +|URtj}o3MMF N#Y{Mݷ^MW=j$ߤNglv[Ȱ_w_y1D)*owKq}ggy?s7҈N +Vsшg5[L Ђ 9jj| WAcl{ptt-z_/0l#[k|n^/Jˋ^]ѷ Q>vVfںփVukϚA:Z:փM VAںfv>QLj9敻h }[ Z_GaW\Q}M+o 6~ 9jmPpcazd{->-ьBTF{I\5FjWT0a 9{98Ss/!j~iZ8[j9dUzxԥj};7fa7)KtX{_FlI{٢bw6SX^T짛jofegWk%c7Fyxig[\DRιM t)#|Ql[r915Jgq?_`RQ5ya WDz#'O% ÜJ^0VHɎ0tILL{zuNo~&*ek (G_5 oplsՁ|n+nc0i!.YNgu}㧞_VK p!>\Ma{\^뻋U%rʙQ qvl~I0rm2/Sj"hP5siAh-*ZqX_]Y&O_𻨰vYz0+OEw f Q1u\|J'.;^;~FZ~[N K'C̲N'g2iǏv뾦۱ndCG1Dӥ->3[j0V5*SYefv3h4??W< : \h.Krm-ze2_ڨ&U3G25gYyз %.}ph!}zkΣYYYYYYYWF#~$ $ߤNglv?~U=H/wl" M 57tO_ظhK? < t;a"[0=sI{n U^^ѷf:b72fsQ>vVfںփVukϚA:Z:փ-X^1$}sOl:F {X:ZS*;Yk4m1=8::?X09=kf{ 6u{$&/覡;st8Vvd|NU׉:issq:fTCfrlyp ӱ]bcb#*`Wioݬ]Y&^:qCVns%15e϶vs|))-RGTBJ oN+!sžrBȧIU䄇O|X|rFBձ Wk~6hߘ'ߤ>_/!aZbȩD_3!dY޶3w^:J4~O'o''}^mjZIB"}йFF|0hߣ-C${[){-K3ݵj426M%ᮊgU3 45c iBx i92_BLtNJ,pوɱ杦,W&<<[i%Ӣ= B[{|ޒ۞F}(.'XU: %uwIr:SiFB >䑩jծNJ$ğbjjNlt6lKN8yQQyQJ *5MS?iz{0c7%*19 !@&؈=U8ʡQc{w5LGf+TCfrlyp= ;H-EfM/,}~ =z"nYֿJZ SN9wN&U;us6䭅oeM#7q_n.,~;GðMÆ- Moyr?)-333+++;;;''';;^cC͟1]r0½>z׏N ejNzY\.~NZo@p׮4e=W5^'/zXSQ}`ɪkKA"5W*-VCi Mo]}nduɰX<453DZ#08,2ˁs$TZ,B# ١ҿEEEEռ3\Ma{\^뻋עjtFЭe<FSӜ+\>7%E8?rLJ宐QE{5>ydPBPW#KoRO5@.v \d~)I9)6%t>? ^ya%ɱt%mЈRkH'SvhSmzp[+"m7w«z:fo6_|HK΃K,6ϲֳGꍚ+8Cጘ+繌ҫ(h MoY-802˺~{'-^Qmb)<]wߡS'_|N[ MXoV| Z/3HL.yAf"-IYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYN2 ˅"X_=!$''6;HPYYYYY9 _: :tjl6"#+@|9e\+ kWdw-熇ܻS[RʒjѨMjG ,B^qq/+s^ÈݺȞjm*>|{;ҹحXÙ=p|"D@;,YZ=;^R$-I.UPRWnffحcRL½;Y˙>.ǹ syO򱘻i%dhq>y3ye}ZA"Òf)+ybNoRRҙ0qUbVA{vk))ïmnƤYe\Rcu?wr>Y 54TqXEXR,) KJ%U^P[7˲ZXZ?Ixh{b@ >) *x B |Y+$.[@&X%E4a}N R,!,"H{J=G-u~ܼͪ~Cv74qb\--}>X/ \> I~Aayiׯ['6S~VVYC͜򴭱Ѳw_o6odH%-G5*[6}>s%&7+~ý{N  {:WKoRcnoZ8ZHKʸepVx|R#J+yl^(tԷOf^rq@<.f+Fn|WVTPR!oN(8?s'z@/\"XR].*U."mqA(`K\P9ڔM$3bh-Yg^$33)ec|$ˆ l~>QD)>yu}6UQ4zW^'"r6wpvaS~Uxs\-Sʭ׿_Sx~]b֡cgnلy}8wܷy%sV20Ûrx>뉝/|I!Twy-~w5].ے9Cgw2K^̯8 VcPd7=1bYs!I$JBNb0&]+l>ٮ'<;u4mq]YDͥ9?3.O|0AWlTs '0bd#Jw'vBBLiHD.бW6l1XFBUM3EaYeb(bE LB(Ɣ"J)96 +Zvr-_`!"n R^$ MaoRވe1%RAzCSS6)n>ߎ };O[aw\W)Sun;rK $c$is<է}5_ll %!)._}~HȴoyuӔfkTЧ#*Hg"ATo\O*6w@'[#Vnz]"A?GM+zfvT|[4iXBL8 o|&Š_ϵl_Vew-5n%Zm'~O*bL1!FhKdGKgm?05k mׄ'-۶|ϓw\5741)V5*,&X[Q4M _w]ۦ?^ۧg}U 7߶>խwFiZۯ?#mҢ#Ǯm ȸ©w[>|\}QWfd\J>t['F]sG0e2ךnno7cz{!5!WV:ޮ\%}* `U{_bۋN^)<@gXBZc ;NRZGFZ'b;#bw-)!njyk3:|-;W0c w=hey^ӸIOԄS%\cv[[.a3H\. B/23bcFԨeĴLLRB66qgҜcG>s=so_f߹>(/uET A_N.$559?hQA= K6SPJ)EHbn=E…' C]Yhy3B,SJ,2Wgp|j@u$gy"1"Dcw- bԮb96*ұjs`9cχ99_VPWw9ۂ8˲W\!dYv\!,4d$JoU}]CϞ\^^U}1 T)"˲,˭/zv Ϳ/6 AA}!bpT\`0Q pf,Yk"@: Dj $0qǵ;RH:9V^Jtz,-WV'N8aa>IIHFGz^h2DQMIDQ$-Ecխ\BRO`0@ `FI4MhhN0>BHNy AH(8oJR9+.."@*g=pFNN`$gbpH @*TRH fp̎, s "2cIENDB`liferea-1.13.7/doc/html/help_feed_prop_cache_1.6.0.png000066400000000000000000000506301415350204600222550ustar00rootroot00000000000000PNG  IHDRg}sRGB pHYs!3tIME+s FtEXtCommentCreated with GIMPW IDATxw\'!lE AժVq֪ugbXEڢT{ 8d l Y)#,_|g徹< N+b,dY/ⷯ"sޔcDd&ZJTmo3[xE-sj²  ض PBnmyGPǩHyc_PG{J[ՔQWc,~~% uiicԘC[E+*)g>б>3yOL˽كкbC,͹~SFѦUBBx/3 ^fĥK:Xeƪ(FDgxwXWVK"탤JZeuaֺ*%wfxY..睜!5:h(*1?-YwskV* no~]{SMEyBދϪ̵VP-SMEr^L*[z;sy.^kDhm^II?{ued^j^Ն@mNn [2*Z1*TTVȯ*%ɫH. Yj6mڳ=Sl\?KmF)Ҫ i|@HwDk'n{oxTM~lxCpV+VeҫhEHIP ^>w*~5-6/%Ky>4;UzHs_m9!2ot-+-_ >O"(j\Br^R~s֑g |~4B]z $i| Ys9m廛k\EAie奼e^[{sJ ᭽'£wBB yEyX&ܴr3Jb8\!!o[9Bb2†UJ\-yUW ! b5"dg^WWr铺g?Pl[AD_ Ͷ@/XKT9y7SEQƨw3%HUNUEMH^miTAw}M%9*[SH~LY >fWբ F{7*fh n>S(6f@S\sW!B:v;=%T$%t!E~HxN(@`0TWQ32SrmeŻռswR+BaBHvr ,iw5\PK7^y(f3Ox,Uڥn_#F?l;!$Y!/fjN!SfjU]יn:ZH]AA`MJȃ2!!ݢׯPѠ檼 Y5ͧz|_X_z׎QQ!EwNڙcEۯ<8!VXNѤ[.{ޅO`oQ On>σ'O >6x}-irN%93Mql+{ۚ舖$]>U7G#)6X_װUαGiK kv%n\uwn=9Qz[im؞dr(logB:;s8o>o78*Ai}؃Lfo'LJjE}7,Gh9}#SBȐ9ztޣf.^zEƖwTfU^՞>z;?~ߢLɆ?;D]kRvY&儬b6_BLF^X]!V_a*{C3Oc`rW8߅C[*3U4gzAd]락B?i''5ܻñE/z<)(Σo4E "]txqkwvk/_}JU}sjWD%(fW܋mJwY^[9[!B`0r% 9 4nCcboԚ;~?<$QY?ω+V]@ψ)_޲^{C[V%u?HPB Ra2]V\AS5c6t ;;[}Sןdߞf]2xBS:hS^M7Ê87w9Ec!P7}rq9ݎJ."H&g8d^$Њ~LI .S-BHݟ%vS~Ū]4]UO QٿK矂}Cq!wurmː^]7ħ",E!w ^_$cL>`À'>"#Oe|Ẏ^fj" }  FO 0WmM(\E4eDNo#+ޛVW-}XZgw5%kxTpix5wx8b.RbIO[V6q-_jaRvNעa{B2wYzBF "iC<$+@ݡYPmk/br 硣3`}W4io~% |v%̼(Z8MOV`Fa3Y[TҊrA?\qfϰ:$7^GegDn}=יܲ3ka2HY? Awwqj*WMKIso 3R ^UYx]Zc$5~@G!r >~ão8O';#Qo+|nIMc,V\}=ߤ=Z;g->k KBAa#w["Z[o|@(Ȏw张A?}+Z5Fsů'_W iOoo:i<%_TY 9,FzNY֟6]kj3z?OVc[8eysHͻ-\ͻr tLl]2+҆}sZ}P~opR=UʗBOؼu=si+̥ yҎj ަmA %1DN=۲]wfZjQjQsZFQDQ * * *@T GкL^Z֙~׺X +H E6BrZҒ1{$NvfcO:::_p59Gy[:uAh-c۹ZWh5m$=V'9͖e>9t_r.Xjah<$sF8vr*'^tihC}}s 6 ZKH}`=nΛFdGyMlibkСXbkK][8qH'S#=Cw.XhJӶw褯iw}; p꬯i$CxW5}iBZUl6[@Q5O.k6&%6^,&FEɻ%J~rm ;rC|FD/;!d?kf džR3ݩphӸ-}z<=+剧c7%i;ǻOS^>{K+鳋> s$HqB-C2ӮcߴZYZ3.zNJZV[,I_i[1yyuI?VjIh}IF 2"˸IkS=mt5 7-%K~eМgoGs৓tTDxqme^V:3`y|Dv=o]U-^m@iOv'IC0q󮚛OZl;=xeAzGX_Jkg5d)SQXպHXʹ/5(2"˸2ЦI bG@ҷf c0j9 -{`QsyEn" ˫!7%~S3]~U땽aO~vUKQ:ՇꝖVN/}5 PLjR}d,DqeB!,E9o6t{\(.[ט>}[2l)C>y Գ#MrאuT+Ϩ_ƪeKM2JM4t(-[lToԙCGegB-Kw/a^MAJ_3}[B1shPB֙`_WQ'Cߕ2YX}IFIu[Z "y.Fq:Lp׌D$U1ۖ -x/qoحuo|KN^y.,'*޻Nm?/KFiX e &5^/v?~NHt_ψ3_Y)};e)SXՋ}IFI6"˸5 *w\~lqUN&VK|OLf2ݾlu=ذD}NfAw/͙uo|Kϲ=绤qN=֟*8tuwٷ{欯#zXKNN47vmOd<#m~voخFuVJNYʔ6V"p5r_jQ 2n k-f| [yN.72P=ٟA¯2CƤP^{aL 0Ư^ť5ʾe9$.,esq쳸/ڨ3Hڼg % ڻ{XẢfE4Q! (N 9; /=:o^o@@TC,sЬzucDyzeq?-#<$f!.Ԭlƒcć )y~ZVf5b m{UJ9RUĀ*BHY_in=*5li|>4[PL_ȱ'? ?WH)L<2ru‰+i5[+6l*d^eym9_ow֯(o:wF`繻i>rߏ帥8Mf}Fnqǟf ʒ]6W$r/?;0cL5f+|-.Ρ$SEE<<6yo7Rkg'4pAKYw>uZ$@TD@TDO|fÈFqDtQ'Ehj-ll:D:aǮC._Y}Z<ZTΛuyo*.b_hT`r Z+]復K \4@ "L=qjĉD;U$@T4$@UQQAQRR222ի,8qu@ZAy^^^WиY}̺Zoೊ EEE.\ڵI444!AAAGVSSvlz,%6= ÔK~ g,BBappaì%CFFF.]4iY+ѡTE[#pSXuD%  _,Di QQQvvvU|Yǭ;w~V_hmmmkkM_CNACפ).<_3^*Ҟ.6=l<R#(6-:(ɎMqgl6MZߡ~gki *BȥG>{]rw!粖PXD^w:%#CF~n?r8NY'fGf{or]qcgTzA\ZuB&JX*ɵ1 wCݧ)Y/kҺ)mY!OӡkiiW1oZVYf Xbp"mŇOj&x<__߹s犗8qB}Z/v FN9/ !ގfO'騈V:,JN_(NWkԴ<`GwJ:ilv@b%2thN3J=<|岏4mT;iV&nr%%=- 5C'EM#o͖B`rSt*BHi (쁍IKOv !,Ŏ^A:7͡1{7Z_^Ik?ۉW3_H. BJaa!!֤rˢ"i ժ}beaR eb&$$Q_ɼrslcǗ؈/ bdϫ(~~ksC5V/-U|5 B>EUkuj+Iv' w?LDR%OPm0ꤥ!cN^y.,'*޻EMm:qY|!?+ l *B>|+WBBB8'$$$$$dĈҾ@?.gء Gnuy;.?8*' c+%mQSh`Ksx'{\b=V5n?Y_G擩}]zxa1KW'-= ;9%,;tT^_4ݔzv2䷿w<},oE2N<$߿]^^NQQQ100/JkT{ˍgi`-$էO߿.~vvsƘ|fZГ8|zM^Vtuv *ǪPUWwc*-  * * * *@i6  BQ+Ȏf7Ui26FT|PTTt…]N4ICCRPP4zh555iwRPRoki:cθ~ l)VBappaì%FFF.]4iI:E)AUQ~N\Txо%=>-UW bֶ%04tM;㫝L][8qH'S#=Cw}W`Iv&/-˼s`zNj7Ҟ.6=l<R'}`=nΛFYu !Oldkmx,Dikmmm?.kY E,LfT@UQQAQRR222jlT7cp.\еkI&ihhB F&qh  fmm-yuuu522tҤIh&l $5ˡ9**N2$Y[[FGG7F8vr*W3;u67<㿸bo8pʿyV%]3P_ܦǂGBC:Ztvi N}`=nΛFdGyMlibkСX.ԚBkFbcc#mV] |_%?69a! .v.3m;l˨yygOb)2Js[_ujjf?mVky\W\pYj潳;n-wϒ]OgZI[kaaQ몡JY%KJ24~`$^NSSkʉ*{r{jM档3S GWcWGLO-(*888<}411檄X{{Z3Z;3arxC<]©'/{sw1U? 5Yjo{5&/gD0~J =fےa"\ Žec֙M#ZPT`2ÇrJHH|>ᄄ1BڗL=^bkddbli::'S;.?8*' c+%m!JiSHxȜ_;/;Zk}]zxa1K׉]G>^3LO , gggBTTT /FxbG⯏ _BZv>9< ?IChhhl˭3ȑ#QM2Ip l;hEp Q5}@UQQAQRR222{ig6N\VPYUUյk D(**pB׮]'MA)(( =zz 0qZ-!D}~1wr U4tφɛ$ ^<h6lؠAY,bٮÆ tP(wտsO Fa ֐㾾bbQ3z%\5ݻلǏc3QQQvvv5WY[[FGGK/ ~ ~s-awloyqPq*YX;@(Ғ.`onc#\!%.muutju:wv0qElim/$‰C:l%+H+$;k`KC]ǖ&Q_/p01Mtχw5XdiNO^۷ի}|||||ݵp|DwRRRlll}]6r}+]pADwG qlg[FU8ٴ?ILz+j|%ZgOb)2Js[_ۚn!$m1+e]_uʒK#@2oһCݧ)Y/uV*H+iܖ>Kv=y1}ڌs,e5#> T޽kYyi[˶_Upӛ7o{ /1d/c+VvWΝ;ֳC*OG6kG$.׼&.DrVuB*Gdrd'yJZYC0qGMoGs৓tT5<Ͳ\=T^FS nGYj/~_Zw K,uO=*OiUJ~I,12~piIaZ;Domްᇵk>QTƍ֭$o֙mO.44յdl6E~}bUk>ܥ5(IS#8K>|jl'[bQ9!!GrKڛ-'zp"5XCJϼxdNK+t'ķo,Ę=zhZg2_Za^K7_/,"Y3aɼmJfh;hAnh!%*jkk׺H3Hb,cǦYG:3 (r?-DQZ {k wö^r;4ᓛ~Zl00OB^F3w\㢯&GqٺƄ\_92dAG i+Ǩ ])nTf)~w~>'r_,..NX #m_/є{prڶyb {iz޷r[X-=Eɿ+k5 3 k@5vγ|O:+]{jr R8Ӛ;%JƱ ;(ҝtee{x3ܨĚbcc>h*[h9xJ;kgr|7mͽ3cqmh"y.Fq:YboDg9e-qj+Ia5@R+υUU%Q7{YVPM#|A{noc{_`FnŨΝ|# dƞ8(O^ n IDAT㽰0\W$B 9|ϧ;88hjjB Fd&t3N|k`uU n<b2uUiʪSk1-oOzI>ԢHvUU՝;wNJ-qA X>}qQOAAՕ~h9Zv3  j|/i *=<F```l ,@  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *Ē1]``  p8 l\AQQQQQ> B]]]-dQsA(˩S\Qp_6e)~8?48nI|`2X[O_a!*AP^ MjwWl+x)( Kr1c.W22X5䃗BBQE(BBw^ FDΛ۾-[yYp^y%-U5M3MS{~}-֭79#V/|ϋћ TURP )_W)KIagkAH !'`0 ,*JR`٩#_**&`ꔚ[lVc2_۩*ݎΨ2*Rhof%TGS-?d0L%d3X YU!LRwLؗجBL!`R'$U|'x|[c4 ={;SB!ALdc1ޅ&$AmM 5.Y_pdAM;=%Z{r JH`P%_HB2BRT\R-*k};v3ެ%ԌyEκ=DhWUUʯv4$*BUM[J;r&X'FD7$.w}6ףWR2~Y<$м/mH+*?Qb72EbJ@JʸTUM@^|;9{e˰{;W￰/YKNN :+Dh2oUT9uY_)c 6k%C]PU^Nj?~,YTn{.q0Sc*i;o -ɎE;.߉੶yګ+WW_m]GKAc%.߫o%v]zmǭs>n3$&ѿuRyf#LKAZRSB+)USUxqxzڳ?q-7i9۲uǮoo1mƹE&JloNfyO0PM+vL &?铲}s9I{~ޟ|0<>1o7kS9vqIXr 9(/b*+ʷQQd0țr@( `k㫕9nue_8Dݿ;(}3Cz2ikkKk(Wr 9CNmw\G+(HK*7TO xڮLE3e~qQzOr}T{cd>";\! 1MKI_jd:4'!d xNZvoGs৓tTD/yqme^Fs 7S)j tvۨ(sXDP`1DEY y9gcMyf.Vs4X@_o+';I`/7_Rj8SNPfq sVꕼ'D'ܷOd4~nO E[b^QEnV l5RJ!,Ŏ^ܲe65:[LOOOWWWGGǠ^yܧFc!=F|Z~Ed00(RD $!REKY++ի"՞: ;9k׫WiyR (?":mbG)uG @@ %XZ\L(W ųo*UmfiE%q8%rE5Op8yy1Y@H("$'*UQ%s_V*,/)UT*"())1i'.u8J.>HRz~Wt4CTpp(J^]HS:[HԊƽ?%ej. RgN+-.W/z~k{MKw/a^MAJ_m:*; j.}+E _=~<kVp*!_@J B˔[ew'i[5HLtF,ot]"PJUUeXe;8"RƦ~wIow*aT0TiUxqFp [[eY8n}(Jg W#f+ϙ􀃁 +xUWYa(+^%EQ/:~jbm#,(_ʕˎ?t|v}i,x4, `MTӺVZW#<Ĥe]۪6Frh{EMBS;NM]9Wݛ=HO+8ׯT&$\+B踌 vK-IV<ō3 |- 9Ul[ia=lJ ̈́l\MVB(R +R뮥R퐽=֎}=,|¦e^PPt׷r"{y28>"EJ|anB/ 3[˾mܭE6#K~QhyeUl(tT{NZ4ǷT554Q ˍLQEg`˪aw'Yߵcm~nGH) .v&͚~B10#_BtaF$:R[žT3YixΒ"{Z};1>LJ;7/=ۑp eZK >k/m0?vWѺ IQA4!ƒUJuw5DN<]avޟoġe׹êR[x ϐ. \265ErWi+MQÒrP]|Y6((lc}^Z.Y'g >DjڌQWt ul;Pf0zI>ypBYE|ʳħ OP,y?XK㧞m^sqs6=3/A4(VHfx&QAO]. EiL雅7^,>ߥzR~~be>qp/夦,=i=}5/2r\H .{KW^F⓪6û5S 26%h=+Աß%RWբ!+E|W]߲"@G'< u8P]Wg}P]S*$< w|2c !D&a @,n3T H@*R T| ϤT>O2MtiiiaC wѣOP?6h=w~ %JյkpH=gՈX, > b3>PWy5;bҟ*Y?ZO*4z`V=7CbxYU?֡g֝LXHLUnp̡qMZ7w(O2L&%>vw#<5y0>Re VM[u5k'.ݻs:m\|tƠ3n=p޽K|ӳA*go~GV`+hcc Ytuۼ%;)T8vd^W?>.!9J͉-^RṪW]bЀ]=j8J\: Ȳ:5`PϻܭW?FS!jI2>^BOhaoҝ'^sn]zyjc = ;ìn^M5mQDZڳc+F!)YIt[z5e1uy]K!E|s߲A|<| Z/DIX!?Gƞm_}0̮9C~ٍ9"\iiCݰaCZeL:kOtZV>&ouU)#ߟtLBͳ–v""0i]uE>!DuiדJBH^&>loDlRs!,'x\f{OcMi;jMyş- vy[r>*mBZ3kƞ} Z&!D v*?vn}8ׁ:fH~l~7TQ[Fxo˟wKw 8,5ņ/m;wNMFS=g] x%NJΙ7o\M?Kҧ΢ Kjs}g z|ot4wF7ю]&pE)y-yq{mF-b}dhAy~:9MjxMzl?81a4wj#˯!1{qJ[cvkaZ8Y#bQč}Z6py"~;Ҕo[y9 xm;>*mڇVz+pM=:ҶZ[Pp!H {~6\5;xxs!?|>Ktb C'tmnpW߬g7l?n<|I%GlY٩#wlia~ߦOEo^߳y-kT{Z)K/̆K%"RCv7>U:f;Ux(=jm :`BȱO~uBȿ{hz񄮴*تtԟ4eH.bXPW֔ɧx^ģ:6߼C}>ؠÊ-#t}VV2Ŧ`}@&reY!B/V3{毗RB~JhdvRt6:r N휙l8k, !2g͵sF/eYaeӵޞGwJx)'CEgZ8snKWc56Kh;˘ 3r#D}B.:CBfsm$|#oEƹfn{ea Yu~C{{+S.Kߘ\:6ʑ'kj%dAֱ*w*JNfc/l %0f͒9 w(ѽzQ c[[]pJ!$>xM?978R/k%b 9/8w@WZOl =es oʍ6_󪿕Ȭ9I+r|"Cɷ9N1pn/Gt˄KBrwibci/u Ɋ.- Wt.8 Ҹѵ; [57R[KJ[81"y)*_r4?wNz]dC[@k'd/~6[#}@hų칻r96r3J9cH "h.Β_;J{O]e3aqjFvf~]=<&B|ky}BrVG-EsPgק_]>{?"[e2R;sv݃\}{VTf*>S_$+eyvڽG3Y6J׻/ h"-oSYKu4.9rfcA;ͤ\3ZucW?t0Rx"ЉfM»Է?I ]OcV腃Ov5x#+6 7#FdNŧ_>lM 3|ѻl>ͻ]ZutzCW>Isf㫮NvN]=k{:iu5V;B{L^N'uԿಸ] Cg7Z2sԛ[*to1{:WOA>\KGJ>degcc L=$:I̬7i{M B]qoȣ|bPAc'񩥹Ta%z.ylz&65+&i/a۷׾ 9cmu9+`ݫ{l3Z k)WE.7_*9}GQ1śyjh>pœZMױsߺ5]+1E'En6'u'o뻻[۠A+Hx~IROOO}6 !$અ[=M1s5ku {Viw<|h{TTK4R|P]!@ t<x H( * P_n P V'S1XPj;?T֋bp T H@*R T H@*@EON06nҾ H,p%VUZ;]m{X_缧Vn QBFva```U㚎\4%,L 4wbq?=HӸsH:nNv5wpvRĉ }7qw|҂] m{swO4' QQaԇ*m̼q^_xҲI STk;ذ_ykD2rYO$Ĝ<+li'u Js\*vb<%72aQS 5ԼT֟|yѣ\W^0}}waaTNc뮰$XIHR]ɭ{rc׼פ&qWOuA2s[j3o:,e[T*-[@$zdS&z˳yNu4Rr_z՜A Eq Rhj(m/}yra 7S]x.$̊>fLL5k۬D3) 7%Dnmȣ(yFv.ݍȒ+gΜbŊaZ OIru:ho#B9W]O+`9t u|qFN}뮽C/|Rad]K^.F\Vq董,r*7xݣ;hw'iQfVXwaOWUP@7h r< OГ*1r8"V~}\.ձgKm$B|ioR|g MMA\'Ӫ<)DH>lM 8\k ݭʬ kC}w{qsH]M K aY²,Uҍ荾2x5g|ܸsg'hBHj7&8y]\wpH +>6ƽq c㓒?yĻ_΄%}k-ߵ*^@!+h'tSqn"e0]Զw=5aPԭ†w\UL (swJiR ۹#k ZgtƅUJWUwɓɅFM{+kJi@EBd2Yؤa*T*'i; R>j :.!ݪa*Pax Txҿ)1 WH@*R }4RrhD*|LTxG'\A PP]}ڣ* Mk5mĥ=>T 1eԻO=nuߺi*hLpoBB9W]O+`9t eV 7cd4ejeߨYfu$u.~;mH!{Lup !q\KԺTFFdɕ3g$< >wAzRhbU@*o[wǂ*#n=r4b z{;f%<}YZ9Wx"W3ъ/(}$*7,箉Ӈojkgvei4`|BHN*6ΕH.+VwY&7#9!cv&݋uIDATqj;ZOTUoyRޤD%Do'fin-$lok*g??5 tq.&i'}wr)ó\}fϽ{|I|n^ʔӯt{~5E[mFQI84`OoQIU(0i]uE>!DuiדJBH^&}6{\f{OcMi;j2[ ;j"ؤYT*- o7Qg;y$GB;fRƣM=L'q~V"$us'ɉYi%O-{Sj^\OOx~횧\[B2¦ ~L_\O6]ƃm-ziѢ.:44٪FF?%Ñ:{6_{nㅵۯԩo[/lnp;{}Wv:v5-to,muJwr{]wk@sX.[QAb o*JϟZB;żη釵/GBOJ2*eG6kIR|WJ vvvJxnsr~ˮ۬w:kAX}b)!}?M#(le, (jLF=&S !V?龪 !nQ~JД!]k8bC]"@uD.A[=<&B|ky}Br<:n)BM=>R}Z ^ޯ=i6trw9^K6wW]&d;07fc(rUTA!Xy0vQxIT H@*R T H@*R T H@*R T@*R T H@*R T H@*R T H@*RH@*R T H@*R T H@*R T H H@*R T H@*R T H@*R T  T H@*R T c9_``  ڣ!2 bW5 H@*R T Ca>u666BH``oBX# AZZ4,"/Gsf $yɾ{7M*;q-':_Aq8Ƶjle٦GBQ"|h<9iǎ:װ)65LRy?( E䄱#N>P3 aY%,a !0%R7Nڄί9Vp_Y4 L-kZyՊ Ѯmynjf:ۄNF"!Rj0,y)W4R<:۸HظHVIxKQB lfGk;;~q+bSˣ8\gTy^^?PRRޒ0۵R"PEXa!J5YU(UO> Rݰ"NA1 EC8.*Eqmd,h?uƮrsv;'cвd[OW(#)E,˰D!jSlAZ/ߴhXǫ7𪆷3/k~:R*TFG:) 7%E@b_D-nvN܆4@qY3Gu~]Jh(i`9]@U7;pjt|t??Cͼ{1rGq Pi^ D !eYZc&,mb[uSߛ2",aY<0ݢU=^7˦5qp96MX7H35'14`xW>Ui}{;K]j}9;$BH?tj^vcC^з}G{{'-إ`IΚYX BB|g[XXuWo=1d~ܠusrtOW'qm4m]o__|+"oϭ|a|[Gmr712r9ͿCw9K]tLD#e5rrM7)SUmdeV=pUkt2/=ji& _x>^lnE_3ׄ|g^[=kɓXuvkI%QMg,b#׺G{OSѭmnɹ.f~p8BHz]"97X~3F2}o18~m)@dccS<Qu{ IK).)likmVY@ tLDLFYZ*523|.*7;ɃdcnٷOU{sODINnݓ#kRU5uyP[CKk kjqJ%~f*˲9HDٿk+cClG{2hvMUa0[Ե_=O[fdrgX`y(X[ hWu7oוK~mR/brm\~7:ٴ>|hT۷O!g>y:A"={ҩS]{k~~Ѽác5oS`Y!;Ǧ.~Ok3HSu5t_Gˇ~#3H8Wb֭u1'CdSa8,U#zXeYBD\kqbrAF"A}VYK&l=qۡ+JŲ, áJ2?!R*F<~bf$1/JI("TpK+/NJpk/X@4;#C 1!*%TR|>G(d27&+eFѣcvvv@ ZRT*Ffee1 S4 |> +RLT(6m2xJ 0L\9R1* K-(߹PT@sH.5W\.-:)R300ppp* 3gϖxqqF9&&r[[V `􊦂P(+ji5Sh)RJ2//O.k (BP$" tОBBa#R@`aaRϊ0T TszPhȑs ހT H@*R T|Xt<(O7 $f!, IENDB`liferea-1.13.7/doc/html/help_feed_prop_general_1.6.0.png000066400000000000000000000545101415350204600226300ustar00rootroot00000000000000PNG  IHDRg}sRGB pHYs!3tIME,*tEXtCommentCreated with GIMPW IDATxg@SWA"Z*XF[guI=REkZPālD 2dx?DC "~0{sIsW!!2 21_W;V!efUaDf) 𵕬}YmV P|dZ.M[{otQ!\]VUw9!?ix\ny93.R7j^QE-c:T{)3*J? Ͷ6FkE^ ΃LH%m7{!Wnh8oɬ0Hr/CHmיy񒮦ZczudFdz[UTORSTmM]Fn٣15RGjXh)u*[u?.*~nUU^^Fy{?q!k>?.z]t1R%95[3JHM^peW#^V6qPì =iОIPPWH1w:%wyf-mEujYkMq6Y7z==9y<Sˬ O# rei.!56ˬyͩCth7 kXnd: I(8PCZ`i6p^Vbs8)لyeu\B._5eil+Q:!ĦD&BVK %7qrOHͯ/Iͯ8}/势H p,F)oPNQ6R5nVUW\/uspρ {oBm^& $(uWB*Lcןy!Ri]TW !U%1ꡠbSBKIfBH-sNZ^ WͺS+|-MaE-}E%QqY'sUd޹v-5-4}YAY^xեo\s?̼AlDC&9pXcm,,)x;'BOJEpպQ6SFXt1Ohz(+;h"qU>!dǥ__R"@{E"QRQJ}j} _5\; ٯ[l_4 |Fa0 YYYYYZI[CCv200 bYRXX~^ھ}˨hM>N8;D _ϯ$$$ TDn4 Y t: 9uIw7骭g޳OsֶںE_Lھ쨠h3H\sa ׬Ҹ))|޹mS' |ؖe܍iy2],?u]lc±]5ҲFNMO~rˤ޹B)gM.-;+Kʫ 6drt:_۾sr0j6cUbE8[ڻ&!<'{nj K69ٳqTmi}.KвǷxϞDI!>9hRqVط;z+>x̬7'r[_0IZ2#7q'g_P@q2`ɞg)٩ϼ3ϼ(feEFJJv?ǯK0qmg(NKÏ>jH=yK^:޽l +$ÌFRizwYS&ZNB~S-غspp[>/ůL۵ad7/Zlg^_6㑗[Wmw? [+5}NM>=6]8|_X&Ehm,**6Ռ>Į0')V_pmHwұ?~Km(+j0ks@qvqX])Edw̕eTy>)feͮZ@ҏ~x`o7sũs}c!vN^sᏢ/ۨNѳ}%p谿v=ɚ8LgDI%,<WFa0M/^h=U+xjqh{4TAA>oPoS_^fTbIU-!0mS~c0fݭz߉Cv8\ 5_bmm'Ty\]׼-Un]ON]*eU6::wurrr>~'HIƩs R4ߓWcd?~ob֌[sx+;0mm4#C;.e7"6!Fn߉qnC̷ĕ٫.o#ľg/B|3tYg^QiGBIUAEg6nkgtKxb~n`slCyiJYv藍Qˤkلy{;"19+V:}O5BȵbfIgL= Yd;7ycTgy-Ie"%oʪ7dm5%w.iyDTNN1m;UnOE+ڻi^Yߤ" oNg1B29h,fGg26u/:mc:T_5XlB6{RD&jEEEfG5?+,^`5yzdyzU+V9 GH!9'%jEsWWB"v[54!'aG%طr'91;kASiwdMWۓ7ƿ&7y+&[Fa|b}G_N\޳yъ8Q;I)^)9;olםOW{jVl#;Oe(꿐_i0R4Rw__M IM B3]&ˏ~FV&z]y١Vb<#L qj'/tu{?QW {[RKRNnK% 6A7Wڊ?澭k:{gn>xkO_հ8%I[>o=y'âj,fGaSęumKZrs$%ڮÖkY7*eߐ? Vn^>sGa=͛^Bn!v"82{}d')Y6(ZMf4nRMڄ(4%i+HQWG"+/+o@=d@Vd@VdWdff...MQi  + + + + + + +|:N1-=8?1LxۭVk % <Ȣ$OΘf1ͻi2ngH1>C,tu{~3&ғ#'\ -}8NkjjXMY5?'6 ﮫo7;;'6.TGW˩eb @(f_qK꾽e_upsi懯s]3>^BOj~yZ9緍ٗKGh#DFVAW Y8Mpap^%!dGoyiʧB~XAZ*x׽lvmqNJYS`mojquuq➠Hw t%Bȶ;u{\)U]c[ht_]ݮO=vj;mcfAK(N=.A< ;8mU??Ewڣ A_z -MCքa@||iY}:u*ز \=xl1mK t:SG8|+H7ڹیNBmSב\.6=4q~n-%dYwuD#L-BJIIi;iUE롦,]UɌNט:gp 惧i,i,tGYj+KWU2$^~Q/!hMmB#1>1wOzz~.,ʽ-Goe]?sŽUyzW9xldϳg^g^-}~FswF v><#Ŗj~8dxsе)ݹtܦ+%4;tMrL=t ϳQo/vճ[qh:(յOu5cxn/׷6P א-_pmHwұ?Ԛ&{p[99^Ị̈hlQ+mx]?se9iqOvXSOUZj2vYPᵸ\|VWV߸e+2z^QQp8o/}IϩjDXFRQ--.*;&7ϲ}L=3=y=:vKSp޼=g? uI}e GI5Y̨2į"B>•תL{GNOu~M7Kj ! !NJiB^늚 MJ [Oieo*9Oe_V-$DShW{h** zu쨺f:vRKj+-]e!Pl.ym(/-Qt2þϜ)npU1kN:LA}iV$S+b wKme}]5YTP,8_L1!%\Ajyӵ7͓h+BlhSg$VT]8ZKGi=ق*HYQ%EzT]\qX;iHa|VܩCfmMI 4_[VX6R&m\eiM-qcލ,9pӧH'6 7ǮYS?>7]m] ?\!&gnoy`Wtݔ%[jټ:ElUOKǠq;Cŏ0)#:x7y ֌>{outO=eun6+)%+Q?)bJVZ@ ~BzYDGV@Q(,,@q y6WGRЭ* !z)ML;ArYtMẎBdwFّc!4 ./OSiZJN]MJ ? 3)ã"o}vMQRש=d⍍Iwzd /-Q"+|;!2?22!UU%SFZjij5ux`QMȩ xv7]K)#iR]!\N4"jq9_ϥBϭfrZRD}hWdhWmv'[e3 r !\.WyB rlK  ~X)8݄]q)[G(Gy𶖿!D7cT;/c|b:q]n6<;wfzVc$x^ӶŮ~e%Ww&|$4-+-[e=R2SygLyw~%荴NQ 5pD/gĜ>AS$- z8z7`0֘1[SZ Ef3@~' `Ŋ222XV]QQvD:??y =`SY MMT⽬Jf,;Vgcߺ^#S invGskkg2o2[pߣф'+zd҉BB4XBQsUݗV&ďˬ~Φ}H^fTbIU-!Dh|(z#5RD;b)fS!DF5DnZUje%i|q2 )))B ; .,-- 5e.&;jOr_o9Kg0|Nmr n*,$)r9ͨNHܧ; m;buͲ;AV:\{ׄקhQqRqBB!dηݻtP m.t!񦦦B7].<+@AuE+wE=;5ʛ5Մ.S_}u:'֯OQ}oNE7MM'jګW6cۃbsm.--tRϞ='MJ).. ;vlǎ<ygkfm6O+m%w$e]~=2pnܸb@ssڵ#G6LCCCFFFFFN9ʕ+GԶ}1:`#8#y/999Ob@mcccaaѸ:&&F:_^?NOGĪς '8Eс-tu-=Кo.8agl;vhqsC]m^NE t6K/>/U D{Oq53:}q|E` ;+4m@GKیULo]ڄN#8b;C,S!:Z:C 7$8#X<ݺ:/iWqwwWWxH_>\БC=~14MtСC'B");v^ܭS.3J_Ԍ@+(*****jן vG̭-uʕę !e\RE[N[^?b]z왨^z9Mz –̄+V͛'0/&&i**ղ[EkldT$m2wx@tBRz*6͟o\:f#uImkZp NQ9"}97DYQOW/'&dQ0*ʬfӸ]֦ g e~榋}HhP~#+UI{ݼ býnXRUKwA!2uͨU]%v2"W[/S-NCy.U?1ݗV&'Ф;r8FJ5{Ninkf=!gOҬ5 5^]qr)}㱃"*J_H+(HIeg5E=*z=l읆2}Tz͖<`:t(+2ZZZ3f`-{e;;ϟ7.JJJE^a\*eE\dG<݈bqX1 |cKصѷu<+ǶM7gf"_Ա3b-)Nj5'KU KEJ1#VG2YWȞxc%;-p"!u_śO |?GFo]g~vQe\.0޽5$3Y\.q{*9{^jva a12ӤuZ#G411b0\nk~+HRRRF222!IIIcǎ,cu_qZm/L)8k&t졳rUݗ _Ѹó},Nfu;S,u]QU!k6٨u}wo%)yӗ!Njٳz;ZkkW Yd7{Vv2~osMw2 Jcj^b+r_(1N2֮:;^W}~z97Ϸ~[YYnll|SUUEQRRׯgښv">#kkkiӦBy >uvc ̀$ڵ ]INNŅ%u Wwp8C_Z* +@Ҧ`>ZD`` |o/@k?da +:qf@Vd@Vd@Vd@Vdt>wlvdddvvvuu5!DAAA__BgΏeR"Q6L\^\Ղ|.7.]ٳITUU !111AAAcǎرջ:w2w;ce>mڼ>QK~aWY5p8k׮9B⢯ʕI&II |rٵy A y/ds'fT]j) @+DGG> kk똘hrZGN=pd֥E]7RGKacY_۾sr0j6cUbE]{NS/Y;RW[ys` lok0dsiv8NT /dcbgLWh$ Y 7NH_[ϰ޿cWrŌH:.Sqt`K=^ouCOGy͈`H/PWۼSQ},j`4]?JZMWT& ;&BjjRkk디q-:=Wm]==;ߩKdם'i_ˌg7ѣp1 =z4}gGBӲn2;w~%荴N1ǖKx̬7'r[Pkȩ;KY{?ad Ma l8 `Ŋ*͛7q+VC~kBu7kxE 'W x馮+bդydFx}R&5c6B=/JdGHMFdQ跧[5j;)+HJJJ%%%JKKKE=$TU(g8 k n*,$)r9BWcs켼QP"Qyv;SA>E$Eb!B)#b}L=w YtO JMMMXpӋjR~}LFc(R~-p'YtUtj}"5>(Jw`NޅtܓbnDNAq΢ 5zeH((F@Ѡ]ȩF&k*N*ƴn]tPF3*~5K7+=qQRRR||mUpX#IDAT^ ^n'xd8̘P GFo]"~`*'=[i>g͉&|}kr#nf8//ʌ p5LՃe}EŰ+lQLV]۸%\SV^ -cזG߾&׹?PQ QA1 S/-33`Œ /kۖGGP|'N~3}ԻӎƏ]|YAJJjԨQׯ_ e0,b1ѣGy^}O޵?>/+wrs]]c=l9u452g=ۯs/Ӊ{ Zf3U_GlU!Cuyu3{p?,oynܽfӋr0504[wjA"l~Kztg}H{קb ao叜zgOk.'VM66?n):z2'7;eA[ٞAaX>ɩ"())ǿxB>::h,>v ̀$R]3/%g_3rڈvvH>|Ituh /~_YYYYYYYYYYYYYYY t:E_עfj"دOGQkvvf-ɴhV}-L /_-:ނC[ +|Zoq}6]Rj;/l>F ^Ws+d\8qxw#}m=>{i|xrt:Wu1`I&_(:pÐG`tGqֵpp-Uc|b:q]n֤(ʽ-Goe]?sŽBfğe8ewrl bˊK?"-߻N/> +1ϼ5m[JWN]r%c%*}q2`ɞg)٩ϼ3ϼ(6=i&;BlŌH`0֘1,//x lL41Oc544 [WtzL7uKVMш+hdT&&i*^U%tZvhlӵMN{`tݗ?d?uFFGۨ`P̱5TR'\נMCN]蜌! :$&H̩עimdt:؋Qj W ⭶w{g„Oi?n+ff7x8׾6}9[(SWx2x9!h4&UQft6[DȞƕS%*FB6R7_JUP̱5ӹބ4 cEp拏nF%TBDMEwČS^KfWwhmR˴z\>\mcLD;5оȴǠG^z/,;va`\0=P^Z"n CkBIJ \ƪξmVŬ;w2Z弹new&uӯCt}jpiZgh둂E+wE=;kӵ7͓h% _WOlɚJ% %ƫ+O.|OQ}DcE_`Nޅtܓjү,KfU!64[Dԛ A bZfB|$ZcO?@ [Z]Ǯ-}Ms{w?p9ɪ~q{cE {w#6aeƄzN8,i0ͳbox{l[t{fLq!eM8#vے EU7~޺jcEj #,v򗯴QDv b5vݟg9aSo.j(#*1kvͭ'r2x^m 25ڽ7R~yQGV9-;o5`EC}CV=T_`NI̩e>s]uY;TFrbdc2{g@(*t0r?kNJ{F7^b(bynܽf<{_^&:7 ls;hj6ޝQo.j(#*1kq|LͫY으bѪ&Zg| :0Am  N`YYYYYY@|ns``  '@Vd@Vd@Vd@Vd@Vdh 2NCC@ tqqiNcdxw _B (\.rڴi8W@֔++H_O_ĪfҤ:w_m@ȑF!+W]UKj;v[QS]eu$6<{{:jGƱ8rK29%uQG͔.#$\VAI]izLr䠁͚9t9f|gey6UmNejj̔6]ܪ"CѤy˭~as92]޽GQ}\vv7m7 ¥ HԢ6`&W[ Z/(-‹+0-r K&B6lvw1I Z|?ff͙=c9nӓb ?b # IŻp}W-*6L+x D"HDID"mQmI;ΫX?VRA @AP*AU`* ÝZRwŧٵ a "i URʦN)5n\2{9I>yP6<# mI 3@c2ht VzphND{bϬhkǰ9oT@pW0Xxq Ayyyե*kv\'ON?wpnVY^~ -;D߮%'ABK[ƜW@p_Vܷ}U#GNm؅{ݿ{߷! LsYb2M s!&Nb\EXiX}`bn:Եd.⌁v(c[ZOrpĻz$%̛~̫v/K>bpr|\bj֘ulGɪQ7$سnO]F LӔj<_새 ?Y%ȝV. 0kv=f;b,na?1fz#G 8}LeN1-KyC޴:'1bh8xMlj埾јKh?.Y|ކL5IqWR<<'{> e;>KkM|5׿$ݿbk IX9'̼^zŋ] 7{؍}㾮fFH "(HIC !Q9 >[c>;uZk[RW&p!NxaH#Ո2%"DE)e`ZNTTT<$uΦqGkC}ԆΞ:yzLQߤN&S{+xv`n_8(3c6;f}GUZZz9S9yqlswj6y $" KHD",A-Jgb-,jOONFJkSn=пꎢI}^)--Ͽj$z@M,,24\,#- 4j Q4_ڜꮬDWWvn46ꯋ3G^FwVNnl wW 7uG Wյq"D8PqqJ99HP5A?(¿Z'b* ԓEr䒁Q/r–G1(K+U98Qei TnR;?7ޡZ<9PTU:Nv]+Jp`*Aʂ9VsW7Z-mjk@Q -ɶFvsXFww_9y80BT8KJ{$h[wIYZ!oVҳ">\ljo>GSK  xk6^w`3Zx{j2Λt츷 ZrFVdkS)c$DK IldYYv]>dO{Φ/0B?l1` bVY7\1\!gdEo L&[5]x/ʛ_.qK=ۊ~}(Sok3r֏{hAG@,3}ckN{O9Wm۵Y'1LȨ7nguP/kv_jlfRϯp.ڴ׬HmgXxGU֟:Lm_! ݿ~)>POfC]uVsZ}gDzzZs5[hC=U9cDD<^ )o\G;Na]o]huq?t--nس;f|]b*X_3hMO/>b}:,!?I8r=~)VU Mq?,/rvnmw={㍙#g}3H77>Fnkĝ@tLLsDYO*;J|V }[O܆U~1jЂ5-]:?cz3c-mL=sf1ztÀn]Wg!FyѣusArw |)SJK!F5ԃ }(DE3Yi bRQ~RcZqCS"5%͍7̅(JA P6̛״eK-slUP<$,( gp[<`8iRUSnbujɡE,˲0 ]S!tUtg`0 mZ7T!C$yTd:1z!;Ƙ繨P@L@j\T$(]ߣSd2%$$g;РVTHO\-aa>MKJ2EG[-j Vk*(b0XM(R}ء3?#T@3 `飁@ @eYQj61 CELj!22K.9* T@!tx !m޼z]~~>!~7cSs YۗgBRs 􀅁֍vY7ڥWYa(~T/XE[[@CZP,-,!6I0 'x&\X6 |/bזw曀jnoTsm_()\ڱMG:mf"@͊ e^ dTcJWXBR"|..SA\*RָW}s-NQ~H|"WMWs#5-UVI06)QDzHj$WMG*+/k oA*@lM)_tʯ/mAg+=U.a3&I(jnVzB^QP @8f|GO+KG3F^/~IhidhcSc1م^$]}W(luWPPNj [odT5G„Ȕ!875ҠR\o9K{4^o3&++5*iTKZ.6 M5tx: OUdIa遨(sU5}B! ĿhGt&ħMЄֵ؄9"yeOT U϶\B(Xj zQ 6!d䐆TkUid).P?c  *0vЖMsn;ÈccA#_5uV.|_71r4Uz|= G _'VJUK&!L~np_\v84K|+.so>WrB3̑Q)Ҕ#7ciQX<0"遆(M%[\TzZXVar-Q|1!6K^P\?2@+y,Un^ #x(`򗒔 ztr%w==:Y訨)]!!$]镐G4!&Eijiu7-_B&|Έ!4I\wf:,F(+ !GדhڽNU6Eijwni[Wi!SxnNݖ};̱2SҤe=].44xֆ+NP(.P E JĻieyN Es}㶮Ub|! ѼVMW4e2 >G+SHzhw{ZIO{ި/: * * * * *a j;KQf[HOOGnÿH~nnݺu8 B^xQ{ؠA55t@7}@TD@T8*899)^e_:]&ul9?+JꇙUPM|{22.HolҬQ=ծ}>>a[NͯonP|Z? 9=٧S#lΩ]׾C=sSS&۝+0tI=62sk=wlnRjɵSjly^wn]ԡCnK|~r)|>-l$)ش]?nqq[v榦6M'-9$e˹{gܺˎCXX۰gO;;_Y'f~WWND4K=-uUGU״nCq+ťEs7,ptrr 9st]ݚ1myDHn[ѡsÆ%=[6quntdJPF_aVkCg$Y3lxeޑ);Kg\|ut3xKIתOE?r"rcOPޟ6tȮ$"7沣Ay{Aݛ4rnչƓ5 Xy.YbZ2-ҤXDTbqu:[}8hiփc>uqviucRlK6_FJ+[4KF΍vsTSFyRai Y^wVD+O# *Tف=aPBƳdAo4=b=xQ yNnVu9!-rI1 NP;׍[ھl}0W+q'uwQ}1mV徳5$ov` T8u\foNڳԦ.uU9lmS9"/[]I3/edG#O+}fo(¾5!ͳa%X4O"Uawpq|L-7lҢΑ $M- ~xBVhí'tU%Y8Cݪtt,Zb$^iРAo(zpzHxċ{{!I\.kj$%2Q4s[SGqI#cgfKSbȮ,sN0ïUgQtq\v.Ǣʏ{ nJ dއ,軸3c$+:R!uƍvU-j-4!dif;gڕ62 ^_@ ^sփ}kǜ{IHˍۚF<;atDq5sά#ʎغ2(M"4y`Yy`#$G/xқ¼NJ^Hҕ;٪(K|Jp7#sJ!UJEצpބBb-o=kTq9*u٤ n &*[vfSEBXsׄg-lW~4ΤiWXo8W;]HM0?d{ND0P)ёhێdv%d!z/ѳ/~aqgOZɎ(._r}%kXJOydʛh }٧Gvڪ FKS$@ 虹j%l׏Q0xjb'Ɵ6yM$W=ȭj$q"+^ 2.+H}SxwC2#L4Hb|ۮ> (?B4los:oM)iO .UnSjjOd5ۓB.uoو~+k}] L SA} q}1.8`h~JUU~)Fw6dQ hg2Ĵ(3yMdM]cq!q"Z8n+If;Yp0!K@o/{)ɛG8b836t)^A0[ ,oWU fvEL]qb sҟ:']aϯ7v((8X,zԫ_*UUg錘' mSrj} LM-btyCf:Ģ fa!uD٘ZO^ƭq?8ҼE&$n3pƶ\rSA;WheqX*j౨Q?bspDd/wHˑ&3}hqkIy8;Iu?ߗbZv7VQauO.ydk;BH6gֶJ-=6Fx҇MWxRV >,oWU(_8揿%(mGk>rp٭kv,gTQnlЯ+-Vڽ`Ɯ?zg2uY7Ixzrj;nɿ㖔Oj::tiLʜ7߮w=\ 1o=@keۼLby 'I77ϊeKU70*Ѷ&O8 !KrV7sj{w_7]ܗo~nm۞l*Խ?"tx{BIaww*BAw{mX6s<myOťO؇Mj' >Tmy+pΏg&Aҷ|>>BTD@TD@T“55=PmWZվ[>  * * * * * *g|>Sqx /[}gEJ-m]#S[W.auT.a;f.p!ٽ Po}):176lҡ_թY_{g !z4RWai:t۩{+D_7{k+ 3S󺭺yN\lSuiiea&Mω6&έ=6|d~ިY- rZ9?^ed/<5hЄҩ۰|NK+) GL~ꨠ8ᑿo/)=iuؾk<:LIeo`ݤ$aTب¾ՃmnVqҺ#_y]GI.~+k:"j5/I}qo dUcz3NT/؄ߐfu:O\L/6#D@TD@TD@TD@T/!_cJo;$ (ʅ]wn~oS#R/J(5 Uv|Lz_ܠ87)U~PFDa$OHH(**"x< ͛s8\AZx5)N\6KS׼Mѻ} .ԡq}[fp162D\޿cf&v.;PIm}ڹ86䐀fi U<Ǖ4:QϿgaf&f;. ӪS ;;OSSsȐ!f͚5k֐!C444=[ ѓ026sj6z2'Oeبqb+̊5|ml߀!kML;r#4%5zgwfb:VZ2u㩨WXDn OǂzI^[tn<!dĆ gN~^ORO84߽{Ν;r8ݻw~E? )---%13{ ՝س]phɡKfNiSZǟ.tԓ,fv,z4{Ǔ/r&-v/Sr7kC%ug$D_U[Zc {{M!ħy=!C|׍ղ^-s,~>ԓ'OڵkW"/>I &8hla]~](J% UXQ6^(hd<_l2Cv._߹%Y3Jԩ?(2evI?R0Q:RgX ,|dmfV1i:!$fPýW= Rv\>ETT#!ⓤ D/>I"899yKC^E?vREצpބBb-o=kTq)%p N3ALBmUymX`a~ NHuBbsdbs57QpkA)(-6l z kT->`f0~j"Y9[I >d\W#L4Hv\由{Wlc|ZIӷx~M?T-n66mЬy·Ӣăo%diQf«)ɚCD(.$p==WNdv`B _3S^7?pBqfli%S|qa@$,y~œm2Z7^3e [%Yavfbyxx;w.66UOO IDATާOِS=+:>2s0Xɫ˚JzHw\| QAY|>?Z*߱ZZZ<8qDaa!!D]]S7^9yf?_.1QwpMӪU*ųO *+TW~p)M@TRHT  * * * *׃6hDlR+} < IOgs^Nj#|UQ 1^g͚%﮲P(gY?OHdSn8ǩnO:I3_xyDH^G".I61w>âyɉ_M-̰{OWq=y,QqUׄ.VܣO:J—ٵیU^j|> @NgMӕ.ԟ^qF(9B*&2baFV+J5]s>olf/n. j]0SBzVo BTIճfgg{]WLFQUbs:YRYge-1:ՔO“SRB;Qʰ~ⅼܰ0[[ oY/o~j"9[}%o@MCl2P}ۛ/rvD(W^  sqqpCG[E iF>9Hr.{8- 7pOϕӾF:~_ 7e6n]ж lXiii"H$ٳ+BXԍ=s]3[V64Uhef:Ģ fa5ڿO܎ uZ7WM:99_;YZvaa_AyD"у !fff?7^'P$GjӪU+ r'!}@TD@TD@TD@TDzfΟ?Aիر#ڵk_@\AD@TD@Tq;w?~|?v؝;wD"wlpM7zv~ZZZw?0Lˇ^/bs L,3G'*l䭉iݻ|-,,.^8d D\>ޣ -)Ӏz߫=@M 5IpplHONN quuU[{yuByA{i5vi::ޙMNj%h5m7 QϼbG:JתOE?r"rck*+:WXaVQʣs\3ake:Nߊߢq ՞r_&~imVa8WE %*~U*_ږ?/;!k}'ǸynC>8B_[MOB*>3=w4R':=qYr Uұ]25430456|xvE#,չ~N]B$\:t1֐GhL{Z0r(0DvNg=ctO/hB!hJ#G݊ܞ3}ٕ/ K!Uɹ2wX$bӲn1>&&>}7!W-p0R#KI0arWi@iZ(sL@uu,CC s=4F %=Fpu[;S}-6#[~~ yP[Xױ&Du=!4CVnCuRi&~ⅼܰ0[[[K맯-2[UMO#M7K2"IHFh,Ʌ/|`ސ..FoyPx}ةs/.NQyTa _zUhxxXXM]cq!q"Z8n+I&o#еӺKһ:~_ 7ߚ/ҩ)^A0[ }Iݤ$˥Kܕ RE -*NO|w+Dəb?$ݘ O/`>͊&{gB_ $,ܹszzz>}(BJ[=˯'SgYaQ^fӼ$k؍Zϯ'ԝel{/2pysM,5j=zN5TU7ڡ/>VX"VU6k۷$KmPW{Oo?)8z.l` }[߹=PE]ޭ-[$dCRaE%hhh=:DT\"Y^-Ji\ [a::ޙMNj%?ZkUԍ^9`Q 3b1}(#CvQax/x_&ȻT6zwYdޫE'넯h94n1Hkt]8p~|%!N?.<<4lE G ^K.<~O{z*F;1MWZ`~)JRE8_y/ȿwȲފ1㩭vdB2|WSW°}UXm(g|f}A:oāBB*]0==K{ֻ˴-994pỉ!vJS|qz7ede]fc|Q+SX㌄eJb(TZa?C5ɢHBT.ɒ:Q~KFؼ*\ qj\~QuuKmQ:!cBYWMnK9\K30ze4h%IW;H߇̩=?/i JGqe%o|SPo*+ʙZ*?6<U9üi]f*\LA]UB$j~9SaQU$Մ2m Rf5>}!$m!/=NT_|QnkS'qɺioeF47Ət9eτ̌siJ4ERSSė e^GQ!](60,(f-fUXcO>/k_)74-&r艬CzlֲS7'sMI9ҥepoFl#m.;Hq?YْJhF3(.L䍏y REΗl.F4FW:섐{7ptƩwԩF(iCEy2*|'SUtmzhźaP/*#r N3ALBrrrJJJjjjZZZ _LB{1!̢xFD"XL40cSw__Լ=/Wqzۨh?RJ(_U슪+vBHVJ KU+\QNeI¼ǒm̖fDnU'N f 2.+H}Sxf&o$ɡ(EBW ^h mF/(5\+ Sۭ9 &._}C PGfרpMW㣠*UTQF Qfؓo,龾IU]LOThg2Ĵ(3yMdM]cq!q"Z8n+If;Yp0!K@o/{)ɛG8b836t)^A0[ [*5w1Or\ _ks[\L\~Sv dM aҾ{u^pB4-[qSw7tmZ7咴{qc_-ꛫٺ]Z߽*ëТ#7YJwqȑ ,*L[cmUzѹ"!ʦul[VXHA!4b\ݷMCmհMK.ISͫeجXa䍏2VJap]ZڌMW$}VODMжL/KUBup%!ʬF3HPUw?6rw 6.~cIgx__߬"wa@ⷸ6zӨt1#J|ycRyg.~~};3 q,@7xbWwLm{:k5*zpf"W|^!D!A@TBQ $@TD@TD@TD@TD@TBoƜ^̾l nx$/K޷;_]a"TjsCW~Qp |#<!~+Hy1W&Zļvӎ fE\֧cIK 863kv[{uh`jlи۲owƖuz2_(*zeU5SDKWE^_2^Rʟ Fe.Ie6UɖBu1۸ZwyT»z8[hUU!S7zEozgwfbIzkCg$Y3lxVC7O9J_Ap~ǒOPoGqK~wFhJjHUUsUx +_@M~SON*9|P[fn6χK/:HZfEdeq[Ӆzna>ϓd-%'N{<,ۈ^-ޮ _AK|qzE담*)?a@/t8q[=viܴMG{S I\뙄0PTgHGS 9O?QcNEM,Jj]0bL/q&TJt5%]tysZ9o9 ҷcQ`y}ͥ'7P{vЖܑ &!999%%%555---55EJοصVv5Yp!&]WbS+S|N>-Do.oBlJ^~n7颹s1Sfo=?$0X~#=7W𞺟ڶlbNV5~xWVBVmJ-QЗl8%YRQUDsv+qdaEop$gof %yUtZHҧzlHX΅9ۖ/|Q<}?X(Ό ];$} Ѣq\9ҮiOjGy4ä)r3H d?6,D"ȃLD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TDD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TD@TD@T@TD@TD@TD@TD@TD@TD@TD@TD@TD@Tz~~~,EIKK@$QQQQQ[`j:CCC !ݽL"H(w ?t < ^> U $KӧAA9  (KZy Vݻ!*OA\X9,kZ=i r2Bo\۶< '8{'r"& 0! !}:QfPӦGSX*%iHlggbyMkEGcĪD$w]Y:OUP" -NATTPOg'K:L\$SE8MQ,BsZ:?YRW74V蔜|-<WBS"R,:Xgbqqcm k^>X,ŦXeq,6b)VIԍ└jؗj;a 7X!D IDAT(04C4)1B1#1!ާATnzjZDA4EEX,b6* ,bB&ᝆjms'/eZ3s  SK;g 9RD\z$(ahh"4+$a\ꛛwi5q%yٯϴc=Drǯի3'a !Dc1uDOWuh+:v=Y7$dtoq&rဨĈ+v}q`6EŢ{o˞5T0 %I<:@av/vֻwlw(o~r._K�f/ |SWO#0 20Ok"F\ː"˱1O0),d s>-*|=eg|ژYjZn=z[`J[?jĨ!Jn߲h[&?x!$RYw=xmaҋ$# !VZ}V ,iڒ}0I]~~֍쭌$;A)6QU9,5U*EBuر7b╁ef;ȌҔ{ӧ]{)@dhh({~^L g#Il.Pl.12kǔS*☈iib#1b!hhxe(+ ɚo $ _IDVݒ_BZ;|!FEaዺ3JJSp ;SA:y3~]ZhT8DCp(.p9DCqٔ*p9HæħXrҪH`gf 6YdDY''FH?}u秗uÇ+s#+NJt -&""JM}$$0%yGRR"b sr(.vu|HN% j]0Sє:?ȬNa;ˏ|٫pcҋB(bB DL3"0 C8lVP®RE}^n;#nP$ l8X4Ci:ϱz+fF84j=t.I>PwVu {Hm3n(`ͦys=I01s$m#]79Rר);`67ݷ2sbC3de֡(f__gnsIZƤkisNVg?wNL}3n$O3Hovhoj-#y1X>|mc Cӄ,Su,52fW}x Nߎnv Z;F \acc5gN̖6:y9EhC|E0 COmOLPsjX&$(ꝥjB=CI PX,>"*jO^G[73a PSbi껤|nXf<4M>ت6gץl6UQ' T ?vDCP氈))\*CwmQmr #joq.m!(9.H)RH"i4"i4Bf˥f6ai8t1m1ZktJ.OOM`pTm-LNN-˲{.KM30v$RɛS {5|`ww?"I QEQNj"Iaz*cDj9ߪ]?R`Hiu^𫁪30Fvk^g{ ʃL&F߮*^=eC O=lV+8 ^ t<^ ^ /,{[yIENDB`liferea-1.13.7/doc/html/help_folder.png000066400000000000000000000006151415350204600177160ustar00rootroot00000000000000PNG  IHDRaTIDAT8˥NAYV4v$'ЂG0L)l,@k4KT*c0I؝bvIdI9{.,xL|}A>/*kZxn}<r<e<:=a]jvP\%?TU&!/&@vEis(4@ա"0 DQ@e(ϛ(T-Jt"EDV佢".~aV9Q,@ .D\~ Ok}:-߁18;NS&$t$yO@Ϲ?kNƨK;:7O*o hKtIENDB`liferea-1.13.7/doc/html/help_item_flag.png000066400000000000000000000010011415350204600203600ustar00rootroot00000000000000PNG  IHDRabKGD pHYs  ~tIME8>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XDIDATxݒMKQ8$\Fm$" DR''ZjQmE MAՙMAݜ{>{ Q1 f/R>Wb:ATp iqT>e`wĀJumO-|Rzw4)q푰2t 5"$p&iq; 5dz  ?UzwaN}v65,v 9:܇ L p $B<66hIJ zb٨^/mWGXIENDB`liferea-1.13.7/doc/html/help_item_unread.png000066400000000000000000000006431415350204600207400ustar00rootroot00000000000000PNG  IHDRa pHYs  ~tIME  )>ͩ>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XIDATxڥ1r0Ef8J]s$ 83vrtZU>..u#$V_/iWxPV54 <1c6xdY6A'л` P@2@.&I@UUdVy/}~N'W 2Hqr>SMpsR^_@R}XDA1Qڵ~۷qR-vK`Ιc`yTc(CDtIENDB`liferea-1.13.7/doc/html/help_opml.png000066400000000000000000000014311415350204600174070ustar00rootroot00000000000000PNG  IHDRaIDAT8MKlUwg&(}HqjP(ZHj`#BPm{₍d4QQkV l( %h )-$PFJN;|=.A\;{Gv|3.tE^ Ź(&ɚ#}B;t>;8+O,hB $7)j ^P7.Yx k_aWj2M 0N,@LyƁ*M W{;n+"B1¹|{eYO Pu;[=7>C=hOٶu,l †5I:[SqyrK <맋ܝɳg:Q`8ڂ] PPچ(J^-/^Bks s\ʡVHӿ'};Ԍ[O(!T5 !x 6w+UUU/@r3x+U˹O>DhBt#g?tL04Sy)oąʧm{R <0#8)_鿊E_hKֱdR+Aa|9}Op{t?+Z9/T|eQ>h|EK e?M\q\>⅃~V0/@1 F[e&k5Z7 BIENDB`liferea-1.13.7/doc/html/help_prefs_browser_1.14.0.png000066400000000000000000001175441415350204600221400ustar00rootroot00000000000000PNG  IHDR`aSsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSa 29 Mai 2021 23:53:41 CEST IDATxy\." ^(xeifffZ̒4ԴN-4NK0R35oEEX",7zky!/3 """"""""""""""""""""""""""ŕ&m[DDDDDnnh(B # )pȿEGySMDDDDDK^U ;96Kp6y)+"""""3!6y)딼f>gy f""""""8. ۠\v!亜)"""""77GQ.l&8;jUв42&"""""$eyrh F (eܼ GBWGi1#˙us"""""rqgLN×!Hؓ].l _B3d_BA,{^Yvm6K `.BjGDDDDD$#{^rf\{9bޙ0U}&"""""7G~$tY3ϮMG!,P $t{uf4 DDDDDF&pf ke셱!,G `F _,g^˪M{}t _Gf!̐:{m:9 gy ]Y-Y.G"""""rp|e W+}fQc9 `ݏeFC  ېGm+ܼz>㥄V H2ok썂(9 8M9|*Uj\07h4 R"""""Rl6)V51)ǟ:ujpH~b0^Ȳ Y|W42ve _KHƍ_)@bR)))lq`Ʉ;W\Y92r4pԑŚ5};= Qx݈WyҤq7<=WVJX4%"""""ʼnfj|2Ԟ9 KsSMlKDDDDD$H}U3+n!,Ӵ;_P{h0F[cq-m!lݾ '<o7#6k.Es|^>e")OKjW?4fͺ@u}$[HNGr%Kח|w`k@Mڴ H<_«?zVeƷB9G| Ժ-Z7eķPq{&/W*^rݶm­Ž\z!l?8w) Sy^XrݜqNDD D}A{s4 ?1q?~#mG=X&OxpjZ9b'̙ħzF?ٟ"?>)ĞXK=?a!f Ȱ(WK 'L/947}\VzzӲ|5rݟ:P:׬imĝ˺ъ\{ݩrgiRs_=i?ʤ1Z1с;LٮXB3v6~տFِLclv%t'^v]h?ތWio֙e wͤ=#O=^7?ܜJpB}A&y~2eǮ(~e˦߹Nn5ֳt_9Ɖ ?֛*Ayn ]̺ZD_'?۵ٜ͙G #Z-kδ)rc?lgf0K3vC])N?>ˑ0g˼BHQ;'ZKYq60 7y,[BJ˾'4~nq˾k>rE/R6S=GPD Wm+O+%KLޔ6I/ >5Bhۼ"8cQ ^*ݲ+!|(t}H> IKڮvpL;Ẁ+&eg`Fp={oĜym9rѝ%E/R6s+KK<.w>D!1~LgNFjW9q("Rـq&:dx*{xJ'̭Ž\qu3!oa-~cŘ>\ŎH2˜ĹCJt{x~D=s["9].})]`Ĭ{ )n%0\$hxc^/jA>XNnۏ僵; qfҨYNqmd3<&'>|ɪ@VL%⚶sW3Р᭷NiDDDDD'~ײ0 S=Xyىkၭ۷Ii%ëHN[R2, -Ӓ܌XdKfWa{t]&7;mfwoY &P|řo"\d#śX|/߳Ma=YDDDDDH؀((KQ$4 H!Q)$ `""""""DLDDDDD(0B&"""""RH 9`cb )bC )1ͅҎ.A)$ `""""""DLDDDDD(0B&"""""RHDDDDDD H!Q)$ `""""""DLDDDDDu쉌,.ȿTF Y* ADFFVɱM6u%""""""DLDDDDD(0B&"""""RHDDDDDD H!Q)$ `""""""DLDDDDD(0B&"""""RH\"W ~Z֣tOZW,ȿFX&^ÜM(dec1u)w9:v%fh38a+~IիBȀEL\x(6Z3`L-V<7G0oG4b₧wi*lLNxe nEb=eu>CɲTל}ɹJ{G.WH`+'%=?wMDDD(Iѳb-NVgt|[sy.]3x?olǑ7k–ØqēbRVxjr>nU|x_)Dꕞ l֦ O=bٿr eD<+ԣ]ϧx}0sv̲'™E{zGÔ##:?ݧ,չ|4+@7orٟz ;9jxé<٨$K9 ˶=]|B~qz=+0쏊RUBjaoqpDDDDHl]6EWo2b纭<Mxp6V23dքO@ , xM= q ,|m(7ŧ9κe?).Zqy:[lT_v%iVsqk{v>Y0*/`h(Dg-{y:.qh.NǃOei޹wv]:gw;?b_V)ӇvIl񎎟HiEr/7- ?o9k㳡ݙ#ի1v T2sq%.?JY¹Ͳ_x*%.-gpK2&pC>!b/^Z1malؙɴU~^);i;f{_ETh'fvpa';OCݻJ4 ÌK\nIp_#]g&:3葟xq!v.xeݪ#?ҝ;kx_ѺULY~ +{c6ج^uNr `׉c1~kGݙaT(S?aO2P9c;ͭ> -Bo>݇Ad,FOOd۶su0ӬELiYe. GA}tF@=xp4/ng74hIV~Xath]H>з{q}]w}Wv71X-vm2 W LTXIz] y/[Whon ?+&[pBB0-Z0wi}k׋2s'xoOS3Z4uPOJsYon;YπS\(Stౡ!p_殢O>Tq={>uNdX +EDD$Ue}5Ms>fllpyrw;r8|8*y^TjFe5<@ < t$1*@vz'Ì̌ Xb9{6x:W- Q1$8<ԪK]vu#_Mv>;vxԢVPfolTwNʺ߰ 6{Wh =ƼCX =oը!5zboիՙgܻbt.G}MLodշ'ƛ3׮z>S `YYd `hR8fRh8t-|0%dlsu;/ta' 'wuq}+Ҩ!At~%6 ś2e _nW+h^ I(;J'1.Ar)=0år5CuʆSiu;g+:JK")OKVO13#;7zRx/1|8Aڤδgg.7< 1qO;okȭMкN|nd{DDDD.?y2ϸ3,.WjN{ARR҄t,22FH>H#1& hO:u+CPn}.L.bI _1q k=.ነr/_yI\׮3uռ =)GFFVȭبvN|#j}1WM68K6 dzMN{ dX[%u7ׇVrdz5CBndDInf=k]ʣDe )8 Ke:=Ƒҥ>`UuD ?0wyR.s-< _"""R8>' 6q5׼qT*UBq[–͇֫`T˝cpÖ~E${q؀G8s&ѹ:E,'[ң[\:DС^9L 9}zay^t"}x͙|F~'g8 >>"\DW/L}QwCDDDnj `9BH'OgLT/wvl z=&zOgʒZzC oJk@x֘|z )n(_%Ͻ؍JE"""""wCI8DDDDߪ8O¡iEDDDDD H!Q)$ `""""""DLDDDDD(=LK MXÞ;sLɲ^ԾCnR=IGkœ S+?UZϱqDf̔ iAA\`7b.dLP:g N0W?6EBs^=|P”w:`p}gb1xU$qڡl}%6-D[ X>==ɳÛO䍻|) s2Uhiz6 Ԁo/+s;0g~?Ng9J&t g׾ϱ9[Cqϟ;ϻd(us4IcJ9ŊWGKwƿљ#pzǬƧVbq|m7:u \,]3|h=JXbp u־/uïs hƫLanyi?rxQL_aO,{v+.݋k8Fl޽[~4tc9:dWhg8͜ vuX9:Y{;N`~7?CҬ)taDDD)IS-4ј ϊҡ6wlqqEs<`bFM9v4݉T"!5֢&G2c})_8͞_Av_댈b!mZo* k 4i(6uRvΏEuYۯ2L+F܏4\ՋMsk-3P+OEONWϪ o\óz6S4~]' rx7jB]$mЩ [֭'f۶XjtjB);So=Uײ黐nV$cEaCMg/fZCs?) `RltUJS<?Ji3=6O.dzfD{{3$jӼ˶$T=~C )$u-* +E.^t#3.|9ǧ'Ƶ:+l5`.t^ZUմYqu~ϔy?yqk\9px4y,޲l-q5[N@mHxx% ;Ѣ~? L[}>\X9sU!6^U8d<l]mϛh-U#Yxl-,$`{ 4HsX/s!\L.`_?sFßKʢ?3=m*4˧\iy`TJUS_R` Ϝ 栎H^ϋuWи+C>˸ضh98z'5rC{ٟ\:0*BPᤇJmR=j-?/X&f4+_Cٽ~+b6Ȣ\B uM?7,8Pl<([J`AODDDFز&[Cma &xwg8Hwڄp.Ϸin/O]=J[8,ᩳйA \ #]zrwEs4];#gsFĦ++Œ zr<ereVW=9c|}ɾt[\+TwvvWq_Req!99^w.t3HG6x{jN[CBLZrT&\,o`03)~Y7y d=3""""N͊5%域 \Fʱmy+ß2 @ƕ֯/F[b\Wz8C2⡊5+ͱo7;}kMڦƲG{g0unMݬDp#7=h-ɭMQ? (h{kA8V>ݑa҄Ǩq޺cуWs~"#>ע(8-2s$""rXǘ4?]v+Ce8KA|1qk}_m ZR=s]qR[*oa[,\ ƥh#d|:)1~aҳ8Ԯ3uռo),K'ٱ8ݟowmޙ SFE9͊.nHgjx͆ |YXB+o`2a$?3_Q-2.\G~K2fRjV)OO/L=6퍡=^]{8zz3'2md\H "`3v慽0No6b(Mg/^{~ Mg]/#14Kҽ|џ19L\`7b.dLP:`ݹ[<>x긁B{'wԪZ oNeͶߧ s2Uhiz6 f]o}ds+?~?sײL,?1W;M)_|E8t!jt{cz։s|b+nƢE}4Ou>O*kˣOL]Y#)6n !} ۶VJ(w:J66fBl>^G 6q5;Fhq?->Ec1W'Z\\.aeL~ ٽgbg[~_f\ȧTph^ZMSo1\,,BQ⛭lM/@"qo L)il]e]lj>%,1]6Dzٹxn|㬟?ww'pP^>m1g13i;k ^+| @>Վ~穬Ku۷ x~%l\G3^eZe s 'ث,szǬƧVbq|6-xТ3}H.РGc*'=%}H=>rfin6Qch8n@/\pfϯoꌈb!m҂fnqS,]6lf7Ü]MxVmLxC9MVJTOXP!MFyO;0s i5YnƼc&gEs+S9~9g0ZmWNiON@XkfG 6Zf es#0b?=]ΞA q(Ј5>:6pKҶH%#=ed&-}/OpNLg@zq |ӂf&=)Kf0jU %)]Aie\=ޓ^۟ʷ `֘|z )n(_%Ͻ؍ۛ?݀o{.O,R@ڌhjcœK9jfxb[_ƍZN_},οׅ;K0kР)-\=歿1C)~dDZ\3{8gzqvՙ^a0wҪz}r/MfrT捼X}/' j~-"""& `%&oj?08~Oޝ3'4q9F0YLOgOaG?5f/7B:Ԧǒ>\X9P5C؇]k}}12nm03Ƶ$6mΔ W\]!111Mә͘I!99N3[ajGuwOd~{~W_GrT(lY9z$ksVcR^w8݅4--fTE4Պ5:h4[ڨ|!"""& `RRl|v{ }^Cَ8Wz?C)lRר!$<[vK|wMՋm(d'*'q4w~ЏYA])'%KB\\,P>Gu^'~i Ce[dӁsNa eJw;]Ҹ#O.ZžQ/*Uy[;ΎqvF#)Vز&[Cm˗z˴?=;2|ޚG1p+tcѸJtX/Ps3:U!>µD{޻Vۄ2c'^ݡep>T]?v2c<:swV&4ԓZ]E_[-}7Wq_R]walOl"a<>wH'6"Vbvlnܛ 9ٖk"T{r4b1ާފ 7Wln9W"]lk3R{jpY,8#frsflm iEDD$)IbI~r(Cg}gŅ~Tj"Mͅ q~[GtJלͽJG1:v,/z׌,ӚF|īRG`h 2--rK<0"I%+а׭X0؍W_:0re\<.LHa'_ۇ[_"l`ϓ0B>\\cлS϶^;8r֘#[JOBHk=mTBNdD&-s0V9`n%ؾC{@c"^5ݳ>G7fc=N,Eh:4}"u.65M݆͙dlq~VZwEH 笣wQwCDDDLLٜe/G] `""""""DLDDDDD(0B&"""""RHDDDDDD H!Q)$ `""""""ĵ; jrQΜ9CRRSe(_<{o `Rl=z84iSeٿ?GZjbٵr![|;ӻU;Dn6V{*@7GCCRlDEEt gİﲃm.o:vV@,rrwXFghLt$&&f&Owׄ X9G?0%1uWDDD䦢&}M-QȺ#AE~ewu7DDD䦣&}ޕw-dj|s1O2Q~a(8H^W,BNֳ긲eOv}И.O=Ge_k=ƙ6^2S.CqWZ92|ykr3y._]aLX+(]?=CK< s#tz3dBԽgo^#hD]˾3*8^P։qп]dGQcOB IХQL_aO,{v+.݋k8Fl޽[~4aB4-?PaNۮNlY IDAT9vW:v[M,eJGݮi JwD7wnLU0B 9AsP0iwaQǿ3 , (f.-FKޖLm/-K-KsR6wM\Q Wg" s͙<>9zxSt|( o mhI^};Ɍ#U\ID1iܷ ?iUZ6Mo|_|Ci:#C O=kTLE73<  ׁB! LZqWn sd`%|/%_Xcy! j8-۳ RNDžK )!\KH*qcߕA&~tdC8QsADٲjANI9&<ইz^B!n$`gJƍ|_44)-I)&?,?;(`0VKVQcCWbXի,_5шj{9!1qnf ({&x3eٳ;# =m8(J07R03/BP^:C!#I* >4?0/kTzqXPd4lHCuGRƻl}U~5 ".6KxX xvJͬ_vk`ODD - w•xi-pvORط? ??ʆبT8i))U'޾(3ٿ-PY5u Bq䏷ޗ`F5kDCB. 7\㉉+Ƭ {V/`Յ޾89Azv'ܳO=˳߽dnMq' <'AjXt&X4w-G@C8 dZ<=k. !_ع MaуeeU;cQAlr> GGʏsSKXٓtk>KS<9r=ѹK swjX\~emw-ESn3^qআΨJb7=҅BqL ^7% ׼ v}rѪ,Gf0~Jy5"5ͺף~#x_63i3W}JP۹aRr=WGIç3 f|>c5/ 7dн{E]K>HEl~%_Con[@'&NyNuS3s~)`њO^f$]Oû2kl-VbeS4`N鼩o{5^єw]Q5ׁB!W|IYaRUu|g+V;F%,nkrqVrk96 bGroSw]bcc cn߾֭[cn~cKddLYuO t_aw0B!0%4 J󥀾d0+MT;#+Wp`C XI۶qS돳&!l2sGʹD@@vRRRÇqq qkwɒ%7;ظDd1$\At6udB! Xk^gݯOMz ?N!{Sf^6U6Aޯ8uՏnW9xy3ѣowQHX~9%94|8f~!B=en<40uuxxx`4P(Ph4!;› p'n5$ZB!- AY,ް\5.M"2n =|˺u*'3Oy=oZ-qv./']g{/bjxi_Y<2dt2r(Ԫ8/Wy 5Ǩ#|d+Ii(l݈1{:J34MɔB!{?3jWXd(غL]kǐ`K{p_8`584}Q}VF2gx9/3J9C_Y_ `~ H޻LgB#ؾljqcƏ|&_vΐBQkV!BQl<*V((̂hJu-K^IcKY8`ߎv GalH< AV)&e۴G8pZ4]hЊ6mGsYdlh٦-5IN!Bq檑 q01tdN^/maDO)ŀ9OɤDmy {6d]}"ְxMV6Fn#3_?A:B!B1~fAӠGALʾը3yջZ3ռ2J7cSzi#zzn/ zJK#F[D?XbYAИi|X !Bq~ 8zMjd;ҦLދVvxU'ٳ(մ: 6pe,pذ^D +֒4C !B!n?I,: ƭxhz.@r{5?-~؛eZPaˎ `̂)La(}[c;ę^cUNp>n;pm1u?oES P88p#B!$CEn6_ ~W# ,e X5oVe/M<.5)1_}WE(ڊH_m1l(;WuݨzbddܞM:AxZB!B:5)*M d?5?[i7Xll,aaa7bcc 7uB!Bܰ%4 J󥀾d0+M\ 'B!D0!B!% !B!D-L!B!j$`B!BQK$B!BZ" B!BIB!H&B!D0!B!% !B!D-L!B!j$`B!BQKLwlYx> MB!I s s9{%Cz:s@cwt]=7锱VB!Y&nC5m'F%2;04boyyǜ緷_e r(ѫ[t;:zcnÅ}Zv܅B6.4 ` ״.3(!]Imq;OM!ⶒLJjE%\:F=zÝN+!1f#l8/;~M}k)ou7u.o^z'u(J 9gIܺ#_95]ֹ?/>1:K%BM0qG4>o|e yXFID5YIOBܧ/m{9=}oݪ-`O?ylK9I+f~v 3.=.fj\3H]ǿDTx;꒿x_E|%grgΌdmфTLo'u|:qa6rr81Zǫ׸.?__:gT16?u H+Z͇FG.쩡B!n<&jx&X*>2s{>fLy&+8{퍷-*.ox^/3sPy1b Ia˪;z,%r8+ؿ%#3(; =9gY=|3NFtR._ئY9 ؗƆ x.-Ư:r){#fs{N{iW+1qXp*řY2#fƳo\?""h~>#hPv\8@ՒQ3ѿCiyg~ Y[?7eIDOPT=ۖ%7tJ*5띱~8G;9<(C+>x4?!B$-`*C= r̬|yo}g{SF7G&;OIS9h I|ݍ<:m7Zx gIZ]J7sBFFYky/ē>C<Ơaث%7#`\Rܯ_\)ΤDKvwF~i Pt'8p(q(8ٟpЮC^tQ>iO<Ƹ\i?YX,5CygӼwF&7uۤ}$*^ĚO!&-`|JO?B3u8$J栻xGϪK9y8-&_Y76cjҏ.XM7F);Nm1Y!(?7YO|X P}L\G+n$cy(OуEKҠȡ_4]`Wvthkf=%Xv;[3gO{1<&$|O&s=ǫ_ku|Uj5j@PT|AP0!Bܜ:m^ʿQXX:{è<џ>;*{'|gf~h6§xp(IرB.돿 ;tb#V64ٖ^ ݼ43t`qdϧ0  '=>4]l^#yhڌff_gкg~}Mi[Y`4XG9a[6h"i eCgNsX|0vdYi^w۰PXRX>EQQY^ڛU_\ 8X5NŎԌ⫷UGB!DR҄[`$ԡ֘5 ŘCM=#궅v/ͶM*<2q*#[NClhF6 o=/,(#{^\J#+L 13 d36WW |5#90h eS\Z{ AóO$ALSsjc)=H_;Bs ֡y#KOl%8%^^NϣknJJ@{T$:Wݵ><2^=MG*B!nRGO?;a79(-o}7«bE~OQ cMdތudcCeh{̴`A7't =PҨe|~z[Srq{SI@X({5z`KUɗ9>^8Y+ -=B5n[w413{ҋlK 2.փek#ɗB!I[{Y 3cs#!B@0!}ȝAOS!BNC!B! !B!D-L!B!j$`B!BQKA8~g.W\;4iՑC]cB!.IO%P>cb% QFՒLnn.zJooo,,,LBqW0 *oنv)}?thˮY(ggQajG0qplf9F; bGx5 N$UL@0C͔e6_,JRZ> [7"F/ 2dp7#9WKHC_ =CՒh4:+LGr Q!gvAG^Z"k:­+-\Hڲ FfXrPy;N䅥tye*8<_̤_h r0|mD@֡$ #Pb$1 m!Ek )5@9x2S17RE353%X]]|^E4{1%%ƍ8"!u&`ޣGh`4>DДۛO>, -۴Er,X~/dXW[^IcKY8'): !ٳgMBqW?Q|\[0h):ߗpxTr%thU|e GJ@|D&d̿]Gih%6Ջ=/62ޏ]ِqL#sc~@F^|GEz^y:GB!-?0++ ڝcu]<6z# W6#Wνyqz+b8˾^{oc`?fAcc 1SQcCgtӫ`YvB!D=`JTPXd2c2{ecѥ3[ظ|՞v$n_˚mp܉F =?c?ڰwZ[&SU qE]rj -B!K\i/TX9ݤ =I5uSG|f9+>bJ.8G쌱 2}Bi]/B9Rڛ^ OmKConO&yy :?JP9~=z! B!whRT8)+L .ԔWljgH`QzȮdiʿ9qYyܷbbb7uBܛB!111>fh]R@_a2TK*3`BڐSpS"BaR !VruLֽ|\hn!!ҫ9='nd!B} Kpm 5vnv_t`݈=SEciyO[HC IDAThOzPbB&MX ZYUB!H&LHON9ra6\8Ϻ <}2ԦNAu塷frd(%8u/OvjvcB!HDaz^4 &U[#!Vv nt57.[a#qջ~7ݺ1cR``G}aD OVg_>lҹ6^qLx?=vѼ{γcܗɒUWÇųߥ]%c%F=8{㼾^_9tTSP!BܷLyXi٠l[8`4'`F8%-*ۨSpo5Eb@X+kI?[ 9vs^|vsyM> A1A8Y9N&~>NXj޸ǝ7bFIDDDwܹs7o^#B2 ?:w ƗGJHe [xeA]rL~ж7ELJ`VІfѷ̸8lmQőD$A#>>}`[ZeS q6ڽ#j;]_ӬU5Cvc~4V؟| _Ft*GhN8o:6^5y{^y&Mİa߿U֭[W_}Ż[1 !.H*5ƒB֦徠0S`wJسffoPM DR#CNGhKGt #zJqH+*:ȀM;ؗ|b`((׸?G5k iii=BҥKYf 3gˤ1 !:\0:IC?:`,Y;_imY/kۢ}QU8!1qnf ({&x3eٳ;# =ZÂ>e?6`<~:%>^psNZLi0@glffpX_ӵó}ݝ3g2qD>#?Η_~ctB! t?ܳEi!+;IJJA $"+7$no6x](m3n%ƹȁkyK]p2~"?bK5lHCuGRƻl]?\

GGʏ¡0~|PcO`7&x-j¥Sәi3{WsQGޗZлos?ɠ{F勺ҽ.}>e 'J>NL^1L/+^Vxu6 6℥(5V86iX45KxbklїL)z~x^i|Y?`k+[GI}G ?YFl0Ǽ#ɒ&q#thJ B!T|IYaRUu|g+V;F%,[Ubcc 7uQ'ɽ)nsùFZ@W鳴|W &c钫KtW{Stt5YdI-D"B!DLѣMB!J0a2QQQA!BZU7IB!B$`B!BQK$B!BZ" B!BIT*ZaqVJ2uB!]M0! ;;;.\`0JFFvvvC!I&DNjj*Ν0arZgϒB!j0! kkk5jDzz:gϞEכ:$qSTB!j Q)J077Gc4M) T*R:N!B0!(BB!)S!B!j$`B!BQK$B!BZ" B!BC:JrIdzaRaB[$ mY:V%!!WWWh4IǴZ-$&&"Bq mUɸPRbR 6Vw'Oꊛ&hpssՕdS#Bb6 7fP.>v#n5)0_Z8[m0JIMM5uB!]>J]8OA@4FF+"av=\ǃSRz|:GB!-rv^mG>H>3Z.]C1 gx>sո4dȸ1t$5/l%)-?frO _;e;}q/yh)8OcWf8cѿˀ²6>Ezfzk4̓ύ !BqWʔ{VZʺ9tk˱œ֎!ϽX,p,?Gp~9Cv E ],=U+@>dr/'Uyai1]y N9&73 9yQ|ge1=wo?e@ETB!I(|Hm !4e`)ھa]mz%O,e}XYdlh٦-5@ 4d÷2oNS˫Wʂm,X~x1_@^0#:Y"",]cWB!6 KqL#sc~i=8CD̗r{?xvW 'WTs^u>0KЪ,@K,ޕ):ҨMijˏqnąB!H0x(tJ||.ed2 j?EBB.~VH޵GLHPDʞl?Ww!BQ޽{Q}6~ac}P6"yrVTU4B\Q\ a7t¢)(PڕIoa _>nX/]*;5#7f7r=lpUօ-Xу@G/d)/5lTGQ HI&]ԁ@6INsEA NpޯE߅B!DeQKQjpt! t0LGgvoxvxA^>ɶ?2/5= tIL~z|ŠcA䨉 hyuZ>Ղ6=Yf抏 <;c,n4" Pj|6d7`}*Mz?Foǜ}zЕSv[icn=öؓqoIfkЛ^!w(4:6U|uAC`H5;z<}3}-ɸ*ȫ~l[瓑qwB qxg[]ݒXr20`Kq1 (Wܪ~b߶9P|| }B/!BLEEEE`m]Ⓝ{i}#8mi]iaYV./oODnmhFhEܱ}g(,.AkPaUߗV"hh}odx~!/.LqM۵ʯ [Wn ͯ;rW"wnam@7K@.>JcٲN=!7O\.U8< -Z?َ ;ۇnIM . NMhy]&ӿ Mq4!B>L0LTx4!519lLF35h]8!k=R€D}iiLuJ/{NR TYtl;Hi"|oK_!6Oc0z>HnmҠ1S?SvlNvj6.hBn䦞@iO`Э #15!BaJ ҢՂ!.fMJ~(Ԩՠ^y ÚH='ͫ=@UvSmi /!,O{)W.Ж'G}s:3a.եmkVsƥ:^HGoƺcҔ.KIImVי|dOcW-,:RRIh|u6~y} {) ;AI(B{ Q'8{: 'NXFр^oD}jhlue7C.Gm&69bqmՋ>!.y28C~ "T4 oOsW 0fveQx[o s 0Mq& oGNF7x!\|8,_R@hsEQ>Y=*K]k,X-~kNSՏּi~]G[+?%zLGK튽"ݜlşwxA-̒".ShR%v$(8<*նi*U^^!)\IWty EK E?8yKth&9Mtf,. |^v&L^;PKkj1`]V•=5(%[CFtPe)&{I6Sϑ+<_%Şi2dfse*+%?YF蒖ǵ7>K{Qs~&lv4"Lݺ{JUOp5;BQ[<^Rc؊“ً孧uǯ++js_dIݠT-,`ۧSҵ}f a 'k7_tJw<IDATƔj_A[)Q(P5cUߖ與KBCRrLNQc*톦0a1:\А8zv|j,(ȋK~!! TNTm+Wko_f?Ll]|ze⮽W?~FlK6E\6Ew (՛3j{]?MBSY XoShZ:mgUCV<*9B?bSy~#R|HTnWDdV!2IIZ:tH7b)h%%5=:|'m[v[M=;ሾ\-U^VTv!h‹14܂莽B2p&;:q)v IHӥɧ)[ז~e3NL Ј*w͏jEӥ=~%Q3Z]FHoRリJ )6Q:Zض2M$E[F[}2m5Td_kvj%"u,:h*÷/ `I6+*e}urB/JT^j_\ڤ\xH}@. Y;KÆΪ ]]1y۵,R'KdWőzc6Q_%F;.'վwkYe5{ R\USم*"Znڪ%OX()/5}c{걽N"K~tgxex]!jՋw3YWMds>k綺:p^" s[B<2JQAgvfSJʴU$)TC2iUQ*?q-c/8Q=|_)i@{˶7's72J{ mU.JlhoKⓕtX7m\m\dI:<=&EME/zGv 3JYQ Zv᧖ R%P|IR+~_@#D54.IyKXWiղ}ɚ8)Soj>^ӨnV 'U+ouIڲ|\3G(*y&&yh+NS3-͙z衙zκ} 3)oY=*K]jS\0 -Uؤ7*.RNh{] w[!vo^|1-HZ{{J+o}z.0ME^pXmPU-ַ)4Vu6@=`dPLee%:Ih\J]BtijSQzcr1 .o RHt쭾;wRgw{މRy-ަq LYz?EzhdVl1+LҰ~0+">C7=qEY!Wʲ }U)%ٔn}wHe <.يfЎHƟGx]sfZY ݾY˺j|8O`4 yd"0z43)ϿЉە*i⪩zd5HM5cսGzV'-G)R5@QIt=STOEyRthBWױi4wpĮȎ;NHڡ\+E1f-%ms#ѮqK4wLSS.RgԔZpܡ>bE販/A-_ _nV]ԩOy&_jqnڤCow%TG_,YY)nu3ە>M{HMݢd/86>*gSuR]~)ޤ>Z'+s5&O-==j>nwK0O[Gz9"m[Z{[OֽS+"%Uj"4GtEZ={,SsMR 5OiN:T3߹WiU?z_,)U-;?裡[b-ަq ee~=p}{ƩzcP_Y -L)uqgε~+! i45/זQIWKN?/X|w΍`)7koz ں*BIvhGiS=ė$mHe:kWőzc6Q_%v&LsQ;kF SfLj-=gv5vUBJWkhM3BZcbπ`ȢJUTHdI&%nӾ8 ^\OzY͸/ĉ^S0 {nrKQ qamܐ+Y/,-{ J:Nkզͅ NNVܩv7M6IRZ#km6A ڱTmEVtwM4'EhMgepuNvw@L)Kԣ" e&FEO6ݶ&Kү?'kPÇ l_nz4S`W (Ƽb7WsIRkWZ5$9Ju{ Ǵהaeh5YT5R ok=]zm4+ysz.l=_/*S@d[=C-4hҍZ;_z鿙}kW%\?S3Qy>}Νw2loi"U6SN}uQlڎ+~F~ o=z*1o') .AY5O0;n~aPhzjZf`999JKK#Ήeddz@egg׺%,4I%Y%;=VT=T6VO700! `` BA00! `` BA00! `` BA00! `` BA00! @Mrrr|= Zvv4)mzР BA00! `` BA00@*//מ={t1UVVz8lVxxbccE>V^^[M6С,XVhҥ|=$={M6!b(&&Fmڴ޽{}=0ǎ;h_IVQQXee%3_ bן# `` BҌ~@4 #ںj_4i,ԛH픪ߦ}= \~{6WƵ7hz ohqGw &B_I-ׯz%sg>^,[]vE%ӸMըaU;k/?d QY|F>bQLMv$w{>#ζQ`"?A=zznI0i;՗v~Ҿ7S53"0W[GU_='B~:^'WS=\4uXI^wOJmvOX&$Wz;u^{uF=_C:d WjK"-S+%I8_׊o{jF:]vI~~fݦme{$tIJ~Uyꯤ.ucƧ3z}?xFZ#N.C;U0kΪ>]PkZ0Ulk*%2n]&L5cn3(}ezbRucQ P&u=|i`f*U-SfeWYkCN%nmTΝqAjs>f_KA}^b۵S;e腛!̀M^^]ͿVvj"߱Ibה!--XCc_7Oɯ?'oՐҮ_ђ;)P~Z7slJRA=&SdKRPMUn_=hAC/n-KivjU=tHz蕹p{w>/(- BwQVFC&߬/fS/OwS57Y8"E&՝/Nըv'gl?hTfj_?+I|\m[R.V JlqsGKsT:ܼqQFFddgg+--VX,i6JJ*wzz^!a9lvsfn„ Yh#0#|=5 3`@ׯ_?_Urǚ03Cz>/SƩ _s8u 0o/,Ƣ.RΩm:޻Z,a4 VUf.zjw]S֩-\/hV5LëZCX=W7Zn맟~b&Vۧuu=9|8W:@xk߾}٬0o^!!!u9M˛[MUu,P???bRv;}-$,"?:aj4psUnL&z2ͭ^N]vC>mԩs\Xm׼wwۡist;b'n&Ur0W?S[ޛz̮auU*7p>s/W#6u0ogfX&I2;m~.6U=tzEGVI"ͦ#t4&M[u,t:ãst=wcpr]֦p,ǭãHst3bgEZ}gLNo1*3ůj?ǰ#SsXy sbf`u#1oD"M0wa6:&gߔts|9Fu2z| 35Y3]5WM;ǘ0s`w5Mu:g<}8\ٰ,ڞ~W[͂e8ƗYPmՏγX5E}ַ"z` nrbΥ^n;t7En65+܅XMaQ}Vg!)ªs`r(7?py 9\jqaꠒΌ07w#4*jGN筕톮n?tBcdUFPW)\)YCRgCjWvy7fQo6w:0w&r, @C6œv;N W$WqEpyϽ 1yx5Rs)\=z{=p.|"IENDB`liferea-1.13.7/doc/html/help_prefs_desktop_1.14.0.png000066400000000000000000000755501415350204600221260ustar00rootroot00000000000000PNG  IHDR`aSsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSa 29 Mai 2021 23:53:38 CESTO{t IDATxyXT0( Rfe.٢fY}+~[ZfefjfZ"n 3(@a@>s{Fù\p-"""""76gQTZ!HKDDDDD 1w!.Vj +ptT`r@YAʊ@BSaRAWΑ]9VDDDDD$3WBRveX\Mجgv5LDDDDD$'tr:.epq_^([ysPUVA+1ɯte`,5иr ^9YnEDDDDJ8)deӺ+_ f$t5p/vVBW^Yna&F4&"""""-  _92W͖Gnr9| ]""""""ziav+0P6K `9]2x5 YgUHz삖+KYw+ sZ٬ƲjCvDDDDDƐӽ_y ]LՙS5%y ]ٽ2 ]"""""r&)t9H,.eay[neb=}م̮m"""""r9]م#]R2Hfb_vu:ήv,Uj*v9"|-"""""7XN#_UZJ[Y/gy `ݏeFDe|r ΑS `"""""7f=L[O)% -OZ\v̡+Qlr 8M9|z!+W9JK9h4DDDDDp9N#j=yȸǏodd?]62ޯ̓iJҖZ͚6}_DVv;NgFDDDDDD\f00LXftҦ_#""e$,mq{M[;5%"""""ʼnpJ}*&8V\&:9_#ia+-l]%iΆ("""""/'ѫL"fC9t*ecv.x浊~X( fU90H LK$Z N$dv09;/dar ^y_,]vY5j,""ٱԧzU""EhVcLw jם!vWccZ+))۩\T.]ݕYV 1sZTqC];Q9\8G"4<`r:N@[yڔ0nhPGX~</{Q*9h' vO=9: Tm y}O -o7WZHU7iJMky.%Lpc1q0RDf'Dp+Gk/;Q*ITHyS$_gcƿ,ʛDr" S9f>9yM""RhαG721}L}b$S,}wwo-_rcY>+X<«.ntgd?K9iTMJŜ[k4JsQ5ewVST,ʄ}>iCV}:Lܻ;3xϯC$}27:ʅw2}V80z\m""O~%_%@QW\\NlNϕ<W7 Gnr)DDDܭN=j3`L%9Zm.G^ң7S'}?J9Z.gӳ=ǺkU'8NkO4L@r);mDC e\'>hBGaSdo6VrǺU,G{""לotlp]yX&f XX?o#Ņ<֫0{ %쉫#&OC86$.&rv֯XȪ6k|F3FrG \#ySh^<|OktGm)ff0k̹KvŹ.?+7ۖ'y `BHQ;81 YqN ?(Wyl)p;XyǸ3=+/R(f:!+q+ fǰqG!Lɚ?c>O֨EXp0("RELD'*vݳ+ힰ!lӹ(]եi q/TkR6&-i-/[_2G`7/_I_kϻ[rÅcK47Yoǡs^T-qUoKޙ|q[6#9wBz2sO :ޓ.sPDs[t-#^^qW=aC2̄9ޜ 5ø<+#[) F#ɘID|~].`5Rb́?JcK$H<^!i8k{ʱ%g1HȽ`F{Ev`'`M]l^m@rbO8-LK pqU6<2>vlfVlr]WbjgLfZ7ʦNQlXo.vb)N f$V-;M!"""""& `""""""n&"""""& `""""""n&"""""& `""""""n&"""""&n{عUHJDDDDD$O,[%""""""n&"""""& `""""""n&"""""& `""""""n&"""""& `""""""n&"""""& `""""""n&"""""& `""""""nQ NDDDQ7ADDDDDQM4)&d0(v#e˖E <ۺukQ7![QDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMt}?bOp2m[GwnVۙlHe5q>mn*c/l[8.rAN'af<݅cQ[)""""ã ׏o-9h2k nkQqŊ9s׃<U}}cٵ$~NoQ?J.ö.~ݟD|k""""Rf̩iMf"k]g Xxhq{7J]?ӎݑ} YSn)|\K:L]V{F}oxhֽqb*n`$|*֧Óxc^y)KlxkNqٳ 德h}P{[JSn,C=l2sY.$)r?o}ڏ O&.௽#&3-}H@p=n,o3C26L?UL'E{=o~꿏}"|+Ԧ#Ե}/2u&GŁW)*V]xC ᖒv@sX1^Y0v5Ocxt;K+NtLN:v'ضp$Cg#e0Ɍ))H 91o-(Gظv897%}\4y.0@.p`NăW`K@v4eJ9 ^¸NhtP!!!/}iؽq&\8*iO-ce +vL?:ͱ8 ;k|L~ eC 0s //Vز|%uC _&/:6z~y5“$v͜$ ]ê/dЦxgْC ^η2 ̘, M8ӟ#~/ݤT\EŮX,6d6cl#~`lf3&OUDDDD XVJV^YofI_ȋflcP/e܏NN| Ŕ@lD~? 6?*CɊCFu ,]jףǷ켼5?DӴCi-kS;(B,X,-ӧ㡮Oű{(U v[Tʥ:;ǟ'E7(٤15`2Q?{rJըf=Jjck1{CUR^@bCe1}ym.@h3~ 9J8Ks!f<ۋ-xj+""""y&sp(Fg_g`8{x4w u͹O3K3zȀP 1ֲx7;y(ccZ#\_Ptf#+xfA$?;[?23u{BPN䧷}ϕ oVYV 4ߋ&&NCi<'$r8?Xw\Fh4atq8iEusRݳJQ7ODDDlݺ5Yb vV59u=[g%M;5ԬUʁ36l"Oθ˻'D v_xS0]̣iZV.[ƻQGPU}*\b{2nJ@\ ݔ;+|~ϋJa8p4Jk^9**幔QFZ^5M:fR_.[QߎH(Ԉ8u.dsiBS|""""rP <޽ -{Ѧt_sY"*qozx߻5{ԢWytimwj pRj^gy$C)Ӗ[_OǢn N,OL=q~ Y/[pG'u>d ..c(Gh a~^9N]Y 5[T.w."""""WOpH4 \$\ODDDDD䆥&"""""& `""""""n&"""""& `""""""n&"""""&zvbsr%so}Z T-:bc'?_\_qgbu/2aC! &pB'?8U_s6/oCWÞi}ki3/. 0$rhgȑvP.""""0&(ã{Sp zm.o!?n}S޼?+""""Rb'ϴf-a_t0&O-.L4Lff ёdLUf:>5ae3WVc[ .op*-L=gG˙shpL8 <btL֒⽸<āsѷf1u+ (L">95W0'p?1 IDAT7lh~ RBDDDD;uDǛRA%V>Yku] NEuµn~bG6/&n`'geY3NrG>]x/OXVwD[ ^+OY6 ȧ "\F=h,dMz[,mASFZR['Xt3_k ޒe-C(9|6UljRbY_ j+3K4Gtpe'lЛIg13;~önmP)s9viItT{"?џ֍Fݶ#+er BJ1C5qpf]gI8 """"y&ŏ{c TǗ>lawհ9Ӂd%ԭ-{466g:1=3{i*d]3Safp:qf//e\p8j p3FOr5(R|( ij""""7:͂(Œd͋+&p+C F2tm\ o۟KLWƫ-O<ʀ%1$w5%!qs}2΃Lx eynNЎT*x?͝Uofx8}jgj=,p:D)^ xg98[Ao Vƹ-AR#]}ێa&""""70)6;_ÞXT!0cMei2([1H-c"D&SߢD~iLESXHiܳ!RL%2WoI .>OϘUZHƍ(u9oeA1bz?:^ҝѯaܼ)Zu2aԪa0”՝%LcŔn̩V4,""&MWA˖-Z6;{,/ɀ C E""""r-ںukb-krz2`O8-LK 7hL]-;zWlѣK.X _u͟id./Ix4WM* `Rlxxsː4{9Nlbqmڴ)&\0)V,KaJDDDDz瀉(((ogyŻ*eS;V,<߭jŋEb2(U!!!xyyusDD=|vOP&-˗G{DH _{bŊrͳZ={{R~}}EDrq8Vt/v\ _^g_7IDnTXnHX,~9B5E"" R}ddE]xТnH ĉE b`%+S~}LYs^ϕ]p_VݮKdXtO ntHhD.Ѕ ԋ5pYL21yF}iCj!fZsp(Fg_g`8{x4w u͗ؿgUËvS?\{D"rS&""""rؖziR=g,*bCfMIۇs7\M,]u1{;Z!_# Y|Öӫ]J,RYȺ0X&ܨ{s_6kAp ^]!"""""` z1,K0+џ[/ 4l_sT|Dn?z61cشN$StBDžwGBOtʽj~z5`>Y֕981`0duP6l6lw36l"Oθ˻gǙ:fM#ֲr2:?@kQZZ^عδmH~uZRFRƜiԢyzGvF9GLp%'oAMe}l ])eū5ůMl߾ec`ɉZ }C& ğf+#x/߈ީǘ{ągΨWygB6rq?2c;ZT.NN\`ߦ9vy],z NÙ?m/h.7Z\ z /|{r*b#@gi$}2jv.Csx{@IBs8XDDDD&E&F#SM,YZQ#{WL`缓2?<ݰ{?˥L.md,mЋlZkr9gK6;zv]p2>R! x`.R69;m&}Gjv O_sEfּ/sW߳ŵq-N`κ`|[1Y̲~N`ܼ ;dN q+?:üM0I>%l1\\ѝ)ga+;9:Q/wM K^,B\o˹_\yv+1-cyg^ub[=_⃖|$+Iaw`w88\8/qkaWgt >A3SWП7M7v';&T{Dth_>}۷/7|sl̙35k[&"rS)q#.spy.ӓ97qmR&AhyKMl=`~5&zKڦ[ zÞ9|6UFn³l0aarߞ(Q-i^._ƿPNmq_r|ϗӅGh*t(rQlyp1z2Ux>GI5-pܺcyc.o<FgCt]>o^-nl=kK 5cT{饗9r$z{Ͱoaԩ;E:O)r?i8/07}qKR$:Dl{E_(70Ss;[Lll۷OY_\"**jwg$V y!,䧃ލ >ya|J}3#~&`N~OK&!3i$-[ƴip>r…̝;'R^"nI#`R<981`7]gԠpJ9O;pd<< )))ٽl?,,INΥ\>%{61K+wY+6E4 -^}S|pÁ0?y||hsWƽ :S_8SͳWRPP'Nd|?L2uHaOa{cGD$<̎r+eR66}lO:?]vqT K_L@uHj __umK._=WJUsIrH!@uR<>PW/:R*U-q+>p +%Z?HY/F۶n+>.\ ODD Gd)T*x?͝Uofx8}je(K@w2՗#wuGپr+gP[Rf NU\U)|^}c=TÙYhxDµsY^!C춻y\߳W[x$K^cIjPKB4*s_xeӸW +9X wD6x$G'w+IM %>Ўeƺ<Э&/éyƥ˙~vRTՖ>+>Pr?s1WiS#?+r32|{ܷVQj\zߐi=bL-9ՒjEDDФI"(""-[u3D;[nտ8d#srP-"֭[%,`'`l^Sד{őnqfZdk@#`"GY`Z"rr\v&gYS1&SH!ׯ_Q7AsyǼnx j()6muD1U9;zu3DDD ("""""& `""""""n&"""""& `""""""n&"F& Z)pVT)DDϏgu3D \tt4~~zvHnDD(((SNqIujr  *戈{zPzuN>͉'E$b2ZjusDD=072ntuD`d2aX0uaHnDD`0U"?U(((I1ω]~DDu[DDDDD )GEN8{v鰑5v[EDDDDD X8ǎX|gy7'UnfztSvt~zy?E!g%~;>c!+""""> `"-˗PB?,x;O܅XXo~Oy?9.ےƟ=iLN\ZYu"""""ٻX_jO~i7h~/IÀ苡P(q;|u^aty\gs?b1ġc鸃L{ mڴ˰<_҅Lh/xgw}.S 7g$DFr6W )g:$E }kaG3ӟ &8->;Nٓb/GԲ %ҝ1-sa\\̙jp#9gv5~< ,yyJ׾_~%DXʤcVżH3K3zȀP 1ֲx7twb `⏲y,>Ekʥџ?Q0ƴG\?j I]g,G;{'`ؿ?ޣ=ӗ?~@+NdlVq-r]fA Y|Öӫ]JdRYȺ|uauc/=-C&:E^pPJecSDƵF4 {GZV+ ?eGJӾSKJS Hx#9<<\k{h5a#mysL\]̷p?tIRf3f|mDP2pj%o΃l-J9OlJ+daÎ=TWbdh y4Xex2owtGIsb ZL""""R\,73qh:ּbűc%fHN4(EetԢRzGٌܤW.OSx^6DI}l D|V_xmfM-MO%ͥш];oi_{|qX,X4I:42SfDӇrkQ3 *w)""""? `^y{[M=T<ijDUN Fq(ét{wZ*ғ. xG]lٱmwV$WS[|jUJ>#qJ.{ ~5W칅mt[zec'(E3j<"""""EE zcd|>w$b $> dh&,_^j?҆>!f0uL^vC ?ʔoDдKWO]ԯoӮϏJ-ϙuvRTٚGvr\n1dZO-t`N}mdZa4i$/He˖E <ۺukb-krz2`O8-LK 7"}<((((GQ7@$Ü:u 1TP0F=ADDDD70)6>L\\͚5ӥcؿ?ZjЪsld +?Ǜ}=S>dz]9#b<-1"" {^EaNB #-+f(,+,`[ (׎%t&2 IDATD! s]5g~3lstXn!/KJJbݺu4~HC<}9o>8NĮT~[}9wo. V̟ßx N~Z1岁\|r\ўҭ]H+s_/,#YϔJg->(I{00vU/..]J9[ᦳ6aKYC.}Y(뿟#Eנ1DԡY|3V s5׿ݐYtNz7kFՈsEw,3%IRtHXS.1VP8OVTpcʫB4sryqH;XރNIҡ/s.^^Q=IrѝR Xxdʇ,Zq`Ͼv,OǯṋO3_ዱ v:Gf|Ȓ QMZp{8 su3Tkߕ7~R칅ps60\eVIv@%0?yRK: oo:3C\.hXgώ_o╨sy΂w N%S2bNF%F[kxIm ?bU˹qTBT b5 n+=/%aUGx,7 ùVDiOsq4(GQnRUܾ[jM`׼߹o^ Kis:Ժ~n+群r5\tpwQM3Y=X(}g*4>Կ+%o^tn%],yb$7+.1l\ӿenHҡ n=y4gзxqqn`&6TQ&hg79yLXۛ{F{n/Vc!!D"4css}۴j\X)S,ِqAޤ1~$RN`_\|dQ=K)*GйC2p$93Z)߾ZeӲ}m׮+} غ$&6,ĥt}^l;uի5w.A[c޼m: 5vԓ')t.;߄yvJ^/v7㳳η#i15.sW}3L]Dso$zR3#4>+o)Bέ̗"i n$wnuY|µ$mxma N5?GcxYxi EcYzy:[mM;rkZ5X"ԡ6?_J?+\ҝ9S1"if6o.9Qc3w@4 4ri<9^159ii;PY{tO~@6|? gU6+=z4Vq&el|`?AlYJk/cREnI!L[FXYkߕ\%o<=l%I%0U<97OՕ=*1` |&:vWsZ?N1k$@- @:]\N9ELj!lBDGGmbb ;+;ǟN%봿p˟Xÿ﹅!)sOڧ,9s:/s&8%S5ɍuZhq?F*-vR'~OؾDCCXsT POQJ:I&Ydgr0IR(0U\Y睟iwѣL4)gH6{c6QM:dCS̛I86Ow|Qb2q@k̝4 &i溺F98-BHnG|-Rm+\ܛw-}Ȼ?fNtmXݓGcClڜi,]͚,կ|O`8S}* OJ8Lr=IS|ЁVm|X}K9lQ FD* zjOKfⅭBP\.ҚSnʗѷ9|31aw{z3Dz.oÈw2:FE&ydx-М6nwœ\̋m,'; _V7X9!i>tlK맞bz CF&O\.87+_[.#sӚ2#(8aY+~U?!'sьv㜷(8D5ץ^=X91ݚj1O!])|CH%}K:~> (M\BNM^vI@ `[ z =w7rR/O[ɎI>xklEs.4z?͐Px$/DŽʏ1ݲəJ_ףHU Ӊݔ>ûӻU2:F5)Op˿~%#25$4?늛}O0οQ.g+!' '{8NhKNE L^zf]z^FSk=?-$NqGx&?'oW%>.1TR*z)Ν;RSSѣ~LR[C.zL.FT̙3Y"&&f0/,[ZNP hvQ004hŋIJJ"..朞Β%KhР/Mt,9MU%r>?DEI#ZTv>ԯ_oȑnYSf;SVC<1WGz$IL8hҤ Ц4#""hbbbJXR9J&Cg%&&0%I ˛?$I$)L `$I&0I$I $I$L$I&I$IarMCCx䃍lݙED\UjjH뎽8A%I$?\eZ~i}L c;VgNexp_]J$I@xڵmޡ ]+`=th+_LnjϛIG|'ֆV2 >_nѓsFg@R|M&= 9_YwKI$IC3dB"ǑleGYp(׍hO_jVazN:.I'xf;I Y +ml^3I hw 6̍c?֐kk}ز T6Mo༫ngx42oDi]T}$Itf}/'x/r 1sChtawL?w:.NE'I$whNUGAN7ޚ[G5)C\’HÑbVHפy1u̝#%7I] mSI]G7}&\{NNgSR234l?Pl~~,$IC3UOBBBzִܘ_7qO @"" 5ԣG<iO˟O$xˍ1x;ΩYY!ƜݜJDScmv?B:eq$IʉNz6DFs9H̼+ XP9)xɱbG?s6_;II, ^-C:q ,cVc7X $4IxZ]$Iu5| t[Ɯ_-8υ׋s塜?w/I;z 8KsߥѹsgCӇV;39oxܾ~sh^[FDZ;gĬ;5~GFtFV5~WG$I~rZ53KZ hXFg["I<,TȄYcWբW΀{J'{x"c9.ifL/lY_fڭd֤a^\AE%H<3B:5PNL$I:DSDԢZ% :o옑14R]TRSS֭[y!I$ڜ9sJ%bbbF 2+o= j9A-P ^0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$IaR ЁeyhrE$IhcY|BUDdgw9R *M!:2ww9$IRt%ZIZ%UHdZBTd^ Y]$ITπe} Q-RN("*Zw""7.r$I `W,8d~!r.C$IW2 DFw$IRt0I$I( `$I&0I$I $I$L$IV67q׿Vw!$I@2sd"~~FaJ$IҁRy>;p̥{i[Z 'SONtom$T $I$H֍?4u&"s[~Z {SG?_-"%I$U`PS##8(N]9放$4G{Sؚ@L~}͊_iк獸LV3Ǧ}uۈވ?-'X;VLi4λsSqSn};V2 >_nѓsFg@R63*oUiοj(':&I$VPart=:Mo༫ngx42oDie\ý]ÖhX{~]A:m_Ni9Х+G&{vk{rKhSy_։'!_H3rItv@6?a۴rBG$zt+r.sp݉.K#I$ !'uw|ɦqgo\ɢpi >e'ҳEݦܻjpKX^s t86SX*zѲеSu^z?ҝҟ$I02]f$\3 ]:O{92k]1W<Ęv1xx|.]Jъ ;EClw$If~XZ^y⟬ڍ{Ն&$FlNUͿ\q4N9cej̟ID3ooᵪT i۷^CBIq7w%+*'%_d_KH"$It8F~]ɖ5Wye=N}u A3bB##:}#+ҚpFĭhѼqY2Z(xaa׻~dՏq׌$ҒJ4%)*3ߞ?p\^\<WL+DΐxaMsѭ`JINfxι{$IhPFT{&7\;Ș*>p: oӭqo%]Ԙ/?=/]>G~e9_|Ik[{q_Ѵ#& dVwx=.cbn#jC:\p$ǵFcyD&˭B=rp>H[e(֦׸ r S$I:QDԢZ% :o옑14B+7pxivV3eż}"S%l95Q-@q$IeΜ9jQ< ,[ZNP h}L$I&I$Iar=vIray!I$i9&I$Iab$I01I$IR$I$)L&8dDT.C$IuX<9wH+*$I `d/H~ؙFT"k'g.G$IVc"O\dewISh"Ǔ{UH$I"T'~]!aʻI$:(§Ҡk$I*0I$I( `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$I.@{i]u6&G?:mY%I$X!n!x7@Хs<; eOpk/zjz_zcmG+~hzclVN?|7 x͑dI$EN=TWGrnzr Kkӡcn "GfŬуW͆_gQOΑS,ے$IB_ލ*թUujĕVDQFY:oU>Y Tgߞ=z&GmdK~Ը\OF$IR+i-2EJ@t2&o=x1##c|i KMM%%%4T 98z(2RSS֭[_$I[s)u2ˬ, ;@/xI8 ];;Yެ~S~K$Ia; ?VIIPI$17ː$I_9"I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&=4K59]$IB.}8f0@Z&DWEIяG:*ԏ'~5"V$I!n!0H'=};Wg^e]lwm*)tTd)={}C\> ]B Ӣ+7n[_{ VCז9i?x3e]zб҃|6g+\r囅QtH/%I 8~FB7.՜Z}tgJ<0sv=yef # <=]rՉ9iyhklgmtLsB=88})OG%_uz[Xi$IP0?\˻W:jסN]1G1}>M;~®+VX w6SFrUI k]sr!BSJϾ=yo~ZD{$Iʇ ZڴI"jڣFoS,zЇj@wXՆQc 4cDc63o Lśegi٧7VI$IcDe)(N;&=8[ -H\̛S&u2[sxA q wL~O2rٖ[p^4&I$i9V"9$,كΠCՍ?[ncӞW%#25j'кI0XI?&+Optk7וٲ.O=rr$IQaOEXnA-*U1yˎKSXjj*)))٥bY!׳~ 1n#~EX9FMCFqA:6njazN:.I'xf#᧗r v[VakfPўʙլ6wSoa58 +xpmؗcJ$Iڟ*f>N:z2c_+vJ:ҥz.]9L";FlO⦳s9Æ2u' 1[69t ԧR%1~aLJe'ҳE}zrw|ɦqgo\ɢp~8$I2Q1XhѲe!π"&rr!(iۨx;ΩYY!ƜݜJTR%صkWHh1L6r)I$<90q"17w=~HH")n3} II_S`g;gbQ._$4IxZogQǔ$IT*U|=r#!O\OpGNj׀kht"=[֡n=y{|޷]b_y('& k䎞hѼqY2Z@CIOq &աZҹA3bB##:}#+ҚpF{LI$Ibosw-"tl ;Ft<\m"OwOK'vz]ٍ-rٸz$?G*Od¬+jу+g@|nٲ/̤[ɎIֽhw&䮴yb|3*kQδ[B<#5&p DרOC9ĔpLI$I姰'" Ƞ*y˘enj),557~Jnʻ I$̙S,3 dYyY@vP j-nO7 $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$IaR (NjjjypH3gNy I$U(lKII)$I$Ly $I$L$I&I$Iab$I01I$IR$I$)L `$I&0I$I $I$L$I&I$IaR $IZm۶]t^:M6%66)L$I*g,ZFѼysbbbʻNFF7ndɒ%iӆ.Pނ(I$UVѨQ#7nlK1114nܘFz.H0I$m۶zwBzغukyQ$$ITβ*#1113t0I$I $I$L$I&I$Iab$I01I$I6[Ə]Hf$I¢?⛵;˻ Ry I$)LWɼW,]qiҺ#Ǟw9v) 0I$P<똱x!ѐX[2 `$IR~3oM筭sV1ƿ0Ӳ723m>#S>dmDToL?ctϞ}ߝ_b8,'CF]Eĸz2$IT?oIqiP*a'OSvre72,1zLJ_CsOrnHX.hX'ol=7V!mqύh2}$ `$IREq9 ukB=%$ -$ե;3oC̖MlʩNǣӡM wϳW1i5wIe| $ITQb1kӫSn "Nj3En1~,=i<-kNr23 dQkN ԑ&I$Ut4RlR ADD1D5ctM96t&C素Rt4TⶻV;&I$UtR8OMVeQn ~LL&9n3sSW̝4m8tȇ0ՙ754mN4IfhW=tc#`$IRG?] n/ΦWfmei|>#֥^=X91ݚ!:V|}o>Ìvwob~Rsh٢>qYH]ڵׇ Mn㖨 9Ȇ <y `$IҡNn}fGL?] m8 4Kve;_0 c q/Pe/?>3`l"Oؚ4JÍK<Q$1'kF:5Ьe=C6vGgDԢZ% :o옑1~$ItpKMM[n]F1gRRRJOLL(`dXfgA-' |뎀I$Iݐ!CJfڴiaD# ,%I$/K8L$ITݻKPth$IYX̾P 4|I$I:c:XI6|I$I:׌7,: &I$`79s_p$I$**.B **jov-)kJuPt$IRUƍ˻ aÆ TVmow/*sW QX$IT vZ~'GRFFk֬aݺu4lpoyf_ هH$IZ*UHHH`Æ Y.Ej8éRtC+[CJ</G$IbDFFRzubbb&ϥATT111DFՍ~EeLM_WŇZ H$I*BDDqqq]ơ`*ͭ!睽 }L$IfrNasuaN&I$I{{ +,I$IҁU3a\XQ݇.mQ$IfrKpۛG>e=`y4%I$ȂWa#{=`~凯-F2g],$I|OE ae>`Q&"=H WJX bQA}F8^Q5I$I:t]EM[V BZSqψu,% ^ȼ킃L$IRqJ `E=&U0X4Vl{5,`p+j)x be$ITPXa!ѰVm{%?|RW /@n_ua> ^$I *J#]Ņ⦣/J fEUk QF{ +/×$I5GaѰLQرKzmnA,j/[&8|va($I$+6eQBXQ? XoE 5OQ(XHVaA`R,/$I|EF7gEQA+ ;^u|,?+*QȒ"~$It*) ~瑯|r b va'4#_.I$I%)vF cmC~KV톅~X !+[ Δ$IUQXa?B(X Ul+8\m/I$I{#I9BiEzXX!g aPWp2|I$I*k?֊gp ۦTX ^e$IT+JP `_/*L [z%I$). < r$I/Kq_`$IbCWp%$IE"g$I$I$I$I$I$I$I$I6E*tEXtCreation TimeSa 29 Mai 2021 23:52:47 CESTl IDATxwxnB !%&M  6 ޮ^HGtTPz)J$g$dMHyy2ٙ3l9""""""""""""""""""""""""""r2\uȵ^VER+EC \"""""r@Vr&"""""rU ;9NE+""""" Mo#8s G\NYœ INpSR٢~rHq ZEWR*S+k)\I!)"""""׶Qq0V㖨,^1)t^`VE*mq& b^k3ᨸkq6Gs"""""r)Ņq gLi×%'LDDDDD)) B3:Z(J ZE.gXuZ(|: N[ ^BE#"""""(h9>Ӝ `nXbtL+ ^kCq~&t Tgq!PVCK}u7 DDDDDRMQ\貑1lEq R))9r6|X/j0VT&"""""Gr t Cm6Pt+6]nUT*j)*4ߋȵV\Wpu!t]XlEVj `%ݏU԰C#Bx7o DDDDD]zxa=PB` ~?k.pr PqSׅ҄@dDDDZp3F`Pe۱llt;vl#pq3QrJ Y/|W4v_]OoYYd[,XV2މ8`0dݝuo - 'pfv WzI=^WV{ãù,99l%("""""R~l69degU+80'7,⦺/̩a}ƒw)+<WQ|6[zDDDDDJb۱l!/W g! ^xA,msXHɵZM= &( fkym6`"/pg{J `^Q4PDDDDDʃ5\me\8R$辯B\œ c+`vaݥ""""""h37^9|\#$ك2ŏ 5.wr ^Xwv6D2/Y EW}}-aYw&~X!C +BzDDDDDLFe'۩};#3 3٘Qj0R/(yDG W^EDsF\1_p:zbOYͬ""WNnɦo91EeJ>zDD*x6p'9Vpc`SxuiΗsh0=޼?Bjb*9;mOv3bYL9,Ye*""H!Y_4n+@mdkHi{ʤ9JMQ/""2>xח{X6 ݢ15ieb l4-&n@d |ݭOdfZ'zEkv2GwgƳPC@Msaoΐwt+^SUס.=kKuw;i'y=d^yn%xS,/jdyB`ϵ=MR@_S9%NKQT).8:FYb.&E0xn>y[3mŜùg{`wmXFŲ,~/4`6bNxt噏Ӣ^kZȹ[k4UïVX;ӧvCx/ a'K<=2~vlC FO<#J#8Hw)YF˹[DP̯0`7H%C Ωs&ϔ9|MQf ְ1 'FV&K ٱXr,Mˡ=ko_b=DY)^F{P4n?# ߹Q u,Zt0oX-LuYϺaԊ?,e|8Y\u]ݩ9u.9`׋CDDZNvhm|e;?[#ZN|i{if(?5%s۵KtKbKyͱht5ayDZ[LLp ]v-Ѭ{&x;LD*Xߔuab{.9b7J &"R H^˼EH9 #_fPKVD"=u/MJn^:~K-t6h\#dWG+iꍿ +ƿu9uЭ}/uq!>@kTwX=_ +r./(C鲆^PDDQ̨?&-{5v[\eLn>#O曑)]7Ov D3;wOS?¬98NjuUv.R63t}LcH{u aڔ0O*:E(Acuaqɷ_LsfEǕq"""""rm̒w,¤x:Xx`KN9@Śo[셖 tLDDDDDR,Mvc!*efWe*{4PDDDDD݄x;f$w*Sp(H*ٍ_LM] >v#!ȯ&$[n=TƃEDDDDD.[BIL d#gs 1Û1T ;p*ȩnIiJ&"""""RIDDDDDD*H%Q$ `""""""DLDDDDDTs**+RUVU""""""b6+ A$ `""""""DLDDDDD(T0J&"""""RIDDDDDD*H%Q$ `""""""DLDDDDDVuټysU7ADDDDDRZ& `p^k͛i׮]U7CDDDD6nXMpHCEDDDDD*H%Q$ `""""""DLDDDDD(T0J&"""""RIDDDDDD*H%Q$ `""""""DLDDDDD(5$]/eODWrM?}[I*_%^G1 `r Œ2y v$UlMNK ۞d{V^*:q/$e`4@xƴ֟;{7ߥ[XEn=)dwg?ҽ*:Œ!qmmxa򨠪EDDDD `RO!% fOLROo)meߎ`q% ?:ǚߵr.\ !+VDDDD*Tzæ10rҎe'?WNoοgaؼTv5 blg`! z,;رo[6bălzmfvlp`6 !>qo3 _7sw2p'a8ǹs@!uto l.*{wTkG.~-WO..\%u?-{GB'r:skS-n惻7gˉ%dd2el2;;w;q*n=`Eir?c*zD*k/w}QZ@4;3{q~S`fO+ұMѩB`CJnGֺq4c# s_m{ǰXXsȱCLL39Q+`X; <4a25?͢m4's_Mmb)e5&S^[05ojoIuV ʣOEA&Mho;lld..%nO/1[w ßɟF8ro>w o+˟=3~]{0RO/4hLcW c+FGO4;Dl} E}h…am:@Ogd&/gr{hGSy`Ȁ~SjE_r1r~ &o`#dd%s2F&dgcy\Ҳ(u$7D[4<:3?=$8ɶ?)*4{oJج42=SKL4̜ݵ\'wwГ6Qï .;m7~Kv΅F:W Qw/_,lh4l8.tjv[\AX:Dyԩ6aÍv;|d-qmY]%u( b;&PҗocFV +4hכNe֛x+ zҰ6›Iwd!wճ.:PܕfF3tv 3/2e8.kGl_ZFzut[8L_1Z ` 7MëcO;Kr;A5ciӦ.ehϓOcwp-i4aq,qH͒Azzz%3ٹixҍa޸X2v 2^\Qu|&@\29=xq@ ½d$b fLK:5.n3%GbM#b&ncBƉ_H)C1oqL翚ϯb+M6oLVJS~|hmNnDE{ w^Y)߇_;֜\<5oԚS Utx!^;a=a`=ʲ>.4ї~{ӱw'͛7Ӯ]JU K=u{`8ޮ(?->CjUuDDDDH7n,u0c)5z.`ͷ-B~tT&P\`2a‹Ogps|CI8DDDDju%O¡iEDDDDD*H%Q$CDj8Ν;؇T,|||ݽ#""W(0jY,oNhh(QQQn\, ܹ&MQDD!"rՊ#440}ؕ*g6 #44ÇWusDD &"WsX) 00n\DeZ%W٬ED!0J&"""""RIDDDDDD*H%sD$g Kirg Uq EDDD*H≠p Ɏǹ-ۗgg#<b܀W\Og-ʩNx(\2;Kl>c?hRbF>q_9Z1]'S1Tcyg0pCS?28ˏt[6ĞlܼímKVn_ݼ݉=+w}>ۖlo*]Vҵe۴7w >F-x(6|13lP 79k@:O[{︈\;cAw9)}94)IcD'̘DV-0Tmg{/l:`Ύ;7AKtغѼT= iY~Ɲȅch|c[uw_[@9yM{)\;7 繥&nz9FGIعB\Kx㭵ywCR5ΡbO2jZ"m?ʿa<)u%y33ÿ*]eoVij - oܛACR+s)o:w}+u%>|8#F}En߰aӦMc2_&W$Ct L6D>lQf]6#803qۘ46g9d-^-[M[Sק5>秦VZ-i|G~{иad\|ݯv xmGE5'n ~Ճ b{vjo~a{Y.NYڪӢM{70 J,|m`]=ׇʓv|]r%bGLFl[P2Yq( ccq+nta>=btO|q-&5*Rv< /C_~\I&oTQDDfA+Yv;ˏ-XM~dabb44It8M'"Сi 5kN"El,m=lާ NAZZZq('f@ i͒11ݹ?T38{ٛKv1Pv엾Vdfc\]/otǾ6ןݻ-Lb):U3hJyO>aѢEL<ܹs5kǏqUJ_0"e_ZQ5nǎAtP ׳t6n혛q85sa=:\f3fՐN|+/!?xX|ꍗrjuq/O H;֛V>|>wqtMO'\it\P/ڞUwG\$뒲ޡ>ӈbpm#p뙛LRr:9&&$5p~fyxw8p'N9}""R&W >cӻDȖq\7bIx DtFS?f:6m ڷeǺ|vnzNUbu܆q -cjS'52*X$q=U©Çqt,w¯f:9:v!Q2e۶e)}Z_rS}T}e߾9_z@ry*ں[goV}9Qڦ4$"2|K R+f܂x>vb's[NӊiӣG/uG-um7_OLQJ;r xgO䥥簸xMLąn0#уDI|22-P͇-UCz#~7Ro.H~6mC%煋r"1]x廩Y7pyfƪ,Oj`>?TT?%O`l0ACJ_awmSdgX6Ӱ ]BK&yY 7iD>{,^\LDDD(.C1oqL翚ϯb+M6oLVJSD͛i׮]U7C¡ 04ϥ3 d%;+םadzumܸQ?rEϦHڸqcll,@NskŖoZ.(0z 4ȩ> nɵIm"=;O-Dt,<}<C,6Ğ`nqo—HLC=TM9k=tr5nv˘'#~, 9w<9_>D^Ḙ [O&UsU#d777T/zJL&F&bkTѨ.|gJVf 2U_Ylٲd2KVȕK̒/8Y>ذx?,nV\~|-oRE...X,fsU7E"łKU7CDDPh;ު@7`jU)ooo ꦈ\wU7CDDPvA&M(r`:)w̖oįr:Nj(\2)x|[<g}œwDpHnry$7?>Wn" |:vJP=K7ڄw^&UbϩS_~U7GDDPv۰Yo0b46>s3ၧeә4Y,S3fQc?xiCfTV3Ts^ȫNcq+aFȔh}ǏcYM:u戈`ޡM}=L3Y0s^j3g֯=k <Pm[ >s˚iʤx}QjC2. '0񋩼 w5΍=qlIeL?͇; +!"""""姨 /|K0j>k 24 ۼy3Z*M)G7o]vU Y,:DJJ*uaZjib*qRg < XB_sϯ|-b/\P`fUb}vBBBԃJY,Ξ=Ν;iҤ~EDHCDZ"$$P}ؕ*g6 %$$n\D䪕B@@@U7CΝ;W+\Vzc6u?8&"""""RIDDDDDD*H%Q$zTtN8Abb"YYYOXXUBʥ&R[ ҫSP-Ts{Prc~Μ9CDD5 Ξ=֭[ nݺꌗH#8؎(ċEzD.a/2,ǣf^гʷG +_̿V_Ql6۶mbо}{WNTTڵ#;;m۶a*c{9`\αw:] )5021eF tz^msy>MeНӟOSIX16ZbA8D_$䞞й3]b=5κoyS?>ږ77 ɱ))Mۿ?4ngeʜdE/"8c3>zwNb45'#oP)/s;kmǤ<ӿ;$brBdZtՔ`jTLΝ5Yg|{O/mk|mjjMOO̙34h  4̙3N2룄ymFOxS7}z!7v ~""RetHOb;?rsȱY3u7o }i?#MacwWmdaL7%:?oJњ==M}0v3Y i3b*0{zSۿ9@k:,DǎLݸnRĉDDDU+8qzꕺNdsrӷܰi6?G9Qhko|/=kYI-+͉L ~~z_<5y}Mz߲j^Τ ]h~TV/IM$j5m}ؿeqgRu c?z_e6݉[äfZ1UVٱDjzY.TрηLG|%d~a,x3tKuQ/h GO1~\{P׭pg&R>v'fp^2ģ?P-v ߔ&Υ^OmuibNln"݀GmA`p1 m֝z&`ܳH~~E$,{b!#3\c(^GtL5fee]4Uvqrg/ Q1te3HU.a@%z 9S^?~];ˤIy^k$:2.&!McjD8}&5[N6|I̦Cϫ@˸%=9GB9uB(x;sf"vvy~vS?rzsn첛Ee&Z>`8r6J#5n`sﳈHyQnfwM̛' d"ѷ6Gnx!<^E=xG] b;s]_GX;O.\9,Â%'oH_n|a:48W`Cױ)₋Z~nc0TД8eNt)@XK:s&rIKkݒ`miINxS@(,""RFY$?_|I>w馤UY88Cz4mBÜƛwȔ%sx^kKL@ N\ QI8EHf נFP<囸rGz̀oAA z^,}Jgf?wsE.;?337"]>;w.@nnne=!Ք |˻o318Er_Rz* e*;KJBp{7o$[\]qņ [Ξ&cXן.=js: 5VPM5CqB ﳈHR)uaBlcc~7꿟=pNed2r==-kp3fkw8ۗe6_ssH6L7 ~ P'=_1'!v2㉏$a}HW׉6\ϏRKHHR &5Cj^^p.! 0}7;ڸsxVNdgW2(0SO ťc ʼn>"y֤VZ@w^Ԇ>I[8-6#F\PB]9.,Woµʹcs՘,GﳈHD|hӦ>6FFT;ɥ1vsc]:9/>}_5du'gX=' x6'PDƺ }d}0Hњ}@#o|cJ7Ӥ)L~=vvFɬxvV\ܧ.#4i3!!!l߾|C!iҤI,),M#|p$ׄ_4 ǟְla^sqp\>~S#5CYPyK sc-:IdVoHX\_.+ 4 P[~t=Jj AǺm51W_i2_g%m]Ɵt'E IDATi PR~߱o7W})ZJDݶէ/`ɩ=qWI2~l3}-F-ſ6aG0`[}ߧqv6_rG{&,ocj^L^޾RJݻ .ʮ]T\f+6x5{' ˘GF2TkYr(ޕ)?|I3`vp٧2|=Yy"#^<رah-M;~M*ZϦݯn/S {~y]E>≡ʄө}"K\܍d쁗{A{okS1)G5.[M;ʕGtTΞ\4a&O}:6>'6\sɱs,).{=k ,**T"ExxxIQB57CN=MЦwз c 䔔vލ-ժUs$,##]vIjհ/uiY"##WuF*rlذ*"R"## KٽlҀ^33ȺVbɱXZ.*z^|дxjx_YqCdd$xxxuAgrr2qqq;v OOO|||+)ɍ]E*t7HN'''$..]v]~Η=TV gg7MH fɄ3XYшdʚNDDDLDnp䠯EDDDDD0bLDDDDD([h$--BZZF0e_a\!..CDDnRJn3l7Yfǚ)oc%ח'O0)qiii?~SN[ለMJ'X;w;:>ȣ ܁sYooaoeʂE=5j ::X233K:$F\\\^:%ܤn,_x!}?s0ZH`xе$bsOg %ѽmΎCDDD@nfCDDDDD0i"Mbo[>ol`cKKKHCo#>!sJjۗ=j~~릍ӟ6s2ݙ ~/Bs`0,k8}xV26.(]nx?cvD/'Jyx ;n=^fTO8{w+"""""O~m>eGͬU|3_˓ͳRWOXkS@^ROhPR`X+diP%:E:A@mydC?}t3gjk@`; =Hqz2zW4h4;ԕ]*a% N^̜^?g`xRL0'%))h EbE9˙!kG+컯=:>@ͮ(""""r0go\9 R@B&&iDzVCi3.3hz{?.JO'L|k;;$#1}h?j:u`FCH߱ZxEDDDDnS\ĻUF,v7/jMmW*(Dy6RjNY[1fBXI}8g1{ :̾)uaeA0M֡o-KfWs\mMQߡ)z7y'hW9ҔS~c^4.c2gıp77" #A3e2)\T(xDDDD4&w|t;Z=2 XK:h7FĔqc Y>[ dعXfzh3?K"ۛ@ X'nTG|}(}C8ܠEDDD䦧LٵM~Ig[z6ʡͫXu4ŹjKgE|U:6+!QDDDD8J%rȬޟv^F8$ߌ㳕G98wS03pO{{,2͔ Sx ]jƙh-'{{IL1\Mǀ#nۙL{blЂ~ R$l9xxRͳӥ5k*_<˞M GOz4T}5?GDgbϩTjp_b-~1gĂޚ/K3m̤/38CL ``98o |DxXh)NFGEַȭM ? Rzwg',ehw7 ֣G9[;6e+qO,1S۷lJyy)d*x߻5 oB.ٗ8k{v3plHaƘZ<<6û9D,ޫ7M] 1ѢEY-8EGxp+$+؁=Z}94&Eٌ_Nap{ qdXߚ-9t jj)]l\|h{{:8:/~ஊn2HLlPʙSo8x|Կ -աV{zVf'|/k%s…+KΞ'2TpLm6'_}Nuq1P 85!H]ȰFfDöbMZw[j?7;ņsX< C:C.4za otkLf✿8xΊ{@~03g7lNqy%q7xՓ?s"C)/>o̊riVϛ(ؘF|"""""rS3K,$e0x5f9^CL C9 ߽.rSgSDשi.v%_)Nv${th Vv܅z/jǭˌj g\Rob?6&q{ݖ QݔW|"""""r35hrX-X/ۃ:Ѱv?Rͬ}t*ab̘'ץSlKmc7sT)7T[$Rm]\.řZP3U|3_˓K =+vz<ޝy/4.9ʶ_eڛ/`| ޕG{ٛNY?jtg=qDq̍ډq06ƣ1f"*ևF=d+OtM\<ԕ]J&xCOdYsލ*ݲwsس|'i#e*W!Ka,EDDDnwf艟_z@׭GӒ<{WXX1`0#mB\LNn%;L8cx'HpJ!$'i$PW8yxH!l8 l1 = -ֵs%`nL^8 fR)^}w]?{8}<nyvB?:\*m4o=~S^mh]9k!̝2ͮlʠ!ȺHPw̧`dD &O|ekesTkc9p0b9~ |쒎Tk0<8g~y}lޕAXǶjÅ=~1:D%w0%珱٬ s#!?L=O̴`L?Ŋ=~Ye{v.ش#Aͨ7w%`Ş:q}~WzԷJF!XgRWUk0\roDwi_BLi.Ϟh:Z%|9u=sep!`&wλ{Y.|.9؄to݈]3Kn;ߝr9u G$rr`GUع)8^ 9m#jtrOH(G}7WHkT5m 4-ɇNנ*_˱S: Og*Sů_A6@*'GIHϹ7T]YʹCIHOO( \s&KvNt!>F:Oј}_ 66 6@ |qY֮܍ ecn^UAPyB6b~͞f%}\G8GֱbQMT F93s'3y0f֬26@DDDJJP9J}zΥMy\~Y̴V4N48eH!2H1\ݝ@ڝhW!ΝGY'6=^Be{O7 5G{yIJ(P󩉉Lj, %`r2UZ'w_ -l+R_-ٽt&!%9 R1:;Y;~`V; R Ti8Mx"!bsDwِ`APP0?m/iy+Bp(EDDDJ0MUoɒN΍`so։$܅w/xsۻo]nP"""" v봇a ޘs,4vՊx:`Z⛯-<2fjؤ-pnߔNV( Ot;'͟?C28)]!wwbݙ1,څU2#l 8Oxĸ^篴Cz%\c?Ƕ8؟_i6sOoC2_ϧw۷ogS骽qD~?~}r,1-wvj݃T51I+>kLp"z V҇=Io)"""R$t Q=%c ߶cCKͶtή^]\ԆŌA?,,یsԬ sy{nZO9YmL=J#5*3;NӯKJ4]˺@ tZ8C3.`@hh?KhKl^ <7j<"dRp`2|C|J]xyJE?HP&w,???H<ɋ`_F1xLVv h "˃ȯZ=7yص ѩyV:͛%Mgjq9Z}NLײ7XDDDD A ܱf3ps1!###ǃӵ(LbڸxXYN̿T}kq/޼Pev c{)(W#@`Fd/+_+kT>~i _²SiTl("""n0\sɱs,).{=k ,**T"Exx #22QvC"U8a; /HKxୱ԰LI)""""";(C5Ӏfm'xp>=Z3^WriVϛ(ؘH'zd>HPʋ| xcvTȞk|_v Z0s9+Mx~t rn k& G8 NmcFDDDDDn w^v5r3 %v? ZBߦ_$6N{RaZ?{[&<Ƣo'u oX5~w{D/?g//&MQ4btMf8:"""""r3Qbp $YG\l^ WOXMg8cqV ݠ%Uc&3m.탟_6YTwԫ>1O:~iԡAXzi5QKY0g!Xt|d:Ղ7/ߢeەV+ּv _UDDDDDMEns,{9Ӧ;xoJbߑxcptց~cNl=9kSLX̉kN8;A҅+c " 7zi5-[OaS.2r8 DDDDDn;/;-[yZ~kF{ɻSc4R>)tb.=zF<͇i:4~5:?Y#TZפyug{8}<nyvB?:\#S6c&a_W;8`[[́`-'?FM޶8aAA_/\p !j""""7:2\sɱs,).{=k ,**T0,k8}nxoQQQ>"##oxuoN[N2iy#5[Y\E`3_dX,9U%9o/E2xcϱМU+ꀽkEjvxoJ8qc¹}S:X4Nf'1ǏD`Y>?#l:"ָ-x9-_Ƒ|bN{x~[&J~8>#ؾ};۷ogvJW#˗ e霏Ф,=7nŽQ{z_ʢ:5iŇ{-1NdSo!O־<74tح·SWгø96=u{KC/v@smY;j.59nxxyvE3shS3rzmdo3b,S*$1i=ûg10+d_h,Xֺ?;-(R!iײ.Ђ6`a?̴/#6wЖ<ؼ? xny EȐL:V$n eՙ>.ջT󔞷_ϥguܱ $'/ h b~cwSw6Lg޿Mjif6/x soak@doAbb6\(ǽͫxhyu7+G(ӑ:pdk߽ØA `և8Xn6 R:#Hm߼pHYëfP(^ Haw]X.EL$/A/3sZ FNƑbL rícaiɮ&-FfKktAGx/p3ۼ:imΡVpCNahK[~l97yֳX<SPVG>\3vK_VvW֨Z|6eҩPDDD$WzJbN;ߡZlݹB]tۡԘڂb&Oz^Ԁ?dݫd=߶EDDDfu=Y#`"9iA .Xv0~A;s/'_ذ&07ƛ })).JDylqc8|*;O_ ?kKmZC[䡖LMŻ1Ia>!)6JD+|>F:⪍Ms*̰VݨXDDDDl()&JDDDDDD0bLDDDDD()&JDDDDDD0;q2f"i_0lb5()h~!s&fu+P}*2  Ǹͩ- Q\IJ-Һ"""".%`"׭=96oDhb#w%\WDDDe[ܺ*䷿u<97UXt~HiLn[t"BiNxץ9uy1ǏD)y땥l߾۷3mCuzΕC m˥f[:ggWD036|{>tfUί~`Mu3lt!Cv0[-Vg쁘uwZ؄y,s؝V ˗bdmuJ4]˺@ tZ8C3.q.oM)'=PB\s?W""""7mB/#x|ʸ݆)pU߿*r6\(ǽͳ iެ׳""a~~~xL~yC|5&k@d/lNMc3^g-U_c#DG'MlJUEPbu+0=Ggzzvb&L&x1KSSI%Bibŀpf?? b]F7xf3pۛ1!##!Fpp!̸kZr^;l>V`!Ŝk;W""""%A ܖ,ױ1=#zѼ4's@ {7)T|Ie!u˖l"ik&KUG=m.CF;Y 쏈`S|tOZ,ݬ#ZT %|M/Pgؿ!(c,kFџ+pUkݥiNߥ: 55KڹFogOghޯJ=zsIVקk}߯1sN˷Q<g?Zizt.3 t4k̵k+1=enfSR˶d%M˶ټ<)11< )))ZZǨI=A7eV+?ݥ*+j ymWu͘Z,a2K",)Լx@f*MkY#JӺJ5tTfo;jo_u]+`v|5g||'E(WF OV0FGl]7vEiv=C=v2~?-Sվ=p}jՂfljߕg'ww)>ZKsU=]!jNOk*afG ^UMEpGB b2 IDAT};uUܢ Q#H:?&шJl6m:V}*: G_MZk7u{\k34vBok ?S1342&JI1ۖj#oxӰ!}kK]vysϞ*ER^q9^ޚw붅s4*ʠu/C_4ut yI#?*-h[nr:$ *3`v F-\dQf.i7^y{WAl௞CT#fڽGg8x~YP`5B;^_gnT,7uFؗ?XOZnB7!M`&0p  ܄nB7!M`&0p  ܄nB7!M`&0p  ܄nB7!M`& ۫MA.@<9,K> (B5bxڅO$*JQMdp[jzNdܸ=;O[4]hp xY pgj5nA(ulN:uU!hC'Ic4bpԓH:k?wI*Է:`ۜݣCf)5_Q9Tiʭ9S:fiXʝZ=_ `'%^- 0{|=ktB;oЁf4cj1:TR+I٪-9*Ige:HmFQUYВמk4$2ܧ+Ngtm&kQu|xsoQ@%$Z|sNhV}:I;J@lz$9܅Ob/u}R~wRCKi2I2>%+ ST\SQck/%xצ<0D*ܦpt_ L6Ӡ?OߥA˱SyWʴe%*c i0-sv Uׄ ˻,Ђѩ ~k1 NPC$qZ89I~j .edɽL_m۔(лz*ffփ`dB {@o-zK}}"YS7)Z0I T۶q2Vr7Q}juzOA Ю>1Y !@Cl6U騶lNÅ`+9l>l7P~}^ZU%S70mYD|Р$?IWLKzǯg/:d;_o7뻯?҆=߽iؐھn. @Vq`ż,#U֍Ңc诐,3*nsz.zje|B#$nH1h:99TykpkDi5n+ԗ>i4LJ13zE*zRʬ@MUXf֡{tzF;]G8 @D2jo4P?sbyj`5Zƾz Up  ܄nB7!M`&0p  ܄nB7!M`&0poO%gLIR IzsGkZ OTOe9_K˩F6||.j|+<}R_%cKsi=]Rd;%]2F/%uN্2Tm:/Pm:z,K^M"<]P'f9_8OQoXs?euR`K>=_nd-ȗ2:?-55%nC$%%yj-PWM`&0ekˏiǙ.@-Es1ܰAt!j:6 b>Z=_^P&٬t?~\)FQ 4Ptt=@WXc`Ry{=V3駟L&OTfeggVǎ}C `աcG/_7o:vB> kW䘒>]ZEolО7n\vvc=ᅠѯǬ m[Mи Zk<|Rp2 u]ƛ$ŗTb#!Ҳ575`IR{V󟧨O.۸d2UYUW{nN7ۻ{@V54oHh}&T]/Iإ]ym!F. ~Q(Z|xr:H[Tn{'?_,^ Jʢ5 ew/ָ<[5Ֆ^cQҲ8ڲ9ܺtm!Gqqs;?E$^)s^aAں#$5VFґe.Og(q`ttLT7;EN[.hb~5fd~H[MsWw05j, }; \#hֺuc?IWJ~Tk34CfZh,lmȕaC5$5S~qzm˚Ƭqaާͯny{\S"e+=7R&w_Iu3m[{Yuw1lN64*nsz.zje|B#$nHWMM KSs Zn>.5lK-QEѯ0g6:!o5kG3G̝3^ nFu#'5N|^χyEϬ͓OHE%c{O0J-N^6fS<7/fD彻D~ægSTBΘ62D?XJJJcHu_ֿP.M?).5[jjhd2MEYR~yAr$Th3YKM%{gjuFڨ.eƽU6:_Oθy "3o4ߕ/6TƑjSx@{R֩KljA_yPjxQ%ՠ뇩gsj:jNdQn8-O);c>X9_S>ߩgPk, |dz ~v!fPꇫ4ѫ jѺC%tS< >.%;o_׿E\__B% "-\A{s kW:Mナw|TV~_stWh읓5wmzu}EtQ\oLCb$wpM0R}_(Zؗz^O{Cknwa"=+8tR}Mnꖦ u%]$]j;c{Y%76Å/l{cPE`|=kt\ۿZ&#ctB{M]qFo_7?hɔe~%%)W{Ӿב48y<[YԊEnde3!J)M=s"WMSGO$$)W;oY_{57ƫN^oO|]}uL;i;pvMֺ]:P{X oS^^Hߢ_zO|tc^m^ܥzhT u쵯5~2Y=KJTR6*{7`/RP{Ӥ+%Ig֦+ٶi|E)6ss?GaP&Im+ގ5!6N} c~.\&Tju?S7ϾGMN Uα0ĪsP-v2OmN=:^j[=o+Y* 𰫣I_hC5l`mR1-<7 dy۸vBPf5ȚwJ@OR/}B˰҉Zr{aX#5 s@VYeC zyyI$(HW&;=F6/ⱝsW5k5jB=;e ֽhw.*;]AWZT^xQ MgOR?7t/8RT+o61̺O?>^7?_X5-'WdQr<*(H=)[]ڿ" Ӵ.貿O5hǨZjAMSJڠ(MYK^e}%@ӾήWGOX5ɕ.y5h"K^OSu馔6a=~V+VjY6,_{>{M25^Eϯ>4f2ҾvMG9;/JZrvd+te ,s[ijղ ~RP5&6ҵeyv蛏M4{F@6nƍ#iU%5w A el mT壟|OT7yȫITQ?jZ]p,<&)Kix}yں99+.J?C۔cT͋"-~%=:O>!M Fe>>fO< sP:tasSίZƛ6Px\? Ҷ.F4iWe PhML'Gߟơ0&m|WIw<{OHJ-N^6fS<7/fl*3͟MWJMMURRR5/|'WSm]ss6HMM-w0L%md_j^P\ b3LRS  ]\S^񛒒t+WT+Ooɞ.8o߾|hM\^(U_?ڃM`&0p  c4e6=]Fg6e4|UW{ Qvv˨󲲲Rׯ0xLDD:˞f6Ç+""_߮_u'x<&00PZґ#G)ѨlRUv=@@xyy)$$DX,Z.N1 22Lv=@@G y T|g\p  ܄nB7!M`&0p (KZZKRSS=]PJq "  ܄nB7!M`&0p  ܄nHڿN<)rjѨ`5o\.!aڱcբE L&OTfeggk׮]j۶|||<]]܂xWL&EDD(<<\t9;y7n2ƍĉ.!a*b2j3t0p  ܄nB7!/<9B4IǴORy{n_z{r(W Ud]>futq N t8۱h߶t<0+Џ|FF^ԭw!KG.zB?H nZNEz~Wq u=ܴߵEZ;fEq2 ֳ`@]WE!\=K7ل/'ih39֠yEK-k/h/)-=3 5&5ruYX^6S~;ҴN-3{+r4u𩮓`@]Or6mr[WWRLr=4*VIݺFZX?rJCښ$=/ u=M2X~amBm U| J2MN UEKdUNZeZI_hC5l`mR4znW?dVYTHr'n!u]j'+R ZeAC5tjucfFXgG|+}Iڝ;:D@9yM;X(iYcwTUykv~2LӞ]k˚CP \qFͣm35 10Sg-ɷhէMС[qhƍ#iU%5率RhҊ-B+Z[n~|%7z+PZ6_AOOcomkCqxi3di_ns 80>맇_YW_W٧T1mmYVkqZa_%^-.bEZvfB[є%4<0WM Y|(<EIy7Xo'W'{ߢר0{wtJ-N^6fS<7/f^:IDAT9vKKKSRR˨3RSSXmL&tI[$%嗚/HL6Tv0KIIqʕ+P #YKxɓ=]BmSm0۷K@tHo-y2 _jWsL3Ne{6֌SP[T$;7;8ǽ7>FQfe fYF":2rMNEoAt G ((Hٞ.NRPPPE7wa*JT(%E؛8ЬY3:tH'f233ua5k֬`hdWnsa34 W0a`˨J܊rީ0ens`jf {c;5UR~OXe'^0{,9ޕxKy7(؆Р@clJ’ATpd^:mUxjP{G)lCTdn/v:!=`R'g,6sTQ׳ v0eq=&U:X9 7bz*ljmCmJN- =0{!QomsvbJzlOJ:t agUQ*;>^J+Ĝ&tOWY᫬K1Q ]+Ov/Yרs˶ٳ_/8^sV8wQ/XY/[.Y6|ٻЕ bJ8 d^賣g}+v {`%=YXX:x9Q @ GQeY(! %JYIsJsdg._QYts=_%*s %J:?pv|8vIJz셱QKYn?7Bm*)#% VVYrePI+alÕUz"\Õѭ>(I{lC @Us5lorMpc/$ W˕ ^18;+ALN]=*J(L]=r|yYaU\5 a \/Uf\U%/ظ\ YpH*WÑ!]z.᦬ TergLYa@UV/WyPENy=]<=cl T4T?ŕpT[ =}eNU*tEXtCreation TimeSa 29 Mai 2021 23:53:48 CESTJ9 IDATxyXTe  .(nknef=틥fپ=ڳʹE,J,5J\3w Paf~5l,2uk93|ϹHEe8O-"""""7?v%"""""EC \"""""r8lnSMDDDDDJiU ;ZVDDDDDֲ.Ee)ͺ""""""E-Su}Rpm]WV`&"""""≯ArނVXI,O9y D0VzUPk.|gLDDDDDJ$=]'y{ݥ_)y d^/Sr/?CYHkeR]UU@歎3f|}L=a"""""⎷0-l _B/u+ _ނk+\DDDDDę _ίyۇe]*it`i&\6vTfVVij)3̬,F_| a% ]s1wru/VfRϗT$^ }*:R{,D׋>GIOA,n J/&f3w{↯b+ɍ= Q_phypv0]o\p0WX*ks\/׃C) F1B5xII~Vqwݗg0Xqz\UX늈KutR=-Y!(~׸ lO 7?US :H\ð٘ sN@;#,feƱ`w%𬜂X{ a}+`dibLvJ )n\eO â;v~YDD1bI +\^ObDo~v9 9|9O(zrυ&"rY όjMpBHf,ky 3n%jwW6wt&<Ȉntz*{βOf/jH;;Qv.̻F6GV"%]4D >畗"NMOFX !ģB]f""<L,&BץuivAZ-<ߥ~sDDĝ"sr`$Z6OO3\;.øxr~^k܅%O!iv""rV{Ϝ%hP<}/Mtr7w NH;Gm޲䝤Q ﺓ:5#v462}#fut5{.VPsǏ̞4ox>dùm227\FݯqW;4b$T vyOV}7o?GLKdKi#3 c쌍ؼQsϨ!!ܓoyUe7Y 谓焏|Mv6}gxg255ėr*_:3^[ sE0rck##i9@q طx8 ո7?یjY@G3NqT0r3Iuvu-BcMڑ,BjӠ5Tn9+2e"DGUziݫ#M,i̮J |yv^˔-6H!SxwL ܬd#tlv ^6N}pj6%"H@Cd/[Gϑ djکeb:, )ۆǬW1{{IQDDΚ| LޒcL6}+2Y{s9U?Ѕ˯ˤ՟suSY8v 0T|Xw և>KƝPmNxhBd\wI˵JV!Q>g 7?f\:9Sl}G 9m %4( voG\0`[ϴQXt̎14uT<5~e!3pC7:?͊ŒGH޺ea~;^jhrsa/}n;Go0OO 6}{1{DD1=MS ;c[6i wKq׃]V!LDv_vhe"$}s$qDf#֬S*;像!t&Zy/mԛ^ߏrV-vhYegHXX #0|9aވgqkDtabHb 5@t`T5_ׄ aGs~xP*թ ^""e(q|V: Yʩ3x3F$#7vߗW{Çu޲TZv44R۱u̫W֤C d:BLaCywU$5+: NoPDs3M p+Hpg\UpMXPpFB\e*g5ė-~ F#F)X_9$v_[XLaT5[{~?qHڛEp\+:'(m?&;c9+:;ͯf|ϟOb v-TatJ'0*Xo7垇9,L&'vҟ/t0'i|\h^S?'ӣh׾mI-NDDDDDo"ru.g'|8Xa8`=`ɟNV 79MvQd*P$=`""""""bqzK| Q>O7SY^S EDDDD̽A5 2x+a`_eu  b"""""R>KbUk"sj%*m7Ye ى~emF""""""v4;!LM N!8b5͛+܍EDDDDDXX]Iir&"""""RNDDDDDDʉH9Q)' `""""""DLDDDDD}׮DDDDDD*r `;v,]l.Dr&"""""RNDDDDDDʉH9Q)' `""""""DLDDDDD(0r&"""""RNDDDDDDI p'))%9C. 6 ))] b[fKpK 0r&"""""RNDDDDDDʉH9Q)' `""""""BL_MMhp_klΉf?AU؎`J`"f=05刈TJ>Oe`]1 `jCpKYm'nClw-%T:ͫh؆&"Cp(MHOrDDDDD* lG2t!U]HTfUW9rZq]HTONeXd=~+"WHJ[g3g,?DDDD! `R)~7poUl3Yߊ8+l߭|L%/dw\AƳxŊT`~D9c!pu0c,òsl:vLJzMZҕZʿ8roog~ń~cuIYorzDDDD* 09+*L@?lixc,|// ֮~+Rsw5fЅ _"""";TNS~sW#HTlK.2Te)_Ո{(_6/|Xnш{)ɛaLVNIKr$- 9z-zqݣ_l}g}jy}h+Xq;l?ė+1o泧`Ɏ #@Xfa wlon_|\FХ/cZ/I|,\ɶLFT4lO]LIH6~wuOeޯi=ͱL[]0q&d &gbw%O {w$>:Z4r9;E/h" 0G,ATMq瞞>{/#o>9N)cA ;vd.Y @ơM,zn7ڗ+ oi:vO%PƒmXϒ?m%o_|ٗMobk_P.qe,_]I&"""ߥzV>B7ҫWfKy؍Msb.X0s>S2_3Ǔ@-.SX湠}$bԹ.j3v!Ka>u4ms-OiKr//|]%|dw ǟ̛ܮyDq,bw RXlkެGw>9H.~j.?Ӄb_""""NA,$%_Μxa@Xt*z XmO;6 4 ؗeB;ҷK8fsA6y}.F<$g>)8F ܃kI;JK5iբl8Gq_SyFcῚ8 uyڙ5nY 6hjj^%i__׎mlUҿw`,"""R(^ѡݪsk~7X \4'Z`a ͊>}kWz@i@I<:c BsսW*t'&~Ħ31߂br|$1(>F5ƒؗɄ  _"""r^Psj=Zjˎ{*k>|Y6|D&ݹ鮑\kzXÈiOie%<{%R20TC;'hhŜc8ћnL,K4Jdbt\{pa)lޜ˖ŐsCjVs֧䲞 HI; | D5Ѐ#l9 a&\| Jk}̀͘5#GEh O ffhqQ8S+""""\qرll0`46vz' O2f&kgNGp -LǴ{ysx3Ƕ~üMP`_>兗~"bx i{9U;pD_Έ+3~aּ1+SՔ  v5LO{ּ6߯Ahnd}e]٘/_ϗγ'#tiޜh6i* qW0yLXo3d ,}}%lRV/|j4ly1hv/A5p˥syd1~z~?=_mC4+_~+.S`>:$yz4==?la~J/>L[еK.v#ݜ=N]hӴ)m]Jʒh?nS^Nzr3ɰ٨%rc[nUH &:);77ajp-2#ӅƵ 4T6ЖV^x` B6%' L{>4 'r4n"ڠ _7?u FMS C]pi"zZDDD"ϝ'4GssveRq KJJCd/zcj)aƔMA5"h4n3c&C6k.gH9#.၀e?-41C<9Y:k<XXk繀i;M"SBCUZ&gdD­ )^bņٍges3 , h>^,N 7PEB1N # PG鋣n Lv;M~M6~"AYN{ &ÕП^o= d۵wӲG2ӎ9|wem#P Ɲ)J8{rukx"`[”}̺\ٿ%dDF~1+lKu6"8(fb:<ȹ)~L)' `@y=-t:~)< FpI:HM>w3 NaNZ=m?/TMz2# Rs]ECЋ0r&"""""RN4, ɜ8q5. jժGpps, 7oN:c6]RFdd_bѣlٲVZ""%SED伕L:uїil6C:uػwsN8ATTːsLTT.CDQ>9L8]yhfSϗl"Rb>Ԋžo8Tq*UTJ>Ztúk#v9Ng# chS]HTGA4E`rXuZȵ)Є1:jDDDDD* UbeDDDDD*s"I$jʦ_&?x1^ "͟MeƊ.D*$-x9k]yML*=|8nԟVP+ٺr46bSrJcsv {w ֩]YrU?⧿\yye.ADs `Gkз7zK.ck@vkΓ:>Ϥמ[Tnc'|˕ qq Z>]o8׼HZF.~hC,}JZԪBph {c7t Glz+Fv`=[Y\%yja;ilXLEpsgjė"eZWEž=Σ/Ai`ˋt4b]r(b_8ZnM@2MkϼBo57ߟ?gO6a6 IDATZ=2n4&dd FkA&Gcu9<0cGOZ QW[ z^b,j筘QWH/r|:z?tco;Y5\1 !azOpjĵu=Y=Ib Gx`pc|rܻj7[eT#Ў5؆'ZGI8=}.,|ld2>ToԋUMp^k3 1odS撴=Lz8CU_tșxfidBӪ}'&TwRmo쩬ye|kU6'8Zrx}ϝyn_۰Ł>/w`Uv~, gO3Vxg 4V1E6#*N>&`גWxb@5~bã5ƐGKO~ox7O^GF.ϯOdmU8 &uZLv~LmLnobl^._wVTW$|agϬki 'kt3ˌw!3]oWo5~̳/#bԵ&%svudWZĎ{1fgx]d͜Uu eԦ۶ecyp~=Ș3G,eFZ{\@ֱ.m8yMCRwy&;p8RHG7bd)ÊܵUW~uDإ0UR vj""R(IdHHΦ}poޟ./fy_[Ķcۦ@j_Κ }tho`vah\u{ay=J]/hu-|4I}j޾).eۉ!^)S +ޘu^xsFXs*Ui׹mfkߞ :6:еG췌Gc*6u^D/ny"q\1k |3==$d0貪`]/k]0{mn<;[&FMB4k;%.u#`vp-:刈 `RaGdщ;E6rH9zmlޙrNP33jEbcq/uԆS9[NY>mZW?vs+q:IƶHzO<0$оm$օ쵭 dheLw3; bUWpF#25[|OӝeѦ%=`/a5m޾{l_͝9Ve0_5ćAH鎱0\ϋW.4xCL&LwV)S }"{yTsge? 99 Wf3flX`'1knKFJֆ%ml5{m84Nkÿy=Wo/CAAyEmۇgJ.6'nރ`]s?sf\IN1\C[TkS9|?Wٝ:ġt]{ `Rɔ#! L{z}w}lm> p%{PTwsiI(aaAiN2_eڲykJֆ$4i<%%h:=ېFHӦ P|nڵ,jv)3.|cw[q %Uya7i<~_{&>=({}[xsaҥD,ϥ˱V.XjrA RV:0狈yEL/=c=[rC<>zGn61NeOf=hUh$@gƄF40~ȗ,%T <ơBg‹o 5A`nq H,bi49|f"#t5{Yr׳fiv||w a!mHF|gCә{)?ݫr׮ePsQ~$;FJRrH",|6GVv}79{y}eѦx۶!1)#{u ~s=о^=0U1`ڢFA؛Ȍ'rKmL7{߷o{oED|&~@xtM~qs0Y;fSV7>)&3w٘k./q#̷yt ,!WO ޿vw3<"o{TIT4ɥ#ncOx^tՂ#fSc Cm<$hо5,䙇NpJL><4N;V6,H[^eJd&}<Cdys,W Xݴk\\=m7+/dڡ؂Q'7>v0UfG1P&u;2 mRpeѦx۶F漩<) UQv{zWq?wAXsߣփ$nLw:\V x3gb£W1'_q}Jrf]^ͧ4lEpQAn)S `.I%^y) ^Ѱ;&a?# Frɰ~u1|Ճ.w4'L;0T zvt_'z[t(Sx4 `=S;|[P5j7Ʉ^9WW9γ;UYQ.u]T.3b3}xѳ+* O}F.y'xx̂O}ʧoy37bn6g??~šYjҠU7_%J_y6d߶0(.t#"""R)Ya4W!d}؜9le-679{Fٲ7:>lj4{ ~8֘A*|xrV4]u؏v[jvmECF38L6><+r4qefvM8F-=v=LZ#iYkыM v1^Lhu7߼+P_]°dMpZ|\FХ/cZ泧`Ɏ #@Xfa wl?~-; W-%U>.S4];7}c{!w< -@ͱLX*jpl/}r;'Bf`P⧉x}I0 neTc 'j\:'{ >|x-KG9b z*d=ž5hޭ9-S@VU|:"HglڍXt/nۻ 9u0EYc6Mob؍Ԏo9m7v`8iY#*; 3erJ—4ۧH^n/Oc76Y`sw1GNoˤ~OOb>[A_Zg_}&`kޙA0L:`߹Z}e6gfUg ojLYl0C^ͨ X-Rlë&.fM ²e[f.O`'zW{?1e:\}Tl5`UFض?ۘ6/ӜW8p@c_ICH.f CUpMN[r:e'Y?^+O9 x'h7X)1w|R]?ş?~ŪǦ%n1֤uZ GSSqntH ֋F37Wn.\G8sYMc IVD|Xb`buoE9~䤑v Ulο+rXé\uմ ɢ)'!ف9r.PEϰq,1~.l]M ڭV@Ʉ  ߾Tbgdش]6SrsuދȹILO|Ca#zsՍ 5~Bb:|YQ"M߰%{z>K[ dgâYNEHذZ퀃?,g+@p}kwZ_Ë:"dkN,8j@v veAP칹RTО5C 0S32Sej i׶w!>x%_EN=]xܓ'00L=Y;ܠq|/:̚7FpŴpr8aಗߖ5^)w/<׆p5Mj~ /Er8q,\ ]}i͛fR7Ma?P==EK:w ᛕY,{j6L&܏~E\« 1Æ`$WwnkMSJnt.,=O],c6w^Ln:DW@Q%٣Hx~4"0fa1SQKjc3ǼLݪ82LtlS:wnAZ7qD0׆KoExZk׭Anf6X4o1HP {ɉGbćw >22r7cr$ҷ_GծFHDR;xFsۼC]pi"z;-4(4 ^؎t뵡`""""W_תDDDDDĿΊV/\wMg?GJ1qۻD)w8e}{+w&o.I(f![ӥ}"ЎNG}.ZR8aԅ6M@bbٳÇcX|Z'((ڵkX/iJַ2NF{q3{bʱWK8Kđ@eش٦{ffޢI֠{2-PuIx`RW0ˏLo(;/ȕ.Gsn={IN imƞ={hذY$-mq7rs׊ʊ&(J?ȁ U +"""RUVf͛v*p2 a\8~ 6+jH 8K<4:jC,y V,dbņPe6cFn52tLE 4 /^zSRRر zIJJC;̪+} `9y1Y)+W]d[奴VDDQwCLw:MQF컶=W O& 4HlFp'vzW`JUc`b:\ɘalX%J;)V*LNN9y[/.{3|w|M%|4 IDAT1yדʣ qq,ܵyIU^J9)ϙ>"""e\ 9;{rukx"`[”}̺\ٿ% I0YROq8i 1rE[9P(TFn^Z.u˸ët =""(}T 1\$.pg㢮Xq"RٸN-53gW#sU.enͽ5GfVds88@|<w|  eb[0*C}?k&1vT 6,c$be㌛`8w,ySơgfE2hlӛsvT#GEU +o?![F|5 yϛ!͟†q~4OS)[=+e'W}gl3qo7pcX^}O1.D61W5T(/BϛLRPƽ!>Iꒆnz`s%v zv44 Jg0.neiWGmc`ˈjzrz#]!V 5i?+z,qh=s>s7@éUY4w GbrVymT H{BЎ(^1F EF,s4Ɉnav1ѡklNm9|>P⻠>B! Y|;M'q?h4 ?.#!-s gNsSzüxPGY=r4w2z;19k _t,d(vQ vQp?h?gׇ݀ %Ҹ翉+&m;XZYк$ T.7{C9Leơeέ.ivNbG2ni ζxH|jB&ְa.[G?7}- ~B_ǧiQ0|/g԰J%GchĨKZ\]%Y4 V/>gry0ctE/67,cH3oDm]T:֡:G!x0!r̹:kz5 eρX}iQQ6r wM햱n *qWKʼn8ˡkPgK8b"PVm]>}`P|eΧ.i$T] Sku#Hu뻑ܻ'[~>F0)[Z&t<,5iP]-rldsu jCξkqz~jr$ro֬xSSp†Խ>OTvم%]M5Х}BLȎjX\0i@W\Ur:OɽSX #- 4dnO緬˸xTyL#;m] Axt!+ Rٚ5WM#pm]=W"B Mj~\^ՆB Mo!t|h<]>k!o7ff )d쏢B1 |[;0f=)mF0vi7uX>z62n3lRxe~˴ڲBi䂁#gmٱy S6<~bhKjT*!)$m+(5KnFZ^zSE^>}GS~G2BЋ7 V(tʿ9C.T 1qqh;vʋxѶ_k7, U;\yO9GGU3Qi@-Lف?cN'KnC%)YDFOt©31{y-~WeP"Ѯ%sy*["1i'nTOiY.M7<]cݎX/佟X蘖ZDS~t#9OIfȹ&X7& 0FӳŒy,]'&͌2=2_,s[~dXLkWv{vJYbEb8̬)a[v„o1wʸeAqܟ߹ۄF%l<$T]w}跐 X})nJȪ4 ?ek2MaCKO86{6@,QF_ᯟV ʺ7aԸϏ^:13YycfiCʵq+`A(qrSF?&.;f^֟Vvz:oX.`ᎅLZM9 Xcѧ|9,4]-l*6bttm|.BǴ4o-]M4'>K!oV=W}< TQ_EsտՒ&cԨQ#7AD>nݺcǨY&ƹ;;$!!4hǔe'x Hڤ}B'Oz.P(g$ YoJ UTy=tƅҥK;&&Yo@(]:n}}}ucÆiʧB!y%0pppڵk|e5cccJ*C^?(B!K%"[E ))d4_/===P(?,B! Q) L !B7\J!B! LB!L!B! LB!L!B! LB!L!B! L!k+))?~LjjjQgG׎8::bbb8d}U%0!$([,rwhlll:@%))DJr=d}U&KB"""([,vvvoԎY¤P(lٲDFF:CQP^od&cJ,YPdI=zp2EAk|L!k)55U~q"(<%PWLB!L!B! LB!L!B! LB!Lk8fQ![dtQg$pa"VU)`KB&DŽN&`͐p߿Fhnk:ė!E 8q/sͱWov3ß@ܯR-!Ȼ:u^R!DŽxu,#z1=b،beј:w~9cq"3OT) &{~C4f |RLgbuDk~N6qFfXvƻ^+|{ZOX ~s+MqdZ ~^oG'gҩ&@qB!0QRyuX^P8 M鳏'%t'sg#(kz4)\roCQ|WA0L~عG•4+XBQ~;E!!o5gHʕӏ`ԨK6U~նL5ljU Xs,W)(a!&m[ӣt=칱{,_>'pzޫ66ø仕]mFmc`ˈjg7xq/Ndkͱ+*=+ˢQ\= wN%đ kG*xZm7{<~-8j6돆sbULni%dXuG  aBϳxCJC߯E?c7IFنͶrlʹNlXnjwݲR=s{jN2D55SN_[ι6p3$NXOkØ.WW,13-!^o}}ħgbU1[ܪ5M.ԷSu-wfG;m-f4ww -0u!qE􍭍ׇ %Ҹ翉+&m;}]`BK5L{Ҕ(SS_@_HRmd(vQ vQp?h?σe.`de))$'Cdk5f8hvNbG2ni ζx)ߡ_jXK1ǟeճg<_e q6,E}~;Ù8$I0\4JRihE+G>OQgFn1Mak`t,go{c @=%?h͹XGa+z)jӨ!3>A-PS5Vה"{B zғ]:tx5'[ٹ~,me#ך֘\75L*<<N`d4>jG@LK䰔&YOJuM~:?`!$&zAu=th X#yYr[גI&Ս:)?AR8Gc[3pM:;;6oaf?W~P g%sX Pi]fXĚo}Oym&8dc9nCi~cN>'q;~{me d" .dԨQ̚5 ˗/dll޼yvN{MK?O' P8;Ԡ^5H5q궒ѝjt 5[¹ ɴn`ăgkiٳQO.s\ [:z7TU* @uQlF/ѧYr(w8u#]UC$رp7S(xʵr,`BS_k[J%)YDFYLs,, 66вQrZʢ}g<7ptUϼ ]]11"7t\Eݜזf~kv`hּ3oCéiHCD7߿bGSh<)B%t؏iۆ5%?_õi잿u TB g̟?o/{;'`?egAӇ7k `O !Lda 64ZņrX6 xpLaw?_[ wҮ4q%.%'`NgZyProM[EpYSxx#vq8m'~Dг3gA/ޯb">кKEeߚؽ[kʴ,4`Ū%Lݘ%0dIQl}zQgvNJ5xm0{?FZKJҠ^E~wAq:L!aO>R| @i[2}V1X vim2Ti,oaWacg _fLnO={跐 X})nJȪ4 ?ek4Av„o1wzL)V]p/gr]05f&6x},m(Y6n%lh f`Mi6LCEY ֲh{_YbEb8̬)a[κg_g^?ZNocWm,Tckʺ7aԸp קrvXNjT{EV,BJt؏#꬝©Jhq:REWMv =}0xZRRҼdߟ5=#3|'5}sKԞXׇģL->^Ų^N镔~~lwȎ1=PwU~tE6ps(EgmmOŸLh>Zª~}򙿿?u-e/gҨP$&)9^bKLѣo_ZU*Q}"bi'G#x3fzaÆ"ʉB‘ĭĥc,L筛+cK?7``Y85zЮrNKq1YuC 8o0*CYc͚e\qLSy,#<}ZO2*lۺ51]H_h{߹̵Z̒_3uKFVp{L7S: B! 릔f\c^޺%B7Q%"^eR^^%-A B!L!B! LB!L!B! LB HJJ*lFHJJ d׾* Bג5/lF:ᬬd՛u7 !x-9::rmnݺ% GIIIܼy;wvvv2EPvvvE|݈Y!ěĄJ*͛7IMM-, 100oooLLLrܜ+r]"_`eeE 077/+ !xm) \]]:BؘTҴBzzzP(L!B䙞^ zB!B B!=ω IDATB !B!D! B!B(27c: B!5`oiZY7rL!B! LB!L!B! LB!L!B! LB!L!B! LB!L!B! LB!L!B! LB!L!B! LB!L"Ӷ,ꜼٽjuFDog͜{Ty=7- }{ ۗ_C&`-ϽDYl#h|)odnb`Y0 Llگ:m^+.e`_:ůt㗥'7 6~҆+׆z[ZS=y>S !m:~/^/_=EvWdQg Cc[ \MT ۷۽ѹ/NdL>[;Gҿck}В:2p?νgFXJ{`S 9x[$7 "(ǣ1,_ٴ_uڽW%L_pm0rLUW)c僛~a`^ vwWU?[U%CKT:em< z{Ujp-co35IjPJ7d )h[2LjxRaqo큞@?dzxb$I];RtfC]h0˘tz iR2`cp+ʦkc9|:DA{&`T)W6-ݏRuw4Q\'l$qp1 6M,RlƶPtq,%Kf6e|;JhҞs1 ʣ4l*4㡃inCY4M]GhTk]=ͻMFIxp(x|J9m $7K+oSOРGOv@m{ڸ8&i܍J@Y&FuU*r5M01D _SHL֞5myӚʚv38\9{-gDkB>e\8-+us_m?7,Q"eh;OOߏT.˄};ӆTf|d/̘'F͆1m<~ٍfٜzߎW.LbPLV\ʕ+\Pq×+kۅtlڸOrƟ{դiL՛:1ou'0o4#\s~p-+bޕ͜~d-Jm*1D݋!oO O~gj6ڃ'-眢 ΞE He}ܼUϟwmiޭ9VIqnKwc]H7d샧`Z59,QIZC//]dI{0ĀՋ}BL XceUBⰫTI%. 驔9 BC;3sE@r,qIXZf#m)mZE&o߼;y*Krn܎'xHHMyy|iCx_Cה-ܿwc8TV:,a4 זXX@b|<ǿa'$WЩ_bLk(kBvHzӲ9+#hً7xb[ k?7,Q޾#`svŚר64Kyé94o,ݐ֯;r%lǩJFwvFUӁ~~_{1N1uF@uwEsV=i_&ҷ9;x?JVii^Sw<:g~ˤ* VRi5.uql[P>6}7uqDmؓA1'<cn[*2|Р;󮔱\ M'֮6h7 Bob-|=,L W.ŴB?義.vu"#:f{8}?1aAԿ:aED0+:ׯk鍻ʷɔ.Wzq I17o~7O˯ԋ\V3u$ j %eE>x%+y}Lytj!klwPu)_O`l*/[^:,a4Q%H5XZӧ(/d>Do{y$ `v:ƭ!ʚu8Re:l']]}ֺW_Sʇ}(h2@{0^ĥdL9F*)~ٛﶥ]hX*ӯ `Z\q4ں\~82VsV9RL*67k4"k4yR% Q0R-4_-XOd*O({x4gɌqYyI 4^Ph7A\JoH$- U$k ҿL'{xO oo_!nLTKz2Fh*cr38Ka(~څ\Y)fہ sw6+__hn:Ti̹lY|vW.#L,EK9ݗg%0 E.%T0‚wXH/̟3-n عu+lk\yeZTnǕ}l~ ~hi8ڢӑ냱.*t(*4nH.w,6C#gvp o@{*F~wk]hB}φ_cAw(>ן=o\~L|Ȳ%FA8{sgTL WEx64jQ3|b*|P!_~T! L ,wǮZ ZbxUE.n*߅ى\$4ёfqʵPs|AAADgoƭ4eȰ)ꘞbvbc͍TWZqgoMTLX@tKU/}evVBO u(emt-0XD<9gDP P‚S)'MJp0f93vor`4͔o[p#/?Y!&ϠU6_JP1)LeTq .S}+Z}O)dφVư&ֺŝC457cKv!2qJ۸7RoYvlEWL=U)r,*[<;RV/T1ugB!\u]p1x[i\c}vOƻ VLRoNc2,XC2mqvgΞ$ّΝ䟻F@cL*h_+;{|^ 3h5`Rكh &3?יHŻk%{:PLZ_W`{"ۇup}gP:d۶{qŗYs s&cR,N {S/ ilY7o R|+1IIIl!IVz6qӗ4hOJW Y< 䁥7ϖ9DeCė@%c`јcXqLԔrfzyJOat9=&pܖ `T/C>dILN2g#}ߗw+2єjKD˱~KdOR׫2_/WO N oG\t //>z a1PY~{&~/xq<\WLgo16zKѺ1ǠgILޭ:aÝN˱^Y5ЗDsՇ@aH8 EsտՒ&cԨQ#7AevMάOjOPN#*O%lէ|5:ҪRkOV>>/gҨP$&)9~6;fyOqu~}8Xo17ac!}+bHO¤j|6-ɷ-ͼ͸3lL'~Wdf]!# =VB 1РzI~? ;No9EBƑЁB!{bP6#?_ǚϋ:BƑx B!BQHdpÅmӶu{K~~Qe93rS7B!kJ&`~d QF?י%pϥL_Ƕs_aq}me>L5Vi<>`ίܔM)Bs[aAO(]l_gwz \z!tpBjs>_(+58z4zsZq.mnYZE^Nfž,iWKpWGnY!x}Lbn{ٿ 'vĬw*,EOKeSG @²M"a!8}_,7xS,Bd&^O˲-BTLm=ig]XWƓ_ٲa/HpZk G.NO$4g.rlm* fٌ_$Y8P˗ jS`O|[rT đukl?ش| )ޓG"=bGբuJ<<noVj]G2KF9 NyfN_NTtn}C,-gS?u$,5rrZ`2~R$QwNmRL >#jhmFϦ"z81ݶ#'.p#)Js[jথrCNiFLl߸Bn$٘m3_U u(sZ?s&9 :ewvLgЇl!^Ų IDATm%0ZJ<_+i?;?!|RϜOUSi\ b.dCC:俖Ks5]*@DPnkU է>0\qד>1?-3ifq˙~c7 iN>cp+ʦ.ٜ0A|0͍'_@o*ǍGő4ލ:UKUus'MI"eRNUϮNlV|1#e}2T^4+{v2K{@b1O u(*mֆI-%vY}ɗBLkrp0n]y'@y{{qvKb L T6˗BDB`=^gyQ se(#~TsL=9wv\HӧL;p86KD6VTS>KҸҵ-jVn?v{⣦n;qV'fnՌ:U_P-~H,aݗNN/)ys7T3KhviFxPJW_>hTk]=ͻE$;g, SNJoSrGϋކ˃iI+,~GJ16vZaϚikÇ=TuJx 14='Zt K\!He}ܼ:;:8f,$HT0"Qhl{+}KAZ VZP H\¢dcMa= $L LZ:=ЍulGei̹.ۭuҾ.Iu+1t樨Uwu|b3:}jԻzJ}pu=lkONl+uv.Ócu&W۪jLڡcG`hr,YڪP\{Ցzg . K! ?HR/_x]#fֱy8gI9JҤccR= SX +>///L2zgvl(TՌࣺSvٴJ!͑iԖ&^_LmW8lU~ˆ|W~Q3ry]'Y崍:R{Xk.SN6.vKA6׳U;5=[u/UV"ѺOdaVd2K[w>h^g)I0gjݪ'p zmi>~e֖H!-黫S|| :_yu2ECoVɎJHM9Yeꐔ=/Imru\>ϲ[lP֏_vnk֖*uIU_Y';Ru%GYKwjG=;]+WHH}pu=j7Y~L?ef5n]-WǺoU?ehkY!)88ߏ]37#`hb+7@wk#zAgJ{L/\?X1G\|-{ROծ@򭺼s_qu,ާs]nT[GyZ'R%ݬ. 4{E")]AN_֒OYURڍR{+ro6O2}g8LܓtԢ!ցCu(,Ic6+@bݸ's:l]ɍ},} }עzDWlA_3sY,ZT}kmOj &_>2CZI7@}_ֺ" :\ڬ I@3Y]=L#F֤ggװ6uܲ[$I>qsaN@h=)d78b__KR&m*qb Mg4uK_]R_. IRhzH_i1 Mf?WJ}dB%UmVnEM5M}%:ۜxamX'Kڣ`!F)-r>Y^R05&%HmXZR$Iq]O\[m]#(UF)ozrѧZz}cfܷF覬3._cQ[7UV}w7=]=11a0?%LzUׂ ̒2Dh])JZ=l.Y5FWE{4|mM)j}m_<Ǔedd(99ٓSl͟߾LO: =&i ̒5*mJM+}FmfݺM',^9Oџ/? A;/?jMnyjh`xM|cMv74 S 00 B 00 B 00 B 00 B 00 B 00 B 00 BvPkxB/Rh`n;5ky_;vW>|Zi}O6KP'j3Bx; 5Az_雹kuhmnypj9_m+ܦMKV׼jQ"j{u]Bh[5_sVξbE;_WQ|P.Ӽ?OMD_Y}E웭a(\M}C3(}kzM; и)ӕqA.M39uBy7P]s@k-K)v%YM[\&LĤHYV(GU)o飺i!/y+y_/ZMx@o+׉nH9߬ѮД^ .?**5p {A~\P#u*T}ׅ&I_JH|.p_4aw7-*)m8;gs3`~-P#:|DRVm-P.—$*,Bĩυzh&^ Ҽoߌе Sj-=YMuhJm}ӢJUR7gMyvҵg0dR**$UUJ>q$Y,:/V#_3>+V^;_N<%g~Mxśk֒(8!A Rtw4kd6X96jZ_P '}W|@*&6V6M Yyhƍg(%&8;FPƍӔeID]tJuRFu_껡Q3Pv>'xig nan}2Uw_oQmTqPKӪZIk ܬ ?S\][@ISV<~ֻ*$FצRPm6/.=[+\规IZrYR +E][Ih=?~<_VcR.tל{u]lu:Sk{_ 2w~?ZjZ.yY V kֽ/V.xZz2Ueuedr&qݾm)~6_R@Tok6xұ %''>gEFFRRR cg 4MIfIvۊ I6ʦXO7 !A``!A``!A``!A``!A``!A``!A``!A``!A݁dddx towhV.@b "!A``!A``!ARyy T\\JowSXX:t@ow)eVTT:w,.59fYTnnz血ow! ^VPP(EGGd2)::ZQQQڵk bihڴi"ow)e|5tN?CG 00 BH:oȌw{# ^~[4k_-{Kt+ݩysp>8^!-koý+|H;7c%y4{ih@|!#j>R՜wh"Q5kgs_+{o|¢5ٚvAUh/6hQ%j´{44>^4wUO*tMrqv,.Ր;>zd~uW*{KaJ@:#kίַ> ׄvIDATzQ,^gbm0;S;Kv)F[ xH_G~~kF=1X#t*L}/T}z$8}47ai jTD"$__?QNY^T$I>O}| V#tKʗ3m\uFA]ëZߪ:u*UVJ%;׼Mu2oW"V Xd||8ƯL~D\\I+c? @ կ~]zyZpw h5dpK-((wrPuҲXj^'*1g,;~'&sHM}ef_ ?T$u%ڒ_;Mi!^~ -mwvo"ݺQ5ehki#үVJmBwM^<]ߡ퟼eu3(I{?2ԵK[UPF^|""JkMq?que*=%4uH5yh>h>x\U .,4tmZ?GrW:Oys5Q)@TX=Uud{˞"UTT`=:&JxT'xs< `]~{G'n4 fY~~~9Uqk<:N,G!44Tv7(44;0 ^V  'ڷo{jϞ=ՓlVaaۧ׷g=`ΆuZHHt]jrN:)$$>UTyrg*[ `}\|no뫰0L&UVVbϞ񑟟L&|}5Ydiz?8OF,;9+ ݍ}d*y5ݐ45fzG5M;+p/uMG5{9 ap+|9{&: `ֻwX;_i`UgVG>=`B}aM2h:bmQswL&$I~vAQGW]U]SHlKjF_hYqcSaG}wb~6uڵO_!˶T: iNu=#V+5tlCSV:34ߚl @]\0gI+W#aVCX}m.69;z!S #0G!hms5 ^ `֑.ۋ_!Q}U-C:^1W_e?UWk9z029 `{u ct:xَzg q8 `Fx9vlhT]kV8 bu}}sԞK ]άAU,VN^8 Du0 uG2vak a n. +Φ#5(uH]tCGhhR"_Q˜׮;rH|>H9[6|ن+b @}(;CGuπ9 a.ېE agԋ;1BpeEy1wZw eW!̺+lz_Ɠfi"|/OB^K,ظ YpH.wÑ!z.pSuF<\+ Np< Lt6OG9g74 N8jȔBoHiR'ݠIw5V1*@CKx~T.[F%Q=߳Y9?+n6YIENDB`liferea-1.13.7/doc/html/help_prefs_folders_1.14.0.png000066400000000000000000001015211415350204600220770ustar00rootroot00000000000000PNG  IHDR`aSsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSa 29 Mai 2021 23:53:46 CEST+I IDATxwtUnB -$ZB M), HG7T]A:"E"~D*4Г@fw?hH&! >̝;wf}S@DDDDDDDDDDDDDDDDDDDDDDDDDD * lcDDDDD aHKDDDDDW5]p2DDDDD$4Pj ˭M9WkZ.4]iʍIΦɼ""""""yMy=pn^G3`&"""""⊧AtVXv܅,Y\"W]\j8lv5/"""""f3&RK7?Q_< `Ҟz0~("""""hR6f3)p] -8Q!̣0M炥:K`^7D E}|#k|Yp.lŏ\*|orr b{O("""""-̣E0b6:&ӶY|EdW-USdˠK/|٣i;_ظo(ø ^^/pDgf^p*>3-Eri_p x/W.۹12/""׼Ρ=_]2CYqy2r`\E\K kqʑoQtԧaUPχHF{2g-j!Dݹ8#ψݖ̥sq'˿ߎ}vgQ, Fϐi\bII4sQ,[{ ""r5?U],^~ m-0W: پ+eH钄-9dEézK8UoiMo< ;+\# ""t)r|2˿.WL͹7;&1޳SeGO0gaUrvqZn;*jqhc2Fox@ -og]XӷS*q~6)zF8>ޏ{T%TV~KDZn^3 ;Sh qh/5>dqsxB$˨W;TCP%I8U>ŻZS~ yjEaO80dfnwS/8~)8yp9٫DDM,W~h7|M\q&-m⧼wi q$gSyҩ,eܓ0wQB̜DD!KJÜ ;C~2Dbsⴕ╛~]D63Y_pJqv?ju7o22KDgTzP3~ aaQ}JqST1IN8˅`Fԥ~lF*tAR/X@a~Aho9h T)I _"r)kryRC`OI&;Oy@Q1_%M6[ޖQqy2y fw{ED9җd?vfm18B#ե~cnl%h{^`M#;f!n_ N+<]h*Dr r{N˵WjTXDFU.-`hҴj]1۷Bv [?cd?^%BqD֍L0Em}srYHSZy0m' rJe9nq::#OL&Sj5nʀ9+q\D,3INx8ouγ5l`ƯBB,z|V̸3+]_DZ3?ƹ;ƥsx@Wg~Y 4^O7m0 fNڵGy[9u LO>M -`4.;=`""""""yb9?x2r0j("""""nl\G,FzKvSOvʵs5n¡ &"""""s~I0q_4 PnJ1&Ă~Jɵr=ŃEDDDDDrTqL F[ԟN1pb͇71 ;pb%k=݄CDDDDD$(0<&"""""GDDDDDDHQ#y3gբDDDDDD < `ɫExl6rt HQ# `""""""yDLDDDDD$(0<&"""""GDDDDDDHQ# `""""""y7+Llll~WADDDDDQ*8T`FҨQ֭[UpJ 0<&"""""GDDDDDDHQ# `""""""yDLDDDDD$(0<&"""""GDDDDDDHQ,-[̗>gQ]81FY/""""$0)`b#0c):dz=F/ą\VۘԷ=myPg_P.ȞHb\ȵ7+ דLэ2,YLt I6 F!P-;q]7Q'm[};/)?_k)۩,b1BtmL#/}J9]=wHnPh.D>Ζ̹'Fs Adgφٰo~ÈnϓOӕt5$,;S|hvWA>CgxD n6EV¢;9t. 7?GMCRc2p |Gpbޜ֐c0'K>7bPu8~:N Os7[y_1owXY~+_;{ &e0;]y '=pl"Vs(e7)u)voS<˒&8cqw,@8~2a9w]/M{˄3`v?E]f럲daΞ'>@P4{p;To]`?5;'a.G;}U16H5` L`?9)0b [$b48p=dz jDV| #±,>|&4O(齿1kPFKL,cÞÜh)0'?LAeOeDV)#{6rá}eۋ `8kc?"jׄh޼CR`۳HfT3ֲpi;i$,ka;0Ȗ/_[{$~_=39s&3_mKaeV{>\og3/pbnuEh,~17;Fۋ/+$VjuxỄ5h.D``C!vng{ P1[aDd;S _6G7ؿv >nltL֊o徏$>08lł 10e+HHpYjԬz[}ƌD*Mx^}'v#',A)%2,2zjv\>dX]k֠l:|fT-`l,8R4U?=i ~Y{5WNs)[@$>ـrzv qn>]~0RTq4mآh[>ʌ=XXfbv2Z2r/xO=bkzt"a'#MǮgŎ9>uhMP8ΟCpT.)EϋSI{RH:޽P6[J bt5.;۠| #q-Kw귅p,.i.Y[-0G6l  38A%X?}Kinr7>md7&Zx͜fo%]{ Sss=RE_ºM\NL†={+–$V-&xޅ utϋrʲwǹ<>M!.s!iáݓ{p)\uHX'P㞘37p1)KV(Q86>}+C ?@ "Bc'_al%nz=I|`@Si[ OӪ(YˆwgFhhb[Mq{֠a~Xehm9{ 1.eCvFo-itsSz!g33QԊV;hud_ØmV*6k|`dZ ó/ѹn8!>!+a4h,cMNs.ş6^;tb4eC1$lDsvwg-SFdm j=|HKRl)_1멃ekvЇ<<(^$!=>v0:Xyԍ $66l:"eWeP˽T1XJ@4ԧRO@JEա]wd`Lgz#7BC$[ݍˎ@0ԥvhQ= _ߐ}a0WsuGySXի, %vdQo_:+F9ɔA;VK p==MFe@;l4q!yilfttM+yRQFyo~T=wJlFί|n,|8k1?x8!@!f?ݓq{S{ꉈHn:l lKה)5`03 鮸"zZ]0 AWrT@Q?8f! X]DԏEnyT1_AKtN%nu?#R84Nss_;/v77£2%%T+n|h=jVvѫOzT-?Ց).\#9|E'yur(Tñ3I1Rn#:O4"""rP3v)^UnYS)'ӱm ݍhFne^kו=TPSlW"""""90)0|?&qH8~+x,ɿƼWCA"Uc_)o'9C켱|umyQqqNeڮ;ޚy/ޒx.?ミf%n]}b}2ogO؄2IGCک-ZVq٫Ul=vߞr}2'9ƚsXuz:ʋ}WD$wD)0N{5Ljjs9ſ`yq)Z,Unɳ=P85ήzEvGBWi$D<[ձἿ}|ZE!ɿ<~])2y];~m9u~0&*HqyN,nr<ѥu|۬*֖"2gNBmuA|eww5r,6mѵEҺ_ˮv^o\sB#p龜)T4'Oz֟ k/`[>dz%_U ]H5Qs~vVjCOមBA҉i3`?ͱ_3s>]# -n_;v-›<$k??od93%m@HLэo1.Ӭ3~?sHa"kKS;,#8#i%\ZXhFN__1c#xmiMO|<7R??I]ï(7xn&N]Wuu(uHdO9{{N[ HW>{uwq_Ǻ?b78n &jy|.ǸXww6yy s~ꝶ/qgϓl:.mr=2cn8M!4}s^#c3ksnœwǒGH&N` =֌U-rrŸt>&ɉǜT:xq'j4d[s [z8{֟ȹCccߌy|%Z}>Q~ zTQlM;v`#vmێF:8j\ǃX $ndHx:%!p~Dmȩ~_g73ɼ1}RBu*?y5fӷ2pdoApv?KuP`5>>E"K](^Y|:U&VY=/b 4(=4eM<̜=_neluw܍mC#7yy w~1zD>=?<;V`~YӴVi]q݌n^.ǫ;+CDυ}";'8ǜ[n/Wy[/wӇzy,yR'5@L ;R~DF׹1`bwfG~|~&S(͘1`p!M*3voz~_8nCGgI _=< [v; RZS mcnnlir;k'zst;r5r7qWR6=ޟwuF17$Q~ a9}É:ԉV_퀾Q$fϛSފ1<'<<1NpF N!ALL _0U;i9J|vwl<`yya>ݹV)̗ؗPj>en28 bmTJ1)Bq۾0FUqK2+P4G8ӬŋÉe f;0C(xW=FIr4rmA1"/Гu Pź܍nC1m/O+뺺]K>dMZ];LRM$"@Z,?.{#J 0aϓ@H(˦wy޽XW|ms {4G316- S8,r}¸|6{ϹPaK<~nm=<߆c ;'l]OZVUR8aew<`6)AA|"==ƀu0բ=ł.&"""""rP# `""""""yDLDDDDD$(l;ů L`gV"""""Rp)e9v+ێ_r1qΟǪ\M#"""""7 @H`ɳ!O{J"""""rݻAX0w+ʷ+kF`[HKhTX&CnǤHHvcԨY?رh,c?ؔ S1)~ Kebs+˷űngXM҄1mV3$-C&Ai,gfx iC ok>]j17 ͞jyjpys]ɩk""""70 Պ5}F<1i'>Ûo>G21~s;dsRX &>ɰLj Ç j~\L83· 6㯼[.sկ*՞^y5х L-ΨQC|lϻ},y/vM8ۺp5s9XN^pUy[װ~_A_'Ǎ}ڶy<Ą}+_M1 GpOofL[Ca\Vj.>FGeR?(ҧ/fSye0՞=~QvdѸNeo?[L3MQ$jzb~/ﴹ'+y^) ",2A_k@ɬ_doyԏҩ""""y`eLu#.v%i 7v"c'iFe~K1TevDvzŝc_t@NwѴb }YX =9l[)WFqgԬ QSDDDັX`iDGgnǎ7! X.10ĭO{+Gs>OޙNX4oGn Y{'=L< %djnxDfO]ű[ Z;GwLt:;3?+wsi铟Eܖqa<7u.X+VD:{'t]h>6}f+7:͊o0Ühlp{r`l|^1=mrDe{լw*<(S{^"M/{)S#굥g}xbRv?],xkƪadb\x:}{Ďc `IN&c6؝N)l7hyE9{ Nce/Ғ ~!%s~ޗ/PÔXК'Gev7_BBvNɯ2oa=ݫǼڮ8%|}xW!N:џJ[8]NhgQ 8zQK#xr8()9MךWUb~GinoFne^k/_Yn.x|kӍ5KbJ:p:A~[ v*V()6HJn|RMZҵY؜oIݶwUY|ij׼G9QI+(ѽg1cwmB._gӻujܩq7 FVOJh.Ջn,zU+ATT)P(&k(yh%fU}iPgc\Ξ-: S,"""rPsHT01>Z F SDzUnkG,fwMo5?!C&1~dޚwCCis$#C2n$ޞ)$[zs{[0}:!S]WRŰ](-EuxdA>6#6mv?>CZ=l@+zm4I/}ipeSvՇ]w|^3?'6?%s~`KH^Y.] .e-Ey4T@F5+t=wӼҍ}ܘnݮ&1P{Rjui3|HO0.˭~ O>5V{_<쪶;R~DF׹1`bwSz+x<8xz )]n7\WL >pmד LS}Q7 /R AAd817!wfHv<>{ =5s|OL ,[oϧ..ؘ7 hA1"/Гu PRi[ȃ0tZ0_c_BY:zܕU i,{ʴ@aM.Eȗ?Lc~NDZ_%kºev7ڨT1 8bc(R$:Ƥuu8Z!0+KDDD&bea9>p9o B >"B)V.M˧?؇>cSx4cYIBJٴ/ws~=ߕܔمW_:<>"lz7'tşf_w1I*Mi]#2Jj|&Mtt ;8]b>`0d|Sګ9}:ɣXll,#igwͬgOś7IlT2+]\{'""""7uQ^=1C@2`>fl{!w~SU;k7Y֭[79sf)""""-0)0>%= *LL4|Iq*2 ނ] L&f9˸͛vEDDDDr0 {R" k[,cfa#~j8C_Lp?x0SXnƺ{u~)ݱ a;s<#""""r]* B1z)`ٵR,]듯 cX#r~FDDDDTP0m7OwDDDDD[DSdn A%."""""כ BLDDDDD$(0<&"""""GDDDDDDHQ# `""""""yDLDDDDD$(0<&"""""GDDDDDDHQ# `r}p+c䘍kg[~W$%em("""\NWcM\eLxU^X6&mO{bAU^;axO 2""""ruwOgذ`>"ES:a4"̝] S{tc>-y{k4:ng3kAgX:f-F*}MWMOo]HbLHC!45,a~K엏 vUgyRl;| &oz=h.D``CPҾ3g}8@JnϓO?}€[3| h+2f}uo<ۑ/㛭0kyO,݅n ",C~,>T9q/4ge7k}Hm =oϛiwpS) nM)c *E{gddz񌙻'/X&o(R?|,}O2T*UH3OӉ߲ai.q+4[fo[3wܦOzQDPVoNiS>{,vޛn`9Yd V a IDATmRm3y66 ;-+"""R,_掗>e66g&pzo>&>p,@@! F}l$ߘΐb xDC(>s`X–#\#^-u-6P o,e>aM2>~i2+vAD7q ΰiAթɣu0p~-F!wޜAiҬ6F,קiڄbMJԭK8`۲M)55:-u>c?oD&Jwtއ' 9 BBZRRTl(H( a{\+*]^\۵P*(D@:iA@|k&g|3s.Ö#se χ≠o~bMlɎLͶ jspNf̈́D˫t3\ʱq#__F~xm~IzTE}_[tĺg&l{vxd=0_#xd%;$8Nw$Oozy8>Z6WsN:j 6k%<և_, hUsps*~_bR%n߮UiUuٶS$3NcKD> 3~/\Kr_]A6 @ơwMT!XU끢k,It,KNv6y>-<Ͻb}L&Fe]H՛zWWveKY=|F֜-ͣ\f?{#ߎԞpmRg·IJ̙=Zna[ #q}WA&n^/ak{JP<p?y)<[x~b9Ps_d,&#4oP _伙[syЅ aN.P%O-O8Pә4>fbh]{ד>WYYz5A ,%m͸%ːR{͛iV~)ޱ{1rKҨYsz}^Hɜܣ l@@>& / 8>VX8t`mva{rG!(VG5R>sͷ~i*\s}[D \J֩Ob1cT&o5MO㧷c@:I1r7.? g]#]wc.ө+ڮ/(GLV=}l^Ogӈ'i`Pꤎ8 7o,/,ʁ@K5Y(Wf)It8Xԩ[w *;72HJrMùλ/+OY}J2yGr#/+ @PcD"MysDf"uyW;wWWTlƙa֧B'q\O=ރ/%&w_#w-fhdiJmeq >yiE,hol~,W_*QD6+(Nbbt@Æ`*fgk,_.>}Jr,oۥSTKY;ްx(]M3gn2NC;K%Ie$IUN&92KQ91B\L8 /oOZTKC>6Lz|=v[5nMBjgz|A]K2s$({};p\8"s3ٜ~ p\T.E0s-r)X㏯Es yg;&_fH6S:W g[8SrC:4^Xuz/MR8,凌 ҐsCpN Ȝ!ibmhR5e>s W!% IMnԋ’895Y7 a>TI~g ep /" b@d2P^x8;;{Njj첋}<p}&foPȬ5ddVsD/W%K?hLbkyo@>mm1׼ zұQ9xul` OuOl4n VD ?{Sycz fsxK{ho\5?1<}os*uA!Y.j,ˊWTL$iii%@?` -X B-KaiCAODz9z9$ЋCty}5>}oo$;8IԪ7p.̰xypVJRBcZ%/idꐷxi~վK\TՆ[×Rν3&3FĴo}!`|eDRҫig$IG#xL3`EWf1% ӡuJ\vAZ$E΀IQ?JuE LC.aQ!It%I$)D `$I"0I$I $I$L$IBķ Heggƍ+r"""ҥKSjUIT `*2|wTT$@Q$٬Y9sРAKtD *UDBBHHHRJ,Yˑ$IE"qFʕ+WeH!S\96lPeH"dSsKG@ ೎$ `$I"0I$I $I$L$IB&I$I!b:'`\.|>nNi?K$Pe xe>a>}ɟ݊m{yYy~\ܥ=z_؋}UW.}f&$IG/s}E[,|m;?η!g>3Q̢q1pjgC;m@_>Z WJcJvk$I:Z+$L9#r''?K{r8$I_/elɋRv\r՜\NI{Q=9\'Q8?ݟ^ɚM9D=&K_h;b}?O_0^h7Қ?Ժ/cl LrhȂmu9U ~t~<߾We5 na pmW=Oripnb~XLt/.YZ>}|ȖH:oY@?cފ̈́J?pocv9Ͻ?_-va+IofUfhW}ˤ0$}}$I:t!}R߼io#W/baS) 廧r[~Nf{ '\sW7ʕKx<[5noo/x{*@1@ _މo|~ε3[227>S5WgR#aNG1&z=oe ^KLB817g6NvL7\X53Xԛ7$,s >ˀ[J?|21> r?Kjc"'afH.d|wv\" g!XJ$I`QDRNhZHEZ_r-Ͽ%gՊȬϘϤOТ,; #F N,XW -5U6䊿^淾ESǹzwdypv_7޹}cD%OcDlM.gi|9 ڜw>pΗ==Fq%;?9/]@RQ`Ue=yɜs߉ 8!-RkM8|}|A?em~)ߒFu@?==^j׌aÙ13m"Y;}:+K"btӊ,bƌ،O@$i;^uiZfeb産uS>|5|Ȭ5l 33|% YÚ]8eKm@?~1b z\MsFNcUiժʾ\\I=20W%`ۦM_ 7@ɒӮ@xx8DEm+`N9䒗';:9܎7ڔ.:FFII4ySӶ1?#' ᏘَO&kޜ瓿s i۟ Gn.Q.lڻ>e5z ?^ZӺUMF% l!mz2AT>z5g}„ܞ%Ia.Xgʹ_3MKNfsKFTQ&=~U¹ϭK+ϢIJ5׮Gbl1, wޱJ5EfmTZZ})"{1bڜʼn_{gpljT czb0|rmjGczzۂ>s=k{M]h[a޾>R];jM&|sW,M˖=w$|ng$I3`:vz>Ƀ"Rl-3WV[RөS&Eȸ%U޿?^=kktjR9sY5>I/G©(]l-?tg`"k0QZ Us~fsvZD.0k_rfÊfqfՀAe)WV3hQeןw7UhN3Q >oj -=.L1wpO'gԯ@`pmk=jk͓^JXҔ؄Iڍ{'F?E6Q2ɤT1}MkGbIvJ4وSW^z_{g{ U[_gS :O--jcs\$yvKA^Vj_‰[Ҷf9]2Ň1GchC'rN}|_#*TJ9ミ}ڻ}"gvLGWs5 >jǩubAnΪ߃J$ep /" b@d2P^x8;;{Njj(==-Z1س._%^Kt4JKK,3l genz.Wj];= k=zا~cǎ=ȕH$I{gaꫯ.$I}fam۶QsAE$I_C/I$I!r̀M~O|Mr !6"E8!!H$I_t VVOueS[XlOý߬ѱ7(+$It:ԫ[wzf4/a*hTe#_/$>ýCps_ [sAtI)7ubK?]ogXx{]FrG0ldLXJz0O/դz1"cc$jE}ᴤhO$ItH;:9lZ9I%R;Vϧb/nS91vn˓<6N& |w.s7CR)`fgºyܒp̙3`i n)q=naVX4?N1L=w#aToEODDCvv6E]$I*BGg00c9=_ɩlYSxi}$wDЬi"ۮŘQ_ХOS=os8U$kg`URD̘ZR5s35jJo.$5aP苁~KrMrYRԽu%_]8^=we͚5$$$u)RH^آ.C$35;ٲ'}o,KĠg=d ix{ ,F |uy㼚>`L{»/:餯H*CbGz47dᩝܥn%E,ai?d@A%nնzYB-!!r9#Vvv6WfʕԪUˑ$IE `%ʓ}Z-5M`C\{\ $orjYg&?$]Q_?'̰߬4Uj-Itzx4M͉y䚉Խ1_bDRnΥuvxqjԨUX|9yyyE]tPDDDKՉ)r$IR::خrʃ?ZR_a%},Y)B"uIT>O )<_LfSPFڙީi0qϿIdmT>EmE^^`K0"""%Il2;YlZʻ+q鍀_vOt.^kǀ8-~|$//O[Ўv)#yqGTp!x"5 .]pъ1=Hj]Ì%W0ݎ7T 2k ܾ>TXXGYI$Iw t<70 "(w UkL.qv_8#>qwoꭸvho$?8&jڵ^ɴ,Ij?_)ȋ*MZmnl1˟pZR4CƇ#6v2Wn&T%Z^=X[ 7^ # 7|{"Nyfz?# o`/qCꭹoo,8:<9,e lɋb^l$ItX8خ+syq;y'w,Ŧe IDAT1wR,]wd  zSo )q=naVqwnӛzX_F͚Ӱv峁S+ZC/N7GI$)dM slq~a@lcF}A(Z<1-SS&4?&Qit%H$I: `䓿cu"v yjڑ?a=Yxj':w@;Nr2tۇ,`AV<o_%ѨaoN#L51j]̒ǝB'ڣ/+#ZaN`Exx8D]%I$7z1-8Mi#yyy z٭5gtӨbD.:J$I:E,r)nyW8&yNH_ODz9z9$ЋC`b#Wl"/4k;Qe7#F_sK2.:6VWNA0{ȎHKҮ^I/G0| q[qtIy2į|;_L\ItBKj:)I$vQ.[xQ" CtRSSg#2F[`̕)}tZhqǑ$I@0rvYyZ~ܥPxg$I$)T `$I"G3`D.u$I2g$I$)D `$I"0I$I $I$L$IB&I$I!b$I1I$IR$I$)D `$I"0I$I $I$L$IB&I$I!b$I1I$IR$I$)D `$I"0I$I $I$L$IB&I$I!b$I1I$IR$I$)D `$I"0I$I $I$L$IB&I$I!R 8}6;C>YMfDQl[sSU$I_cۍMk~!3]ݜb9[Xt&?N0p4)^J$I:9'4Wqbpg'JzN6Ewv{ڟݍ>\}&1.Dgӽ@Z /.噹n}>潁8(*!cT/NI%I$,}^Qy&>e;rpO6w;7<7so. xmL\O }m6h r֣9f\2w9KIMvѝ$I[$KNvYY[Xd&ymVF5{ߺHNeɚK#$€fMve/ƌ.w74d /m6h֊ej&>3gД,I$!~OÙԵ04ӡܟd ix{ ,F :>Kb[XWן_V4铿d YʪKjc8$I΀q2D(E\|F= #,:m Ɏ!l|,:Tbh}Fk= 7fTV8f$I$1gdeIƱ ۗBJ:fL_Bpg fZO__Oҫ\6 huL3u2jiKqn$I@nekd} xu2ӦMf.5Q%%rmW4{$O~TtT9*E{>$I(NŃx,{mw=7~͵CrAbK&?^p <+}+×ώ$=k:kX_='H$I:vR.[xQ" CtRSSg#C؝!&3"c(S6ڭs9[i '&X$a!V$I`BfJ9rvL&~/`iR|O{V⌻P+I$-4Wqbpg'JzN6Ewv{ڟݍ>\,ctS@>_=ڕSzbYî͜r`n]о\;;D:Ѿ}ss:1\$s$It3`^Qy&>e{qK@Dx 4iՂd$ʌsnvM"!N<ʖz+<9[GAL~7qB~\R 5눩Ifp3SpLfYBLU$I7dguKffeT#7KTZ6N!"S[,z3(9ә:Ԍޜ6;:-C'eм1v|䞋v~Y+{i8Xȟws#h|SC/%IÌ ``:9bZp7С~#ДM1xWl=T3T_gyvG|d-Y hp[<'Zqr! %[lÔYʪEp$IC Kzu K". ebR4O:eI ~M17{Ɠ{V_.ygLޱk~>) {&ojOʹq:J$I*0S'yVi}/|') /ǂ:=cJS%Ў $1cz+ڗǻNge8-f$IBLjt< 3F>ɔvth @xRuHE gEwxDnIdiz$,޾=.];dācuN:JM$I NNg uLW7<߾ac德7Q$jU1 Aex)@I\t}>&i2b$MgLI$Iӑ|Oycd۷Zccy1ʯ1>ԫqI>{.N"ȚK#}5Mdە3 ݆u/IfiX;S)J)3^ozڕ+q|47"$I#35g"JSy]êT2YXix/ `dӺp߰$5gYJjH0,<tuؿɥX!~ȎKܪmpAS$Iqd^nۍ@yF^ [߈D:=<ĉÄL$I: O|gӦ@X2M3y,DjD3|zs6mkn,a.2xKmY `$Iґ\DC?G) #,ODTˠ4.Ox{MuCy$i.n/{$I#]TN94{Ksi{  ~LMuLO`G{"Uq7su\Tֲu(#$ˆr} DHY0 { 2"zqȧDCK//v5V01<9{gw.6sz9s=yN׮Ӎwoվ7KtO$Z5 תA o0y}gu֞:pr-C8ӛײeoc_4jvlU;׫wkޯk-oXKܼ\}SOU.յԷQm^ |j'_u:u 0`>nպwco˳j]S՞tBRCҿ~jxJRZֶy0m߾==G;LX={tz͌5 #`722[NCF\fgX _zAln#`Y&m"{AF"s:u$Qm FCIhV&Wsa"u={۷1'LNN^aJW 7x Ӛ`$Fq9sFEO3=S9`8 J\544IlvzN$Zz%|LE`+<6ËQ0 Zjɒ%jj6j*$QVSZha,M.X[mQTۘ31w.CnmSs\w{ߴÖt +0H+J/o\̷}̖|3M,kBVVs\eo JfƗk:bGֶ(ѯ4ZZ IU]\u@*>$2./4 bli,Ut1ZC,1Y'])2+Ao.#`˙4ҥdKN ; @HV~&eUHk5B#`E̼ LE)v|`\ba03²!`HKG2#u.VM=Ax!,{+_ 33Vfp}IDAT_[hOϧMt1~_|| q7g!׵Vh o,=f|Ƭ~HH!{{+a׾߀垊`Qt$bv)v"|Q5+|!zae*G*4IJ",}/DCWDsa4K#^p\# o:bh$c:o.sM7tM?thFVzF{DFw,cY(#`vHV0ˌFPD̢1oaˌ, @Fڵ>SHL$W\eË㛝gDŽ2cyfS(sc&_dEXVl|.)>jDy7/E08op/)_QldŮpH|GaM r9'd P(B\yU$p#]-XgeFO(s~2S s/G_&t:2' = S3[!e~rJ!4D .[u&O 9ؖIENDB`liferea-1.13.7/doc/html/help_prefs_headlines_1.14.0.png000066400000000000000000001027411415350204600224020ustar00rootroot00000000000000PNG  IHDR`aSsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSa 29 Mai 2021 23:53:43 CEST IDATxyXT 0 垚ifjeͬl/=,sϥ6R+WrM3qGQAy@tfPuks,s<9 """"""""""""""""""""""""""*ݶ\lب'Bn bC \"""""r(@VRש&"""""RU ;EYOI+"""""E M%5Cp\Yy.eY\ I湔e]r)ᦰeMޥ3WXк vA Ye"""""ru+( cEYo.&ԸZk(h9 _u15]Ƌ {ߡWQA VP +}z Y^ we( ] \Ls>W!"""""W?^P*j +p55|:^X jDDDDDęPaaY*(] WaAQr5jBg¶apa^.6dR;ڎ=gYreȿ>q\ݰdܕ0 CA~%tY͂BX8 %t9{u6 DDDDDBuQP貒1q"),9r5|;,9ڦs5]Vr3}38l٥ր9 _BQ+9"N~GA/pŠ(5;4^Cum0WA7% 8 2μ'o\Y-SNan_y@djZE_sͣ&S%h4DDDDDdl6,V|8qϞ_ rp8rBV3 .74orAAOgeqlb`]T흈 ^^^||ɓHH CnMX`{y6c *jW͚ɓZ QDDDDDXVsr:}k*\sС 6K̯sYY j o]ϯɴ4xGl6o[-,uxQ׈j*EED IȸFzF/ Ys2gr/VbVV͗&6 Z(𕿧Y|?b 5Yrr|.r[""""""%&b!XE _Ev1b.C|6Rfr(_y\ `Ҟ~05?`4@o+޹x$e{- 9U(!̥0V0G{uT7`Y" ѧ|u#-ݛũ$\T{q^5/ԥ4Aw7D;4+dcps*z[QM岙q̏%E~X"MsBjDDDDD ͤ{.k2IEٌ SP*r{At0R,""Θ0q8z]sW)CϾ㧕?5ҳ^]_n)w[i9g9QW+V H 0w T&?1ZZO3{3m^7L4+dv3ew*ߙƒB 5bLMfcgYA̗~83!VU:MlV @,~0q4ۥ8Rr9KuQ/""%WVy:Ò@mwH+ ?_tP%Dp՜Nه Թ>:wWo\_&ADD|_{ F??ӿ.%słp'mn!Ά0峘ZSDgQœ%,,ݜf_m'""%!=OgS2d–PBBr:_n~ilM`+G3'ъ=6熇ѻy"RBx)l2C&h(*Wt*6̜q~wM@vNfT|h{WW¿Z]Ozu8=jGZ<^vM@6Ӓ]IInrRY}Wj db&"63t-%'4z,|i4l21;,jCu(YD45T 'NQ.'d/K@}H9A@H^>ζfׅwп~ rIqµqͨmaNOdVY*D41d켃[,>@NI@*Wx#O9~N T!KKD -t5b!@nl9fNG/ d*z[c%)n)e2u /jRseń-g~ЋwDbncC'JcS-OGƭSgMfѰLbd\;Ϲc{y/ׁ>FZiZn{O\ZO>7V}aݮuqqum3i1UcLܲ ȃ%?i 7*b`aۨI`C1+FK[DX 1`MuAe#)d{䙋_pipBDD֧C>]i;/XȆٜA4"#nE_A `1-bC=ODK(%~MIШ!*uטF& s5 ,њAF&ԅmՉsMT3 ni U{re8۵\Z+5\R8DDӲLX1k짥E%.8c^O-,~wksp?6K6fi2O&qzV,˒ =/Žl9?GFkl#gS `oyK[vDÎ2g0dW^D{.Xbϫ&"ibCO];Ib$Y{Ie+UJ?I8͞k} /ڎi\ؽDͷPLz/aƛ@o]نwl3,&?}b*98bI^$>?U~8+!hÅW(]R<%`\KD Yj6i+\[8+>.FGgT7wd̚v-[HZB=\zl_JdY{GҾwk%^GNn'请Iӏ3|Z{EDJ=&݇ i0׺|2;'2|ڜD3uqv\R허HI01^""?MKk_*f@ʚٹ~1 dـMM?/w#R"dݻ?I,e0Y󸙲.lۘJTCU}'xwe\nqf'lVR_a+ 0g)E\Y4fLn>3Lv&qFp"""""ruY4C2 с|y…?lgll `v-ߐ q%,u1D*rrLM EDDDDMN@+fk(63w*Sp(H2՗}=4 >Gs_[Cb6x%;cdj?Ӓc.8m,·71x H6:q07Qq07Qq07Qq=xJ6%"""""R*-ع]) (((((x{$$$x"""""rjڴP `Pz !!-[z"""""EvZO)5Aq07Qq07Qq07Qq07Qq07Qq07Qq02߰hFR=]w`S~1+zl""""r0"Xo:?If>|;{?Β6OD]lc6{St+˲WgOl5Tڍٗ;ZW]9u$ 4=#JhC{/!f|>~} ?<۞~Jh.r(I:k#^2R} O|{ U'X-zޠ!"""R:(I4V%68`W?q{ c|v O}l( ,TGGsԙ:|>a" VohN9"CJcyv(^-:2|xOr Ҳ VCۻHڜ$֓lr*Sf{R:#5:osmvݲ\(pt,5q;Јx\_RȕGyq6,u)z1\JA>vIxl9fBy/;^صg+;TJqv/o?oP2$ ؗL:YHݿ<[-g1! }k:CpdM*q`FVnǥ9}By3~gdEd'g%"""%fBV Gpo;m$Pn$w|>XX¢_087ԙ[wK2m>s=O߲@en~sb)_lbB|=GzIX-wRR~+tzy >W{Vsyv7qqqg}/pG/ jă?Y YQ]BDDDDdjxt./y[t2gJ*tyy,#W߷ ;BsI6zw)z8M hFALXkWp49`ر@.CQwhEW#H{yo@LVcŹ x;$Jb> ||hǷ%~VcԌ KnANxS>}f>3%l=) .l>@pQyp^*EQXyeiD̙ew 6U."""#eQAM&v$f>Tݚ{̍Q~.N?ʺ3$,6 L축MLIiVՠ9w“9}fԟ;5[=-\uYZ,~62  ˂Z)ƳބJ=¦=ɐ(Ƕ}l?獷~|"RpJBq0n_2y{%ԃ./vDYӜL3@hݺ俧@_nTsYs IVƽ]weL_S+d^";4n67NbO[\KQxlцMrvN9qk Q[06*2^1p@M0dLv3V+ON:ifj֧jzx4 kf6ՈiڄҏGhD,]W/OO罇oШy{=9kDMJҫI8A^ATDZk9jrt/S֐ͣqn{䶈e$|n33f,!!MebpK&\:HTT?஡ 8ce܆%;.xzOww!uoeҝl}|LEN=ʹ@˖-ݼUKK:-$ oOϔ0tDDDDk׮-r0LÁsf< Vosӌ.㪕P11xC 7<>?ɿ/$2}_6Xoxz>f …;d݌@~L 7n _""""r(£IgL;׾V㻀ڠVZ~5Mo"n@]wd鼶2ATܘQXSvI,R-O>ӇjstCZ;_iCЋ(:q3Lbb"'NRrEr刌RBLDčf36mjժDEEa2<]$)!fGe4hs-"" Ubb"UV%,,L_ȯp&0Vʞ={<])%DDĉxF!!!z""RJ(bQUd2^?9KLDDDDDMDDDDDDDLDDDDDMDDDDDDDLDDDDDMT[9I^<åH/&1.#lY59\qrLKtDDD./ `R;LYib-erpo}*)$,YO;NV;m*6,"$y/}3|\=]=}U[rڏ UQqstB##XJy<ÿ i]Xxepv\w^~7+ֳP:^1}u_/s-""reS\fUś ]l*CYVFѵ)\nr+o<ɼ>F47t_?"""W60]YM/=]Dwg”M0уp^f?`X}s$(GdniccIؕ9 6^s79w݋?jc_9M7pS`o3{>F:Ayc^ ll>])6*Ԍdž;66NwU}+yCnI̸y1@;y+'4A^[wv_rd6IxCws˺񌝿,֠󓸯m=š'OٝjrL?Dh? O`쬕l;l/T~V'3 $ =%zUƲ=/~3A{;k-o}?۽aM\E%Fƾu|ُXnŝ>y;y M-yᾦx'NѹIqV9%h 6 ]\*8֌\["""&S54UGeg O]}^`ƍ/^[_,?1c1p >Pc`/P|xS5|Pp IDATxGcYzG:֕[e#7Lv ^ݳYt|p$ C& }-6}!ӏ⾇yA(c?3yl9l-- ~!elI/AŌl 8ۏ}Y>'iqׯ-aO٥A?**3cHoŌFVJȩ*ȵMl [ ,MplVZc[H߼=9_76+3W8٢#oԙ{Nu|wcx.|9r:;GmDi3pTrTV:] 91K>a̠ɘ5Us|trm{0`UVYӧG"'s*Q;ygM-Zwέ=дjR[/0P' h@F@C*[y5hw{'41#Y֪pyߊDGW1m*hJCN7us5+d9?QwFckɩ]djzSa({ fo]1b_lyjݙz*S#0SnôzO'Y,<>8f-KZѨ p5~MS0fӽ6l lY6l`;˿lܘF)礫Vu^E?%.~D5fǷeXs˳kkk11b< [o=oڷ~ԩS=zt+)xO2}lZ=,SfdhF{6j4HߏEmj*p8Ow'% 8Ǐ0OvgӶDC4M3s6'pSi2.Kʑus:{9%%oX]]s`Gf05)$|lݓoiB2s\}ѹ'zB\͠ AV}Ooֶsm |?\Pg=FҺu5`+绤Xk, =3~͂ `4:,^ƠǪOڜ7Irs}jZne^صZеVصEeҤI1$ ``ܹ,]'qGODD$xo%t4mr'mt#uv-eb܃/B'n>>>ig+?LL`ɱ8ۀL&LX)` [N>xrF«(߭glr?ƨGb)g;Gx\Vt1j+W$=ߚGOym|=4w﬑V(mZb n.O$;;02|Ẓ|Ӫ[/zB2# a.,TR}-5ck/J\דٽ \g4_(Z-Z+rAXX'Ndȑ[ڵ)S\"""sMc_MQxe(;>g^ZʡyBzzZ.:XlHHBl3X"!$hQ\c<Xwne{N]z FԈGD+Kx\«St oʶ ժS'{2Ikɏf,޻,/eVЁI+ijֆErjKr&ڷwRX绤Ь0ޞ3A讅<۷7>5tlWfpou_󝯌|cݫ}u>1Y~׆|{EGSՂ53~xRSSIMMe _""R,T&c:WfѨYm p@cǜ?J'ˋsb|_x# `3YT'I @:v<_ctycj\ cF55"q"oe;c&zCk`KkR8e|GX>AZx\∿+!y[?ԯANu!VEBBpFj(|֙:w Qsk V p3fL)x٧<'bф>iƻlEc;c;xu<ћ|Q]"?~La`J~.>a1>ө\-C' ]zio2i4Rl0!^G4/1.+؊4],>Κ¨%'0{T!jUz\0 e'0񳉼43 jڊZ!tzrx*'mK70ǪI,ӎ݅;oе{SLN5ϼՁNuOtqU j]O>Vvތw_3BՂ-uD/""|n33f,!!M=߲eKOC=~+4k6<])k^埭:輋ڵk%L&p`#`,vnb^5`1N3gJR sV=) C٪6:Ǝ; QMl6LRRktqDD͊b9XI;Gf'??|Oap"}":cW>t$CL=gQ,{zY?ko٩xE/[BNU .#"/ 5kra8w\Q Fx8""RJ\l͛s?ʹ+5wwDa7 sfϵYgfC?<ӴEyZ'r!7ĺӖk;[覴jsn[sH-hkbP{Fb`<]$)!///L&F\Wwk؟;woE"?Yi0DѨa0^R3$]p_7;cMiBSblw['Sny3mj K˔`X@Uj\x͆ & &@nx|"9_yEY4K/.d.oQ*?"""""U8Cq6nAD+Z3ucg[]Co ؉zãIgL#"B ;~5o~[s2_^1#>=/!CȆRm?>"""""R2-Of·xњ!lؿk[oD6>d鼶2ATܘQg" ̔?dYqn ̚_}GNb-Gmy>TsÑ.'Cqh7x ހϙWәqfy\Q @ӦMZlb\f38qB_(WtED֮][,a235x`v-ߐ.U&"FfM6QjU +lѣlٲ \N8DD*11U/W8DXXUVeϞ=. `""nt BBB<] qRSS=] )%DDb*c2t&"""""& `""""""n&"""""& `""""""n&"""""& `"N%jsl r8N]邈tDDDML Yy;u7VqWʶիt(ϙw5ÿzMk1t8o >]q|ҹr]׺S[vŵ]?F?giM}71{4b/v= םt2=Lbpm$n뿛2FFY)C|U>6MptLm3qc.\3V>o//!n C^cZ(Y8|{7q"mx@혋IύSAc .ђ;Om?_ |{0at}~Y_J9dNZ_OĔ۱BZJԮoNv&ڿُ>aJw/9A]J+׺@L<\$^{-Cyv⬍y{C>M:WE w>gOB 7ŏ?'SM4ˌ~q=m<ou'.~:-_ۛeOqC9_ˣǎA.Ub܏YI5qd;ɖţt{W:v;_5GF:uM?- :oYky.=>lg]dǎsζG/\ֳ ,wCm5vNiEDD. `RsDfβ<ͳKӸn֣t4blͽ/1sLfΜK+\?:ǻ`}kqgHK'4kPR~Yz&c9KJ87uɇ}4rm–3'nbS:u/66oނ~Sk{ 䱞eybM8KRh f%OmD11cwV& }O}aӔa\p:wz~2OeZXڰ9uS?8֟!#3Omw}Ⱥu p8gvJ^ 5kN_f  ( UCLJ%Ct |I ~{hWЩ1{7[Ƨ|DGG^-i׼!5}#odqv;.kq\uyA/u?$V7rBѯiso6l lY6[/7QysʝlRS3)Lpg|G7ݟg]^. 8WCTw?*T'3VcZsF݉V-e@_/>׊g,gSq^=Rn%v;LUGx ~ysn3ĤmlK ~hǫ)svv#F[o_^0o7+-qzYӞ=vޛ6,P>)gqd^_G4)n{f~G[H@|)+Ζ)lU;|~3uXk2>Y0֭[qi'!R $<=[lX4ɓ@$ng{V0mDgMF\=MC{y󿈃{5a:0iHk-ܒ1௡ yz5w}'W+sVˤI1bIII 4ܹsYt)'N$""D-""W0)lv35Qt1uϟϿbm~ &cz$r|9z+7>jDzͷ^~O)771.!)tiӺ?gvC4-wwd~#nֆT~ۉ_ h{7')yfVؾfgMK\y jIlqOs |Zw՛oG`,ia t0&Nȑ#y뭷صkSL!8ȕDM䳉p=7 jթΎ=T$n p~ [wne{N]z FԈGD!@Ҟ=!7ޢ@` 9^Nh_f#K_APĕ)`ZLZOsW6-UX6^ʗ6־=J'=lV~nF>CF^%8g͆?S% *Xov&_'I}~7]lӿ5]eXpVpJ蜕kx3~xRSSIMMe _""R,T&ƍ1ml ?%'XŐOg}tmXSf2ӫѫ[]ƙ,;:U1*7eS=L؊x'lj+~dm0ZV 66dQDf$-^qߖ%^@V4}_hׅs>wSL{> Ԟ6p8X)NHCl*7ܒHo,id~x 6f=hRv}!2ոc@X| Os?=㟽}OYkyuf k TV*3ɍ& '%{[9_Eq3G-5zc̘1nٖ\=<)v`]`t^V"<>|.FSDQíX,.///Qz亂X:KGXgNط_;ɣ-DDD?Iv:ؑ,~] !***eH 0!m0|6%9vkI;@Z~6gY ν6"2uS< Vg$Naas^-gt|r8cz%GYq3[)SڇԂ_c6K>γcb-嵏sYcI$I:X~!;+wY'cpO`!  zS?b5_B:^}&Яy$=sf~\y1;(U"dLU;nIe|=~E>;@kdm^6,e[ŸV$IR@lw{y㤝m_0r2N 32ջmS/ǧszSDhN]+Òϻٜм&d~ovyY,W˰o}z"͛$iV~5)~y%iԬ9'Չ%I$±N!4m#j(ز,fKIk|r!vv 5ߎud"redtdvʕoBPӵ ϣMgW$I0~+Y> R?NINe(W}e"a[<{wސL_xo@ڵQv6rg \rrPcX"MxkDn"{V?`I$番Q-9u#1Sh$Gδoװ>FvN6g>@;ErSR9eX/n8H?yJI$I,-WtJ?+ +% QI5:|L(]l+3 _-=&s{n(yIP:3OJƕSxsz>5#*g-3fK_I$I0{<1y͗xLcʓت'S!*c ώ}DVXqb⒨]i0X(I]٘xyx6BJPBcZ'/&ݻ1e<64#ƈ ?17r3X-DCK$IG - b@x2`qVV֐),==2FԅfW&Ɖ'Sˑ$IZZZ)))ODDD_`d)Xr B-n7"9Q$I$L$I&I$Ib$I1I$IR$I$)@ `$I G| '?sk8n `!I$V!e ]T,2]$ItL:Xxd/E鎄F3YNh_r_I$+$d,^B^0 r]ұX8]dv5{Q\VBpxϖc+XbM*Ds}֕5zjsOұJHj$I D)"NN vDž?ف*]+>\ f ~hFvOܷeڭ+M+*m̏+a Pյ{.L&v:V9R$IvsT0NsM6 ۡfq`ӿ˳|W^# @ V=%7/E?ߺOբ㩆/I#LL d/d"H=2Maږd_Ob,Bа^cxw 7R.uU!r3l拁]hJEVW+ʇygxoϬٰ-!X=,Χ@y{إ)Jy8Mp2V:+2S"w=?-}ua~{|b+!lZ67 I$*0B4l¹s͛7i[v ZALg޼mؐ2_ õb-͈`lep$~]ܔHY7ry=6o?O*MU.{r,cǎelTJ5~&8|`<|:VG d;G|<nz sPx ' )b]WC4E㢿_LEk|kj_uY~p_H$0ЪКVՇ1DžLTSj@xSi!LM{ Q+jP#[vt!n9'c'&SPNK( soΤj$ܭSxe?-k[Vyc3va{cSؾ*Vm@b2c&g Mόw?d@M:$I?{tUU{J<7 ^ A(=/'ͤ=ӓ> {2=zs9ȣ8'_ם"ςX 8Yq`+=p1/ȹW¢y¹\yvYBJYUE:kzv^}I-̹\yl;q9q =z)%ICvVpN>㔝ZjUH}7ljc `KV'l@]?iئ5c ۾ Yᔯٚ#$gZU%X[AVnQꌦ/3%oՄFN]dDFGc{24MLqINiBRQ 1z#N۟˫呒$I:tX[hV 녗La餤.:iѢE8lYr)d-g­=xnV5znGC%IQiii%"""3, {eNz[j{]v{Cf3GS:62ֱa{.o%$Id J$4&iŏ\XqL#/IcL:d8!2$ItrI$I $I$L$I&I$Ib$IqDIG,,Y¦M v9:QtiTBTTTˑ$% `YYY|ǓDDDDKq,++k2g6l$HQQcɒ%Ǔ gҥ.Gt8Fz+Ow-MLlEj7n]:*?#٦MHJJ vnʕ+NJ+]$(qWv7Ք[>&ؿ(<Jڗ\Gtĉ~DIRw D׫sQ3_ajhTyߎ/ V7@qo goqc>q5pM\=y1w~߁#{7/UW'/`wqH8s/Uγcb-嵏sYw֔Q,l ڭ:'EO$I `ey&}29f9{үOgo$Jǒqwpvu?z$#xd4MN"Is'Ϝ9sɯ߃FA%-<ڼzCpw1F~$It;>GiQ#/EuAKTKe_Ȝ yw^DЬi"{d̨ا)Ccwٜ:u3fT)f`-̚6wYWF͚sR#',[ޠw*^>O~}'I$w|& 'C^[weԽ76?q,]84IIqv>b/yS>u>} ;$H_uI_HV |6vcљysJeYͺ:x׉%kH$I:V3UM lܛ?'V>BH߆rnY&1ufnlO/rNH[/ iG5q"]7z=KS,0o˝JӢy2s{gj;jv(R.WtaIfN|Jըe۩HbV.گ跞/}o. v!Y.[x˕f _~XU=Jt8>G6-'?'ͫ|!Kj}ҽs-w}5"! tNO>%;gCl׎Z/̄X+|jT/GTZf, $6؂\ѩ*}&O6@xZdTD9lK4V"- #3]# 7qv8%~ldW_}DVvIS$@["-7HJĞ@ڧ+]J+t 8{7Al\?7wLUsSk8/u^da9VRކƛXTݖ~wubŏa$xcܛ/kǔ'UOڧr|UB]}_r'C/ۛ﨓5ɨsC%Ųvt?ϓR<0ul\ fue3`HW~}@$I.T '6O@iɅ7LJb9z!M y ~)Ypl~YVn!To0SOsw;9:iP'ӲR~x6WADPG)lӗtR:􃌙3n6JSѹtjO{!5dEWMZ=Ln&lU7_՘OWg YzZ^`0՟C݉r%5}<ϼܟk;ZDŗ,x-wmH ޲ZЯh(Nf.F2__r_M{(ut-\z_Շ't }欆j㱮gϞՋ-[)S/3bĈW&I:^|9dge%3xY٘+GBye˪ܬ ~WaJ4)"&RZCzCН%ua8(6ΕЕȰ^R iڨpAL4 aw0mJsb-hWoeXiW2hQcFWoJ&u}R~oLfuy;?{Vv%5=і?ԝ=cK<#yHZJ^u4~Ǻnݻs|?#<$Iu怂ˆ֊^noG& =M1j<ҎZu;Эg { ȅSj3R<{9=W#Y:a׽m+#nyˊֳ~ Hr0g& שcy~'Z1Dn"}}W #r҉9GVҖ?<(uXm4k\8jcL:u6lvVk%$$q{1tP]$v$'`/2b;J)>BA; 5V= @]jժQF2 ŅeE((ը=*`޶mÁ1E_ GDTOs: ֟2ZMbI8:㗢]Zۯ?mKf6};Ŷ̴Fϵ G+v>],vN-*Rgҵ(nݟ%<~N?;EOTT=Xː$g `:…R -,C^{;7B\Ͷv̢G| ^e热qǽnD]dIF{0b+Xnj+Pr3*Ժ:c*WkHפ?EU:_yzs&ŠSɕbd/tٺ~-l}|q蕤zƔM{ĶhΝ/^Kj®k 8|)~ꎤI1Q#h%bQ$rcp - b@x2`qVV֐),==EPzz:-ZvnҎ˟^k8s')Iǡ}@2`=- =.Stی;6H3T.Mb?V'6It40)h` I[Y=&~:3)[j']Pͩ$I~4rJKhZ07Itj%I$)@ `$I 0I$I $I$LQ#,,`!&++`!I:Js( IDAT$5bbbXvmːvfbbb]$(atHHH`ʕ/)貲XbV"!!!H>LQ#::5kzjVXAnnnKq,,,jԨAtttˑ$% `Inn..IDZˆ 4 J$IEctT !***eH$)$I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$ZWI$IG !Ek3vVΞBmJ$IґX |1jl,A UӤ \|-+E{xRJ|$I$Qe_Hʓ ,`_>Oe]$Icq $ҰaC4sϦΝx汧i4!N+ !m0|6%9vkIQ@6K>γcb-嵏sYxmsy[\o>|> |{ gssiao goqc>q5pM\`!i0b$M5o2`z 6/bww:?~_/=.dO~z畤QT'㋁6<0uvdD*BdFNXH#$BfMޫ'cF}M{QiƴLIмU-zQiwGK}H$I: `䑷kubggnb>z;y>][\<Ԉm:me͜[/]84IIqv>hKͽ^MJyLK$I@0 E,jN8%9\Uw튗D:<:ĉ1fO^Mn !doW 瓿 44"#<]%I$][-8mTF -ND rѻ+ =]X'1!#R;i~XWRV "67dFbrsշ\; &NQ.YitI^$ծŇ3t 5ZstL}2 WfЄΕȔ$I{(d-P +ԊˈYYYCtRRRfcrF'-3Wa?[zz:-Z8$I}@2`=- =.׽L$I&I$IrvKrUː$I9&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I b.H | K i%RU$Iҟcۋk%#+O^ۜb[Yl&?E0piR<J$I:9/14_Ѯ5 WĿv}ܳ -ԿuO6/3s w 5ot=dGU0wmV0sp`9JX2'g\6$IX(A$ؑ lfY^|n}pч y,w74ċC1aiՇO?bAv>#W4MsjƄ/ Θ9KNIN>$ICK%?L23~L]VE6s6)Q-'s2#',jyi!@lՓ1mis͜r / 4kMbO0%m3;'f0r)Y$I:FLy9vxIx+}.`Af _$8͟2tC+|>vq;+՚Ӛ2o `2$Ic#`rU Q%JW21QNBH6Ȋ&dzW!mxjs*ҦY4;dH$IR9/%+Qn25&P( )̘]/a ON&2u8O|Kgѳ\]VlZELgʔl{ I$I ءՖkt} x+Mk+{"!ۥI=a̼Z ;bāc N=rp?$ICvHRA<ݫ9{ě!,1~\P 4B\Ro^Az#$w'geCTJꇑ$Itn= B-P+,# /gee 9III9] y6VtZhK$IVZZAg cSjyZmNqdblμAx/I$I숰-AD˗$$`J$I:l_Hʓ6XV-ɤO wמ}b%I$ռ &ѠQ3uwu`ߎ{vᒿN<`9{//ąs v;rFHˆN9/<.<8v&v+b+ D.HM= عL]2'g\6g$I$Bބ(A$ؑ lfY^'4xFªIp&36t#1`3!4 qPD>kΰWdD k tV]re2]Ot|85&Y@q3=g))P']#I$/0琝IfV//˪Ftn&%вq>Қf2-t8dOgthvK3"ikzeXyw>ׄ4ƎOb㊤)k֚Ş`JfvN a4$/%I `w?ԂyMiӴOʶ$b/;>'Xx^)-]Gfx)"A^F(ڊE,GIУTkNk:_}sR^0Yx7I$I:BN!׶ D)bP&&O.Eԓ 9dJ/fGVOι7q ɔ]G!{N16gه?ͩJšg,\I$Ia(Yu nJۨ|ǔ*6$x rҷG*cJS9Ў ըӗ`%Z_Dj|tNYNvk$IxFt(E0c0&Gy-MA|0c-b񢅬,$Ihow^jaZ1 `Q^x8++kNJJrl˛𮷱x8=EhⰟG$I::KDDDfY@˜ P+h^wȖEٖyo R̩?|I$I:< `Gsq*5:{j޵'I$ `Gj\$ v$I S$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$2x.D$IQVd?y2Wmv!$IRł]{]N^cxsBK$IX+JLmxD|#I$IpRiа!axc_>(3k6nfkV8ejB[UV3g_/WBz{9Of5Ǎhe7crɂ YpWm!T<-}{* oک>oqSM|;j0\Ͽ'ZAF cgX)jKhusJ$I c36̜ɺs5 ٺǏ;Hׇa,p;}GoU׾\Y k[X2nӮG~x)1~yKl[xyyذmXf9{үOgo$J1R ]n'd0m]Q毜S$Itlo3sn?R ~,^D7IӬjS&/Ci7#I9a!U;Kwn۬i"{d̨xo["6g}^I5kIu":QJTKe_#',[ޠw*^>O~_?$I `k=`aMC*WXXEh `dѦ!I4:)1gmY|6vcљyצ\٬|)wO.9ĮgsJ$I:$V5j=`{AyyBO KãioMcMuO2Ŋ;~N8%9\Uw<9%I$#%TZzfL_BܙwLrzfL_K9kœIm(R.WtaIfN|Q,^R5ghv*%&Xu뜒$IټY3g.S'y_;jCNI_YO |JӦfʖ_Գi^-;q1xOW?k+q"VNԨ^XAHl,T]d^&dqvrMlQmSULa]ImX̵,ɨ 9%I$ӱf[ZR7F8;)*9zfW׷MͲލ)5v .>q&њc藷'&&#Vn&74k]\pJ'xn|=b)۰)Otp{%y-$IT89%I$` cp - b@x2`qVV֐),==o"==-Z I$頥t =99@nWv&$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$I&I$Ib$I1I$IR$I$)@IOOv ǵ` I$S$I$DI$I $I$L$I&I$Ib$I1I$IR$I$)@ `$I 0I$I $I$L$IX $I,[-[r:aaa*Uʕ+r&I$Yvv6#>>ի쒎:YYY] Pn]Ã]^y $Id˖-#>>ןABB,_<L$I -[P\`qL(W7ovd$I,77בC$""∾&I$Ib$I1I$IR$I$)@ `$I 0I$I~/'{]qsw(A-!+c1kme%.f_g:q'!2"ᗬBb0*:u˜aBjaY)" w3r~%O<|}#9 錎yPoiyپɥ{mm=/jI#K5Jo {̳j8>Xe*LV:ޏWip~{'6=-}Vط^QMi [}^s_pYͿ^|CGZ)R2Bw?V?Vnm= z0 ץW[55Zh/o5c -Q-7ضYs+R^^s@,׋åP6m_o:Puϼoj<|^wև`@koVyr8Ku?UYC=JRUNqO)ݬwPaU*>9@#?7[/Ktx:"Ic4& Ua tk%jz䫚>ntUw2by>n7BZX7Nv%9Fjz}>j4@NY{S{0t9EJK5X jJ=;z.6_}Lmj>_OT]!Ze&إ_IoO^9^SJ([4sB}Z1ՆZgVJNS5k+/g0HVӒoІAaf}WŴ%ymX[ԴL[}ؼH$GFUWWg6rơC4y@eKJHlwvwJJiK[60 yf׮]NdҥK} }M5 #`@-[OV]$ /1'07&%8a2 /;'hٽy{v0bJ$پH$œ3S IDAT2N]uNA,GP\\lFNhkkSqqqӝ&2*MmU'Od$,D":uJeeea/=9 p*rZQQ*++֦%lRT\\QF(%Rr/?S}`#7ËQ0E4UIIɤiT$Q,S<W4jSY\#i3>HDپ_A"лM7{`ms,NS nT~qw200[|9}'+sZf|2MuVW#,W\ۧ'~.hn:bmw~IYͦ}DWV]>\D8hp/)X#wC _~od]‚ƍ݂m+5[ (Wk: 8A.tdϹ^F;2>GL) }&P T~ zd"(gxމ @8̮G,d .ho^ 4~➅_IENDB`liferea-1.13.7/doc/html/help_prefs_privacy_1.14.0.png000066400000000000000000001104251415350204600221210ustar00rootroot00000000000000PNG  IHDR`aSsBIT|dtEXtSoftwaregnome-screenshot>*tEXtCreation TimeSa 29 Mai 2021 23:53:31 CESTht% IDATxw|O&ݥQ(-eo, TLE"8EdodʐwJ[#M-6ҍ|߯׽s#{B!B!B!B!B!B!BWGtB!BG(ZI$^B!BPH$B!B<, 4!+(eJ&B!/yM5!˯dǞr jZ!B!֬Hpai+B!)I,MyKrck^s㳿L!B!QhVD,l%Y*YB!f-!pJ)צ$5J[%Z/iB!BVnZ̽'1Y&4J#kDZBf}!B!ģKIrd-ɲk%!?z$Tp)g<%q!B!=ZeoBf+&3&_J_j!0!B!%![ɖZRd9ښɗD\ҥ4Z!B!L)ReeLkVn0k eksB!BS0K!{y* S)ڠZI2f.K !B! '2d{mk֒0IY~\hOe鯒0K!B6%]2r y,%cٓ0J,~)Mf^g)3LK !B!e)e #1MTfYZ ɘ,-`/sI\"frD,/B!xtXK|&WYIW`0S$n$`2w١ppOdamْ !BakK H[@R{kȞtYjZf-F+#Tl!ˏpjjZ\J!BQF#zvŋ\c91`1Re1!de?_;0m2MІ )$5%Դ4z=FcZB!B1JZggg"%,k0zKlg浭/+Pa4ON`G!B!?]z:)+d\StY$V)M:;7OH/!B!D2$$$Ҭl2w,wh\'"$j .WnpbRR^bB!B|c4ILJ"((5%IX:Y*=4 +7JJ˗B!81 =gυ~D{0g}z&B!Bkx<ɗr fkrdW֠1׼B!1 *@CFbg{zW l\e~0P!B*(h1#LjM:2!%a3 Sf`Y͵ev03B!BOcW Mu8ޒZ̨CzBaI7TLRH޼i]Zvǩi{|~Uy+.EØkx2}{,%K֒(K76 !(y~D)pg3 v > վ4ʀNMmp*6_$"@x} ]V%?^z.fpDek^- Oaijq1ŒSK@Wԡҧ'^|I\STPΟCrxl$q-֟"Z;S ˹cL՟3l!6F}1=CpJ֥L1 W !DQS7RvQCKpn AHQֱn:fE(ɒ,NQR"}%-`2sblQ0!(4̓WKĒuMgb ԺnܔRߕТkkkA/)Hщ:m͛ޭ %R_)*<|ˉ^ ؃l~>5BX:M)gr|A:ZO!B! [TWTe 6W@TI>vƔu3={XIB_[L˦ſpjyǂl 79t)_@@ƧsY\e$ qo_KaOoZϊE+v1Z !C'?5+쯭u!tZ96/eq yt5߬HbT6?zii$߽Ʌؼf)ٺՎԀ>==CJ~qwoFݪ3’3zBLߔLʺe?Z޳ O+IBsg9kj?\lf٪( ;y7ti\~g߈#oD-'ۡRp] dl3@ צtX?^& GIT GO|e{2,WPZ6.Ò3vԣBc7tDw|*uOX$v Jy0KA_0.B+nzMZorbBXįIHwob.+ u¿k,\ܒsTpٺȼnm =1Vл$]<OT̝D'9Q"=.]R !Dgv%jeTH&ruOS$쁞w&i콜@smq&lIB࠶DJ䷯ڷ8tiw&]M:?\,B!EЩ+H'p!B!D!L!B! $`B!BQH$B!BB" B!BIB!H&B!ОWXB!BbQQ(!B!V-%B!BQH$B!BB" B!BIB!H&B!D0!B!($ !B!D!L!B! $`B!BQH$B!BB" B!BǢȢA!B_~Q`VMVڣ 22C!B޽CH.AB!BB" B!BIB!H&B!D0!B!($ !B!D!L!B! $`B!BQH$B!BB" B!BIB!H&B!D0!B!($ !B!D!LiG3KƢRr5 7SԍB!ģ-ʼħP5 (>Ov X1{Nœ~LSsam!,Xȯ{pv**lL:Z=(wk {z*WF!B(' Wwn$>Z75$]ܽg|3noeSLe:=Q I&KP\4?Ly9F=zC.B!vL}2g ,=Eٹz#W۴̌yw7os'YGP>[: 'daحëD~piQ?"F9:q~&o͸$Z/VoNϡ:)3.9)Еu>6w[~z>gL߅Q7| ߴteNίu@ ;뻣sS 1þeә.NQWOFF'p2-3ќY5~ZW|;uӊ!߱½TU8p?E3sǙ\7~S|BU}nC' B!Vbٿfu BS6M2}.m͟쏺B7w5nLrI:;q8xִ hpH4'|J㪏?ӕ\eo9KuUT4k\pg~ˁ8?vwwIUMwTMp,F1gfهÙ;){U& Jzx\޶ӗI Og=w`q}4i6!̡kxU&Qغ2F?l9 i{S$_B!FZ v^NwQ d.e3(@0T7ӥ-Ѵ췇G@%7+@a8LGB]yiǁ2ldHōC3kΆ$pKhFI8vy~"ҴK$KwൗeQ^|ּ-]^z+{ȼ.cP1p4"ۼ?/~MΏ.~`H["S`VDo`W0S/`L o.Me.u0:1+YB!%IȆ;2i1 /ں~HSz,7T޴іR6 L'OЪ$ΝH=žUYx,W=Ӟr.p t}!$<n΀sKZ՜ʱgرa:P-J[7 ͲfO}AoZ~e)8y̎hlٯ\*Pٞ :Z5J¡܎Np M oT+טfb$߼ؕ?Hn4ӂB!DAoZ|?p}GYWT-.^O=Pמ>O|1&ɩ.`/:~iJ)'3znUS-uVp85svGG4uÇV1>+!a-lзz O1ZWzM!<ьr @ܧ&omZNph:|(d~s9v͚E ruB!DAԨY7hv/͇pWC@& vV86{~ˌpSN`1Y Lgʒ휼ʣ4N`tk SU/b,x-&KiY 7npS4x[,!j877\NhI'11wwgHr| S/&C]ڵ _Q9 'ޙ޽8+RQ G 1${qSAZe\eCE g g$AYKQ[ !XǓv'ף&=XCm_ck}Xu-ӳB޶B!0O0s zT8Հ3G3O^zC$odxߑ2 P]ck|:Gg4<{$Ĝkk-xECEoSc:?[?W@y"Uß#D+}y¿}[ m]uZ#84KER87kG+Q@&_Uj@MM:u]?CJ|gwU@ 5tIlyNq&EۚI_#\h,1lf9'ti]ӸqrL,B!\gdή/iӺ5gwo+ _Ӳj4 ')Sgޒǚ\4.aXb 7|aan C\ډzg3us*&=4-jP)YΝVorL#!!/O-iz[rÍ&2xzi$-ghNnn8WBg/lm N֋B!fUצdp0M_mkӿu&Xdd$׷g| S57;u%8yqJb4Ku4zgƕ_l}|ՁiEBϔ### /ZO'wtg!~Hq_ @e՗Ҩo1{/B! ݻ%ZpM|M`6dyGi:T yFwxs*>8 nY=zk"DƺU?h}×]+:3u 9&\{gҸVW*bGr=1uv\뉪#rJ60 t׬ )"eطv)ke;ؾ#ד -*HūqKp;BqTʖ`v(J\;}Ώrm?q~lȹȽN[.7R󿯇ӳSZ=Ֆw- IDATFj߱Աg}&??;$_38Pv|" (6o%5WYJsrO1Qs#h[7mKϻҶU 7oб`n=m.A,4ysKl QFXZ1NH'a%TP Nҏŭ&?3?-j#73Znrmo5qʗ;t>Hh<nClŏ&Jnt"VDrt'$bq%q?[-CdATuu<1b22sk2XCӞA.2<[С>ӭ%}rx\]<;IN׊Cq| ƸCn.ҡŕXJX΀/~%jQqv y3y1HOecK֥* Z9ٿX?Lي;N0ʨ/}k}gHMrVtN%,9Eb.-s^?\¿eC~ wI~XO %JTv)kGz*@K8|}{RcQ̈x[48z&겨7xo=+ Ypb_̳Ocu+,#Sm(9lK2ra Ooҧ/ʨQv*[^0n1a\S/d׹*t>zڵ/fU֏:qzWXVϏ;KCo'Hl>n̨.oSR3dHb؟D*> #7FF{wfƬZ NQ=3_b$DŽ.#1<z*=HB)ĺ=_z{Ǽ2ߴl-+-wyF9x4ןByw8)ff9}ϋV 77HH$`1É64~~s-D),WWH'w;qw=fe :}WYTm2tFwZ=>ZF\|-Xws^!4|>m052U޵b۽a0^q3h4@*jjAш*kEUZ-Zgmkܦ˵-0 >E1)]7'jG3nI^vi϶&^h@zUsP@`5V2pƈNGo86J#eԈfd^6cTf>,KPEL6}JO{ͣT=U_jX`Z'֟By:V;u.S3JERpm$滖yIoX22rgDq#ᯜx^~.鎳;2ǐgtf7o@ T$rb2e2,>n%T2?{u{>Yy!63Ddz5:oK T N9Osݧ-?g#71燐0œcyϳ`.aac69'س?j(dBƬ (&#+Ufre@C gz%v!i+JPM̎ [lol;(9l Eݺu3+PA)nCbb_|,[(׮e昰S __FGlF(v }ܬwyrCBEwy? Xu 6e }&7)akv r!ŗbːnৡ<;g6ȚvQG/o RFuKFi4zk&@8Gy!7^-x`h^<[h'sۚd*;Zy_4Q^H%*˂y%cfХzo{/;e1\sn+Uƹ psXv%62?O͗=?`Y4>޳#ն}{{2IWطnkndh3??'-CM^eY#uJ⪿Ǧ[:TnNMzа/eRʄ %eFz8CϡG2ړ]R2b.(5t3uwR6=d^͉x1G1؛gjD{eyX^9XBVWD ~-yF^E55qg1-9DCZU1,=IZS ȹOZ^S|02>1c~gW u ʶ3g znykn z:߻) "_1b~6e Y}a谐2}-wx#eiQ[y$ IDY;h}~bQvjfYʉI0Rr#Mxu3>F qضr5ţw*A- Sf&,KA@P<B{EM1<31[LZ4Oe~R-3^qk |Kգipuy9Txy"S]0yxFJ2m])g}0Ǹmn{w*ԫ|6.nђQҦL"<vNi?7'|pu1i>.vbt\*Ne^תTNτ3􇃌_gwZ>`wO)ې1eTvO-}|Ni3j4/mr{Srٿa0c SG SxŐ4s٠y~ |i׻Ol\ˑlw;e~=[{qc*LmGS֛}ʼ/Y|6 Ny8ppŷ,s`"I*_w||bf~jx2,> -zC&v2 |7ʅҶm\,8W'*kAm28 &6ߺiii ,223G2g6Dh^҃[GsDB]&:J19*{D<SG擖Zx~Yn%›gRl`&0ŚW{3O-ZL|t[ƌelkݻ%ZpLe:Л mU&j]}?>""4K,)H_`I\O_Ԛd9s#*\0$94gc_IY9x:33=">JOLvZ'4S:2Q~ng5\܍AzCN0Ql4z F)n s/AX4zIGg:! DYhzMÖ+%`033TVpTלst[-GX7rZ7H.HDZ pgSI承Yi7UrKӹؐzQFͶU~0>W[^LQ)U+8ԃE#(#?.cw6/])}沯q|)OE!Ѿhף .,}sNR|;t*sae9: FNX|8z]9q/t=gf/4!>Ɠ|<9mKO1ӹ7\[_mAr8O̸zвKzcc$w8}G'[_3,Й^$sNRqD_ɦ\n^\gl9[PuDAo{)'bcڥ~nRuȹȽNɧþ(Qh62=?Rv`iǨ ʑܿf/+e;+_JE7G/i@oťl:6u2&s O㺡},X;u>F0-UQ$Jkz`qC3bNWw~v~+AA${3hRTJM7yk-NAo{)Ŝ%B/vVS5ɗrOq*śu34`fޓ\ut4eKg83+m&fiDO:^7#ⱿVo@NѴ#|F/rhE$G4;#&S)Vsk2XCӞ@<r)H<[B!ޣJhh_FPdN=zi44ěSUQ.>Ns(G 7֭ZA6Z܀^Aytuv-8$n]*gM`ŮSXh)ڔL8&-ʉ <Չm\b~~|6ry|z7OxÿCF<[2rY~?>džLvķ}g8m3OygAnp-Ov1W]kG;!ԟsCA``eGϱ0yԜo KL{p+.Ii뤴=*3h|ҏCoXq55%~pkfzӮNu'`=U1!65KIͅ@:vXH ~8DT^^xt=jQgJdݫ+wIڦ~Àe\?t5 /coS_aԨkGYq1<ۨ1uj6gq-GHH)h[F3y6U0> kl;5R8-2^݇ "%l\R|5B-ct@ ;^6_r h0{g{`v|"罗kڄ({Ngm7j*hLCj IDATa~N҄j*kH h1j94?}.u6*z̊;;8N,VM=x}N7 (b!  LޜqSRUBQ$3KMHϯ2⃻U 'S__yOovM\_؀j4)sr.uiu}'1s:3a܊4{Obf;ώkw=S JUi0vy]އ]}ue Fֿ\B.Fq{B'ZMz3~vtX73yxOND.i^{l:]@%6=K]M$0$*{MWs^Gځp)4ge[B:,sQ TN꼗Wc}͊}G16ʾȓԊӜ_w iZM%7@y] :(k8ˍ6iN'{Ө i缷x,)[1{(B&`&QyRQLJ){^&Q&a/b F׼MKL\h[Gp?{wEqű%ނ(xWmZiYjjfiڝGUii2J2ADv삸u77 NT;;Wŵ걹\h4b$d76_d w*7͌Ʉ쯑͵_&gK{i԰S㉊ $l`k\b:^F hfb?l6CRII۶Wf3f+Ւ*13 m ۢmt/HO 0KeC{q28~?,ߙ7 )OyC{]LٲM7pknz+~u:2ٌԓM ~ J=(T26j=0䪴ݖA\ [uw&= v=OLAw1cq lN۴i~-Wͩxr+!xa S~0w{T–~Ɩz'//y'\ƣjk]m.sR_y}>vƀpꝟ˰7~نmEDDn{ fa1ufߊlzSܵ)ݟๅ3I,ĕcRbToD%L|mb&!e||Fϟ`2U:PtŚӢzln#g)8e3ƷEy ; ge5_Hͭ)zk0ғp3Humӑ5{=CO1h0nBK8?ZOٿuzOO?eŏA65ԓ6K8+8eҙ|}/l!R{Z+&D>ssĘpi\JR-L}JͶlQ6McPמ-@K1_w=Ʌ'1Oxk~"m ۂ+>4}Ô\^_c3x5z'+ƣOcH*T=jѢzl1FVe;1L=^$ "vH"M֎F~4OS\{aM>n}^7edq?kmZU6gh"[K5EiNnG 5ԓ89BBѨż3" |7ggvޟω-gVʉ^9LY|д_KpFgaaDf[-ۢ NghL{1$))ibN &4Ty/ф۵OL tNq+TғVes흈3r%F@`2<&=OR t%p gUWnڇoDDFFfܹsT|C, ~nB%= ~oxgDDDDrLL 77 lJݾ d}888`003Y5i$K10)PF”ܵ=`""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b']5)MC8)T“נf8ns"""""FGXlO@lO6VfVOeGx-γ!Zr0 2,+w!9t}ʗOd3?/m35^4yQ]H۵γ[$Dmm} R)I㻭vAW8u$p \'dzȓnƜUZ=gxt'j]%ۯXRcٸdw^f2պ͖֗>EmX~` :D@<:z5Wo=ǿUOuW<3bӹ䭶پ8cF {s Úd&l}F1f})y}jD\n\;;>"yMLHE=8 ,fے]y}"mΦxᇹo.>5|YmʖlP>r+bރдⵟaΠDGݫ\}${բ-̘9ۻ pNbh8@.X6o:H_[귣Py{ `r.ãuŽgvѴLȜ G9{$E+7+iz}S;djVN`>/&1s&\0R2:tY;̗="Y`: ,OeU>sCXiLx+wB(i\JBS_/1g:G^4z lʗ9{ɄKr~/ Z1f܊WSxZnIδ_|2=~ɖҏ.}ի>M8~0ЦY.sHԘQIٴfb;ݎuymrLbµkH&tyq_;i[[Nmw)N]qVum/0{;O\ӗϏgdDφ.mg˸ա CQhg[S5VZq\ ,gϞxeo|GeGd9$oQ:SgfNLА7m.s2^ԗ&RguZ-md_">yTgܣ#/dp_,[G_΢˙2۲ز9Hey,9e֖lw_=T%ov=p:<W!V=uIQ7fab=t'˩9M *w5s3eW~5i{ ?ddbpR;s(E o3er<߼T7t -7>?OؓyFqϭd?B51$ װ 5 {K3DWxW?Ɣ~I(hCڰY䐱l3z?Bc3-붎}ͻw^bPH&sFom}2]9 JMm]TY2UR@v&ha+@v2S}U'$<4M'7VH'0$y{[zHPmI?6_ zH 0g~3¸|ŏ9j/ˑ аv?NӴժH~z+OfIY=0gYH&h»SzSʩY.g%+K2D8wp-'\B]7-Qa.Xum;Þoe~JS.ڳi99Yps\AGjgs߿ E6%¼s i3n4ZN{W`z~g3Rf= mZaY_: wߺ<]>4NԮìvrNŶr_mUiOߙ8{Zu#ѤrԼ03)ӚZٸl9/IpY LD(W~܈ᅩ_ l>C8p؟V4 7sl\: $JM.KSҽo}eMvo:~~@r 1k5[j4/$~~qN֑[Sz׌a 'kf/m}-]je9sk SZ::Vg9\ؖIdys(w50)RL),.^4?o#PijEe =Ʌ'1Oxk~"m ۂ+>4}Ô\ 3Ʌq"fωŘvz 8Yi/ˑ߬\#_;̈́E*HPk>v"$&yNb7ycV"24W_.=/1Mbc^e}8r_ 3RHsNNV2x>n}^7e-_2s~V/RAMyu7=|F-条Ix[9C?Kc7޳hٺoO6OER$-8UoRߖ>˄c Z6m1Ԛe?ffAmy޽<) F#>B=7v 1,7&^j(ݦeپΚ-2۲9@=7uu .GP`NdwtY~:2-\pjGф۵OL t添PIO[5(+_wZ̞S]L("""@TTThlS@Jnd-N~"ux(-]`ڵlk=0~kw*s7C7822sռo+糝)V:θXP?bK}oӝ:w8~xa&c-n2ezQ;/^(o{g|C, ~nB%= ~ox{>l^G9Oϩ 8{"fgJxED ?5_ڽf|iW~wq+Jie0nn4ؔ}`7rpp`0`4|?&Muyă}'So~qS? n`>CԬ)} /(Q!]e@ꥱ~ɰp "eն/DiY~)S!|pB63=^OkcQ\D|"DՐ}&:Isװe_Oc~T¸~ܪE8)~~*BiߛV?ɶV>j!-~`m7B IDATT+gFyΛ Qn5@p[{}O27i3_;}&?NkcXH!SWl+9 $9.[䠩( ӲʔXVjl a+RhѨ(""""RUV.d,""""r~{ ~*tU3/”J7B \bㆽkTSm_kLo ɔ Z]ߎ%"""""E̵1:7IDr0Ώ#P@ǹ4g9R'M֘A9ZvrjFPZ&/k<%1*/kY6[C `8s2mg0Wqe?W0itF}$'7|w0#]13>sYEJ0-mZ>Ճ5"hK]'.RR3dX'a/ȭs3 c) i?$%%MIaфdCфw"""""9,a4$19y2n0,knI70;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q0;Q.|Ʉ?rJ2xyrHcډ.ODDDDDb\p*u$s~Y0ჹ/Rː `xSjvSPE6{9zy@?:2tk{fIy#:ɧÇ}5:L40[=Isװe^eм+z8 > IM}~ ζ88s㈅w`֓Z?{a! e^t;~%8{"_swV?ODDDDD {3}-0㑷#CPĵ\r]f0uһsfC:TsLmhܦMi&vS*˖͗ضc缹Q3N*f.8Ly'U_>_Oc~TE}"""""Rݛ3|ַ?~ˬp)ݗ4v;чF! jaΆ]HXG,ގa011ѥ._Db&? DdK=ۢ=whG ^<}&?Nk YH!SWnkuo0>/_ju|Х?+4 ppm(FtY{O}%.>egiž OfS'.Z}Qχy< ldrc nE\bEDDDD${L$S6Dy6rgl[Pͨpj5+eCz+Y [kQYS*^+oX͋ /@C{_tC1.;ٽyn݊%9K'b~-cj3Ƶ1OuDy֋,<D7ۜA3r J?15nMRY`kʩ:%1*/kY6vu*GEo0)%1$`GT&\.Y ?3x,prݻe+O}ٝH`LpEcq+4义TIZ͗y i?ٓ܆?2`%8q”ܘ!:zA='0k*Ӗ|ƻ 1xǿA/ZF&""""r& t3`H{4=O41'EGGI$EGGeXTTThlS@JndI\׀؋(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉(؉X:Ϛ)CyC]@ `y*1lb_ĶDLkU"""""R08wsUcf3ę݈6>L}?W;԰. pC{"""""RC,KgOɇ`J nW'ڍ"EDDDD.v4^ԨQ'a4kۚࡽ Ԛ6g59oE%*7k> pLu*a8xުm%`X?& i'|+|qX.cT&/,ThDT(mfY;e4>̩sOTftۋ:&""""rGXF%iܣ,}f?zopF.kQ+3QFVbPFS'>xP1 (Gq'y2hz=ؿ| |&gL4<Ð*w0J9إDDDDD$T4z$a{PP?Wzbάutcڨn! j֠rV^!FDDDDDfמ>s'bZH!SWŃDdK=ۢ=whG ^7Ny/]>D=o?݉>4 I _8Ps60t*dɖ>9_p#0/L+W  2;}_0>Mh9ٙ ޳\|R3}X Nci~{ ~y\.W|Cߘ/۱-cWY"Pꜟ,k7Dy6rgl[PY[/H,;v7o1 vYyzDӭS9/zND(!,x8:'ֳ$BpM>˦Cq8x{LCǒQ [8"Δ!(ȃMp{M'+ڌTJ7nhcA| Gήlw,wu `Nx+N x8)Y]xgd{}]s2mg0WqecvfDKaJUn̐a(I.A rEҵ+ yc?e;QZkҼZ!eT&/ˈ]!MGR_ED2EWҁL;AfW9dx~pL78Cڣ1yǐ9),::МLr;^Okt[&<<#""""עr%F@`2<&=OR t%pMLDDDDD^DDDDDDNϓ3Vd~!"""""#`""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b']@AjLƯՇHxu.P/0oc'BTJKoG6h߱O |g8e3͡NVHj!Z5}Hص^ зgxOx #1k$`$V%4D-t,+=gquۧE\U 88d7R<{6%T[ڕt=FuoEb֯?BM(};MDDDDDX^rmS]*qh(F/^ƍkX,<Dᒴ/'w!t W&` HcU:ĎEc~_&GDDDDDXr$X&c1b,K1twТhy]8vcEs Sŝ%z|uaDDDDD$evCtS0=Ӟ IJJ¢ $wNF̩yWMxxx/""""[QQQ9Fq HLӞ')sa&s݄`H}$Xsd~/|V ^fQ7jaĻɡ"""""w!.{~!"""""؉(؉(؉(؉(؉(؉(؉(؉(؉s~ KlX| _k̜pH9uS~sW8p*Q .`刈ܕ |3'\ƴk*P1]]ɒO8U HIDDDDD:pPʵn#W+r }R`)T6(˸gX.eܕ |sH1ȗYMX R`""""""w 0;Q0;Q0;Q˵|'| .EDDDDD]@~[5 W!o%a=A"Sw_vVDDDDDd|t4qA|lΦx̲1݌"ݲ4_,VDDDDDh:˟ժRV]S/oc2p F1WW{(:֯fzAG5#fK<}LpǑ<٩"Hykfbo("ߍDDc ab7^nv—^ `1~euïVFۍ@]'""""rR+2w"""""'t}[H#aJ{q,\]]C!o#y:L7B)K;9p,Oʱt5+^D~qOx`~ """"r׺#؏=DGGS"kkDDDDDD `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""v&"""""b' `""""""vftt, ;.kZ߅9 [qLʽ0KIs453W K˷rDDDDDDn `e^N7qՓcG9sITjB6N<¤ֲ| nkI;KX:ɋd >y@?:J_2ivgi?;ᭊKe>;L7<.e@l5ryOg59oE%*7k> pҦ䧻3e)7ss:"Y7ㆺ??՜,Z g_A@W+y IM#_9ŠC!JX%v^ˑRҿ:n 8R7g8#yѵ(c쉌~YvV `OG~?дn#vB OQ̝ .3x[ٵ P > cAIDAT:n:\݋wQ$4" LI9B#*jPb]eR9 */Ҟik5v]{) Ć`D%8drwLrLs޹?~s3mxS3L~DG0i.1$$QTsOZY~@4v!}f 5.5 j͝O?rC}*WKj٫E41N|Iҁ=sHc}pqj;͞ w^q90'?uvk/# KtHW5 kQ6π0  KR8|ŭ/I"Q4o߮Go߮?Ǯle+;[O\}O (_gnsĹ&tbbĬJUܱ_x"?TV:v8p~}5rUV{>]]_ֻb "}ܬ`"c,RU6w|0 3mşs:]@:ef~׮32T5N't˭M7T-ͺjP:Z,RkH饚\>Es~n7OWVI~mk7W$vD4rDrڴq U(I*ѴiZ?ݭAM&}NѬ.]ַ5c lfEʍyM错f-w󙿫|!Ή~UZkFl:@QQQCt9zk<f-ӓyj2-X*yXP3Jp{~KzB9T2zT}E9Zqّ2]8$Zk^zZKT`y7t ɈsMcڼe6i˖ݯ`pE"kllTmmm+UWWa khhH% ø[NIAI}W.I!6muM<!#x` _mH@2Zv#'-k{ǥ"/ܴ@LJ NAku/TWn% ~w~{td.ῖj˾2ZF/_ꃳO_\~} 5U_E&N5ROn|][WP\ozgWjw#CWk^2 t+xգ_qNRsꮍ|ʧ}<ޣີqx> ,ֲR~(}~H{?@#Zt1ϺZz7/E6 tmMj:.U-s =i*goЃ_Oҥľ9ZWi2?ԇMs0Ɛ4'~g7?м+ $I8Q?}g.ß L(")+ݓES%IU:ip՚SV=K]5S^:j੥N=>S?}BRIng2 d UJ)ra"qf.ۢI^ܺM:o}sHe (__\VsǞyZސdZ]>m_Y͝*HǪюJ&ծ \_r=Q˿R[ݒT1B#ށU  a xt·f|zM}ru.]ӇXgjR*٣5o-͛#}?yZ[RI:֨)H;M7X;ߢ/]\"㰚W/W]ӴUa^ jh]ՠ"*y_S?;i˵*oVl{T|$O-uO`7y Ri4ݿF $Us*R^Xl9^?_ӿ~`vwt,[ilIڼ WGFե{A cݒvJ J컺_wI iX(kfL7{lڴɃ7,bH {}M5 3`@:ujn=c:d6 ̓I%혤'x&%8 2 H_Duۭ+ {! 'sjqꚄZ'[\֖adÇ+???ӝ&U8())QkkBt\$a9Ǘ[]q`79bRAA P(R$>'0 ee%uS$4}Df"aN[pagJVD׽2n790}MmTXk;vlpKTi&.\+ 9x޽-:PEt]̣V1ۧ'.h8vY"s;as%-[ͦOW] *$y ggڢ鐲Uv5̲\iL/rZY+.̓N;+RY>9Btf4Ydug; @,1)kXś [ 1d|Qh0sz 50v]9͆#,mIq`љ.ΜƗ9/S;zh} 5}Mu+V|Zcq)XS|9?z_</ 8i6,8wI݂4 ˖ǘC7bnCX"ggmܰdمtۡ i6*֚N!;fwR]#*gaL6{9 DbN?<-9A%a7 u$2EtvX3av1Y7!vCVH4GVt[+%2 Y΂Ŋ1ㅗE9$%>f )>ez1 np9zh SI3\"4fns:')nH+TË*tEXtCreation TimeSa 29 Mai 2021 23:53:34 CEST8 IDATxwxUn)BI  Uz/RD j4Q^_H/"͂DQRRu# lB๯k9sfw<9s΀TV"""""ruTI+")xHeQA<\..i bT`rLYYs҅KuMep1SVDDDDD(gBc.S.&ܔT.&8lвu\IA낂؅GJ Y'"""""rusXi-хg{l-{K=c"""""r.ziYIm*mq&9 ^@Vvz9,{֝91e1#˙}s"""""r)iVuGvl)mrv2=%–(9s^\K:᫤e+t9ĊXsB{z[I08qM2x1 6mGDDDDDĚf/h9`gi0gg7t;l6:8Ue.^9CYY܂Xeՙ0PM(tfe셱!TJ `z _F cim"""""r.{hO b 6;aa80{V貵 b%ݎEDDDD(9W`1ۨQ+cٺP8t^@0 xXC:[LDDDDhÂu[ @.$mE; /5u ]zr&( ^]u<l4\Z\ٜu#ӏ; 8 `?e/t d%?Ut2 .UDi232"77zDDDDDDf0pqqdÃٱ%Oz kz;=mŸ8jzWѥN֭t6)lfy);f223X$ĢMu_SK>!gwHHH*%"""""bgZ:؟8$4h3᫠+8v1)V2cXHIM%,, řvA `*|.[aUsb`6~_Egj/(9z biCGnN%"""""r{O)M$6Gs VAq]s b!\[3V-v p5/UYG=Ņ~xInA,։0MU3:`at`:Lfz42U%ςhâ'^؋}ೈ֔ыOf0N?n!t}ɆVͬzDD.{2_nRclek%\ȅX=`""Ј[.9[vfœִmGix|oFK ɸV&TŴݾ:_w#s鉱|nk wL'Ѐ Fm(タ ~Ju#.۩8R rc%@GSԋȥpb=o׏w@H*=s nHY.-7kVY)fBÎ!4؃f/ˤ/N_Du/ADD䟉/gҿ[UXwf_r.,\nEaluʺfL!ռpL?gY|u- ep֜ɰ&t|!_|K nOE7MP5?|<,M[I?ƒrk^f-}v8V$qw|x?%ͷJD ;F2Hz9=3%XXglމt&%NL Vl3=`%2[ hZT)7tsI>y3@b|1;S=/5wT nw䦝lUsR|BIeEu ΜJ+uΔ;dGhyV¯|5 /=>ԠY4pr nIzfԍqqrR ŋDri<(xt/fR /Tw-6<@wB`w$/Ϲ@f-Lu%NKa+8>m}I]D{CSIhJ`pp#Ww[9t8Nxk_!oϦ#9ZDSv-js]s;CDD*Zv69o[%FE(ߑ*Y~OimXr$)wkW΄ .7ՄɫǒΜ#g/sGays7k{vcA;3\fMzW-cŠՕSMao[69; a"" Mx&~{b03{ek֤ Y+ΐ@{Mŧׯk$eT-j?d኷+)Μ՗jƿld>{]ƐPK>Tb']1Z/U=U0&h1r_PrEzXR0%"R"ǬG{29W8v=~0עK|t9ў;({znbiFPX'xPE]n]%)k', |YR,_܉sDe[1{Twj^ѣNG@JlM1꩐#U0&̽ͤ^J5vKaLϖzDD.b,?x>x~',7>nY>'3=.xq$jsNKk$ec[O"׽ nHljs9W"\G?[F\n$zi3S؜p#Ue1Li}YV[~Wb/ hѲym\ndt`z07֓:q/<_'1@Vm /VjY ꀻ0rQ;8Ots:|[ EDDDD͉xʴXQ̲ΥCALDDDDDljbvL:yeZq9F~J5;q9ev_gxE127֓wb=n2?E}|SƲ|xs{H'̮薔&)' `""""""DLDDDDD(0r&"""""RNDDDDDDI=,̙:HTnlu*Rqss+Dr&"""""RNDDDDDDʉH9Q)' `""""""DLDDDDD(0r&"""""RNDDDDDDʉkE7;vTtDDDDD2ժUnM6A}Ӯ;v} R۶m[E7.݂("""""RNDDDDDDʉH9Q)' `""""""DLDDDDD(0r&"""""RNDDDDDDʉH9Q)' `""""""DLDDDDD(׊n\Y ʢC9z%5>ތ/&╭*j0Lgcˉ`g,If FU lE[51+xK]b~c%|ANĥbn ,#*[kEDDDI0`40/Bm&+.%め,O")ӌ O#goz~r$~kRee}2,s./tQQDDDDQT}nOO?ؓxݳluVg@=V|޽'],yqN5_rd`2=8>ܾNőn'V^\8$Z}3ٔ^Cbd<0sx8FڃA%{yT"yMdڶZׂ{6O7cȽ_l |[yx?w'au9aO4X ݣ`x08FtLϫ۲?o8t`h̾ M ~7DDDDlp \+>S b5o∿2Gش[v8FB'>^fΚ2oC bt3pvLkcH]ڢnxY?%zM4SkCM1;w7 I@PA$r`O\۩1y|77Pυ[x LߖkS9o[܉NDDD䊧VNuj6^ )Ss]v_sv:odͷ6T&qhW|={5?c{cƏ]͗_|[N&͟O1&O77dϰ)èo]hd@Nvs ٟ'Z11w@2cd0#֬m|{ UX䚀q8-GԟG..~ɣ[ I'0}g=mWy.WFcto1c;l]\?=0/ȑL&Jngsq:G1lSFЙ8dS&?l:ݻyIA7մJ/Z{]a/zٰ=OS| ط=f}̋<ǎq_k𿎞-0?t;7# Q lEDDDPLѪP Ra͹E\asKb䆧_0uZc]o_eD8]2,̜ܳx"o%F-k;{~&Q=c3B@ oܗa 㙾%~}Ʉ+e{tj[n_K>혱'B6Fs}^EDDDl IӦgeے,75ܠw?8=KOc8Mp\ұ:W6?d2t:OqkVi؄&_Ͼu/?<0<3׾5̦-ә)o~l=˙u$\ZpS_{8}qV-셋ɗ`SZZp~7Ǔ׍ngm\wfķ{`6u]/"""" `Y̘ssl0b4\4~<Ǹ/ORk8Jڟ&z }1n8?‰x?(/ 3GHQdcൌ]~_Ķ7Ldf`D#$mb8k2 _b]9b7>8Mj*xt'L{e.-sյndĀUޛ}>gx hW)[}afwv1yoMcJK_9 7fYj2wj;J )1zIVN112ٿq{6v#=6Ou{N&X> ug7}R3g@}͆֨6U87<3*ͭ`ȚucQRPJ516zٷ~b(ZCImWȹtz4Vnd:7u`j!v cn[BOyEަ=suNzŞ.`Ibx3P^WzdQUh֬.h0ޜ-Ui. 1Jtzdg'<@slOR[L_x]àgf3:naxﯚ3{.[]15ub^g:{ļ23 _N\:<6IVCpmBby[~X~ IDATfXV0BOP 4- )= _gs*|B_KŒFOx\;wJ؂ l=4>HCL*?]lC>IP?ڍ[-Žtn?jz4g> (eg5wQ]3=ƃK9r"Gw=Qepl2 _g߇''D?"'DWBV^{rK(gG(CeX,x:uZa׮!9ϿSV(|Q?/0-r:V-~LNU{?Ft}LcTV?_nIUHLLcy$l/YnCBoXMS;x?C|gٳqGkbΩT>KXUʡc1x VMUT|'; 'Lvv ^{hⒿxXƚJL$￟BE|Yj6YM6h泈_=EsKL*gm\Z۟k"ez?~hi: OX>q~C&.%<}gm0E?rsg̮G!4 W1'dK4ia=i;.d K%,z 7昡bur|wC)|M`}Z.zE?‘TFyc3pq TL>|~8'ۺq胕<B]؟:_t:^72틉>Y)>_΍p%}.g:ҾUВ'elsMtslݖ-7@vlvOÍX,֞R\v4_)GRl{ދ6- Dok^DP (CL* D`1[of0d58Ars}~){OCڄw[[uvCovnLhDE^KvPf HK !p\kP#v'j^;䅗$''tH&Oͪ,?CZ;/܉Gϰ=ִq;fv}9G-{WWwOQۼ~WW,>,vXWJs H`2a›OeXyoOIN1m3ፏJ/rVD$mcM϶aoںQ ^m~iW|Y\_Gr2x{{&u;ѫ;3on`@UҨ1XL`yC,&9;4vm5_>#dQm^x{CJJ2`uTDQ+ٹ0y= C QQ&L(Q Z1ukJatBFxVP..uعE{Y.% psmE|B j]ul4h!̐-@.{_\Zѻg_~7g5Vgݼ9ݙ;yII]R?AñO' `(Ϸ ^])K6ur,*k)vG8!00VAJKL*F6c_mL,6X[ۖ ~Hn[SȄ>=|ԝp](/|VdqCTs~DZmX4$Wn݀KHԅxEq]폋JiiY07=cO"r>OOWrSH3_=¯11gMF8?U~Eyy:0~[VK9HY]Ki];^ߓBvmKzKBL* jD~ LPEh26f;ѽ qH੼WʏFج- <2)n{m??,ڜ^M{4fyd0)uajsD|ްG'V4lͫBsE?YUg2k\^x/o0aGҧxmOX \1>3nY?n:е~kԡug}̦?&Ь1Y4]rMҾsO4@GGՐ7 isߥɊ|<.KHf}y!ܸ`Rt3 gRM!2 MR~y:H[]rE.k)vݴ#;K|K"֋jqZ\S[ki؎;hժUiHڱc۷/MeQ9d$f8uÿz#.ϟXBK7?AJ>^DD:۶m+upssY@vל j1[-"KBkVT*^UQ|4|e=aQܶn+::کs._z;Y5fooo㭫Tt䪓ˁoF_KDD.? `RT 5É=p?柭G8{<9_UCN͍+F*&_U\ 2JnE7EJ9B0rZb&~BL*%wwwj5 !~0e0pqqd2a4:ҵkhUȕ(|/Nx߱Wt3DDD.TZFaRN%""""""VDDDDDDʉH9Q)' `""""""DLDDDDD( w_wpL;׾ʟϔ[DDDDDR`ۼ?N;8$[׬fߎkE7b6}̫˿I"""""rŻJX2mDB݀>U*9RXjj*'N !! <<Ö2ZyU[,^yaXX艓X8<\""""rWMDFf`4ggM.w4c2a›ŐwyVs]6_^J_ռ2z5FkwԽ>'NZCG\]] ĉԯ_1ƨԉy)0{xT$vɌ7 uGskqUh֤qwE[g0p'|ô _gs*|B\I_){3{4v1p8Md /NX~-O[4vpuH"=yK6a~ yd}cD~lސ"L)8NHԶZJ(bi7wLwj>SWaK-^*\>sxwy LA8b7bXoUѠ ODh9łEM4ccF0m:OYx}KqlϒrnؼW\>͎̹|CzyugB6ͤ}1јh 8,\̛_͎1I̹A-KXmM3I̺ Y!Eʹw;jÃt!==wwc Cmx {֍͎2*>U ף(Εʟ;awx&nazCW?Ihe T+ZJߟRZ5禒7cmkBZOEG;ww3*܄ШQCr&|9ym3y+?FyQv3Տ8h./`}-[R[7̽)""""TիWرc8]&''Ǐtޏ1T5ž={ ǧg ߣiQhНNc] & 7 +Lgҟ \0ƄAl;s|;cIhڏ>!e<dlB1Y^V _׭_[dee(MvAVJSDЎ;h߾}7##{JÆ qu=L1''={KÆ ;ɼԨ5L[ѭTmV,6"99@bZ,EzI8A?l۶R iiiqq]y/_^&8itg>w3—DL*///""" "..={{Η4looo5\NkOB[^XDDD䲢&;& ooorssύ 3 `20]GSQ]. ` `RiF%""""r L""""""DLDDDDD(0r&"""""RNDDDDDDʉH9Q)' `""""""DLDDDDDVtDIMMĉ$$$ժUVZxyyUp EDDDDJGL*9}44nOOO҈'&&`իx%uƲi C+1""""RƮ\ `6ٵkYYYtЁpqqqi߾=ڵ \.Cq~ N?Pt@+W;,"""RR9x 4iYsAǙ7@Ǝ {-]>]Dz6 _Gpͽջ?>EYJɓs۫FX72iD4RSS9}4:tpx@Æ ٶm!!!6DŽ%>M.|2{/J$qvoG.8s,dҷ/)/0t>*\gfp%[DDD&Ɖ' uU+!!!8q 9śnn! FhiΗ9i\+qIٸԡM_`/8},Y&\''/7M`jWaܶHwl?K<{q1l]8b8FDOx>3g]w{{^ioq X9:ʉkH9Lo#f@p5<%n k<ǯ^fVJ^+T 4nܸٳg}Pw,%km(agaGg6y2>oL!L"Xg`-ۻ!.|O?VC~\>W l8@vMĩ&]^,r`$ė^d|do4^JȊ4?;~y}^loy%]#+e ya9/}Spks\7IDDD &FFFƹKJ*禩/,TL>|~8'ۺq胕<B]؟^{иq5oߏB=lU\KVQ@K߲ 4iTr{ߦ{YZN}nb-}hU3){8B'>bDO:ۗikPŪ(^+Kw:<2oiʿӼ7%/"""r%S˞b`5MD2I I+`5X1ڍzL~$?/gopn9% kBB+} 8;䚥/ۑ&[w\r;y)fU,r?͏6#5 `RixxxwʥndɯDn'oz .D9$ OOWrSH<#Q׸I^u g<&ub·yiE7DtK?@8[P7Yw IDATQ_dL2Y:lHMEPTTjVKVU[Vmzkv7***uED $gu&3#AUy?㜜93LO|| i$[pTѢש%7ɷS4~+ $te6A$ ,̹k9=cTɼkp߳xH]v]QDDDD 3ˆf˖-3u}yfbbb h&N:blkGl6k <<yq"SIO:n<X:=v"iQ oۿɪ^oN۟ 2E`o}VaQ$f\v;dO^| -wrD _?/~+40sL1)0I< !9# sm""""r~co3Uc\VRRB^^ީT9`+/5ndYS-"%%% z]]]lٲ@233͛xw1=1NDZZ555Htt4ttt޽{!99׼yN ?Z"NRtY+ԎRȹNxDЏy?f`6ϲsٯZo\؂IN&9&t.ȡ.$$Tbbbhlld͟n'33c~[`,S#-}.#:FfRWNe\q8Y,L&x<|} F#&>}`* ;OQ!"""rRmxx T46﹋Y[0?~v#Mn,)\yߝ|㛥ԻCIpbcOTó{O@+~k^XV]&"GMQx ^s/p.ͅzp\xmÎ%6nϸ;u>o~H‚m̫߿BT/r$ '7|[N3M[aFh\uPYVF辑ګY|{ a16,7~HļI1\E0Au'`+q<[gLY?_}ftĴղ"\v+lI#QGڭ LC:2i|:K~L=k^m0qM8 %'2@),"""""O~'?bA^pizzf?~xz,4yUMt°toܽT26ጆmHJ"f48s&-pܫ:¦EDDDD1~HfV֑ և?L?72j@k/sx;jy7Stx'7<,3zzN"Fc2Y&k_w-"Ç,""""rTRMml$yoakO&6\@8I~;8Iĺ5f:FS`>߀/~{w-wfEK9&EDDDDS4E?a>8*++ %`"DEE; CDD0 ǣ/z{ СLDDDDDd($JDDDDDD0ALDDDDDd($JD;nW@DDDD;S{KՆ&+!6bRF1fLVsQs2]m3G ﮥmYSvA_F6Mn$nAL+ms2?:nXś}9L ‰aOia -$bUU6I72cfbVWq #dcC^]911Vʗ,fc4Q4\Kx_ͥY\7{Z'qXzyiQJuc2 .ּ5KnҔZ^}3 4/MJg0ċtVI@\6K ϛF@uym<]M_僢RNRQD.9何QK]g.V1'3P@>/^uWš;˓BWf"gs`!ϋCDDDDMJq\u[8dgl]DR?S D$%>Ud]Y8WRʍew 3H RHXIUM7iS@|0|ꬺ °AB|b2b0@gcےo=(""""O Hpؓɻl !ǩHcGF;VJkhIA$>5q*_Mq8U07Qeܱ869G$guY;Xb2IE^idY8#-'|""""rP&a31@ k0HL| xc455a.I0`]Tv]]C{H{(mc 'Iܻ,-dH0LLDDDyc #&&!'|b;m鷱}bk)aeַ1ZT'aI+[b&6n7fpO7%`24:?. Jål<Mn:n*FO~-;;{bʌbl9Ijj"f*54Ұo` w pbQʦ]zh᳼\\/Q+T (a 2rldo-1s::$.gOXF^,f*7P7>'.7 ~ dXÆec#!w#5m G/_ L}KszezT+))!//TTRRBAA8(^mnd90D k׮\3s |/P Q˞ӯxQ#Wqy8ԄMfJDDDD(9VvEAi̘]#iR&r\vrB FA$JDDDDDD0ALDDDDDd(0\.!w. 0DDd(l455; kllf; $JD/թ'L$Emm-p;$LD"((l*++;$Ae4l= #""d%`E6ك!([0r1`wx"Cl&--a !ђv s|vj6xxm}1;BP  [2zsdbmbS=$d͂yqv?@6&<<V64O-]->#rfnz0ke(܆Ǿ#}~w~'M<χpx?_DžC)c@AiS ӆfvMM,yo3hbJjnbT#{Ky?9 zw>q)?[|a@&>i[*^[H>6mڌom4-G~HļI1\Eǰ =Klqwl}$w3tDDDDD\74f֬_ϋ\< s?еgnq|މȟL܂U̽g l+h*->, ci)[)$l(;+':Ob\<[g}|XnF#>9 l;6< G̷NVvٙۛ|`p3sk*K6ᛒ[ɾv,%5i*.ɓ!y&-pܫ:;'@v4=~=?ܐ=Dw""""""gLcp:=!ͷJؤ|0`0 2D3p$^R̶eN; ˚?lm#W?\gP-Zݵ~#'dD()yd H˙8{sbݴjzrCp84 24@8R;IzU8z{|mhƚN2`bF,[ʚ̋ :)V- >!ODB޵ܝ7-2&1TS۫;I,M3erQ^^N\\)))͚BESS6m";;[!bh&`k(//E2o5r ƭ7~o3+v3M綟O0 Kӽ!^tYK;[>F &Ҫ6 Dܳ\l6 .}Բo+~wT91,JDDDDDD0ALDDDDDd($JDDDDDD0Cmv;9MCn"f9^+w@JpQFui+{4DD|^iaW56؉y 8뫃YCn I%rꋈAJw{s5WΚĬ8\;WQ!9keeu iq|JVzO-?`ƃDKg=}3R_DDDR‰A|N{ Tuo! N{ֶ֯N<#@77] 3UFιFRd1o1|\Bh.]S/q:9ڰ%]w^rRK gt5:P |C^ϖ-g\8H&̢e@:#h}\hy`2Obe%C'0 p+ࣹm7<XxlG!E%Ν)\4s{+Y\­.5cN';AC ٳxT0󝅫o!ر֬,lжk 1a@k= K;ߔH/Z 09x==xz\7ױuot=T~C#l;II,t}Y8qN!եrc݂c RVRUMF|76:^|=O91ZñCmIfͷ<1}?fM$N &@LDDD,%`r.b3E}?`'w2BS+{v㭔4Ђ(I}XAkTdx*VVVJKXG;q x5DsM.`fy"Bpk[QKZ}L gșL?3gb& A`-']ǀ7Mc]#^-PQV/Eew5G$X#Ǎd8K zjX[)LdXҔY D]:!(니aQ?0bbbrR`!&ږ~׉)&VϹ h m}{I eێZ[Juv:DfldLsϒ!SL?dJ?\ƃ:1zY㦒m` R!(o\ΖӍ㸕É6spG)gw;]гrqt. K$kYȁZ^O/aA`Fffz^lfŚ]?3P_DDD -2D\UbVNF98-eTc#{{lYkֱ1@&t9{2vv8@Z ޣFB0F\ȝ55).,Ǒ}FuFd_ʥլ];{D$gfA:!g67>QK@bWSܷr\l6 .}Բo+~wT9숧&r\55jYz/9=JDEoQvDx|3f`j#Lν\!""""("""""2H %`""""""D Q&"~a4q\C\.Fa Q&"~ahjjw"~؈fw""2H_8멫SO I.Zߏw8""2H4EPPTVVRWWwH"h4b=z4AAAGDD0LZZ4QDDDDDd(;P?Q^]xOVSR1Qȹa=GhsLDSR8@o?}7H3xsJ<6-}cw͵[_/;⪇ǿF;H -l'q񕗓=As%့6PyDln.f97=/b! a~1,mul/_!-Kܻ>w%FyjGja1w7sCv /vO\׾s;ԛ&""""r^z _;_ୢ.Ǝ%6nϸ;u>o~H‚ݰ @;֗?Nnm{Xs<3waP.>%`HhL:# 㯾Mw3tPr'1. d|&? X7_<Q 30o2-ly ν#lG 'yHn}L8櫶 qv[SA59P=S&ƇxPH꩟yQSSCCCnfINN`P .%`ovvp8dD()y~H 2^[kYwG {)o!wdh;χX@@@XXNy0'\.!%%,G(;k$/5 ?ސfTRMml$19~%: "!Z~xK-ZBEC?/_-aߧk!@[#cptUoURkz: 9S;>FI'_p8hll<ɑfoѽV{=@?}}6DDDB2zVQVVF@O'{+X,f4v{ʶD=,^chz)jwǿ=|17@HOaj5$hqM녟J]{ӹSj\"&"f,N {Gub\.)%_f\.׀6#7?&23ٽb1+w@JpQFE{'CL+39q7{4D`>xvMb_hCDDD.$C(3b!tB 0cs>&~5$}_mOi߻0bH|;fy7+_^VpEpe~2^-ulݰ7w52)$##l)LP]92ȏ9ӊ0F]|%6DDD|40+\`cW੯ Z<Ǩ7/@MOM7{~7{h3Z؄'qrxiQJuc2 .ŔHhqi?-(p{X{ Ipس~euҍiٿUHمdŘW60ruLg|b6O#EMcu\ \}S;Z>BkHO֖W3#ߨuz_4kk/%ynjX;/Kw\[c$8IB_^v {Zi~LrѸʫ8%0$1!'@kS.ħO0'\~IȩjNlv븗wyk&]JAh7{K?b]_@ItVL&]쯬`avn|4͒27g7мck|uW(dWԹTQr0f yo%vRיKEf̉ 08ݻ7IlpJnfgG*8wdl.[م0;JH Zh>D ?~l|7wȝX+NLa|t!).u_eZg7_GDDDoJDNUg> 1BŽr3G>;q\u[8dglg_DR?S D{'OJ }`1Ⱥĉp .hg$n2,૧!a<ƶՄ];xr&#`f2r8pmD; $rxz']c<Ք51:f9"q!JˮWعNxn:}ѷ7dB 22pFZz&<^:V.U+% M]]Ăg~bO&)d'F{8' v㭔4Ђ(I}XAkTKgq2c{?.f*7P7>'.7{1pUP1oZ˲=-!;&02L~ s*S1Wn'XLS@Vb*ʱ=G, T5Fb GT=qce;n*WsC'flȬ`lc.efwo,mdD GtVaò1; 'nyWq7}̲6qްt1ۉYN_"""2t ?%_1+./s].דXII yyyREΠ q_LfwreƏ";O(^mndˉ9Or.a6J>jӷxo;vSߚYF[TUUrt.*bb5@c-+V͒Mp%_""""E y+99lrIl&&&ݹEoQvDx|3f`p""""F )));ܹ0DDDD(݂("""""2H %`""""""D Q&"""""2H %`""""""D D"2\.vMkk+f4l8 %`rn"r\.ˉ#%%ds\455i&QDd8/0o!zWᩯq; h& Y{n∏w("O-VVVDDd0 `1ӸiBՎz+xj!q; #DEEQWW0DDdp8bi|Eki &ሜGy9l6yD!O<SƐkk!0DDDDD.H|f5|=n|F!""""rA:0 0ALDDDDDd($JDDDDDD9?ٶ{KՆ&+!6bRF1fLVk쵗)OK:╦WD\HSL6\9\LEGK[7]̾n &G("""" %`XM<67B"J clrl"u)({rp{X{ Ipس~euҍiٿUHمdŘW60ruL5%<|G5+BsbW$pMĞ xiQJuc2 .7ŔHhqiq~>v {Zi5$wj!i_N>:<G>œxt?H{g7n,ǧ3ᣦb'#pLgjVgk^}3 o?Q6wߚD2Vo;B9 &x.WVr0t 0y; >fIyȳhޱ5o纫v2y|PTB܉X*(9E3GU;j% ~_3|bN'M,_ kT.O \azi*}1nҥv#@Zh>D (z' kl:lڍ|2-p('%&#/MtVRBgBIĊϋ;ﲈSv,dGgag.㧵f㼸b;{ ge_+_{}ĮS{w37=֏~7V']Ye}wRiyǶ}gBr_ n/M/Őq\u[8dgl]DR?S D$%>Ud]Y8WRʍew 3H RHXIUM7iS@|0N멳:}ci{774fƏ;c_Sxw{ x9rވ.00y$rVΖIW2{L$F#F v59GupOWш/^/ph3K+Û>bSyFKocx}~?CXV?UW\5_[{wqۯߡl˻I8;TئL72v{Q_I.hغ ]YꁳAa #&&!'|b;m鷱}bk)aeַ1ZT'akĊ=‚o6PSM@ۧ%fR0mnVlvFM-=GDѯO}_~d䥑m'2:pˉYUDDD\?)o  #I-٪χ~Olg[v\!!6Ȫ+*0W(jͨ4|%~d(Q>Miױ`4y3=hqגmؽx-;aC,YQq9:"ɺ4|fs12263[@'m`s[YIT#*2n1Q 9 ne#3-  el+݁mK@Ǿ(V("""9%`Hff:g:A/Q Go/ XI}4}/[O|?=:~{-q@y/o5h:g 1`ϹLŬ._`r0qNYZʊ +F[eQ8Y^4>(1a֖TGN"|~}Tobݡn|F a1N]4ob&᪠b>޴e{0ZBwL`d%%Ya oc0a m"`g)Vs@}5~دt| `[u\OJ`%%%ݧR {㇗DЏy?f#o§y~πe䛷3+v3+ 涿>ōq[r7`~yioWWp?7gMG0gFb_=EEۄ9޿''R\\L~~kRPP0DD.R|/P Q˞ӯxQ#F3`gT_{'ȠGyGY\| T-z7}sYl—+|t!H\;.P5@c-+V͒Mp%_""""CՏ-7/dx˟1;x)4p+IrajeG[4ƌY95Ȑn:|ly) XN[w""""2蔀o {$\C5RusG 9!y _w"""""rVEDDDDDd($JDd0{wUy3d/@BV܉"#RjjkkݱV\ZkV뮕E"!$?HCL8uΜ{Nx>1n (v]fNn#<<\.8JEENfnh.8!RSSUVVRZwv]%%%ڳgRSS] ~Dsd5(s0=EyU'fSNN UZZ*1 e] >YёEJRdsE]F'GV0VU.tS>B#e=TuWHu.k ( .1|w5@$:2et <%]I`$IZӏ] BD݊nU]]0aSSSnnkڵիRRRdZ]1ݮJ[N999{n.۷W^?vwVUի ] 0Fuubbb]p߿e Bm8ZXVGn!A``!jUr}߅W0XV+]q۾ZFv` VHpťӠAlsuuھN\ڂnc hоRm\TmP޴J:g'mA$^z6ɍORZT^6QB]V. Uꈂz:##XYek1c4 Ϊ#E۟+i,9wmx0ŕtɴh*j;zgC\9Z=T_7|V֡D*>k W$AU[lu)(L)#i=l Ѯ% QYM8WQ,:>ug*Y4:9:i%ZDA Υt.-ڝ~.pb aX9ꝒkOaѽdiU@D$VRIDATWשOxFTeӦ('uN>_\ G*hbOij*PKv"%ɮU&P\듂{fiĄA HKU~Z{'ur6II=}5(L{$i_ҡ=rOIҞr9{*ʳ0yy *[>^e֡=[f;t6<~d? lP^zVj:`)vF%*%!c rM"OĜ(IRRRU+44?u ھOPMRRhegI2(>ܾJ}d53XII1R!:qf5y^UAY:TR JKTũRG([},v}]9BTHe? ӯO ЙmW<^ 'y۱X^Y#@AQ=g{x9BJMrY`Y}JSLPƌJ՛۠q")(M}jIa왙V{zk}NܵF߬٪UUg)Nrw֐m[VJj z޶ْTkKʤ8 \IPJJTg9VW^6@ؗ^9_NTtLiy-V(.9Yq[,pr=LLR%HwumV֩B Vgj*IKղ/Hbw %OV!TM'OUؼ= BϹTśڵݵZ?< Vm-߰S#Y12\m*ӴCa%ܗg!jha_\oأ'[\OYOkokү.K p)|IS\`JKRZYb)|-aO=zz4iI̐mR;)&=MaYG =(ETDP*WKگgN%S72=Izu智o%5'.ҙ3ii 9aIݯk.P&kUuT_i˛fLӲGsy3udM` Ѿ5 U\\LСi2;ʴ- \UU*9JhuYu//QLS+g_~P/{]w TWuٺz`{#ޢ~#Rykݺ"ȽI?Ť!j%_.ǵRpLF?Z"Mڷ:_Nnl 16_W8uJZv:E TDNpP,קa(($J`*]iRX\Or$9Ug?Çjoj⿵;hc?\ܱ^~@S'Iu+l4J;Vu;QR]/ؠ+_oJ;ۈWkʹRwZYC`%ܙH 6Sn Rρu@7G @q]cR5s*kmP[}/@܌Sn5ɗ nfyB44==?FޞUc59{C4k p2 IҲ5Ǜ~6ZYk@=kt꛺\ ׅۢJ)PԊVYk4bhڱht<ظFkFk@ЁJ9g2m є>Ǽ.&I\o-[H"nkGMN)ӣ_諚RɽU{H*}WB՟wi"%#yPq54A&q?|yꗿLhD}qr֖h~"3uCH.LBտ|clˉ ԰e644\"OBҭ{jem ;Myѳ'+Ul2&NP|k?EijxxgGkKjI}^XDueH&OkuN_n@u64ɺtk'_?JRϺ]Ou˘5{&*1"XN=÷N:>unvnޢCZw>\>oNX>q VsuCW)v3ͲZ.na' Xw@J3(: ӻ St*++SiivA7gURR={(55 B nf)''G*--wIf5p@l60 @bZ2@7ED0 B 00 B 00 B 00 B 00 B 00 B 00gZ.I}ڰd]B]Uu-7L0$hX5gfQ{!~+’4('Gc6Tj3Ԃov|_-7AblM=gD[:Gi:j}sWڶϩt¬ЦTszfbm}@xq{Nz_h 5^WYW!|-\YUugGI ʟ^| [3sϺEgZ&̣C*\Zgtp.|IcSYhv_j:ul]=0ZΊU.[?_PoSק_\] G"gܡ?twVkâ%{5HudNЖkOm=kէȯ06 ,5H]3}98WN6 N=EeIqeϴd, |KUi2y%zMJ%si#'ƙzmR]xxYjoCVIm\C_whugIܵ[__@M-&k!70s=jJJRJRfm҈/I*ڨ4u_S k7h+#|Zm>k.p Г;hK]*\9+r^eY'BzoFwܰZeC L/INkN?7÷K}֧oӅɍѯjTZ-GPzgםLIMWKwz =ZZH WVjOSsze S3uvdmLˮSdv%Y֦ePt[ +((Pnnn;(((ѣ]fmVuUZ뛖%9\bjvM!A``!A``!A``!A``!A``!A``!A``!A``!A``@MAAK]Хtย "!A``!A``!A]N;vЁp8]Il6+,,LIII w9?>}jnWEE6nܨb$َ;W;YV%$$(>>^;ww9?;pbcc]Fj 3AqbZ;;t00 B 00 r-z>=.ҥHڧimi ]ޡ->_M5-JCuu=зzgwjaaF_|`{\k]]Az}pkzy2 -ΜڴTvaj{=mr*:ckZvhu3ӫ_j}4'st^ {Vi>zeӌ?դt Όtu K~ú%|r zVgtnL7i/tEJTzsE^{S*ԡM;{k~{7/Cw*qd9Q7۹bHi9^ߨt2I9"E^W^^iLR a:rJi]gI޳[.yU|w =x ]]SNIfߏ)ܠ 4~Xc$5lH}^E~џWi9iS4!#q\W/ԙ7)V-q]  bd nC)9r$>]BX~ aӧ}hȢPq nчtA?̉eNIll^,(T*9k*gg+lJ~nU͹8L^KRRhSQRR210˳iOY7ߤ/8Wtj'\Yb+|s4:inާoҤ>^]ٺ$I%_魂e􍓭\dR$&+tѽkt޲֖k{M.:/GuDDO}/UμzeA5X3F^bS4-}m=u\=Gso<{IQu۳kZJc VC6-?גj9"5Qww%Ife8Ws#ܷ^Ѓ S)nҤmˮSdv%Y֦ePVPPѣG.#??_m:jΖJ]R]y}r$29[L\i3f '"9[7N6',tq&Lw hr<i Йg:|-dkiwh Xk&|8ЌӞiphO~isikswq[{n|.fvvfs{m-x5m:˅Ahh***]FP^^)t8x5; 6nA޽UZZZnXwV޽{g:0Oq nu.-$$Diii*//WqqK:f*55U!!!9EdljU@@:y0m?\[ZܼkrЗ by\×krʷV/Z/r2yz>;`B久5doMi_}܅$wu0OZ.ʲ,6kR+ѽ[}[ޫ#_ɍvQx^wetOǻ2ݕK#%tkʭlN(w3kwU8EK7u㣽W#eP*%W :}_>[&so~>Kz5\sC!^#'UN ?@Ñ<@Cwy\g>u7sm^|%f'>/IOPIpvvv}@N#O-_]F {şh"^HNb/& 1ضP`+~ 7)+*vͽyT8VUݞt Qi1Vy2U$t͟$k=o,*djDRy.qBc<[0;1m8Տi $w( j|O𼛓CVC:sؾklg2F ơdNS:׉`sMl#atiĸƆ-sE \١nAb6I9>'>^Mΐy-ϥ$'=u䞺bO2'ݎyU$Y}  NbS`A:K?[>'(bD' ݹmq[@ILЋy=^29 U2yjL|ϵ3o8] m8gȚiڝ[P3o=Sh?bD#C|J Iڳ?-$t SaƵ 64U{`FO#}aTzܞb 3(&n5;h y=`gyԆ}g /{o|ӫ͸dǗfDz)U3; T OgdW_p8_D,;NUnT]^8iPtDk6Ԇ=;F k܀}̪V$ %a1æ.p Q[>y1)N,%d-<)BHGh.L͙v="ÖT=i^s_#S޻2+Q괃G׏j4<ĥ.T?yi٤7N۝'+οrr <JKK]7gMY =HY s(|Ҧ#hob.4组{vס ud>|CXWخ* OYV9B%;|꓄vnɇW7[ C"noފy{]wח$N,VIpY7tUVp hVW^kyy#az^bj~@Z'?4P/QS"G@'͚$\(R2鰔56{SI@(uWщ  'Ez.]6%WE?4<`PUlxgDH7-mb{MmlX|/YcFz~C?+ˍ]×᪚>Čq>7mW^8jў_vG~ڶ Sg9Us>&^>lsv]s|G|6ù/-gC{t~:w)yvzQR44&aNھ\U{~3<8׸.I,N*9f'?|y+" w4hƌ7ogioT|G@hj1siFmiFsiП?B8FϥBiF!F#H#0!4B#F!FaBiF!4B#jMkn:t{ƃ#Gd Yǚk :}4siF!FaBڬ;w x;wpyZ&u[mQkJb&2i`6weȿ/:"zr$I Xh$I x7m[D ѣx{{Yvdb*;jhDu.oٷ s[s$t:]1h4]Nѐ$Ibw`p:M)M6=<ޣZQ+** :5kW{=ױd2YZ* D2YTzkw}bSeӾkIoOI}z7cL%-HžppwiU$^\`7݊Mڝ- LvIC%f_ƗjT$X( 6uэUw_t tlgVSzZN "<Aߝ8RwsYLwv|C_]wsTN[yH켫[ݒg!%IB;vqigFckC=vС'FvvvTT$p8**MUUvlxxBP: A*uk0 ^ߩS'\zӦMMSG0-gH;PNY{GOn7S\b͛;Er& r G~sm?T NIĘAuܽπXqi;.Ӹ<5?>,xWX48G%a)ffT-'&x A8:NӼ^^^ ///WEdddTTA*N]"P}"]YYf갰0Xl481 RiHXXXϫkF솏=)6w+ ֦κk*|UYcSq)o 򊲤<^doX};[V@)' 3TM%xG&,nj{ k0̢¢7o+C?p8 &I@bش}עmvY  r.T j˅ CNѸ7\9I/ܧNˇm0xsO渪zOZpldsXO*>Čq>7mW^8E?EuPS{xY=_Vl=J.oK!|G{~5ewϿ;;m0uyC+]5g#{Z/;iScZ|}}X,Fu:1B  Battttt4I[-Y.}>)+3$i(+:yZ[tUU՚~@r<\. ELp:@ YU<:p}fi_i&y+" wZǃisFIw_W\횝(ihLœ}S }6_Qϊ9 o]){wgffƮ&v"wI4 5n!?%)+ I,N*9f' kzF:.yGxآgqsZN   DQ:}z>,<^KPZ%(HRD"RNE6nfM&JhʶcnPN.X,1Z>+}ڑR eȐG ǍL&׹K_>}TnU~!G=scoJܺy[bk"r񫓧vhL&ft:d21 _r: ˋ`X,4z -#;t:Q>Bi>Fx\.U 4@YH7 G]ϵf?'F!FaB鱌 f{ M}FԘ^jsiF!FaH#0!4B#H#F!P_@m !z:ǀ-RؐIENDB`liferea-1.13.7/doc/html/help_subscribe_1.6.0.png000066400000000000000000000253721415350204600211550ustar00rootroot00000000000000PNG  IHDRsRGB pHYs!3tIME46ttEXtCommentCreated with GIMPW IDATxw\GǟJ@ ``7[lѨ}ŒhnTDc]b7" sHwB}8ffyfs{ $J*/Rc#7Y1zAA`b#M>wy8qtP5At =@:a݂r;^{} /\ 8@S)Y(}]= :55eq>z#:NYgnԮ᠆zoTָMM D|f3GBN$h56[[|WOԛ:dz+#4OrpGputdssbsC-&Ko7F߄ϒII酏ó\}3>ͬl LgiŪml糉 /kTẙFUCUVBV|.ٹ"JfZoKW >LQ;)%~Vϲ jiB {9;O4F6wGn|eT9roi!6,0Z@AAbguȘK+@Wo=/\ǫ=q_um ?[OTI?׷djpK [k1 ?r# ,[Ƽ+cдQkTUhlc;xӉh0ؽQ/n2S|.~q7@tBSjVO}6xsty' -ȎMӤCsF1\;vpFFxzr8 ZmCZGI\yzA=IG i*)%;ioԕ"  yM{'!(R$E+J⥯(ސE\ѴlzdJwSh-J <+vSnś#H5Д"P1ЌވՈ]&"m?J`eDYL&qso} YV\=s>lico`X , }?LX~D)^q'J,VeDѴ-H{QJwt-x.lMG|.vM$LJ {xS+z^e:YдBua<}JKߪUǟ ~Z: ZL@SvIQf?jG6UV@_Oȓ@Fd 4GH+z#SJCnN\AyY**+C"E(pûs,ьc,6۹5dǧ)sP0E POܣ@:݃ȌϢdi[!QJAV|6AaIL fh6YX:44=RﴩRbz@"#w~dw ñVc|G2,)'PH _J)/>K&I;bK.-jǤR·m'?"hX "L zZnE<|7-t1E' x,}F^ W^Ҿ}W˦(?mX/\TWMno'oɧZv A_ZPQ)qaL5D6Kzڜ!zg):<{A'/(>X=\~§TO_=8{A0wd΄;ONIS. bGBSC/XM U~Ӫ[ J` ick=#.ikZڈvPwE|IE}`f _S,sۑZ&0׈:>zW$)M{ Zi;uH 6_[Z~FC~=Gm{"<:Y,gvW~%JDvNS掴7$ 2'߅ÖVگhaݥ2R&!|~t A${7O,۸o&Bv]46aneZSѳVmWI MH.*lgce~{Wuߢ ߵqo笞JIڍ93W\{JLx=mѲI=ýF. }^xfƽMGyŸ#8u`#"#W|ƒH~gb f bhC474=aNys}R!]HQPa2ȹC#K夬(Yۏ:RxR$_8to53YӶ=0Sr27)b>(mLTopſI($6_ɫ՘ 0dM8D5&S5}M49J`0g$=] pݢ뇏x3fjMRm*0a*%M}z9=c}7ia&Xv -=dR*Y$%[#`0}rd'+6m.A}x|#Kn_PH$:z%`[:L]Wb7Զf gn!>Bs6c3yIjUWڠ΍JCN7<)y##5ˆgr#1ҁ/B~&aGV:AYIߞy~5уͳsj :Dw0 :  (OUkᱫCT3MʆO4cA Irϫ 'Q0܈?:?xR'5erQ\Y 2^b"wZs|*{g)(G/uNt#(OA4E+KϠ1ؠ, ǩy5 6wm;Ƶev&DtV$YZet=X-<]<{yhOQ\haiMe_Cis"HALS$a|Vl5 l)sO5!ܱ2V_ F+CuZe|8btdUzLp>t hj}-RWA]b' d"ɕEgUVfl<X;jEw౵qJ#M2>C1,{ҩՠUi4ZR9t65K|jʹUm>]x]5toj-\,_^d@oG45omCiy"ԟSͣiCUcA Sk,h[va'4Ҟ땆tYҍ ŲnS9ۆm'bMMek546#;?;yyT3mj 6Sd q*b5 <VC2GStYҍIsCmə#m۞<|聡R4Z6VCi3RyA}i/fαu[ps4Kr2}ɠ_'88h1noټsVKO]n?ΑoЩ̠|ME;כ'{jӱ&TN 9FN 0x[5qcѤ[>H9%ߴj1ml*NYrhF(y̞.<59{}s>o{ʕSFSk`2d9t_r~ӵvww5q=RUU7Z]Il!TBDP#5 SR7`APAPAyBAyB AD BBByQF xs ȧ (O (O)7пwz^eZh%֍>~|;H>F4Ml9m@ HM[s6c ,Z,&O*j{+ncM;p4T[8Gs3U׮ls '{[goG;wkγyO?c`g?OIl38锁pH\6+,xU7/;]{/]=(E_ח:sHV;2rnljn>Ok>s=+ٛ\q3kjͤVfoE]pn㑿_G^0&vǛ;("؀O4ʿɄhyc#sdbtɼEMS4UTlb+xǞ3WdŐgn{:)5t1cOFnt4:)=AÆTgY0̈O [IDATeJHV˔ُX>c_q)Otw:X=6?XJOa.'$\3Ŧ1Z!Yp?KU,^qV/Κjh:ر`خ[<Ϻ![ u ]K$I)w/zJruY?՞_˦=k䡢wu}?`Ŕ)za ;%%86.]cd*W;gFw%)y{{}-Y ikW P֖1!!!GVy@GnfSR9P@@@,OˤEsǐY4EҔD1b w8k啇yb:w!G3KWPfqa3;VDƐM7爜Vp\$~a*i=6[ihRKcqPEu9DSOйXJ%f"Xrˉ&)` 44Mp,\cWɐ[&;nwO4׳% yi;gGw_%Z`fC6am`!{|w9"EA)/MGUH83\ ztLեCZ>˨sHhf|m;5ۦu#CJ,Q<\{3]rQUn{FN$=e|c32tvNpw?nޓz ]]2lTś7GC7Y@ddfaE%EbqNc˵6;Gt7na]ܵ'4eNۗwү+mm%o]8vcfS[Ͼszцň }}[j!:KzȬvן|޷ǥ =p8!b>bs9WU)uV Z{o=s 5sht/&ƃ;zkZue+"1ڹs7nLYY;qԘNjKHIlnݼ쭸EPR|@"x=EMS˲wLrY뵋uY*/;xWAW'di]u0tww Lܾ EP,(%i@πm%HoҴ61R=ɝwrwX,B*KՑDiӲBBM vTˀ P~KͲbsr$l6 RPx lM96yŪ_I#r\$.?v,1;Cks.'6m;61?_ogggddDjB!rCS'A^EU%.rx<^k'S2L*IڷSϐ1adi\N}=x6%HR jqSꑳ1Iʛ;6f˖QԯnQX̼r ?1*AC[7y;v1[+ ?=2{U@ΧO$8rcP2uVPU).ǵ-1i,.qY2.q>qٰO0VxSPJ2h*͵Z4ńCbW!85+(Gݗx"r;hi(/,!^b͛]-.]x ^.T`:Sunue9@WqSS }Nƴ]:2TE׈$_s:RMd+NDE[9 %/ߥXP?lp_Ư=m#xm ?tN.V{؃ٷdHj@wC1_m.Ғ23Y\x2yuO  5SAG~E , }h#4wAo͎OX:vـѻ03 y5x*w2NYPRpyʘʘ*ҕ($/`@qQYphp'tJ̠Bu81`fں72ʹ) U;0@^gA"L7*=gϿMhy8u,0{zׯ$W(.j*ג.f\NUjuJ<.'X섌z*݌:v+*HuX4zlۼ5! ^q5b&󹘕]ixPf+YVQW髺\   X=hٷ$MI^RpǴOx$(:li74-# 8"q!rm']ģԠjKS Gx”ҥȬN]4)v핆3n<4%S04S./44=# WHzaF rOk1yz挑d),VUJV7;aWGWr奪ҫyr@%⏕b nJڌ6FF^FG]޸ПnϞaINqg2Q(c=xt;Ƽ{W2\kx_%UJVܠ52+ ?yک(O#qԮ"Jdgcm,>^]KA{'\+_=y= |y#H]Jn!ZC` . = ҆]^5!&"#sJ@mAT ?=T*__ ҆@ <A4y hA!CA &AM <A4y hART*Af^Eox)>c'eJ5-:[84.6\}ӯɆʍxz̑8|mM;1htC'N>7"p0;sm}GE˚JrRqSI5@Ww )WNug#H/r$s#6zݗߒ ?ˋk²=JKw-6RܬveR<%tT6r-~攱ځ?s[;o?%7m)B!ȌI^}JKofN:33!i!6[MfPNYO=)GcRǪ+2KA!5LJ/4_[2u]T7H)^Zv};F̒I M0$Ofg#Q)5z<]cVGA! R6y" iln]kJh0טN+bs2U)kiHw⹛W<5%%!+Sh䲳&/=;GVh#hC+edP5dAZk8b83!'F%v86cfxS RgNY<U A\--ϹL <R24eUSʴ_ǭ*5+Ͻ`z@|ac l~9NZ*QLS)b=.5BMȯO]#Ǚ;^M-T}nR(;U PdtΠ+OYlN^S748'P 0mrsRNd Hy7%56}NhB2gy2:^e>;=uhܒ2OZ^[~^3kN^هt=o5c1<9}.nk6._?˫iAT_YEZFs A!h A[AhA &AM <A4y hA!CA &A$pFu'}Yպϐ N`M', ]YYܹsōں։ZIRhtS^swWql{M)eRJEfpw Jf&xEF<˯;o<= 66v[WR=iZ ZI2PAfhpRZҝNA$0%\.ۣF>| Jupp5j͛7\{ۜ} /т.BSmph3ztV]ez[uV |R5[粤~"*z,&u?}!Ѻc~}l oZ]o̤{#AE񻺎y}ǯε4%*0vݗЎ[l+#67؝ׯ_+**<ͽ:#2;H=x𠺺Ɔx "'f%d~hͧ{i^ZE "Okqx"3ko>U,5 j"2m:)>*R~SY i4NHs5orQ[nҡV`%9d$$$XZZOb>إ,7_gէOc~xc55ZY8q|g 9b@ ("#ϓZ3jC[ ,YOyyO)NR47CUUq3;'Ϳw]vĎj#%W8Y]V:U`נҞ鳴}fJ,vVyGJ)z^ů ZT@:Yƻz*JѻrBॹx h>=y13e3F?kӑ)l.;5"u1[=Y^=,`q٬;v;WVVsS"=Fӗ,tƋB] oAQ5vӜ?ڢ U;:y46yuRddRAAXwN-,,l߾Ȋ RRWe" |H{(z@5W sT.t^Ѯңn_VrG]m]ۻ,?w.?kŋw7O<`.h |Nr6cc=?]3GWx]}Q81?fm.]wؼm֫.i`hhmGgNAL?3vtxȊ#3KSJҎ+;2z]u"f߂Lߩ H[: fV o־\n``ywN>]JJĽ*UXX"mM^tCku湱olƱAUizw{bQWW𨨨ǫvj+q*ct`BA&%~"*4ymB#r]Lϟ A! ')Fe[4jB <A4y hA!CA Z)m<A׆(,,~z׮]OwqܹNjs\v^LJ6EY3~Y>U hZ)p?žT~tuuo޼)Γ ]'vA]:kqXUZ=GUDDȊQmH[NZJIjA Ͱ`up>"?"pC. :~*V=DA4dՂ.lG1::^K5ֱc\ ܏H8I\E@s\{m1s$95?{})6e֜k3F-;ꋩSN31p$%Ya,\H TZǏ_|~~~ .eM{l'MY88U9Tej3/1<43=|yFcSNuX㘞]k023=v3=6ff{qY"NuDZā4 `WNu^UdOs4ĠR4s6+QP]`@F3&ZS`NW13M|!xm+ ܏?yn_ܡb_ruTfvNNV*m&!#p? -"A lz>1)PqLiJ_̌;AVy]tttwMdEe7i德 \s3s i6/#?m/*n;& bfFN9AfDJjAAA999l6f3F@JF}'cԒf ﬣmlޠ.sj^l^]K=Kg3 ӥpni\H ol[)A S%A.N1@ dgyCA C>cǎE ??AM hA!CZxǏ?>,,fG&$*w߾o٩0ƶ5"YPQÿ}Gxʏ/F'VH*ihwfPmQ믿JkH{Qơ^޹`9dt/v7A& %O}ݭVqP̈ >{Cb n7P{:c& J7yby1{Oj'ح_}{*_e)1OsߵfyyRZB)v:: #C|/VhomP/3J) ($<*;DFQn`/s'͑1g"wdk.Y^C⠎"!au'cҞGF~:ATF}m춈${B7nӆ8͎TRqȬ6xE gNvP),|W'qp!2Y6/H&3p3S^^~'zMom\/e6]owh?8qbŗ;bn^{kf>Fc<,>;{-%[x:hlݼr$&!e>f)rGa'AI Ditд2E-dQeUg?x(eb򍕇֏`h~fA555Eu$p EkkҹL^Ǣ>('3q~[iUC3UC(J}y1\'`k{i; 27ﴭ: ҞU$ }f~M+湘y;2eLd~NPM`h~f$(Xn{2jN~J(~QdI{ȢIn(d+ޅvs윿~/*1UXBo)igu-ĬJ6ˮMO|tNu2C,> p7OJE5%:`O}ƃ ?bccQQQǏ'Q^7 zcYabr.qA~N^e%VTYivjcsAvC7 [#HRy2iiSbodS_G۴-_<0~ N7x)3 H˚<#ERY7]#A^3=nis#3# ;lO$=g«2/tԴ7wOv"vhAy)68z\=Ow+.Rb`:YgasY{?I# JIO_oc_nX_[#k)Vl3^Z!!!!"S(wBgϒEƱ [""_9 ]i sl5j^kS5AIUey=lQMA\VKKeNew3}B̢?Ti˴Ԍƹ>fϪ IDAT SԦF"bZ'3qv÷]%'vT<W(*ۯq"t"-bנҞ鳴}f"MM^񿞼WчWOT6:蘭,/מdlV|w;WVVsS"=Fӗ,tƋB] oA‡yuA^1,Qz}GK}qn͎oS8j:.euYs.^}վ]xSwh:\l3az~gؙ/37b904 ZA$%8?E{R&  <仂M HSp? QK m*[AhA &AM <A4y hBR[g 5VL,Pe4&v]~Хh*xJERcOT Ut˲-KJ7@JA./;+3'wheߎqۂx쭯r#I|Yߛ]'ĵ%RE;J)u#u_}?){f`Cgt?ﻉ,S=*z:;1eyi*g"|zЌ tt'0\0=@ T*UKKfִ* +"p0;sm}GE<#ՎK͈)kZKFBV'u77Ӵ,;G ~#ZhoY~eT~:Z)r$s#6zݗ 2.hoډA:a&Ϊ?)~wwx &O` 3WS»{C. (>U^UUŷw/M~܆AC.AƁGw{fz1wwqٮLG>=\&$-Su"aT!#3O uf3-_syhɢן F!4K8w$>k^wq7 INK 9j~V^^wYTZBmV&Ϊ) ྃ>Oϯ?>/Th[OxB-LOdkD2el cIBz(tQu1f1p]8l ^dzzي|?M|BSnu.Mǒ"%^G LZtYmIk+beηh[?LoP-!.r{37(Mg͠4m~JmSY;w]pK =2YCQPw&cXԍhOJјԱ LRPsH=C,6OÃte(MˠZ|4}oLVLRUN3 dwU y=}yW;XJV۰pkvd5ǭ/qp= vyaחM)3"KieA33shPFx)xu"]:CUp'"`K nt"'Uj ٩$p#$=N*ꊬ}~to oB~EE~bҭQ]$/&/=$TZ2td(꾧muBW{_xU &Wq?'EW;i@G1YNU~ FlPl,wˎ? n&OKod1zץƾ'Cw4A*í :[ s:޵-WsJZee]i6QM^=\?Ċqإe#W%h2=F,VyB\;38SJPS#.Zk?{πqkL?hҵSgpQtL]AjKܻNL}P=NPv E]cT;G+e4T9U',0gwҒs K\r*F*$\\U>^ZeYǒzȑ,b&cO)̄)\kPZ"`K,<\v>].+3:Ygֹ,T ,ve%H1.JZ/șwӿD\.7fҽ-?]%BvA[Ĉc1kd&]{O8s? jԾ(~W16ѹ1$nPĪC dϽ>j6 |^o0?7ˡ_s`|9|aʖ{zO@fZyFcSK ?<ޗЪW* rT͖U#*@F3:Ybd3'Gg Oezv3֦hZZZ:VقIm`rU%9 je&z7O9S@#i׼GGyػ:NX asʿd M:wG52rSr:q "h"+}i'J阳Y J H9iaw䯔­U4;T7MS+iPJVC|ӿon p_ %}4ϳ(H5]Lun;& Ovrd~bL/lhxA#?m/iZa)eT5&6㦻N,SGGm8t)Y0i?e.W'4j?#(:ݧ[/s}DZT-{ _uC_zfѰrIs`^P}~YוfA{|!NV/ۣb?uI哃RLc7-dES}թM+w(QcNe_,(,A=o+ق=S5,,qD]hOi~|btAi6JgH-ܗw[zFD&qu*xhޭV* EkBZzCm ` H%(AhA &AM <A4y hA!CA &AM^AZIyg---f}hYQ!!!ԱE]lx 9٤#yU#&!.-^h^T_bк!ȏw-I6m6C}Dbo?؆AY\+:_쭃{hv AvC7  ?nd4{jvEM>6joo]&JʈE0gxT*` QP&tߟK`&Ɯw|ӋrmKw_IHM{snn WƓ1IiO٣j#?=W@P239(+w4A{MɈ{{w)'dך~]Y&e%YHȬlhT"w999"w¹%G"`+ٳ<:򧴮uCy~7]KCgߘI[oc_nl۪Ju6:%w}<:թ?]k023=&ղˍcCk`3tO*$T*%n,J 2j#J ]llثuρȊJz@*{iV?j^ktUey=_d;s@fz嫒E=TJ^lPG5y͆c8"]s좏3Y sx~g5vŕSMpiYS,tO۰qPa6uuHj0]Vkߓ`,$$;}Gn !/9`[A ?+w5[;fAꃞTA &AM <A4y hA!CA &A付<݅J#?""C`M쨹1{+2yē21O\0ߝoIEC84C o.ч lUDW$={>iȤ녲.*td"dU R!xb\v[o? _^aiSqb[ Rjgcngs6Q3OlbcFצ[ ]wq86OhH ^Gg}muu/beqk۠A JM޷R omSf͹V_q 6!&YLJF}w𙷫GPѧ/BЎ&F7R29M >kǕGҒs23x4OMNKsݳDlGzxDŽ?ýtd凝 2$?-8p.$CtstƤ<..tDȮX{™ _VTЄh$GZG9 -,J?]gQ zsebfSV[(_xƬ頽 q*n*qva=f {Mأ^be +@eL>h4AɊPe[XP^ MPkz7s/_]SK0R2myIgU%9=6RnK/&N \'َ01څSBx&K7KubIE//-2W8h4AMNsKw`0c{iJ#JZьl棐Wo^2IdKJeH&*SNG:{7 Ox-L"IY^A>h4Aɳj@i)eą&SuYD[wK8) e?w>̺jީ;֜MZ"w ;PN#?#S4'V/?މK4{܁&*Όtx4y5MPr؜MY]\n2u 8IffŇq)jbqq*Cǻ^LZ77ͯD\NdN:)`b3wb>0\6S4j \1vL=5"uh2HKGMkӰ ;Uvz6~W,gnlmRYG$o"u8Pg2V0Wy0u?+:2ꚟGwcNf=7\:xCY[8xչfjf>_y#Wsw56yD\|G]y۬W]C~F#v42^0h_iC7MAoS8j:.ebb 󽼪ytz#\[vZ`FKァ o%<1'Qmd[1r_drΌ}=էۯިFr B3p.l- <AZG>Foh ӄ&>J9j7mGikxU8sGZRNtqnAklЏ#yY5q$)_Rˆ+&|%{U4ʻd}wϬ:IO'SJ ܝ{B%Fd%EkIq~:ť>篱A?}dk$HR= CI߫"祱%E`fAғb“l2!pw*.哕 Fv-)Ot߇56ʏ#qY N?~% zJW yݑ6ղ%gIj2Lfi&_ruTfvNNV*ǭafffVVVvvvNN񢏝3Y #NAY@ѐHX}߫"I8_dR\?OʚCfDY )EkIq~:ť>篱Q~ "HF \ob$)n^]Rm $s#d ܝ"/-y4yuZRNqklG.5@כ=JPKӽKJ0-D'3R tw*.}JfEkIq~:ť>篱~ "HF \o(A17/M.)̶SJݩ,Ғ*AAO* pwڼPVѭ m4y!` lA` &O4-'omq-%}eق&lF؅7{ Цu2:nhZ~nDZ?%Mޫ?]yUj4wђQBkwYR&N#^?΂A>h0 [eWkY'"OXw/+}T;5s W2: P6 bܲ{$3Abs)bͲM)Iohaj+$kZɫンQ#_$%9z$c_ MIs\寓_ODɼ61R J)%KQ$ x$P8Đ bEZH2 OMYI.aTw$k$'F95*Lm8/_fG騎M4U0)68z\=Ow+.Rr̥8J x$P>I72D 6` eE0˂M)٢o+z%ٌ%QF~U6U|U5[VQ&Hi <5EfIqnKЭQ\ą9p*׮L0wT7&j5l`o?URIf7:eˌd-|(2#^d Myc3N )@&sw;0ޱ4Ecg3z pO^D95q.F πsbያE}yU$dBr[h5q4j/ɦ }#,._OĦx`8 Q#K|s]YcШ,weg5ߪwyQmH[NZJIjvC7 ?nd4{jvEM>+7`@0t#˞7bqyceD_- $۴f Q__`A;OlbcFצ[ ]w14q~>UVZcع[k-ra:8ɾiA[N,FmD##X0yD{lqz?+ (;f wި~bS2ޝ@}1u E&ߟe d?e\Q&嶱" ݸNjʊGgFfegoP9_ ?#]U]31漿^ ^1wӒ=j,eeTki&AMYT%5x@V:B{fjئ"M'3E֒4u |o=($W/vw&D,rjF\@}364TEʳ<;⁸Bע'6ئڹ{Qsgi+<$uP8beYlWV-\Kv{R0ECpBQ0UDoW9Ag-=Q"HBMŲsvrۄj.U9UK}Lq Hes٩S`}~`bF;~ExQbWW 2HT=6+*˺',.~} k k՜TW߉SdsO=Bi동y%.;}x4j?/r2n{2jN~JǨ+P0.cd2F-k:W 2篚;<-/pu\3{Giv]λ̗hzY*;c=}S+g~A/?Xfŋw7O<`^FKH+്aWv݅N;5\zs/r2|Nr6cc}WI_}B~cԧ<1',ol2ȼm7 [\(#m[1~[t *A$M^Т;M}~HW8N[yYT8i*yA:BM l  h $z"xWkSi+PT| HM hA!CA &AM <A4y hA@HkGSSbb_`;$}C~0 <o...C% ugyC-SQMnHHѳ'%,T;c@5 (T<ǭ_5 ;ޡ8' 249F}]:kaqzr<ҽ3zCoMg-PSÒ(DH)UP^PݠqrDܳo`̝kl++sx6<(.go=&A5e 34;xqHKS(RE".ǕQdbV)%%ԩI*ҧAYQDjR;wEy7\{"A@$ (D,EAhlE `ZGr$hbnfwgCbi+}cwfw9ށF$1x6F>3y8^D%^9^`+-?WZrrN ܖ+Ǫ^^pQ  F1Ph;r%_LHvR^Aq<2,\S6uMQq٣IN݆_\r$ccJPP)G(4=Hի(;ɖ;gHZ8Л><3cΫ0 V,^ܻW"9J^b\>tػw˯O+?~ ܬץ,~mgř=;&u-;HP A? z]P@p_Vܷ}U#FLmTkݿ;߷yuUsYf 1>* ֯ADe$"^\ogit:Աd.܌6"[ZO[9{h-z$YSGM?U{.u/JI&fonY"yMD[7[yI 2^)jS=keIٯ~֢2c7g'g2t*ࡴJf։3xg^S0/po&ۈ/P9a(h{;,l{"U92Mod6:1їZѠ1,Y|ކL6Ăo}Xmф+^,ɼ g솲sutMO_/< ;Ew-XC~wW=_bKMozut뗵ԨGA8$"opqB3}W >Sc>9rkG[R'|8,/5xah=' qnłN'*wm"*#*# caQ-Ƙu77n |I1ߏݺǾu_r}PDEN'rI' b3u[KDǧj s ==k~ꎢ܉>TR^w%f3آJ -X0]:DeT# rv-YIfٚGW/j;44j-3E_FwVNfZ%1&wqᦃpV<2 e*e01>Vqݑ_}/buu^_:J6dXwN_3ˆa0&^'<3cSKhCǿk.4cGQWU:r|njB(0 aAB9ZsW7xM-AOPe%G'&._}7dӨ}Ƽŝo#yx 0I>w*c&s]bzZaoVɬua ݾk/60>+qm>]1HoƮK/-*㼉7ȶ·)Fo+n[T&F#J"f(.=2,')dF幫,8'2}tɞ?zMc!Ddg{͔Q;UXRˎ*v)#+fߴPoQwlŃN<7gſ+ e~O=ۊ~}c3+m޷=SGθ]i^y&(eo؁vs5NxnduUZmӜaG+65]{~S*1{Ѫz((sA-vZWXȷo?{[WlQlazs8Sch8Zc̨5[HC=Qm^U.^yyG>3{(ig0zk>IfbZ7{u;ǹg|Yf*X7pM/ԓ>?J(}^6?mtɤ ^@HCBlDލYxA5{>XS: |P#}KJ? n{3GWlѕ;@W_}2rd8-ms=)J1JOH.;Lt¸qUoW/G\ҥB2]b<23ӋV,ό78<: d17 q泵 wʻڭazia~Ⱥm{@Q׆y~+y(:YZj//O ӛm pN/1T+k5>!DjB)Uی%)inl<o#؆+ F m\.ݼyM[ob 3G%ȂANxYf5{&NjjUVlp,FUUEQEi?%}(N$I:?FB?ƾ ȐFٻysMN HAVPE 'Geg[(@:vⴣW-RVA:~Fr[B`HLL,;РTTO9Ţ&'bc&dMMMX,:1dYtBTh!y(t`0~\ 8N$Yz}xxhĤ9e'y'}:.::K.ٟ 0BlBW͛7c0E~~>p-B"#<FBa!FBa!FBa!FBa!FB!|B <Bj]IENDB`liferea-1.13.7/doc/html/help_vfolder_1.6.0.png000066400000000000000000000355501415350204600206340ustar00rootroot00000000000000PNG  IHDRVbsRGB pHYs!3tIME841TtEXtCommentCreated with GIMPW IDATxwXSf&lD@nQ먫V[՟ZWպֶjUZ.Z[+PTddCFdC$"" %G_'$ ,$M5472w.|m=EBRs/Nf6&hxY+K BFtt9d۸ Wf4yf.BE޾=HSԅ>?hOv沞>kk5}$jü~{I tęe=gvMM]{"Igr^uDX}>?LV{¼Ǔ XB hg?Jr@dh#\fyߧc&e.rfRE/~ڼ9q{hDQB$g!>G=~ZIl"ыGHBEɄǜ3n\|T4g%2?P[K D WEDgIY3'wХcG3ޥ5CtԖ+ʗViՠOҐ|Ҷ#`4sD9T<| W2>YuRRIfFV^%nKUV0 X1 Jnlk/Z"?EdlCT<ŷ6ԖnxPѕ'AVmVx#O)tϲ)9oK,dr1OK+iBe~ߝto_D$25 Pn~/)*v 9%ewV^W}=zXZ^ǎR'\;Vn<111li7V>KD"@حs)|R>Kۊ cjnܣ}.ztu"Q@{SKRGԣ!PԳKlx5'K%iNVqDT߸餻nmL-E,5rǐ';4g|8tIѺcwp'Ԛ+dQJ̃oF koee&j-^ju<P'Ej3R):xB@x@x@xx@xx}e¯UHH:^]@@R|MGU'jj-xxatR< ſ2XA;T{T;ѥecGzMyⓚLP6==Ujͤ{m>|>Zx iQ5%Kx)F2)s NmRG,L\:}0?jK#Oը䥻luNBܑ=qqpv:x\9x9pr`uw^皶ݻϘPm;/*Y#_wmɵyÃWiSm]_uig_c!ԶsڢS}d_5]| BKo^vI}Z[ͺ_]ޣp%)Hp:6Ԣ>X>}ᨸgw>gIao9dEn=Dݚ"vأڣwz?SR(%ۀF|˿o4v"&فkUj B{t3d"1L>xRl̃Uslpcc_0iȏ嶮Z3{g2eZ=i>=g{DͲDriQE\f<#+Ϲ٭n/-!+Ȋ5%gft߮sX?Z!"TϮGqЯov͛4lܬ}&ϟ )ȹYᩆyhvڝ8+5$i߭b)ON<y/ov–YLD\v bqBTzNs-E7f=Ո;nl2`^lCTC:ZWQJJ)XNu6ᱬdMDM5ۭJͩyBxڗ+V^Gyi^ic#HI!K\`'s>';XZaJyI,ME{]O="ѺWacrԜ99u67آ]Qna)o?(g7wY|_BYQŻu̹}JI<9e6xcDz4߆=1ߎ/SV4hݫoc56옭F6^􋳒R~5g؂PNv&kdhGv&z_IZUKpۧ|:xkr2?N:k޷62\N}rA˪|ʦ;9r%@NvFJ7I`cxOUʢu o.ϊc@8?t_ﻥgy'w_w(4#_e}hr"F3wRD^Cc1*V߱6JxSu?wNNn}8MmY{_y~znuf9hUmssm/rOuO7vg[-~Vy/,:Z*}|Ock,}/Wzml~um|d_ .G4d;)X{kg-Phï~Zbo+*vkM xmP]oLJڐJ/@w?wu^>4 P;b,ZHHHHHH՛b?ZMǾqou51ج>u=uu| *%D)ZXsF)&4G0;ΉD*G;jމ#c BSQ 5dWy^N^ [1@D:$hX,w9ceBJįtF嚬;bkZlwGWg'٧Gv{...]X""Ib7Qw~[ݣUC4ֽyv'|]|;pVѳaΚUUzڗU9 H3dͽ[DF=޷݋G>D=3'^-9{U6D*jO fkGDGMkrJiE=pzл'J^jx-M݈f * iQv8ђ >ur0=cX" h]IB1UOAGiaT Vwh-G9׍|1wʹ }'":p6d){"d ,B "')ơL^b=ۜsnḿ+{Cb⟆X"{aFnb\i_V:$V~:K.3Xes#'LIN =%䬺=фv]PV ◷ø HU* .rv-E*ɛFX8VXe)"rs/ǐ}] .nK8Ihw^'~O/Uh-^wu 0sX|}TF0"[v̧{:7߃ #Wx[Uh/*Y=Ʉr~h\^O2gUCR5"vQN{ꘓPY/C{— B["Xᾔ<"*.,5!ZPM% ܊n,<_ag VWK1}zj+nor^5#/y/Je9J#z7/Q3%Fr&4'iQf%Ѱ&#UOD4eY;1iWvTSK/CD&Ir1ټO.|PqڍK~{Ow4*lͫ7>'rbV"Zl9^ãGA^ӽZjŗ_nX7_Y2֦h(@e=J4-dx$`-^Dt(m+3ٌ6GdbHv8_DSQn=c-8) i%3RAwiaT4WL7KW<z"ڵafm>myYUwQgy:vfNDO2+uS^ǜ*0yHa֎_<.$Hg;y]xV+I A/%dM,lMu16HlLDkG)7OZ*U頻tFE|AS!|'i51`)xs)D۵Z(`+hJJMcx 3ӟx'R>d[1 YtY}Դ1') 8vYvzL =[l.=PGsЦw}}vNٽmv i.῭Ǝ'8Xt[~tK&.?Ш^RAwiaT4WqVۤYU{BQ"ݭ ~Lg Zui%D;tXyd_{VFXΖ㮝5`x77gw_1헾icNBTO@ 2θz15X,אTލS̄k>OD5]P /5xxYx5n=S>n 5jj3xx@x@JٽE}9HJMME/X@x@xe_}effdz^h)FgdXrǟJ:"ݫQN l[= ૶D_ b< Re`ГZ ޹sΝ;R^7w]B]MWE%/jUV3/9mc*}T& R*|W?1djj:|ٳgϞ={&&&~JRT[55S O2_wuz6L5gjӄAcd}''k:i'- Zѹan_E,{ԩ޽{N Xлw'N,[PbkwigJyI79jũR_Qo3оn9['.q7TQ]\tJ=Ų'+%hO8]NBܑ=qqpv:x\3:ؿugG杗d?dN)z5put :I(wtQ)Jٚ3aU=֎'_\br'b}y֍֧\aaa>>> {dq\(TO&M+*1y;G_p.C>'N;" ]y~c#NULU7yv }\-uO.KZs00ɒMjvCVtXIԭi-bG=JZ9"J 6`0: o| r>XpNk=;1GA,Y*XU9Lz@çC(95g@)Tήw|¬쬢[,"66v߾X ߣɀ?13&|tވѢf}7nh3#bwM( )k80l0;;#W%>novP[T\e*w>ccSU~Rì'>i[hHbY*i~'i_^ /V)Ws%oRi҅{op"mXSOX,D*\%^.b?Oy_(tY&BgvKObچIgtQ)JmlM0P&srWX%q 3a۷n.YCUV".vښD,]oӲTMu.$YYYؗf jbiiYJPH;MH V׬aF_L]NugnLk(Nn9{a|Iim-]ANEK/Ν2UϾ덬J-*taT>~] ٦oN>N&b*=^c4Lӡf' Ԕ$D"IHHY eY8R?TĖ^eIO-<oPuۈ(rҀuJݧ}픴{ LIRRRrrrJJT*MII.Ş`q`_~o˅K0I)RirDZ_5VIWrpyKW[GoК6=LçC; M( ś-bA-&Ld槟0Qi#ݒ$9"gFavƁ>g>zE266ȰkRǢBM:Dinn^(ݡ j}4cg\o"Ɲ7 Dd-?sK-A| ä>azb#[s& Ԉeܼ߮菼@. B,_yۖM[7o ܴa˦ [/1G]ވo༪#1D4!?x ?3aOt޽{DԷSSCA}[:ݻwիW(ݡw_\=lij yX__~dYI؟*yB>LJu'Qu5,,,,W*u˟ZLڔMcwsJdR{{KGG9zvfR 딁e;TU?kw ?ߣGGQܹoP[o7fxAm] w6k֬1b+'y3o=#zկh~{`۔#ͽ =--f~-p1?TnVZϵ]}Gwv׼s_tsi7prkO79fFzn Z/9bɢjjJ@tQ)l͙0PRkDMIϞpŋ7o{S}5D4lښ""" `ffV^&a`V<o֛D'|FweUs1իdDdll|7>ojT 8,yו@ СCTbo`3{׹o|)U99:mظ8KKKݷU)~ *HHP) t1R_`oX,B @HHHHHTא^Q@@@ML2jj-xx@x^J;;;{ |l &쭦*x#HMMXc0{j))MbGX_\[zj5ŋwpnfu u>xK(;rWWʻ~/JRirB~hwoUҫxUjB m>geEGɫQKwYN='!lȞ88wΎ [vK=]N)z5putõ[Rr7-u'ݻϘwX,>yN&^:X'_wmɵyÃWiS5n-:985lmɎDQ/]jaU~u<tJqĩR`U ohC%W5E-0cӭDZ[ZĎ{T=W?ͻnt\%ypiNJ+,%wӡNM싌M|t@n'T`BD7``ؘ+LfF=gIsW4DgQ}+)ڪR./HV/CBBJ~]_F! #aUػ;lO'jJܝsO8w河gD,]oTM%OaT>~] ٦oN>N&VY牊YreTTŋ;vrWP6@TWL>ieZjܼܕ+V~s0D]r[6UaV!"MB'"#u.PL~$-?oVDkY5i+3E΍5q=c_)|c$1z;%-ាHIRRRrrrJJT*MII-3.sE$Hq>IޫO^9ѭ#7xMhuyb|Ν;usN>*[$d5 VąH$񒄄xIff9eWmV)\(Z7[qާDt"^Lg8<ȭq-'w5$i G>v4YO.%JȽ?svk3|ȧ96Q`Ygt<޷3)2i4> "è坥nBSðci'=Q̓o @wz| 47ya4x<_hV G_=_`zf=M޹jSĸs-ʽ4މ ,/UDpi!vZ\Ϻz|Κ "WS\|Ћ)y?oǫXU\ C*DG9zvfR 딁e2z+A{^Y6f_%rV%=kO:_ S-]yۖM[?7niÖb?mZ**zOC' {>orerk5dӪ:cIDsЫ_onbV~߲[ok7hա5q>a\q۵;w_}h~Uwwur5Ệ),?}빹7hP?7k[>~NRlM9ɭްYӂ=kO:_ *zr`YkOk<{料S}֏X3u>F5A[~O|H :kD @Uѽ:ox/i@xx@x@j>~G`6c0{_, R< R< R<R< R< R<  R< R< R<R< R< R< R<R< R< R<  R< R< @9}2T*EG2b 5R<R< R< R<  R< R< R<R< R<t@bggN tvHMME'sj)jŲnL ɺr)~*_x<-ڶQܻ71 )>jW_íܢo+#k e^~4=Ř77'CWWU% };*%8#l+ʕN5tZIbs6&֞ń?ҩcK\ƍCUJxy[PE8HŒ%˨Y)vZ!ٺhRf \]40iVJ>e>dPD}]xR*d~ulb%w P:]e2\y9g h{w{NG'D_\9׽]k]_eV8"c,)Ԕ'9m_@e.]O0T׮SUlɳ$nQ;/R<.}8:gk!V8` ^ qqd,8͢UElqkkfy9yFv- ^4'!lȞ88wEDW}*jɥFYRﯥxgz|^x8c'/DBScPZLf5+~tvh:6|v31:~iI IR\<5]Yߴuo$ðNV2|2{_Nzs@}TVqjVrEGhpt#I2!Ls{YsA\CM Dϓv^צ[xy3XP)Wst$RiJЅC (5Kn 10B $0B>#P@n>8>rC'WuogIV:Se8iiݩc<9װKJ휒b1jB>-P,jbĪHњ) rV%UQsjB+?Q~izzbgH߉H Vk~ߗy#UNΝٽRvvWLA% cR9"cx<"#H͒T,VsqG>@22WD}?ܾ7XxE۶2'0`՜GlM~g DpbS8c'fzy*J{Z~Y'Zg\nHJJ8U5GWc$III)))R4% Sf#HjVji<1#?1C%Oˑg)ry*P$LNqV7X2o"ڇV>CkݗDR_808Rl[6**Rm g)ȫ dm^o>ټTY' J<96xcDzMTq]}#$qnZ&SJM8S95 < }!;7sV2y~+3O&bW5oo8R<b뛛ͲĒ@nu[$R:<@~tD2*RmAXa,[*ZUÙZ>rͷR,'yv܏:iJ߷62\N}rAmʦ;9r%@NvFoۧ|:xkr2?N:{C[9s195:tnb߾}ľv^2NTWEMwb)gw7i;Рȕm[O5 M<S< /76O޸pLGNݶŬнv_G2ƨ~P:ӆi_oz{ͼX;T|!ks#'kR m~|;1v]IR -nvl&>QPHޓ'}l9<Co|X/9ޏM[߮~OԼO]$bُvNIbwo//sc6o$sX8Иo+NHJ3jҴX~0=pg>q#HTr<Ҿoש9}3eJJHHdDؑM{FJ1C&x| +=ML.U|>LeaYVu,?NO/|ǘ<޺Dx(JTjdI;Y)B Schlagzeilen und Sammelordner

Was sind Sammelordner?

Sammelordner sind Ordner die mit Schlagzeilen befüllt werden können. Sie sind selbst keine Abonnements, werden also nie aktualisiert und würden nie eigene Schlagzeilen erhalten. Sie können nur explizit Schlagzeilen vom Benutzer zugewiesen bekommen.

Da sie die Schlagzeilen die in sie hineinkopiert werden niemals aus dem Archiv entfernen können, wie das bei normalen Abonnements immer geschieht, können sie zum permanenten Archivieren von Schlagzeilen benutzt werden, so dass selbst nach dem Löschen eines Abonnements die zuvor im Sammelordner gesicherten Schlagzeilen erhalten bleiben.

Wie erzeugt man einen Sammelordner?

Um einen neuen Sammelordner zu erzeugen muß "Neuer Sammelordner" aus dem Kontextmenü der Feedliste oder aus dem "Abonnements"-Menü ausgewählt werden. Dann muß dem Sammelordner noch ein Name gegegeben werden und er wird in der Feedliste erscheinen.

Schlagzeilen hinzufügen

Um eine Schlagzeile in einen Sammelordner zu kopieren muß im Kontextmenü der Liste der Schlagzeilen "In Sammelordner kopieren" ausgewählt werden.

Schlagzeilen entfernen

Um eine Schlagzeile wieder aus einem Sammelordner zu entfernen kann "Löschen" aus dem Kontextmenü der Liste der Schlagzeilen oder dem "Schlagzeilen"-Menü ausgewählt werden.

liferea-1.13.7/doc/html/newsbin_en.html000066400000000000000000000036451415350204600177500ustar00rootroot00000000000000 Collecting Items in News Bins

What Are News Bins?

"News Bins" are containers that permanently store items you copy into them. News bins themselves are not subscriptions, get never updated and produce no items of their own. You only can copy items into them and remove items from them.

As they store the items permanently and do never drop them, even when you remove the original feed, news bins are optimal for categorizing and saving items. Just given the news bin a speaking name and use as a category container!

How to Create a News Bin

To create a new empty news bin select "New News Bin" from the feed list context menu or the "Subscriptions" menu. Ensure to give it a good name, click "Ok" and the news bin should be created in the feed list.

How to Add Items

To copy an item into a news bin select an item and select "Copy to News Bin" from the item context menu.

How to Remove Items

Select the news bin and select the item you want to remove. Then select "Delete" either from the "Item" menu or from the item list context menu.

liferea-1.13.7/doc/html/onlineservices_de.html000066400000000000000000000110041415350204600213050ustar00rootroot00000000000000 Abonnements mit Online-Diensten

Was sind TinyTinyRSS und TheOldReader?

TheOldReader (http://theoldreader.com/) und TinyTinyRSS (http://tt-rss.org/redmine/) sind Online-News-Aggregatoren. Um diese Dienste zu benutzen ist jeweils ein Benutzerkonto notwendig. Mit einem solchen können die im Konto verwalteten Feeds

  • mit Liferea
  • mit einem PC im Browser
  • mit einer Smartphone App
also praktisch von überall aus gelesen werden vorausgesetzt der Benutzer hat einen Internetzugang.

Warum einen Online-Aggregator mit Liferea nutzen?

Liferea als "Desktop Aggregator" ist nicht auf jedem PC und von praktisch überall verfügbar. Liferea als Programm muß auf jedem PC, auf dem es genutzt werden soll, installiert und konfiguriert werden. Es gibt aktuell zudem keine direkte Synchronisation zwischen solchen verteilten Installationen.

Mit einem Online-Aggregator kann das Problem umgangen werden. Mit Hilfe eines Online-Kontos können mehrere Liferea-Installation dieselbe Liste von Abonnements haben. Durch den ständigen Abgleich mit dem Online-Konto haben alle Installation dieselbe Liste von Abonnements und dieselben Schlagzeilenlesezustände. Außerdem kann der Nutzer weiterhin den Online-Aggregator direkt nutzen, sollte mal kein PC mit Liferea in der Nähe sein.

Abonnieren

  1. Zuerst muß, wenn nicht bereits vorhanden, ein Online-Konto angelegt werden.
  2. Danach ist "Neu/Neue Quelle" aus dem Kontextmenü der Liste der Abonnements auszuwählen und im nachfolgenden Dialog "TheOldReader" oder "TinyTinyRSS".
  3. Um den Zugang zu konfigurieren müssen zuletzt Benutzername und Passwort angegeben werden.

Kurz nach dem Abonnieren sollte alle Abonnements des Online-Kontos in der Liste der Abonnements automatisch hinzugefügt werden.

Google Reader

Bis zum 01.07.2013 betrieb Google den Dienst "Google Reader" und Liferea hat das Einbinden der "Google Reader"-Abonnements unterstützt.

Existiert noch ein "Google Reader" Abonnement in Liferea so kann dieses in normale durch Liferea verwaltete Abonnements umgewandelt werden. Dazu dient die Option "Convert To Local Subscriptions" im Kontextmenü des Abonnements. Der Vorteil dieser Umwandlung ist, daß alle von Liferea geladenen Schlagzeilen erhalten bleiben.

TheOldReader

Die Benutzung von TheOldReader setzt ein existierendes Benutzerkonto bei diesem Online-Dienst voraus. Für dieses Konto muss zwingend ein Passwort gesetzt werden! Dies kann auf http://theoldreader.com unter "Einstellungen" gesetzt werden.

Wurde "TheOldReader" als Quelle ausgewählt müssen Benutzername und Paßwort des Nutzerkontos angegeben werden. Liferea lädt dann automatisch die Liste der Abonnements.

Hinweis: Werden TheOldReader Abonnements oder Schlagzeilen in Liferea gelöscht, so werden sie auch im Online-Dienst # TheOldReader entfernt!

Hinweis: TheOldReader Abonnements unterstützen aktuell leider keinen Abgleich des Markierens wichtiger Schlagzeilen. Wird eine solche Markierung in Liferea erstellt, bleibt sie auch nur dort sichtbar. Analog gilt dies für in TheOldReader stellte "Likes".

TinyTinyRSS

Liferea 1.10 unterstützt TinyTinyRSS ab Version 1.5.3.

TinyTinyRSS muss vom Benutzer selbst auf einem eigenen Server installiert werden. Das erfordert Kenntnisse in Serveradministration! Einmal installiert verhälft sich TinyTinyRSS aber wie andere Online-Dienste.

liferea-1.13.7/doc/html/onlineservices_en.html000066400000000000000000000102161415350204600213230ustar00rootroot00000000000000 Subscribing to Online Services

What is TheOldReader and TinyTinyRSS?

TheOldReader (http://theoldreader.com/) and TinyTinyRSS (http://tt-rss.org/redmine/) are online news aggregators. To use these services you need an online user account. With such an account you can read your subscriptions

  • from Liferea
  • using any web browser on any PC
  • or using a smartphone app
which means you can read your news simply from everywhere you are.

Why Using an Online News Aggregator with Liferea?

Liferea as a desktop aggregator cannot provide the global accessibility that a online news aggregator provides. While you can access an online aggregator from virtually everywhere, Liferea needs to be installed and configured on every PC you use. Also it ensures no direct synchronisation between the different installations.

Using an online aggregator you can workaround this situation. You can subscribe to your online account from all your Liferea installations and will have synchronous feed lists and item states everywhere. At the same time you can still use a web browser to log in to the online aggregator directly if you are at a public terminal for example.

How to Subscribe

  1. Create a online aggregator account if you not already have one.
  2. Click "New/New Source" and select "TheOldReader" or "TinyTinyRSS" from the dialog.
  3. Enter you account username and password.

Shortly after subscribing the feed list of your online account should appear and items should be retrieved.

Google Reader

Until 01.07.2013 Google did provide the "Google Reader" service which Liferea was able to import into the subscription list.

If you still have the "Google Reader" subscriptions in Liferea you can convert them into normal Liferea feeds. To achieve this select the "Convert To Local Subscriptions" option in the subscription context menu. The advantage of converting is that Liferea can keep all downloaded headline for further use and while fetching new headlines.

TheOldReader

Using "TheOldReader" requires an existing user account with this online service. For this user account having a password is mandatory! You can set a password on http://theoldreader.com under "Preferences".

If you chose "TheOldReader" as source you need to provide user name and password of your online account. After doing so Liferea will provide you with the subscription list fetched from "TheOldReader".

Note: If you delete "TheOldReader" subscriptions or headlines in Liferea then those will also be deleted in the online service "TheOldReader"!

Note: "TheOldReader" doesn't support synchronizing the flagging of important headlines. If you flag an item in Liferea, this flag will only be visible in Liferea. The same is true for "TheOldReader" if you "like" an headline this won't be visible in Liferea.

TinyTinyRSS

Liferea 1.10 and later supports TinyTinyRSS versions starting with version 1.5.3.

TinyTinyRSS must be installed on your own server, which requires server administration knowledge. Once installed, TinyTinyRSS behaves as any other online service.

liferea-1.13.7/doc/html/preferences_de.html000066400000000000000000000143421415350204600205660ustar00rootroot00000000000000 Ändern der Einstellungen

Ändern der Einstellungen

Der Einstellungen-Dialog kann über den Menüpunkt "Einstellungen" im "Abonnements" Menü aufgerufen werden. Der Dialog ist in sieben Registerkarten unterteilt die im Folgenden beschrieben sind.

Abonnement Einstellungen

  • Caching von Abonnements: Hier kann die Default-Anzahl zu sichernder Schlagzeilen konfiguriert werden. Diese Einstellung kann jedoch für jedes Abonnement einzeln in den Eigenschaften des Abonnements überschrieben werden.
  • Aktualisierung von Abonnements: Hier kann das Default-Aktualisierungsintervall konfiguriert werden. Diese Einstellung kann jedoch für jedes Abonnement einzeln in den Eigenschaften des Abonnements überschrieben werden.

    Mit "Aktualisieren aller Abonnements beim Start" kann das Verhalten nach dem Start von Liferea angepaßt werden.

Ordner Einstellungen

  • Anzeigen von Ordnern: Ist die erste Option aktiviert wird Liferea beim Anklicken eines Ordern die Schlagzeilen aller in diesem enthaltenen Abonnements anzeigen. Ist die zweite Option aktiviert werden gelesene Schlagzeilen zusätzlich ausgeblendet. Das Standardverhalten ist es keine Schlagzeilen anzuzeigen.
  • Abonnement Icons (Favicons): Dieser Button erlaubt die Aktualisierung aller Favicons.

Schlagzeilen Einstellungen

  • Schlagzeilen anzeigen: Hier kann der Hotkey zum Auswählen der nächsten ungelesenen Schlagzeilen eingestellt werden. Zudem kann der Standard-Anzeigemodus ausgewählt werden.
  • Web-Integration: Hier kann konfiguriert werden welchen Lesezeichen-Dienst Liferea verwenden soll und welcher Blogsuch-Dienst in der Schlagzeilenansicht verlinkt werden soll.

Browser Einstellungen

  • Einstellungen für internen Browser: Hier können drei verschiedene Dinge konfiguriert werden. Erstens kann eingestellt werden ob Liferea angeforderte Links im internen oder externen Browser laden soll. Zweitens ob für den internen Browser und die Schlagzeilenanzeige Javascript aktiviert sein soll und ob Browser-Plugins aktiv sein sollen.
  • Externe Browser Einstellungen: Werden Links im zur Anzeige im externen Browser angefordert startet Liferea ein Browser Kommando, das hier konfiguriert werden kann. Im ersten Auswahlmenü kann der zu verwendende Browser verändert werden oder mit "Manuell" ein benutzerspezifisches Kommando eingestellt werden. Mit dem zweiten Auswahlmenü kann eingestellt werden wie der Link im eingestellten Browser geöffnet werden soll.

    Nur wenn "Manuell" im ersten Auswahlmenü eingestellt ist wird das Kommadoeingabefeld auswählbar. Bei der Angabe eines Kommando darf ein "%s" das später durch die URL ersetzt wird nicht fehlen. Der Platzhalter sollte doppelt gequoted werden um Shellprobleme z.B. mit dem #-Zeichen in URLs zu vermeiden.

Desktop Einstellungen

  • Menü-Einstellungen: Hier kann eingestellt werden, ob Liferea die Werkzeugleiste verbergen soll und wie die Werkzeugleiste aussieht.
  • Anderes: Hier kann ausgewählt werden ob beim Markieren aller Schlagzeilen als gelesen eine Sicherheitsabfrage erfolgen soll.

Proxy Einstellungen

  • HTTP-Proxy-Server: Mit dieser Registerkarte kann ein HTTP-Proxy konfiguriert werden, den Liferea zum Herunterladen von Feeds benutzt. GNOME-Benutzer sollten hier keine Einstellungen vornehmen sondern den Proxy in der GNOME-Konfiguration einrichten (sofern das nicht bereits geschehen). Liferea wird dann die GNOME-Proxy-Einstellungen automatisch übernehmen.

    Zur Zeit erlaubt Liferea keine Angabe von Rechnern, für die Proxy-Einstellungen ignoriert werden soll.

    Um einen SOCKS Proxy zu nutzen, muss "Auto Detect" ausgewählt werden und der SOCKS Proxy im Desktop (GNOME, KDE, ...) konfiguriert werden.

Anhänge Einstellungen

Diese Registerkarte erlaubt die Konfiguration des Handhabens von Anhängen durch Liferea. Mehr über Anhänge im Abschnitt Enclosures/Podcasting.

  • Herunterladen von Anhängen: Hier kann angegeben werden welches Tool Liferea zum Herunterladen nutzen soll.
  • Öffnen von Anhängen: Mit dem "Eigenschaften" und "Löschen" Button kann die Liste der bekannten Enclosure-Programmverknüpfungen verändert werden. Dies ist nützlich wenn man eine Verknüpfung entfernen oder gezielt ändern will. Neue Verknüpfungen müssen nicht manuell angelegt werden, weil Liferea beim ersten Download eines unbekannten Typs nachfragt mit welchem Programm dieser geöffnet werden soll.
liferea-1.13.7/doc/html/preferences_en.html000066400000000000000000000142401415350204600205750ustar00rootroot00000000000000 Changing Preferences

Program Preferences

The preferences dialog can be opened by the toolbar or through the "Program" menu. The dialog is divided into seven tabs described in the following text.

Feed Preferences

  • Feed Cache Handling: Here you can set the default number of items to be saved for each subscription. Note that this setting can be overwritten for each subscription by setting the item cache size in the subscriptions properties.
  • Feed Update Settings: Here you can set the default refresh interval for all subscriptions. Note that this setting can be overwritten for each subscription by setting the refresh interval in the subscriptions properties.

    The "Update all subscription at startup" check box allows you to control the update behaviour right after startup.

Folder Preferences

  • Folder Display Settings: With these check boxes you can control what Liferea loads into the item list when you click a folder. The default behaviour is to load all unread items and to hide all items you've already read.

    Usually you will want to enable both options as this allows to quickly read through all unread items of a set of feeds.

  • Feed Icons (Favicons): This button allows you to trigger an update of all favicons of all subscriptions.

Headline Preferences

  • Reading Headlines: Here you can set the hotkey for skimming through all unread headlines.
  • Default View Mode Select your favourite viewing mode here.
  • Web Integration: Here you can configure your favourite social bookmarking website. This setting is used when you invoke "Post Bookmark" from the item context menu or the HTML view.

Browser Preferences

  • Internal Browser Settings: Here you can configure three things. First you can specify whether or not Liferea should open clicked links in the configured external browser or in the internal browser. The second option allows you to switch off Javascript. The third option allows you to enable browser plugins.
  • External Browser Settings: When you click links in a article you read Liferea launches a browser command you can define with this preferences. The first option button is to select your favourite browser or "Manual" for a user defined browser command. With the second option button you specify how the link is opened inside the previously selected browser.

    Only when selecting "Manual" as browser you can enter a browser command in the command entry. When entering a command don't forget to include a "%s" within the command which will be replaced with the URL that was clicked. Please keep in mind to double quote the place holder to avoid shell problems with the hash character (#) when launching the browser.

Desktop Preferences

  • Toolbar Settings: Here you can select whether you want to display both menu bar and tool bar or only one of them. Additionally can influence the toolbar button detail.
  • Other: Here you can choose wether you want to have a safety confirmation when marking all items read.

Proxy Preferences

  • HTTP Proxy Server: Here you can specify how Liferea should determine the proxy configuration. The suggested default configuration is to figure it out automatically from the environment variables and the GNOME configuration.

    If Liferea incorrectly detects the proxy or you want to configure a different proxy then you should use the "No Proxy" or "Manual Setting" option.

    There is currently no direct SOCKS support. For a SOCKS proxy use "Auto Detect" and configure your global desktop (GNOME, KDE...) to use SOCKS!

Enclosures Preferences

This tab allows you to configure how Liferea should handle enclosures. If you are still unsure what enclosures are about please read the Enclosures/Podcasting section first.

  • Downloading Enclosures: Here you specify which download tool Liferea should use.

    When you have problems downloading enclosures please ensure you have installed the configured download tool and that it is working correctly when you run it on the command line.
  • Opening Enclosures: Using the "Properties" and "Delete" button you can edit the list of known enclosures types. Typically you only want to do this if you want to remove or change program associations. You don't need to create this definitions manually because Liferea will create a new one and ask you which program to use when you open or save a type of enclosures.
liferea-1.13.7/doc/html/reference.css000066400000000000000000000014271415350204600173770ustar00rootroot00000000000000header { border-bottom: 2px dotted; } footer { border-top: 2px dotted; } header nav td, footer nav td { padding: 8px; width: 7em; user-select: none; } header nav td a, footer nav td a { display: block; white-space: nowrap; cursor: pointer; padding: initial; } nav a { text-decoration: none; } nav a:hover, nav a:focus { text-decoration: underline; } nav li a { display: block; padding: 0.5em; margin: 0.3em; } table.reftable { border: 1px solid black; border-collapse: collapse; } table.reftable tr td { border: 1px solid black; } td.ptitle h1 { font-weight: bold; font-size: larger; margin: auto; } dt { font-weight: bold; } /* General-purpose */ .left {text-align: left;} .right {text-align: right;} .center {text-align: center;} .fullwidth {width: 100%;}liferea-1.13.7/doc/html/reference_de.html000066400000000000000000000047721415350204600202310ustar00rootroot00000000000000 Kurzreferenz

Kurzreferenz

Strg-U Sofortige Aktualisierung aller Abonnements.
Strg-R Markiert alle Schlagzeilen des ausgewählten Abonnements oder Ordners als gelesen.
Strg-N Springe zur nächsten ungelesenen Schlagzeile.
Strg-M Umschalten des Lesezustands der ausgewählten Schlagzeile.
Strg-T Umschalten des Flaggenstatus der ausgewählten Schlagzeile.
Strg-O Abspielen des nächsten Audio/Video-Anhanges.
Strg-+ Schriftgröße erhöhen.
Strg-- Schriftgröße verringern.
Strg-0 Schriftgröße zurücksetzen.
Leertaste Durchblättern der Schlagzeilen. Entsprechend der Programmeinstellungen muß Strg oder Alt gedrückt werden.
u Cursor in der Liste der Abonnements um eine Zeile nach oben bewegen.
d Cursor in der Liste der Abonnements um eine Zeile nach unten bewegen.
b Cursor in der Liste der Schlagzeilen um eine Zeile nach oben bewegen.
f Cursor in der Liste der Schlagzeilen um eine Zeile nach unten bewegen.
Entf Löscht die ausgewählte Schlagzeile oder das ausgewählte Abonnement.
liferea-1.13.7/doc/html/reference_en.html000066400000000000000000000041251415350204600202330ustar00rootroot00000000000000 Quick Reference

Quick Reference

Ctrl-U Updates all your subscriptions at once.
Ctrl-R Marks all items of the selected feed or folder as read.
Ctrl-N Jump to next unread headline.
Ctrl-M Toggle unread state of selected headline.
Ctrl-T Toggle flag of selected headline.
Ctrl-O Play the next headline enclosure (audio or video).
Ctrl-+ Increase font size.
Ctrl-- Decrease font size.
Ctrl-0 Reset font size to default.
Space Skim through the headlines. Depending on your preferences you might need to use the Ctrl or Alt modifier.
u Move up cursor in the feed list.
d Move down cursor in the feed list.
b Move up cursor in the item list.
f Move down cursor in the item list.
Del Removes the currently selected feed or item.
liferea-1.13.7/doc/html/reference_it.html000066400000000000000000000044011415350204600202420ustar00rootroot00000000000000 Guida Rapida

Guida Rapida

Ctrl-U Aggiorna tutti i tuoi abbonamenti in contemporanea.
Ctrl-R Segna tutti gli elementi del notiziario o della cartella selezionata come letti.
Ctrl-N Salta al prossimo titolo non letto.
Ctrl-M Commuta lo stato di non letto del titolo selezionato.
Ctrl-T Commuta il contrassegno del titolo selezionato.
Ctrl-O Riproduci l'allegato del prossimo titolo (audio o video).
Ctrl-+ Ingrandisci il testo.
Ctrl-- Riduci il testo.
Ctrl-0 Reimposta la grandezza del testo al valore predefinito.
Space Scorri tra i titoli. A seconda delle tue preferenze potresti dover usare il modificatore Ctrl o Alt.
u Muovi il cursore della lista notiziari verso sopra.
d Muovi il cursore della lista notiziari verso sotto.
b Muovi il cursore della lista titoli verso sopra.
f Muovi il cursore della lista titoli verso sotto.
Del Rimuovi il titolo o il notiziario attualmente selezionato.
liferea-1.13.7/doc/html/searching_de.html000066400000000000000000000134701415350204600202310ustar00rootroot00000000000000 Schlagzeilen suchen

Schlagzeilen suchen

Die Titel der Abonnements oder der Schlagzeilen durchsuchen

Um nach einen Titel zu suchen kann die GTK-Listensuchfunktion sowohl in der Liste der Abonnements als auch der Schlagzeilen benutzt werden. Um sie zu aktivieren muß erst Strg-f gedrückt und dann der zu suchen Text eingegeben werden.

Durchsuchen des Inhalts aller Abonnements

Liferea unterstützt eine einfache case sensitive Suche in den Titeln und Inhalten der Schlagzeilen aller Abonnements. Der Suchen-Dialog kann z.B. mit Hilfe des "Durchsuchen" Buttons der Werkzeugliste aktiviert werden. Nachdem ein Suchbegriff eingegeben und der "Suchen" Button gedrückt wurde startet Liferea die Suche. Als Ergebnis wird jede Schlagzeile deren Titel oder Inhalt den Suchbegriff enthält in der Liste der Schlagzeilen angezeigt. In der Schlagzeilenansicht wird zusätzlich die Anzahl der Treffer angegeben.

Permanent Suche mit einem Suchordner

Ist man immer wieder an einem bestimmten Suchbegriff (z.B. allen Schlagzeilen mit dem Suchbegriff "XML") interessiert kann ein sogenannter Suchordner erzeugt werden. Dieser erscheint dann in der Liste der Abonnements und enthält automatisch immer alle Schlagzeilen auf die der Suchbegriff paßt. Um einen solchen Suchordner anzulegen muß zuerst eine Suche durchgeführt werden und dann im Suchen-Dialog der "+Suchordner" Button aktiviert werden. Darauf sollte ein neuer Eintrag in der Liste der Abonnements erscheinen. Der Titel und die Suchoptionen des Suchordners können über die Auswahl des "Eigenschaften" Menüpunktes im Hauptmenü oder Kontextmenü angepaßt werden

Die nachfolgende Tabelle enthält alle unterstützten Suchregeln und ihre jeweilige Bedeutung.

Schlagzeile enthält Hinzufügen jeder Schlagzeile deren Titel oder Inhalt case insensitiv dem Suchbegriff entspricht.
Schlagzeile enthält nicht Entfernen jeder Schlagzeile deren Titel oder Inhalt case insensitiv dem Suchbegriff entspricht.
Titel der Schlagzeile enthält Hinzufügen jeder Schlagzeile deren Titel case insensitiv dem Suchbegriff entspricht.
Titel der Schlagzeile enthält nicht Entfernen jeder Schlagzeile deren Titel case insensitiv dem Suchbegriff entspricht.
Inhalt der Schlagzeile enthält Hinzufügen jeder Schlagzeile deren Inhalt case insensitiv dem Suchbegriff entspricht.
Inhalt der Schlagzeile enthält nicht Entfernen jeder Schlagzeile deren Inhalt case insensitiv dem Suchbegriff entspricht.
Titel des Abonnements enthält Hinzufügen aller Schlagzeilen jedes Abonnements dessen Titel case insensitiv dem Suchbegriff entspricht.
Titel des Abonnements enthält nicht Entfernen aller Schlagzeilen jedes Abonnements dessen Titel case insensitive dem Suchbegriff entspricht.
Lesestatus ist ungelesen Hinzufügen aller ungelesenen Schlagzeilen.
Lesestatus ist gelesen Entfernen aller gelesenen Schlagzeilen.
Flagge ist gesetzt Hinzufügen aller markierten Schlagzeilen.
Flagge ist nicht gesetzt Entfernen aller nicht markierten Schlagzeilen.

Hinweis: Die Reihenfolge der Regeln ist nicht relevant. Eine additive Regel wird niemals von einer Entfernen-Regel betroffenen Schlagzeilen wieder hinzufügen.

liferea-1.13.7/doc/html/searching_en.html000066400000000000000000000133411415350204600202400ustar00rootroot00000000000000 Searching

Searching for Feed Content

Liferea supports a simple case sensitive text searching in the item content and titles of all your subscriptions.

To open the search dialog click the search button in the toolbar. Enter the string you want to search for and click the "Search" button. Any items containing the string either in their title or their content should appear in the resulting item list. Also the item content view will give you a message about the number of matches.

Permanent Searches with Search Folders

If you want to have permanent searches (e.g. all items containing the term "XML") you can create a search folder which will be added to your feed list and contains all items matching this search rule. To do so enter your search term into the search box and click the search button. After the results appear click the "Search Folder" button to create a search folder from the search results. Now a search folder should appear in your feed list. If you want you can edit the search folders title or change its rules by opening the properties dialog either from the "Subscriptions" menu or the contextual menu in the feed list.

The following table is a list of all possible search folder rules and their specific meaning.

Item does contain Adds each item whose title or content matches the given case insensitive string to the search folder.
Item does not contain Removes each item whose title or content matches the given case insensitive string from the search folder.
Item title does contain Adds each item whose item title matches the given case insensitive string to the search folder.
Item title does not contain Removes each item whose item title matches the given case insensitive string from the search folder.
Item body does contain Adds each item whose item content matches the given case insensitive string to the search folder.
Item body does not contain Removes each item whose item body matches the given case insensitive string from the search folder.
Feed title does contain Adds each item whose parent feed's title matches the given case insensitive string to the search folder.
Feed title does not contain Removes each item whose parent feed's title matches the given case insensitive string from the search folder.
Read status is unread Adds each unread item to the search folder.
Read status is read Removes each unread item from the search folder.
Flag status is flagged Adds each flagged item to the search folder.
Flag status is unflagged Removes each flagged item from the search folder.
Podcast included Adds all items that contain an enclosure to the search folder.
Podcast not included Removes all items that do not contain an enclosure from the search folder.

Please note that the order of the rules does not matter. An additive rule after a removal rule that does match an item that is to be removed because of to the removal rule will not add this item!

liferea-1.13.7/doc/html/subscriptions_de.html000066400000000000000000000240401415350204600211700ustar00rootroot00000000000000 Abonnements verwalten

Diese Sektion beschreibt die Möglichkeiten die Liferea bietet um verschiedene Typen von Online-Abonnements anzulegen.

Was kann abonniert werden?

Als News-Aggregator kann Liferea verschiedenste Quellen von New, Blogs oder anderen Inhalten abonnieren. Liferea unterstützt folgende Abonnementtypen:

  • einzelne Feeds
  • OPML-Quellen (z.B. eine Planet-Feedliste, ein Blogroll...)
  • ein TheOldReader-Konto
  • ein TinyTinyRSS-Konto

Abonnements anlegen

Mit dem Menüpunkt "Neues Abonnement" aus dem Hauptmenü oder dem Kontextmenü der Liste der Abonnements kann ein Dialog zum Anlegen eines neuen Abonnements geöffnet werden.

Abonnements können eine Vielzahl von Quellen haben z.B. eine Feed-URL, ein lokal ausgeführtes Kommando oder vom Rechner aus zugreifbare Datei. In der Mehrzahl der Fälle wird aber eine Feed-URL die Quelle sein. Nachdem der Quelltyp korrekt eingestellt ist kann die Feed-URL, das Kommando oder der Dateiname im Eingabefeld "Quelle" eingetragen werden. Um zum Beispiel Slashdot's RSS News Feed zu abonnieren muß "URL" als Quelltyp ausgewählt werden und die Feed-URL "http://slashdot.org/index.rss" als "Quelle" eingetragen werden.

Im Falle von Kommandos und Dateien als Quelltyp können diese mit Hilfe eines Dateiauswähldialoges mit "Datei auswählen..." gesucht werden.

Es kann vorkommen, daß ein Feed-Format von Liferea nicht unterstützt wird. In diesem Fall kann ein Konvertierungsfilter verwendet werden um die Daten in ein für Liferea lesbares Format zu überführen. Solche Filter können zum Beispiel aus dem Snownews und Liferea Skript-Repository (Englisch) heruntergeladen werden.

Um solche Konvertierungsfilter zu benutzen muß die Option "Benutze Filter zum Konvertieren" aktiviert werden. Der Pfad des Filter-Skriptes muß dann unter "Konvertiere mit" angegeben werden. Alternativ kann das Skript mit "Datei auswählen..." gesucht werden. Ein Filter ist ein einfaches Programm welches die Quelldaten von stdin einliest und in einem von Liferea unterstützten Format auf stdout wieder ausgibt. Solche Filter können z.B. mit Perl leicht geschrieben werden.

Abonnement-Eigenschaften

Der Eigenschaften-Dialog für ein Abonnement wird nach dem Anlegen neuer Abonnements automatisch geöffnet. Zusätzlich kann er mit dem Menüpunkt "Eigenschaften" aus dem Hauptmenü oder dem Kontextmenü eines ausgewählten Abonnements aufgerufen werden. Beim Erzeugen neuer Abonnements ist es oft gar nicht notwendig die Defaultwerte zu verändern und es reicht aus den Dialog mit dem "Ok" Button zu bestätigen.

Der Eigenschaften-Dialog ist in fünf Registerkarten unterteilt: "Allgemein", "Quelle", "Cache", "Authentifizierung" und "Anhänge":

Allgemeine Eigenschaften

Die Registerkarte "Allgemein" erlaubt das Ändern des Titels und des Aktualisierungsintervalls des Abonnements.

Das Aktualisierungsintervall kontrolliert nach welcher Zeit Liferea ein Abonnement aktualisiert. Meistens ist es nicht nötig die Defaulteinstellung zu ändern. Es wird empfohlen das globale Aktualisierungsintervall zu benutzen, welches in den Programmeinstellungen festgelegt wird. Auf diese Weise kann die Aktualisierung aller Abonnements zentral gesteuert werden.

Manchmal geben Feeds ein eigenes Aktualisierungsintervall vor. In diesem Fall wird Liferea automatisch die Option "Benutzerdefinierte Aktualisierung" auswählen und das vom Feed vorgeschlagene Intervall eintragen. Dies passiert nur beim Anlegen neuer Abonnements Gibt ein Feed ein eigenes Interval vor sollte dieses auch beibehalten werden.

Es gibt folgende Modi für das Aktualisierungsinterval

Nutze das globale Aktualisierungsintervall
Mit dieser Option wird das Aktualisierungsintervall durch die globale Programmeinstellung festgelegt.
Benutzerdefinierte Aktualisierung
Mit dieser Option kann ein beliebiges Aktualisierungsintervall für das Abonnement vorgegeben werden. Beim Angeben des Intervalls sollte man sich immer bewußt sein, welchen Datenverkehr man mit unpassenden Werten verursachen kann. Ein Abonnement sollte nie öfter aktualisiert werden als sich seine angebotenen Inhalte ändern.
Feed nicht automatisch aktualisieren
Mit dieser Option kann das Aktualisieren des Abonnements deaktiviert werden. Das ist nützlich für Abonnements die man nur manuell aktualisieren möchte.

Quelle des Abonnements

Die Registerkarte "Quelle" enthält dieselben Dialogelemente die auch beim Anlegen neuer Abonnements angeboten werden. Daher kann hier die Quelle des Abonnements nachträglich verändert werden (z.B. wenn sich die URL der Webseite ändert)

Cache-Eigenschaften

Die dritte Registerkarte, "Cache", erlaubt es die Anzahl zu sichernder Schlagzeilen zu konfigurieren. Diese Einstellungen sind analog zu den globalen Cache-Einstellungen. Normalerweise ist es ausreichend die globale Einstellung zu verwenden.

Default-Cache-Einstellungen
Für das Abonnement soll die globale Cache-Einstellung verwendet werden.
Cache Deaktivieren
Verhindert das Sichern der Schlagzeilen.
Unbeschränkter Cache
Sichert alle Schlagzeilen. Verwirft niemals eine Schlagzeile.
Cache begrenzen auf # Schlagzeilen
Sichert nur # Schlagzeilen. Verwirft alte Schlagzeilen sobald mehr als # Schlagzeilen vorhanden sind.

Hinweis: Markierte Schlagzeilen werden unabhängig von den Cache-Einstellungen niemals gelöscht. Durch das Setzen der Flagge einer Schlagzeile die man gerade gelesen hat verhindert man deren zukünftiges Löschen.

Authentifizierung

Mit dieser Registerkarte kann die HTTP Authentifikation für den Download der Abonnement-Quelle aktiviert werden. Nach dem aktivieren der Checkbox können Benutzername und Paßwort eingegeben werden. Üblicherweise ist es nicht notwendig die Authentifikationsdaten im Voraus anzugeben, da Liferea automatisch nachfragt sobald eine Abonnement-Quelle Authentifikation erfordert.

Erweitert

In dieser Registerkarte können verschiedene Spezialeinstellungen für dieses Abonnement aktiviert werden.

Alle Anänge dieses Abonnements automatisch herunterladen.
Diese Option ist nützlich fpr Podcasts oder Torrent-Feeds. Wenn diese Option aktiviert ist lädt Liferea alle Anhänge automatisch herunter.
Link beim Auswählen der Schlagzeile automatisch öffnen.
Per-default zeigt Liferea den Inhalt einer Schlagzeile. Ist diese Option aktiv wird stattdessen die Schlagzeilen-URL geladen.
Ignoriere Kommentarfeeds dieses Abonnements.
Manchmal sind Kommentarfeeds fehlerhaft oder schlicht nutzlos. Mit dieser Option können sie ausgeblendet werden.
Erzwinge Popups für dieses Abonnement.
Wenn die globale Einstellung für Popups deaktiviert ist können diese trotzdem für dieses Abonnement erzwungen werden.
Keine Popups für dieses Abonnement.
Wenn die globale Einstellung für Popups aktiviert ist können diese trotzdem für dieses Abonnement unterdrückt werden.
Markiere heruntergeladene Schlagzeilen als gelesen.
Neue Schlagzeilen haben immer den Status "ungelesen". Diese Option kann nützlich sein um für uninteressante Abonnements die Schlagzeilen automatisch auf "gelesen" zu stellen.

Abonnieren von OPML-Quellen und Online-Diensten

Um eine OPML-Quelle oder ein Konto eines Online-Dienstes zu abonnieren muß "Neue Quelle" vom Kontextmenü in der Abonnements oder aus dem "Abonnements"-Menü ausgewählt werden. Im dann geöffneten Dialog kann die gewünschte Quelle ausgewählt werden.

OPML-Quelle

Wurde "Planet/BlogRoll/OPML" als Quelle ausgewählt muß die URL einer OPML-Quelle angegeben werden. Wenn nötig auch noch zusätzlich Authentifikation für den Zugriff. Geht alles gut wird ein neuer Eintrag in der Feedliste angelegt und nach dem initialen Download der OPML-Quelle werden die enthaltenen Abonnements hierarchisch hinzugefüht. Ändert sich die OPML-Quelle werden mit der Zeit alte Feeds automatisch entfernt und neue hinzugefügt.

liferea-1.13.7/doc/html/subscriptions_en.html000066400000000000000000000243061415350204600212070ustar00rootroot00000000000000 Managing Subscriptions

This section documents all user interactions necessary to create and maintain subscription.

Things Liferea Can Subscribe To

As a news aggregator Liferea allows you to subscribe to different syndication sources. The most common use case is to subscribe to single feeds. But Liferea also supports subscribing a source that provides a collection of feeds. So you can subscribe to:

  • a single feed
  • any OPML source (a planet feed list, a blog roll...)
  • a TheOldReader online account
  • a TinyTinyRSS online account

Subscribing to a Feed

To create a new feed subscription select "New Feed" from the feed list's contextual menu or the "Subscriptions" menu. A dialog to create a new subscription will appear.

For example, to subscribe to Slashdot's news feed enter "http://slashdot.org/index.rss" into the text box and click "OK".

Creating Special Feed Subscriptions

Liferea supports obtaining feeds from a variety of different sources including Internet URLs, the output of a locally run command, and by directly reading from a file. Most often, it is desired to subscribe using an URL as described in the previous paragraph. For special subscription you must select "Advanced" to open the following more complex subscription dialog:

After selecting the desired source type, enter the source URL, command name, or filename into the "Source" textbox.

When a command or local file is selected, the command or file can be selected using a file navigation box by clicking "Select File...".

Sometimes, the data in a feed is in a format that is unknown to Liferea. If this is the case, a conversion filter can be used to convert the data into a useable format. Many filters can be downloaded from the Snownews and Liferea script repository.

To use conversion filters, the "Use conversion filter" option needs to be activated in the subscription dialog box. The filter is specified by either typing the path of the filter script into the "Convert using" textbox or selecting the filter after clicking the "Select File..." button. The filters are simple programs that read the non-supported feed format using stdin and output the valid feed to stdout. Conversion filters are often written using perl.

Changing Subscription Properties

The feed properties dialog is used to configure additional feed properties and can be activated in the "Subscriptions" menu or from feed context menu. After successfully creating a new subscription you usually don't need to change the feed properties.

Only if you want to change the HTTP authentication, the caching behaviour or other feed specific options then you need to use the feed properties dialog.

The dialog groups the feed properties into five sets: "General", "Source", "Archive", "Download" and "Advanced":

General Properties

The "General" pane allows you to set feed title and update interval. The feed name is the feed's name shown in the feed list. Multiple feeds can have the same name.

The feed update interval controls how often Liferea attempts to update a feed. Typically, the feed update interval can be left at its default. We recommend to use the global update interval preference which you can find in the preference dialog because it allows one to change the update interval of all feed simultaneously.

Sometimes a feed provides a feed specific update interval. If this is the case Liferea automatically activates the user defined update interval setting and enters the feed specific update interval. This is only done during the initial feed subscription. You should never need to change such a setting.

The update interval can be set using the following options:

Use global default update interval
This preference is used for all feeds that have the update interval setting to follow the global default update interval setting.
Feed specific update interval
There might be situations where you want to specify a update interval which might be more or less frequent than the global update interval. Then you should select the user defined interval option and enter the interval value you want. When specifying an update interval, you should consider the web traffic you will cause with an inappropriate update interval. Don't update more often than the feed's content is updated.
Don't update this feed automatically
This option allows you to prevent updating the feed. You can use this option for feeds you always want to have only updated manually.

Source Properties

The "Source" pane contains the same fields that the advanced subscription dialog provides. You can use these settings to change the feed source (e.g. after a feed's URL has changed).

Archive Properties

The third pane, "Archive", controls how many items of a feed are kept when Liferea saves a feed to disk. Similar to the update interval settings there is a corresponding global preference which sets a default cache size for all feeds. You should use the feed specific cache settings only to implement exceptions from the global default cache size.

Default Cache Settings
Makes the feed use the default cache settings, which are stored in the main Liferea preferences.
Disable Cache
Disable the feed's items from ever being saved.
Unlimited cache
Save all items ever downloaded.
Number of items to save
Limits the cache size to a fixed number.

Note: Flagged items are always saved, regardless of the cache settings. So if you have found an important headline and want to prevent it from being dropped from the cache just flag this headline.

Download Properties

This tab allows you to disable the use of the global proxy setting and to enable HTTP authentication when downloading the feed. Enable the checkbox and enter appropriate user and password values to use password-protected feeds. Usually you won't need to use these settings because you will be asked username and password when subscribing to the feed that requires authentication.

Advanced Properties

This panel provides different special options to control how Liferea handles the subscription. These options are:

Automatically download all enclosures of this feed.
This might be useful for podcasts or torrent feeds. If you enable the option Liferea downloads attached enclosures.
Auto-load item link in configured browser when selecting articles.
Per-default Liferea shows the item description in the HTML view pane. When this check box is enabled it will automatically load the item link when the item selection changes.
Ignore comment feeds for this subscription.
Sometimes there are feeds with broken or useless comment feeds. This option allows to ignore those.
Enforce popup notification for this subscription.
If the global notification preference is disabled you can enabled this option to generate notifications for only this feed.
Never do popup notification for this subscription.
If the global notification preference is enabled you can use this option to prevent notifications for this feed. This might be useful for high-traffic feeds.
Mark downloaded items as read.
Normally new items have the status "unread". Sometimes you might want to use this option for new items of a special feed not to show up as "unread" so you don't have to mark them "read" manually.

Subscribing to OPML Sources and Online Services

To subscribe to OPML feed lists or a online service account select "New Source" from the context menu of the feed list or from the "Subscriptions" menu. From the following dialog select the source type you like to create.

OPML Sources

If you have selected "Planet/BlogRoll/OPML" you need to supply the source URL of the OPML document. If necessary provide authentication information. After doing so a new OPML source node will be inserted in the feed list and after downloading the OPML document for the first time new subscriptions as described by the OPML source will be created. If the OPML feed list changes over time old subscriptions are automatically dropped and new ones are added.

liferea-1.13.7/doc/html/topics_de.html000066400000000000000000000026641415350204600175720ustar00rootroot00000000000000 Hilfe Inhalt

Dies ist die Endnutzerdokumentation für Liferea 1.12.

Hilfe Themen

Online Support

liferea-1.13.7/doc/html/topics_en.html000066400000000000000000000027111415350204600175750ustar00rootroot00000000000000 Help Contents

This user documentation applies for Liferea 1.12.

Help Topics

Online Support

liferea-1.13.7/doc/html/topics_it.html000066400000000000000000000030161415350204600176060ustar00rootroot00000000000000 Guida

Questa documentazione utente si applica a Liferea 1.12.

Argomenti della Guida

Supporto in Linea

liferea-1.13.7/doc/html/updating_de.html000066400000000000000000000055651415350204600201070ustar00rootroot00000000000000 Aktualisieren von Abonnements

Aktualisieren von Abonnements

Wie man ein einzelnes Abonnement aktualisiert

Zum sofortigen Aktualisieren eines einzelnen Abonnement reicht es aus den "Aktualisieren" Menüpunkt aus dem Hauptmenü oder Kontextmenü des Abonnements aufzurufen.

Wie man alle Abonnements auf einmal aktualisiert

Eine Aktualisierung aller Abonnements kann mit dem "Alle Aktualisieren" Button in der Werkzeugleiste oder dem entsprechenden Menupünkt im "Abonnement" Menü gestartet werden.

Die automatische Aktualisierung benutzen

Um alle Abonnements regelmäßig automatisch zu aktualisieren kann in den Einstellungen ein globales Aktualisierungsintervall in Minutengenauigkeit eingestellt werden. Dazu muß der Einstellungen-Dialog aufgerufen werden. Mehr dazu im Abschnitt Einstellungen ändern.

Soll ein einzelnes Abonnement öfter oder seltener als nach dem globalen Aktualisierungsintervall aktualisiert werden kann ein spezifisches Aktualisierungsintervall in den Eigenschaften des Abonnements konfiguriert werden. Dazu muß der Eigenschaften-Dialog mit dem "Eigenschaften" Menüpunkt aus dem Hauptmenü oder aus dem Kontextmenü des Abonnements aufgerufen werden. Hinweis: Abonnements sollten nicht öfter aktualisiert werden als sich ihr Inhalt ändert. Ansonsten wird unnötig Datenverkehr erzeugt wenn z.B. tausende Clients alle paar Minuten das Feed abrufen!

Manche Feeds geben ein eigenes Aktualisierungsinterval vor. In diesem Fall verwendet Liferea automatisch dieses Intervall anstatt des globalen Aktualisierungsintervall.

Aktualisieren von Abonnements beim Programmstart

Standardmäßig aktualisiert Liferea nach dem Start alle veralteten Abonnements. Ist das nicht erwünscht kann das Verhalten in den Programmeinstellungen deaktiviert werden. Mehr dazu im Abschnitt Einstellungen ändern.

liferea-1.13.7/doc/html/updating_en.html000066400000000000000000000052121415350204600201060ustar00rootroot00000000000000 Updating Subscriptions

Updating Subscriptions

How to update a single subscription

To update a single subscription select it in the feed list and select "Update" or "Update Folder" from the program menu or the context menu from within the feed list.

How to update all feeds at once

To update all subscriptions at once use the "Refresh" button from the toolbar or the option "Update All" from the "Subscriptions" menu.

Using Auto Update

To automatically update all subscriptions you can set a global update interval with the resolution of one minute. To do so you have to use the preferences dialog. To learn more about this refer to the Program Preferences section.

If you like to have some subscriptions to be updated more or less frequently than the global default update interval you can set an per-subscription update interval in the subscription properties. To do so select a subscription from the feed list and select "Properties" from the "Feed" menu or the contextual menu. Note: you should not update a feed more often then the content is changing. Think of the web traffic some thousands of clients polling each minute could create!

To help with the decision for a good update interval some subscriptions provide an own update interval. When you create a new subscription this value will be set as the default update interval. This setting will overrule the global default update interval.

Updating all feeds on program startup

On startup Liferea automatically updates all out-dated subscriptions. If you don't like this you can disable this behaviour in the program preferences. To learn more about this refer to the Program Preferences section.

liferea-1.13.7/dtd/000077500000000000000000000000001415350204600137655ustar00rootroot00000000000000liferea-1.13.7/dtd/html.ent000066400000000000000000000725631415350204600154560ustar00rootroot00000000000000 liferea-1.13.7/glade/000077500000000000000000000000001415350204600142665ustar00rootroot00000000000000liferea-1.13.7/glade/Makefile.am000066400000000000000000000010761415350204600163260ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in gladedir = $(pkgdatadir) glade_DATA = about.ui \ auth.ui \ enclosure_handler.ui \ liferea_menu.ui \ liferea_toolbar.ui \ mainwindow.ui \ mark_read_dialog.ui \ new_folder.ui \ new_newsbin.ui \ new_subscription.ui \ node_source.ui \ opml_source.ui \ prefs.ui \ properties.ui \ reedah_source.ui \ rename_node.ui \ search_folder.ui \ search.ui \ simple_search.ui \ simple_subscription.ui \ theoldreader_source.ui \ ttrss_source.ui \ update_monitor.ui \ liferea.css EXTRA_DIST = \ $(glade_DATA) liferea-1.13.7/glade/about.ui000066400000000000000000000100501415350204600157330ustar00rootroot00000000000000 False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 About False center-on-parent dialog Liferea Liferea is a news aggregator for GTK+ https://lzone.de/liferea/ Liferea Homepage Developers: Lars Windolf Nathan Conrad Arnold Noronha Adrian Bunk Emilio Pozuelo Monfort Laetitia Berthelot Contributors: Code, Patches, Debugging James Doherty Jeremy Messenger John McKnight Tomasz Maka Karl Soderstrom Christophe Barbe Juho Snellman Roshan Revankar Johannes Schlueter Niklas Morberg Pierre Phaneuf ahmed el-helw James Bowes Marc Deslauriers Amit D. Chaudhary Christoph Hohmann Raphael Slinckx Bjorn Monnens Thomas de Grenier de Latour Aristotle Pagaltzis Norman Jonas Sebastian Droege Daniel Gryniewicz Remi Cardona Frederic Peters Don Malcolm Ed Catmur Chris Pirillo Eric Anderson Daniel Gryniewicz Ori Avtalion Stephan Maka Alexander Sack Gustavo Chain Lars Strojny Jon Forsberg Mathie Leplatre Mikel Olasagasti Maia Kozheva Peter Oliver Sergey Snitsaruk and many more... Lars Windolf Bart Kreska Wojciech Myrda Piotr Sokół Dario Conigliaro Emanuele Grande Gianvito Cavasoli Fernando Ike de Oliveira Og Maciel Leon Nardella Khaled Hosny Sargate Kanogan Rodrigo Gallardo Takeshi AIHANA Takeshi Hamasaki Vincent Lefèvre Daniel Nylander Mehmet Atif Ergun Eren Türkay Ihar Hrachyshka António Lima Bruno Miguel Martin Picek Justin Forest Oleg Maloglovets Sergey Rudchenko Leonid Selivanov Máté Őry Gabor Kelemen Pavol Klačanský Evgenia Petoumenou Besnik Bleta Christian Dywan Robin Stocker Paul Seyfert Gil Forcada Iñaki Larrañaga Murgoitio Mikel Olasagasti Uranga Adi Roiban Erwin Poeze Anxo Outeiral Aron Xu Joe Hansen Marquinos Iñigo Varela Yaron Sheffer Yuri Chornoivan Rūdolfs Mazurs Rihards Priedītis Mindaugas Baranauskas Trần Ngọc Quân Pauli Virtanen Jorma Karvonen Guillaume Bernard net.sourceforge.liferea gpl-2-0 True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK vertical 2 True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK end False False end 0 liferea-1.13.7/glade/auth.ui000066400000000000000000000163511415350204600155740ustar00rootroot00000000000000 True False 5 Authentication False dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True True False True False False 1 False False end 0 True False start False 5 vertical 12 True False Enter the username and password for "%s" (%s) True 0 0 0 True False 6 12 True False User_name: True usernameEntry 0 0 0 True True True 1 0 True False _Password: True passwordEntry 0 0 1 True True False True 1 1 0 1 False True 1 cancelbutton2 okbutton1 liferea-1.13.7/glade/enclosure_handler.ui000066400000000000000000000222121415350204600203200ustar00rootroot00000000000000 True False 5 Open Enclosure False dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False False end 0 True False 5 12 True False 6 True False Open an enclosure of type: 0 False False 0 True False 6 text/plain False False 1 True False _What should Liferea do with this enclosure? Please enter the command you want to be executed below. The enclosures URL will be supplied as an argument for this command: True True enc_cmd_entry 60 60 0 False False 2 True False 6 True True True True 0 _Browse True True False True False False 1 True True 3 _Do this automatically for enclosures like this from now on. True True False True True False False 4 False True 0 False True 1 cancelbutton4 okbutton3 liferea-1.13.7/glade/liferea.css000066400000000000000000000001211415350204600164010ustar00rootroot00000000000000#browsertabs, box > box { margin-right: 5px; margin-top: 5px; margin-left: 5px} liferea-1.13.7/glade/liferea_menu.ui000066400000000000000000000204031415350204600172570ustar00rootroot00000000000000 _Subscriptions
app.update-all Update _All app.mark-all-feeds-read Mark All As _Read
app.new-subscription _New Subscription... app.new-folder New _Folder... app.new-vfolder New S_earch Folder... app.new-source New _Source... app.new-newsbin New _News Bin...
app.import-feed-list _Import Feed List... app.export-feed-list _Export Feed List...
app.quit _Quit
_Feed
app.update-selected _Update app.mark-selected-feed-as-read _Mark Items Read
app.remove-selected-feed-items Remove _All Items app.delete-selected _Remove
app.selected-node-properties _Properties
_Item
app.next-unread-item _Next Unread Item
app.prev-read-item Previous Item app.next-read-item Next Item
app.toggle-selected-item-read-status Toggle _Read Status app.toggle-selected-item-flag Toggle Item _Flag app.remove-selected-item R_emove
app.launch-selected-item-in-tab Open In _Tab app.launch-selected-item-in-browser _Open In Browser app.launch-selected-item-in-external-browser Open In _External Browser
_View
app.fullscreen _Fullscreen
app.zoom-in Zoom _In app.zoom-out Zoom _Out app.zoom-reset _Normal size
app.set-view-mode _Normal View normal app.set-view-mode _Wide View wide
app.reduced-feed-list _Reduced Feed List
_Tools
app.show-update-monitor _Update Monitor app.show-preferences _Preferences
S_earch
app.search-feeds Search All Feeds...
_Help
app.show-help-contents _Contents app.show-help-quick-reference _Quick Reference app.show-help-faq _FAQ
app.show-about _About
liferea-1.13.7/glade/liferea_toolbar.ui000066400000000000000000000072221415350204600177610ustar00rootroot00000000000000 True True True app.new-subscription True _New Subscription... Adds a subscription to the feed list. list-add True True app.mark-selected-feed-as-read True _Mark Items Read Marks all items of the selected feed list node / in the item list as read. gtk-apply True False app.prev-read-item Previous Item go-previous True False app.next-read-item Next Item go-next True True app.next-unread-item True _Next Unread Item go-jump True True app.update-all True Update _All Updates all subscriptions. view-refresh True True app.search-feeds Search All Feeds... Show the search dialog. edit-find liferea-1.13.7/glade/mainwindow.ui000066400000000000000000000266221415350204600170110ustar00rootroot00000000000000 100 1 1 False Liferea 640 480 liferea True False True True vertical True True True True 170 True True never in True True False True False True True True False True True True False False True True vertical 199 True True False none False True True False vertical True False True True none 0 0 True True True False page 1 False True True 100 True True False none False True True False True False True True none 0 0 True True 1 True False page 2 1 False True False Headlines False True True 0 0 True False 0 1 liferea-1.13.7/glade/mark_read_dialog.ui000066400000000000000000000075341415350204600201020ustar00rootroot00000000000000 True False True Mark all as read ? True dialog question Mark items as read ? Are you sure you want to mark all items in the selected feed as read ? False 5 5 5 5 vertical 2 False True end Mark all as read True True True True True 0 gtk-cancel True True True True True True 1 False False 0 Do not ask again True True False start center 12 True True True 2 okButton cancelButton liferea-1.13.7/glade/new_folder.ui000066400000000000000000000120321415350204600167470ustar00rootroot00000000000000 False 5 New Folder False True center-on-parent dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True True False True False False 1 False False end 0 True False 5 6 12 True False _Folder name: True foldertitleentry 0 0 True True True 1 0 False True 1 button3 button2 liferea-1.13.7/glade/new_newsbin.ui000066400000000000000000000127711415350204600171530ustar00rootroot00000000000000 False 5 Create News Bin True center-on-parent dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True True False True False False 1 False False end 0 True False 5 6 12 True False _News Bin Name: True newsbinnameentry 0 0 True True True 1 0 _Always show in Reduced Feed List True True False True True 0 1 2 False True 1 cancelbutton6 newnewsbinbtn liferea-1.13.7/glade/new_subscription.ui000066400000000000000000000403101415350204600202200ustar00rootroot00000000000000 True False 5 New Subscription True center 400 dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False False end 0 True False vertical 18 True False True 6 12 True False Feed Source 0 0 0 3 True False 12 Source Type: 0 0 0 1 3 _URL True True False True True True 1 1 2 _Command True True False True True feed_loc_url 1 2 2 _Local File True True False True True feed_loc_url 1 3 Select File... True True False True 2 3 True False 12 _Source: True sourceEntry 0 0 4 True True True 1 4 2 0 0 True False 6 12 True False Download / Postprocessing 0 0 0 _Don't use proxy for download True True False 12 True True 0 1 Use conversion _filter True True False 12 True True 0 2 True False 12 6 12 True False Liferea can use external filter plugins in order to access feeds and directories in non-supported formats. See the documentation for more information. True 0 0 0 3 True False Convert _using: True filterEntry 0 0 1 True True 1 1 Select File... True True False True 2 1 0 3 0 1 True True 1 cancelbtn newfeedbtn liferea-1.13.7/glade/node_source.ui000066400000000000000000000132101415350204600171270ustar00rootroot00000000000000 True False Source Selection True 300 400 True dialog 450 True False vertical True False end gtk-cancel True True True False True False False 0 gtk-ok True False True True False True False False 1 False True end 0 True False vertical True False _Select the source type you want to add... True type_list 0 False False 0 True True True never in 400 True True False True True True 1 True True 1 cancelbutton1 ok_button liferea-1.13.7/glade/opml_source.ui000066400000000000000000000145701415350204600171630ustar00rootroot00000000000000 True False Add OPML/Planet dialog True False vertical True False end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False False end 0 True False 12 12 True False Please specify a local file or an URL pointing to a valid OPML feed list. True 0 False False 0 True False 6 True False _Location True location_entry False False 0 True True True True 1 _Select File True True False True False False 2 False True 1 False True 2 cancelbutton1 okbutton1 liferea-1.13.7/glade/prefs.ui000066400000000000000000002645361415350204600157640ustar00rootroot00000000000000 1 10000 1 1 1000000 1 1 True False 5 Liferea Preferences center 300 dialog True False vertical 2 True False end gtk-close True True True False True False False 0 False True end 0 True True 5 left True False 12 vertical 18 True False 6 12 True False Feed Cache Handling 0 0 0 2 True False 12 Default _number of items per feed to save: True True itemCountBtn 0 0 1 60 True True 0 adjustment3 1 True 1 1 0 0 True False 6 12 True False Feed Update Settings 0 0 0 3 True False 12 False False False 0 False True False Note: <i>Please remember to set a reasonable refresh time. Usually it is a waste of bandwidth to poll feeds more often than each hour.</i> True True True globalRefreshIntervalUnitComboBox 0 False True 0 False False 0 0 4 3 _Update all subscriptions at startup. True True False start 12 12 True True 0 1 3 True False 12 12 Default Feed Refresh _Interval: True globalRefreshIntervalSpinButton 0 0 2 60 True True 1 adjustment2 1 1 1 2 True False 0 2 2 0 1 True False Feeds False True False 12 vertical 18 True False 6 True False Folder Display Settings 0 0 0 _Show the items of all child feeds when a folder is selected. True True False start 12 True True 0 1 _Hide read items. True True False start 12 True True 0 2 0 0 True False 6 True False Feed Icons (Favicons) 0 0 0 _Update all favicons now True True False 12 True 0 1 0 1 1 True False Folders 1 False True False 12 vertical 18 True False vertical 6 12 True False Reading Headlines 0 0 0 2 True False 12 _Skim through articles with: True skimKeyCombo 0 0 1 True False 12 _Default View Mode: True defaultViewModeCombo 0 0 2 True False 0 1 1 True False 0 1 2 0 0 True False vertical 6 12 True False Web Integration 0 0 0 2 True False 12 _Post Bookmarks to True socialpopup 0 0 1 True False 1 1 0 1 2 True False Headlines 2 False True False 12 vertical 18 True False vertical 6 True False Internal Browser Settings 0 0 0 Open links in Liferea's _window. True True False start 12 True True 0 1 _Never run external Javascript. True True False start 12 True True 0 2 _Enable browser plugins. True True False start 12 True True 0 3 0 0 True False vertical 6 12 True False External Browser Settings 0 0 0 2 True False 12 _Browser: True browserpopup 0 0 1 True False 12 _Manual: True browsercmd 0 0 2 True True True 1 2 True False <small>(%s for URL)</small> True 0 1 3 True False 1 1 0 1 3 True False Browser 3 False True False 12 vertical 6 12 True False Toolbar Settings 0 0 0 2 _Hide toolbar. True True False start 12 True True 0 1 2 True False 12 Toolbar _button labels: True toolbarCombo 0 0 2 True False 0 1 2 True False Other 0 0 3 2 Ask for confirmation when marking all items as read True True False start 12 True 0 4 2 4 True False Desktop 4 False True False 12 6 True False HTTP Proxy Server 0 0 0 _Auto Detect (GNOME or environment) True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start 12 True True True 0 2 _No Proxy True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start 12 True True proxyAutoDetectRadio 0 3 _Manual Setting: True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK start 12 True True proxyAutoDetectRadio 0 4 True False start 12 vertical 6 12 True False 12 Proxy _Host: True proxyhostentry 0 0 0 True True True 1 0 True False 12 Proxy _Port: True proxyportentry 0 0 1 True True True 1 1 Use Proxy Au_thentication True True False start 12 True True 0 2 2 True False start 21 6 12 True False 12 Proxy _Username: True proxyusernameentry 0 0 0 True True True 1 0 True False 12 Proxy Pass_word: True proxypasswordentry 0 0 1 True True start False True 1 1 0 3 2 0 5 True False warning False 6 end False False 0 False 16 True False Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per application proxy settings. The system's default proxy settings will be used. True False True 0 False False 0 0 1 5 True False Proxy 5 False True False 12 vertical 6 12 True False Privacy Settings 0 0 0 Tell sites that I do _not want to be tracked. True True False start 12 True True 0 2 True False _Intelligent Tracking Prevention. True True False start 12 True True False True 0 True False This enables the WebKit feature described <a href="https://webkit.org/tracking-prevention/">here</a>. True True False True 1 0 3 False start 24 False 6 end False False 0 False 16 True False Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or higher. False True 1 False False 0 0 4 True False Use _Reader mode. True True False start 12 True True False True 0 True False This enables <a href="https://github.com/mozilla/readability">stripping</a> all non-content elements (like scripts, fonts, tracking) True False True 1 0 1 6 True False Privacy 6 False True False 12 18 True False 6 12 True False Downloading Enclosures 0 0 0 3 True False 12 _Download using True predefinedDownload 0 0 1 True True False True True 1 1 True True False True True predefinedDownload 1 2 True False <small>(%s for URL)</small> True True customDownload 1 0 2 True False 0 2 1 True True custom-command %s 2 2 0 0 True False True True vertical 6 12 True False Opening Enclosures True enc_action_view 0 0 0 True True 12 True True in 100 True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True 0 1 True False start 12 12 start gtk-properties True True False start True False False 0 gtk-delete True True False start True False False 1 0 2 0 1 7 True False Enclosures 7 False False True 1 prefclosebtn liferea-1.13.7/glade/properties.ui000066400000000000000000001263061415350204600170310ustar00rootroot00000000000000 10000 1 1 10000 1 1 True False 5 Subscription Properties center 300 dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True True False True False False 1 False False end 0 True True 5 True False 12 vertical 18 True False 6 12 True False Feed _Name True feedNameEntry 0 0 0 True True True True 0 1 0 0 True False 6 12 True False Update _Interval True updateIntervalDefault 0 0 0 3 _Use global default update interval. True True False 12 True 0 True True 0 1 3 _Feed specific update interval of True True False 12 True True updateIntervalDefault 0 2 True True 1 adjustment5 1 1 1 2 True False liststore6 0 2 2 _Don't update this feed automatically. True True False 12 True 0 True updateIntervalDefault 0 3 3 True False 0 4 3 339 True False 12 This feed provider suggests an update interval of %d minutes. True 0 0 5 3 0 1 True False General False True False 12 6 12 True False Feed Source 0 0 0 3 True False 12 Source Type: 0 0 0 1 3 _URL True True False True 0 True True 1 1 2 _Command True True False True 0 True feed_loc_url 1 2 2 _Local File True True False True 0 True feed_loc_url 1 3 Select File... True True False True 2 3 True False 12 _Source: True sourceEntry 0 0 4 True True 1 4 2 Use conversion _filter True True False 12 True 0 True 0 5 3 True False 24 Liferea can use external filter scripts in order to access feeds and directories in non-supported formats. True 0 0 6 3 True False 12 6 12 True False Convert _using: True filterEntry 0 0 True True True 1 0 Select File... True True False True 2 0 0 7 3 1 True False Source 1 False True False 12 6 12 347 True False The cache setting controls if the contents of feeds are saved when Liferea exits. Marked items are always saved to the cache. True 0 0 0 2 _Default cache settings True True False True True True 0 1 2 Di_sable cache True True False True True feedCacheDefault 0 2 2 _Unlimited cache True True False True True feedCacheDefault 0 3 2 _Number of items to save: True True False True True feedCacheDefault 0 4 True True 0 adjustment4 1 True True 1 4 2 True False Archive 2 False True False 12 6 12 Use HTTP _authentication True True False True True 0 0 _Don't use proxy for download True True False True True 0 2 True False 12 True 6 12 True False User_name: True usernameEntry 0 0 0 True True True 1 0 True False _Password: True passwordEntry 0 0 1 True True True False 1 1 0 1 3 True False Download 3 False True False 12 6 12 _Automatically download all enclosures of this feed. True True False True True 0 0 Auto-_load item link in configured browser when selecting articles. True True False True True 0 1 Ignore _comment feeds for this subscription. True True False True True 0 2 _Mark downloaded items as read. True True False True True 0 3 Extract full content from HTML5 and Google AMP True True False True True 0 4 4 True False Advanced 4 False False True 1 prop_cancel prop_ok liferea-1.13.7/glade/reedah_source.ui000066400000000000000000000170301415350204600174360ustar00rootroot00000000000000 True False Add Reedah Account dialog True False vertical True False end gtk-cancel False True True True False False True False False 0 gtk-ok False True True True False False True False False 1 False True end 0 True False 12 12 True False 0 Please enter your Reedah account settings. True False False 0 True False 2 2 6 6 True True False 1 2 1 2 True True 1 2 True False 0 _Password True passwordEntry 1 2 GTK_FILL True False 0 _Username (Email) True userEntry GTK_FILL False True 1 False True 2 cancelbutton1 okbutton1 liferea-1.13.7/glade/rename_node.ui000066400000000000000000000115251415350204600171050ustar00rootroot00000000000000 False 5 Rename False True center-on-parent dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True True False True False False 1 False False end 0 True False 5 6 12 True False _New Name: True nameentry 0 0 True True True 1 0 False True 1 cancelbutton1 namechangebtn liferea-1.13.7/glade/search.ui000066400000000000000000000332371415350204600161020ustar00rootroot00000000000000 True False 5 Advanced Search 600 250 dialog True False vertical 2 True False end gtk-close True True True False True False False 0 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-add True True 0 True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Search Folder... True savesearchbtn True True 1 False False 1 gtk-find True True True False True False False 2 False False end 0 True False vertical True False Find Items that meet the following criteria 0 False False 0 True False 12 True False 6 True False vertical 12 True False 6 gtk-add True True False True False False 0 A_ny Rule Matches True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True 0 True allRuleRadioBtn2 False True 1 _All Rules Must Match True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True 0 True True True True 2 False True 0 True True True False True False 6 True True 1 True True 1 True True 1 cancelbutton8 savesearchbtn okbutton5 liferea-1.13.7/glade/search_folder.ui000066400000000000000000000332511415350204600174310ustar00rootroot00000000000000 True False 5 Search Folder Properties 800 600 dialog True False vertical 2 True False end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False True end 0 True False vertical 12 True False 12 True False Search _Name: True searchNameEntry False False 0 True True True True 1 False True 0 True False Search Rules 0 False False 1 True False gtk-add True True False True True False False 0 False True 2 True False 0 True False True True True False True False 6 True False Rules All rules for this search folder True True 3 True False Rule Matching 0 False False 4 True False 6 A_ny Rule Matches True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True allRuleRadioBtn False True 0 _All Rules Must Match True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True True True True True 1 False True 5 Hide read items True True False True False True 6 True True 0 cancelbutton3 okbutton2 liferea-1.13.7/glade/simple_search.ui000066400000000000000000000137011415350204600174450ustar00rootroot00000000000000 True False 5 Search All Feeds False dialog True False vertical 2 True False end gtk-close True True True False True False False 0 _Advanced... True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True False False 1 gtk-find True False True True False Starts searching for the specified text in all feeds. The search result will appear in the item list. True False False 2 False False end 0 True False True 5 12 12 True False _Search for: True searchentry 0 0 True True Enter a search string Liferea should find either in a items title or in its content. True 1 0 True True 1 closebutton2 advancedbtn searchstartbtn liferea-1.13.7/glade/simple_subscription.ui000066400000000000000000000145611415350204600207310ustar00rootroot00000000000000 True False New Subscription dialog True False vertical True False end gtk-cancel False True True True False True False False 0 Advanced... False True True True False True False False 1 gtk-ok False True True True True False True False False 2 False True end 0 True False 12 12 True False Feed _Source True sourceEntry 0 0 0 True False 12 Enter a website location to use feed autodiscovery or in case you know it the exact feed location. True 0 0 0 1 True True 12 0 2 True True 1 cancelbutton5 advancedbtn1 button6 liferea-1.13.7/glade/theoldreader_source.ui000066400000000000000000000151371415350204600206560ustar00rootroot00000000000000 True Add TheOldReader Account dialog True True 12 12 True 0 Please enter your TheOldReader account settings. True False False 0 True 2 2 6 6 True True False 1 2 1 2 True True 1 2 True 0 _Password True passwordEntry 1 2 GTK_FILL True 0 _Username (Email) True userEntry GTK_FILL False 1 2 True end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False end 0 cancelbutton1 okbutton1 liferea-1.13.7/glade/ttrss_source.ui000066400000000000000000000177351415350204600174010ustar00rootroot00000000000000 True Add Tiny Tiny RSS Account dialog True True 12 12 True 0 Please enter your TinyTinyRSS account settings. True False False 0 True 3 2 6 6 True True 1 2 2 3 True True False 1 2 1 2 True True 1 2 True 0 _Server URL True serverUrlEntry 2 3 GTK_FILL True 0 _Password True passwordEntry 1 2 GTK_FILL True 0 _Username True userEntry GTK_FILL False 1 2 True end gtk-cancel True True True False True False False 0 gtk-ok True True True False True False False 1 False end 0 cancelbutton1 okbutton1 liferea-1.13.7/glade/update_monitor.ui000066400000000000000000000163171415350204600176660ustar00rootroot00000000000000 True False Update Monitor 400 300 True dialog True False vertical True False end Stop All True True True False False False 0 gtk-close True True True False True False False 1 False False end 0 True False 6 6 6 True True True True in True True False 0 1 True True True True in True True False 1 1 True False _Pending Requests True right 0 1 0 True False _Downloading Now True left 0 0 0 True True 1 button4 button5 liferea-1.13.7/js/000077500000000000000000000000001415350204600136265ustar00rootroot00000000000000liferea-1.13.7/js/LICENSE000066400000000000000000000010741415350204600146350ustar00rootroot00000000000000Readability.js: Copyright (c) 2010 Arc90 Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. liferea-1.13.7/js/Readability.js000066400000000000000000002016721415350204600164250ustar00rootroot00000000000000/*eslint-env es6:false*/ /* * Copyright (c) 2010 Arc90 Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * This code is heavily based on Arc90's readability.js (1.7.1) script * available at: http://code.google.com/p/arc90labs-readability */ /** * Public constructor. * @param {HTMLDocument} doc The document to parse. * @param {Object} options The options object. */ function Readability(doc, options) { // In some older versions, people passed a URI as the first argument. Cope: if (options && options.documentElement) { doc = options; options = arguments[2]; } else if (!doc || !doc.documentElement) { throw new Error("First argument to Readability constructor should be a document object."); } options = options || {}; this._doc = doc; this._articleTitle = null; this._articleByline = null; this._articleDir = null; this._articleSiteName = null; this._attempts = []; // Configurable options this._debug = !!options.debug; this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE; this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES; this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD; this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []); this._keepClasses = !!options.keepClasses; // Start with all flags set this._flags = this.FLAG_STRIP_UNLIKELYS | this.FLAG_WEIGHT_CLASSES | this.FLAG_CLEAN_CONDITIONALLY; var logEl; // Control whether log messages are sent to the console if (this._debug) { logEl = function(e) { var rv = e.nodeName + " "; if (e.nodeType == e.TEXT_NODE) { return rv + '("' + e.textContent + '")'; } var classDesc = e.className && ("." + e.className.replace(/ /g, ".")); var elDesc = ""; if (e.id) elDesc = "(#" + e.id + classDesc + ")"; else if (classDesc) elDesc = "(" + classDesc + ")"; return rv + elDesc; }; this.log = function () { if (typeof dump !== "undefined") { var msg = Array.prototype.map.call(arguments, function(x) { return (x && x.nodeName) ? logEl(x) : x; }).join(" "); dump("Reader: (Readability) " + msg + "\n"); } else if (typeof console !== "undefined") { var args = ["Reader: (Readability) "].concat(arguments); console.log.apply(console, args); } }; } else { this.log = function () {}; } } Readability.prototype = { FLAG_STRIP_UNLIKELYS: 0x1, FLAG_WEIGHT_CLASSES: 0x2, FLAG_CLEAN_CONDITIONALLY: 0x4, // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType ELEMENT_NODE: 1, TEXT_NODE: 3, // Max number of nodes supported by this parser. Default: 0 (no limit) DEFAULT_MAX_ELEMS_TO_PARSE: 0, // The number of top candidates to consider when analysing how // tight the competition is among candidates. DEFAULT_N_TOP_CANDIDATES: 5, // Element tags to score by default. DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","), // The default number of chars an article must have in order to return a result DEFAULT_CHAR_THRESHOLD: 500, // All of the regular expressions in use within readability. // Defined up here so we don't instantiate them repeatedly in loops. REGEXPS: { // NOTE: These two regular expressions are duplicated in // Readability-readerable.js. Please keep both copies in sync. unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, okMaybeItsACandidate: /and|article|body|column|main|shadow/i, positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i, negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i, extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i, byline: /byline|author|dateline|writtenby|p-author/i, replaceFonts: /<(\/?)font[^>]*>/gi, normalize: /\s{2,}/g, videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i, shareElements: /(\b|_)(share|sharedaddy)(\b|_)/i, nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, prevLink: /(prev|earl|old|new|<|«)/i, whitespace: /^\s*$/, hasContent: /\S$/, }, DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ], ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"], PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ], DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ], // The commented out elements qualify as phrasing content but tend to be // removed by readability when put into paragraphs, so we ignore them here. PHRASING_ELEMS: [ // "CANVAS", "IFRAME", "SVG", "VIDEO", "ABBR", "AUDIO", "B", "BDO", "BR", "BUTTON", "CITE", "CODE", "DATA", "DATALIST", "DFN", "EM", "EMBED", "I", "IMG", "INPUT", "KBD", "LABEL", "MARK", "MATH", "METER", "NOSCRIPT", "OBJECT", "OUTPUT", "PROGRESS", "Q", "RUBY", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "SUB", "SUP", "TEXTAREA", "TIME", "VAR", "WBR" ], // These are the classes that readability sets itself. CLASSES_TO_PRESERVE: [ "page" ], /** * Run any post-process modifications to article content as necessary. * * @param Element * @return void **/ _postProcessContent: function(articleContent) { // Readability cannot open relative uris so we convert them to absolute uris. this._fixRelativeUris(articleContent); if (!this._keepClasses) { // Remove classes. this._cleanClasses(articleContent); } }, /** * Iterates over a NodeList, calls `filterFn` for each node and removes node * if function returned `true`. * * If function is not passed, removes all the nodes in node list. * * @param NodeList nodeList The nodes to operate on * @param Function filterFn the function to use as a filter * @return void */ _removeNodes: function(nodeList, filterFn) { for (var i = nodeList.length - 1; i >= 0; i--) { var node = nodeList[i]; var parentNode = node.parentNode; if (parentNode) { if (!filterFn || filterFn.call(this, node, i, nodeList)) { parentNode.removeChild(node); } } } }, /** * Iterates over a NodeList, and calls _setNodeTag for each node. * * @param NodeList nodeList The nodes to operate on * @param String newTagName the new tag name to use * @return void */ _replaceNodeTags: function(nodeList, newTagName) { for (var i = nodeList.length - 1; i >= 0; i--) { var node = nodeList[i]; this._setNodeTag(node, newTagName); } }, /** * Iterate over a NodeList, which doesn't natively fully implement the Array * interface. * * For convenience, the current object context is applied to the provided * iterate function. * * @param NodeList nodeList The NodeList. * @param Function fn The iterate function. * @return void */ _forEachNode: function(nodeList, fn) { Array.prototype.forEach.call(nodeList, fn, this); }, /** * Iterate over a NodeList, return true if any of the provided iterate * function calls returns true, false otherwise. * * For convenience, the current object context is applied to the * provided iterate function. * * @param NodeList nodeList The NodeList. * @param Function fn The iterate function. * @return Boolean */ _someNode: function(nodeList, fn) { return Array.prototype.some.call(nodeList, fn, this); }, /** * Iterate over a NodeList, return true if all of the provided iterate * function calls return true, false otherwise. * * For convenience, the current object context is applied to the * provided iterate function. * * @param NodeList nodeList The NodeList. * @param Function fn The iterate function. * @return Boolean */ _everyNode: function(nodeList, fn) { return Array.prototype.every.call(nodeList, fn, this); }, /** * Concat all nodelists passed as arguments. * * @return ...NodeList * @return Array */ _concatNodeLists: function() { var slice = Array.prototype.slice; var args = slice.call(arguments); var nodeLists = args.map(function(list) { return slice.call(list); }); return Array.prototype.concat.apply([], nodeLists); }, _getAllNodesWithTag: function(node, tagNames) { if (node.querySelectorAll) { return node.querySelectorAll(tagNames.join(",")); } return [].concat.apply([], tagNames.map(function(tag) { var collection = node.getElementsByTagName(tag); return Array.isArray(collection) ? collection : Array.from(collection); })); }, /** * Removes the class="" attribute from every element in the given * subtree, except those that match CLASSES_TO_PRESERVE and * the classesToPreserve array from the options object. * * @param Element * @return void */ _cleanClasses: function(node) { var classesToPreserve = this._classesToPreserve; var className = (node.getAttribute("class") || "") .split(/\s+/) .filter(function(cls) { return classesToPreserve.indexOf(cls) != -1; }) .join(" "); if (className) { node.setAttribute("class", className); } else { node.removeAttribute("class"); } for (node = node.firstElementChild; node; node = node.nextElementSibling) { this._cleanClasses(node); } }, /** * Converts each and uri in the given element to an absolute URI, * ignoring #ref URIs. * * @param Element * @return void */ _fixRelativeUris: function(articleContent) { var baseURI = this._doc.baseURI; var documentURI = this._doc.documentURI; function toAbsoluteURI(uri) { // Leave hash links alone if the base URI matches the document URI: if (baseURI == documentURI && uri.charAt(0) == "#") { return uri; } // Otherwise, resolve against base URI: try { return new URL(uri, baseURI).href; } catch (ex) { // Something went wrong, just return the original: } return uri; } var links = this._getAllNodesWithTag(articleContent, ["a"]); this._forEachNode(links, function(link) { var href = link.getAttribute("href"); if (href) { // Replace links with javascript: URIs with text content, since // they won't work after scripts have been removed from the page. if (href.indexOf("javascript:") === 0) { var text = this._doc.createTextNode(link.textContent); link.parentNode.replaceChild(text, link); } else { link.setAttribute("href", toAbsoluteURI(href)); } } }); var imgs = this._getAllNodesWithTag(articleContent, ["img"]); this._forEachNode(imgs, function(img) { var src = img.getAttribute("src"); if (src) { img.setAttribute("src", toAbsoluteURI(src)); } }); }, /** * Get the article title as an H1. * * @return void **/ _getArticleTitle: function() { var doc = this._doc; var curTitle = ""; var origTitle = ""; try { curTitle = origTitle = doc.title.trim(); // If they had an element with id "title" in their HTML if (typeof curTitle !== "string") curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]); } catch (e) {/* ignore exceptions setting the title. */} var titleHadHierarchicalSeparators = false; function wordCount(str) { return str.split(/\s+/).length; } // If there's a separator in the title, first remove the final part if ((/ [\|\-\\\/>»] /).test(curTitle)) { titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle); curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1"); // If the resulting title is too short (3 words or fewer), remove // the first part instead: if (wordCount(curTitle) < 3) curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1"); } else if (curTitle.indexOf(": ") !== -1) { // Check if we have an heading containing this exact string, so we // could assume it's the full title. var headings = this._concatNodeLists( doc.getElementsByTagName("h1"), doc.getElementsByTagName("h2") ); var trimmedTitle = curTitle.trim(); var match = this._someNode(headings, function(heading) { return heading.textContent.trim() === trimmedTitle; }); // If we don't, let's extract the title out of the original title string. if (!match) { curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1); // If the title is now too short, try the first colon instead: if (wordCount(curTitle) < 3) { curTitle = origTitle.substring(origTitle.indexOf(":") + 1); // But if we have too many words before the colon there's something weird // with the titles and the H tags so let's just use the original title instead } else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) { curTitle = origTitle; } } } else if (curTitle.length > 150 || curTitle.length < 15) { var hOnes = doc.getElementsByTagName("h1"); if (hOnes.length === 1) curTitle = this._getInnerText(hOnes[0]); } curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " "); // If we now have 4 words or fewer as our title, and either no // 'hierarchical' separators (\, /, > or ») were found in the original // title or we decreased the number of words by more than 1 word, use // the original title. var curTitleWordCount = wordCount(curTitle); if (curTitleWordCount <= 4 && (!titleHadHierarchicalSeparators || curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) { curTitle = origTitle; } return curTitle; }, /** * Prepare the HTML document for readability to scrape it. * This includes things like stripping javascript, CSS, and handling terrible markup. * * @return void **/ _prepDocument: function() { var doc = this._doc; // Remove all style tags in head this._removeNodes(doc.getElementsByTagName("style")); if (doc.body) { this._replaceBrs(doc.body); } this._replaceNodeTags(doc.getElementsByTagName("font"), "SPAN"); }, /** * Finds the next element, starting from the given node, and ignoring * whitespace in between. If the given node is an element, the same node is * returned. */ _nextElement: function (node) { var next = node; while (next && (next.nodeType != this.ELEMENT_NODE) && this.REGEXPS.whitespace.test(next.textContent)) { next = next.nextSibling; } return next; }, /** * Replaces 2 or more successive
elements with a single

. * Whitespace between
elements are ignored. For example: *

foo
bar


abc
* will become: *
foo
bar

abc

*/ _replaceBrs: function (elem) { this._forEachNode(this._getAllNodesWithTag(elem, ["br"]), function(br) { var next = br.nextSibling; // Whether 2 or more
elements have been found and replaced with a //

block. var replaced = false; // If we find a
chain, remove the
s until we hit another element // or non-whitespace. This leaves behind the first
in the chain // (which will be replaced with a

later). while ((next = this._nextElement(next)) && (next.tagName == "BR")) { replaced = true; var brSibling = next.nextSibling; next.parentNode.removeChild(next); next = brSibling; } // If we removed a
chain, replace the remaining
with a

. Add // all sibling nodes as children of the

until we hit another
// chain. if (replaced) { var p = this._doc.createElement("p"); br.parentNode.replaceChild(p, br); next = p.nextSibling; while (next) { // If we've hit another

, we're done adding children to this

. if (next.tagName == "BR") { var nextElem = this._nextElement(next.nextSibling); if (nextElem && nextElem.tagName == "BR") break; } if (!this._isPhrasingContent(next)) break; // Otherwise, make this node a child of the new

. var sibling = next.nextSibling; p.appendChild(next); next = sibling; } while (p.lastChild && this._isWhitespace(p.lastChild)) { p.removeChild(p.lastChild); } if (p.parentNode.tagName === "P") this._setNodeTag(p.parentNode, "DIV"); } }); }, _setNodeTag: function (node, tag) { this.log("_setNodeTag", node, tag); if (node.__JSDOMParser__) { node.localName = tag.toLowerCase(); node.tagName = tag.toUpperCase(); return node; } var replacement = node.ownerDocument.createElement(tag); while (node.firstChild) { replacement.appendChild(node.firstChild); } node.parentNode.replaceChild(replacement, node); if (node.readability) replacement.readability = node.readability; for (var i = 0; i < node.attributes.length; i++) { try { replacement.setAttribute(node.attributes[i].name, node.attributes[i].value); } catch (ex) { /* it's possible for setAttribute() to throw if the attribute name * isn't a valid XML Name. Such attributes can however be parsed from * source in HTML docs, see https://github.com/whatwg/html/issues/4275, * so we can hit them here and then throw. We don't care about such * attributes so we ignore them. */ } } return replacement; }, /** * Prepare the article node for display. Clean out any inline styles, * iframes, forms, strip extraneous

tags, etc. * * @param Element * @return void **/ _prepArticle: function(articleContent) { this._cleanStyles(articleContent); // Check for data tables before we continue, to avoid removing items in // those tables, which will often be isolated even though they're // visually linked to other content-ful elements (text, images, etc.). this._markDataTables(articleContent); this._fixLazyImages(articleContent); // Clean out junk from the article content this._cleanConditionally(articleContent, "form"); this._cleanConditionally(articleContent, "fieldset"); this._clean(articleContent, "object"); this._clean(articleContent, "embed"); this._clean(articleContent, "h1"); this._clean(articleContent, "footer"); this._clean(articleContent, "link"); this._clean(articleContent, "aside"); // Clean out elements with little content that have "share" in their id/class combinations from final top candidates, // which means we don't remove the top candidates even they have "share". var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD; this._forEachNode(articleContent.children, function (topCandidate) { this._cleanMatchedNodes(topCandidate, function (node, matchString) { return this.REGEXPS.shareElements.test(matchString) && node.textContent.length < shareElementThreshold; }); }); // If there is only one h2 and its text content substantially equals article title, // they are probably using it as a header and not a subheader, // so remove it since we already extract the title separately. var h2 = articleContent.getElementsByTagName("h2"); if (h2.length === 1) { var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length; if (Math.abs(lengthSimilarRate) < 0.5) { var titlesMatch = false; if (lengthSimilarRate > 0) { titlesMatch = h2[0].textContent.includes(this._articleTitle); } else { titlesMatch = this._articleTitle.includes(h2[0].textContent); } if (titlesMatch) { this._clean(articleContent, "h2"); } } } this._clean(articleContent, "iframe"); this._clean(articleContent, "input"); this._clean(articleContent, "textarea"); this._clean(articleContent, "select"); this._clean(articleContent, "button"); this._cleanHeaders(articleContent); // Do these last as the previous stuff may have removed junk // that will affect these this._cleanConditionally(articleContent, "table"); this._cleanConditionally(articleContent, "ul"); this._cleanConditionally(articleContent, "div"); // Remove extra paragraphs this._removeNodes(articleContent.getElementsByTagName("p"), function (paragraph) { var imgCount = paragraph.getElementsByTagName("img").length; var embedCount = paragraph.getElementsByTagName("embed").length; var objectCount = paragraph.getElementsByTagName("object").length; // At this point, nasty iframes have been removed, only remain embedded video ones. var iframeCount = paragraph.getElementsByTagName("iframe").length; var totalCount = imgCount + embedCount + objectCount + iframeCount; return totalCount === 0 && !this._getInnerText(paragraph, false); }); this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) { var next = this._nextElement(br.nextSibling); if (next && next.tagName == "P") br.parentNode.removeChild(br); }); // Remove single-cell tables this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) { var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table; if (this._hasSingleTagInsideElement(tbody, "TR")) { var row = tbody.firstElementChild; if (this._hasSingleTagInsideElement(row, "TD")) { var cell = row.firstElementChild; cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV"); table.parentNode.replaceChild(cell, table); } } }); }, /** * Initialize a node with the readability object. Also checks the * className/id for special names to add to its score. * * @param Element * @return void **/ _initializeNode: function(node) { node.readability = {"contentScore": 0}; switch (node.tagName) { case "DIV": node.readability.contentScore += 5; break; case "PRE": case "TD": case "BLOCKQUOTE": node.readability.contentScore += 3; break; case "ADDRESS": case "OL": case "UL": case "DL": case "DD": case "DT": case "LI": case "FORM": node.readability.contentScore -= 3; break; case "H1": case "H2": case "H3": case "H4": case "H5": case "H6": case "TH": node.readability.contentScore -= 5; break; } node.readability.contentScore += this._getClassWeight(node); }, _removeAndGetNext: function(node) { var nextNode = this._getNextNode(node, true); node.parentNode.removeChild(node); return nextNode; }, /** * Traverse the DOM from node to node, starting at the node passed in. * Pass true for the second parameter to indicate this node itself * (and its kids) are going away, and we want the next node over. * * Calling this in a loop will traverse the DOM depth-first. */ _getNextNode: function(node, ignoreSelfAndKids) { // First check for kids if those aren't being ignored if (!ignoreSelfAndKids && node.firstElementChild) { return node.firstElementChild; } // Then for siblings... if (node.nextElementSibling) { return node.nextElementSibling; } // And finally, move up the parent chain *and* find a sibling // (because this is depth-first traversal, we will have already // seen the parent nodes themselves). do { node = node.parentNode; } while (node && !node.nextElementSibling); return node && node.nextElementSibling; }, _checkByline: function(node, matchString) { if (this._articleByline) { return false; } if (node.getAttribute !== undefined) { var rel = node.getAttribute("rel"); var itemprop = node.getAttribute("itemprop"); } if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) { this._articleByline = node.textContent.trim(); return true; } return false; }, _getNodeAncestors: function(node, maxDepth) { maxDepth = maxDepth || 0; var i = 0, ancestors = []; while (node.parentNode) { ancestors.push(node.parentNode); if (maxDepth && ++i === maxDepth) break; node = node.parentNode; } return ancestors; }, /*** * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is * most likely to be the stuff a user wants to read. Then return it wrapped up in a div. * * @param page a document to run upon. Needs to be a full document, complete with body. * @return Element **/ _grabArticle: function (page) { this.log("**** grabArticle ****"); var doc = this._doc; var isPaging = (page !== null ? true: false); page = page ? page : this._doc.body; // We can't grab an article if we don't have a page! if (!page) { this.log("No body found in document. Abort."); return null; } var pageCacheHtml = page.innerHTML; while (true) { var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS); // First, node prepping. Trash nodes that look cruddy (like ones with the // class name "comment", etc), and turn divs into P tags where they have been // used inappropriately (as in, where they contain no other block level elements.) var elementsToScore = []; var node = this._doc.documentElement; while (node) { var matchString = node.className + " " + node.id; if (!this._isProbablyVisible(node)) { this.log("Removing hidden node - " + matchString); node = this._removeAndGetNext(node); continue; } // Check to see if this node is a byline, and remove it if it is. if (this._checkByline(node, matchString)) { node = this._removeAndGetNext(node); continue; } // Remove unlikely candidates if (stripUnlikelyCandidates) { if (this.REGEXPS.unlikelyCandidates.test(matchString) && !this.REGEXPS.okMaybeItsACandidate.test(matchString) && !this._hasAncestorTag(node, "table") && node.tagName !== "BODY" && node.tagName !== "A") { this.log("Removing unlikely candidate - " + matchString); node = this._removeAndGetNext(node); continue; } } // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe). if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" || node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" || node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") && this._isElementWithoutContent(node)) { node = this._removeAndGetNext(node); continue; } if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) { elementsToScore.push(node); } // Turn all divs that don't have children block level elements into p's if (node.tagName === "DIV") { // Put phrasing content into paragraphs. var p = null; var childNode = node.firstChild; while (childNode) { var nextSibling = childNode.nextSibling; if (this._isPhrasingContent(childNode)) { if (p !== null) { p.appendChild(childNode); } else if (!this._isWhitespace(childNode)) { p = doc.createElement("p"); node.replaceChild(p, childNode); p.appendChild(childNode); } } else if (p !== null) { while (p.lastChild && this._isWhitespace(p.lastChild)) { p.removeChild(p.lastChild); } p = null; } childNode = nextSibling; } // Sites like http://mobile.slate.com encloses each paragraph with a DIV // element. DIVs with only a P element inside and no text content can be // safely converted into plain P elements to avoid confusing the scoring // algorithm with DIVs with are, in practice, paragraphs. if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) { var newNode = node.children[0]; node.parentNode.replaceChild(newNode, node); node = newNode; elementsToScore.push(node); } else if (!this._hasChildBlockElement(node)) { node = this._setNodeTag(node, "P"); elementsToScore.push(node); } } node = this._getNextNode(node); } /** * Loop through all paragraphs, and assign a score to them based on how content-y they look. * Then add their score to their parent node. * * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. **/ var candidates = []; this._forEachNode(elementsToScore, function(elementToScore) { if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined") return; // If this paragraph is less than 25 characters, don't even count it. var innerText = this._getInnerText(elementToScore); if (innerText.length < 25) return; // Exclude nodes with no ancestor. var ancestors = this._getNodeAncestors(elementToScore, 3); if (ancestors.length === 0) return; var contentScore = 0; // Add a point for the paragraph itself as a base. contentScore += 1; // Add points for any commas within this paragraph. contentScore += innerText.split(",").length; // For every 100 characters in this paragraph, add another point. Up to 3 points. contentScore += Math.min(Math.floor(innerText.length / 100), 3); // Initialize and score ancestors. this._forEachNode(ancestors, function(ancestor, level) { if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined") return; if (typeof(ancestor.readability) === "undefined") { this._initializeNode(ancestor); candidates.push(ancestor); } // Node score divider: // - parent: 1 (no division) // - grandparent: 2 // - great grandparent+: ancestor level * 3 if (level === 0) var scoreDivider = 1; else if (level === 1) scoreDivider = 2; else scoreDivider = level * 3; ancestor.readability.contentScore += contentScore / scoreDivider; }); }); // After we've calculated scores, loop through all of the possible // candidate nodes we found and find the one with the highest score. var topCandidates = []; for (var c = 0, cl = candidates.length; c < cl; c += 1) { var candidate = candidates[c]; // Scale the final candidates score based on link density. Good content // should have a relatively small link density (5% or less) and be mostly // unaffected by this operation. var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate)); candidate.readability.contentScore = candidateScore; this.log("Candidate:", candidate, "with score " + candidateScore); for (var t = 0; t < this._nbTopCandidates; t++) { var aTopCandidate = topCandidates[t]; if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) { topCandidates.splice(t, 0, candidate); if (topCandidates.length > this._nbTopCandidates) topCandidates.pop(); break; } } } var topCandidate = topCandidates[0] || null; var neededToCreateTopCandidate = false; var parentOfTopCandidate; // If we still have no top candidate, just use the body as a last resort. // We also have to copy the body node so it is something we can modify. if (topCandidate === null || topCandidate.tagName === "BODY") { // Move all of the page's children into topCandidate topCandidate = doc.createElement("DIV"); neededToCreateTopCandidate = true; // Move everything (not just elements, also text nodes etc.) into the container // so we even include text directly in the body: var kids = page.childNodes; while (kids.length) { this.log("Moving child out:", kids[0]); topCandidate.appendChild(kids[0]); } page.appendChild(topCandidate); this._initializeNode(topCandidate); } else if (topCandidate) { // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array // and whose scores are quite closed with current `topCandidate` node. var alternativeCandidateAncestors = []; for (var i = 1; i < topCandidates.length; i++) { if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) { alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i])); } } var MINIMUM_TOPCANDIDATES = 3; if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) { parentOfTopCandidate = topCandidate.parentNode; while (parentOfTopCandidate.tagName !== "BODY") { var listsContainingThisAncestor = 0; for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) { listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate)); } if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) { topCandidate = parentOfTopCandidate; break; } parentOfTopCandidate = parentOfTopCandidate.parentNode; } } if (!topCandidate.readability) { this._initializeNode(topCandidate); } // Because of our bonus system, parents of candidates might have scores // themselves. They get half of the node. There won't be nodes with higher // scores than our topCandidate, but if we see the score going *up* in the first // few steps up the tree, that's a decent sign that there might be more content // lurking in other places that we want to unify in. The sibling stuff // below does some of that - but only if we've looked high enough up the DOM // tree. parentOfTopCandidate = topCandidate.parentNode; var lastScore = topCandidate.readability.contentScore; // The scores shouldn't get too low. var scoreThreshold = lastScore / 3; while (parentOfTopCandidate.tagName !== "BODY") { if (!parentOfTopCandidate.readability) { parentOfTopCandidate = parentOfTopCandidate.parentNode; continue; } var parentScore = parentOfTopCandidate.readability.contentScore; if (parentScore < scoreThreshold) break; if (parentScore > lastScore) { // Alright! We found a better parent to use. topCandidate = parentOfTopCandidate; break; } lastScore = parentOfTopCandidate.readability.contentScore; parentOfTopCandidate = parentOfTopCandidate.parentNode; } // If the top candidate is the only child, use parent instead. This will help sibling // joining logic when adjacent content is actually located in parent's sibling node. parentOfTopCandidate = topCandidate.parentNode; while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) { topCandidate = parentOfTopCandidate; parentOfTopCandidate = topCandidate.parentNode; } if (!topCandidate.readability) { this._initializeNode(topCandidate); } } // Now that we have the top candidate, look through its siblings for content // that might also be related. Things like preambles, content split by ads // that we removed, etc. var articleContent = doc.createElement("DIV"); if (isPaging) articleContent.id = "readability-content"; var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2); // Keep potential top candidate's parent node to try to get text direction of it later. parentOfTopCandidate = topCandidate.parentNode; var siblings = parentOfTopCandidate.children; for (var s = 0, sl = siblings.length; s < sl; s++) { var sibling = siblings[s]; var append = false; this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : ""); this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown"); if (sibling === topCandidate) { append = true; } else { var contentBonus = 0; // Give a bonus if sibling nodes and top candidates have the example same classname if (sibling.className === topCandidate.className && topCandidate.className !== "") contentBonus += topCandidate.readability.contentScore * 0.2; if (sibling.readability && ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) { append = true; } else if (sibling.nodeName === "P") { var linkDensity = this._getLinkDensity(sibling); var nodeContent = this._getInnerText(sibling); var nodeLength = nodeContent.length; if (nodeLength > 80 && linkDensity < 0.25) { append = true; } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && nodeContent.search(/\.( |$)/) !== -1) { append = true; } } } if (append) { this.log("Appending node:", sibling); if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) { // We have a node that isn't a common block level element, like a form or td tag. // Turn it into a div so it doesn't get filtered out later by accident. this.log("Altering sibling:", sibling, "to div."); sibling = this._setNodeTag(sibling, "DIV"); } articleContent.appendChild(sibling); // siblings is a reference to the children array, and // sibling is removed from the array when we call appendChild(). // As a result, we must revisit this index since the nodes // have been shifted. s -= 1; sl -= 1; } } if (this._debug) this.log("Article content pre-prep: " + articleContent.innerHTML); // So we have all of the content that we need. Now we clean it up for presentation. this._prepArticle(articleContent); if (this._debug) this.log("Article content post-prep: " + articleContent.innerHTML); if (neededToCreateTopCandidate) { // We already created a fake div thing, and there wouldn't have been any siblings left // for the previous loop, so there's no point trying to create a new div, and then // move all the children over. Just assign IDs and class names here. No need to append // because that already happened anyway. topCandidate.id = "readability-page-1"; topCandidate.className = "page"; } else { var div = doc.createElement("DIV"); div.id = "readability-page-1"; div.className = "page"; var children = articleContent.childNodes; while (children.length) { div.appendChild(children[0]); } articleContent.appendChild(div); } if (this._debug) this.log("Article content after paging: " + articleContent.innerHTML); var parseSuccessful = true; // Now that we've gone through the full algorithm, check to see if // we got any meaningful content. If we didn't, we may need to re-run // grabArticle with different flags set. This gives us a higher likelihood of // finding the content, and the sieve approach gives us a higher likelihood of // finding the -right- content. var textLength = this._getInnerText(articleContent, true).length; if (textLength < this._charThreshold) { parseSuccessful = false; page.innerHTML = pageCacheHtml; if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) { this._removeFlag(this.FLAG_STRIP_UNLIKELYS); this._attempts.push({articleContent: articleContent, textLength: textLength}); } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) { this._removeFlag(this.FLAG_WEIGHT_CLASSES); this._attempts.push({articleContent: articleContent, textLength: textLength}); } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) { this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY); this._attempts.push({articleContent: articleContent, textLength: textLength}); } else { this._attempts.push({articleContent: articleContent, textLength: textLength}); // No luck after removing flags, just return the longest text we found during the different loops this._attempts.sort(function (a, b) { return b.textLength - a.textLength; }); // But first check if we actually have something if (!this._attempts[0].textLength) { return null; } articleContent = this._attempts[0].articleContent; parseSuccessful = true; } } if (parseSuccessful) { // Find out text direction from ancestors of final top candidate. var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate)); this._someNode(ancestors, function(ancestor) { if (!ancestor.tagName) return false; var articleDir = ancestor.getAttribute("dir"); if (articleDir) { this._articleDir = articleDir; return true; } return false; }); return articleContent; } } }, /** * Check whether the input string could be a byline. * This verifies that the input is a string, and that the length * is less than 100 chars. * * @param possibleByline {string} - a string to check whether its a byline. * @return Boolean - whether the input string is a byline. */ _isValidByline: function(byline) { if (typeof byline == "string" || byline instanceof String) { byline = byline.trim(); return (byline.length > 0) && (byline.length < 100); } return false; }, /** * Attempts to get excerpt and byline metadata for the article. * * @return Object with optional "excerpt" and "byline" properties */ _getArticleMetadata: function() { var metadata = {}; var values = {}; var metaElements = this._doc.getElementsByTagName("meta"); // property is a space-separated list of values var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi; // name is a single value var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i; // Find description tags. this._forEachNode(metaElements, function(element) { var elementName = element.getAttribute("name"); var elementProperty = element.getAttribute("property"); var content = element.getAttribute("content"); if (!content) { return; } var matches = null; var name = null; if (elementProperty) { matches = elementProperty.match(propertyPattern); if (matches) { for (var i = matches.length - 1; i >= 0; i--) { // Convert to lowercase, and remove any whitespace // so we can match below. name = matches[i].toLowerCase().replace(/\s/g, ""); // multiple authors values[name] = content.trim(); } } } if (!matches && elementName && namePattern.test(elementName)) { name = elementName; if (content) { // Convert to lowercase, remove any whitespace, and convert dots // to colons so we can match below. name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":"); values[name] = content.trim(); } } }); // get title metadata.title = values["dc:title"] || values["dcterm:title"] || values["og:title"] || values["weibo:article:title"] || values["weibo:webpage:title"] || values["title"] || values["twitter:title"]; if (!metadata.title) { metadata.title = this._getArticleTitle(); } // get author metadata.byline = values["dc:creator"] || values["dcterm:creator"] || values["author"]; // get description metadata.excerpt = values["dc:description"] || values["dcterm:description"] || values["og:description"] || values["weibo:article:description"] || values["weibo:webpage:description"] || values["description"] || values["twitter:description"]; // get site name metadata.siteName = values["og:site_name"]; return metadata; }, /** * Removes script tags from the document. * * @param Element **/ _removeScripts: function(doc) { this._removeNodes(doc.getElementsByTagName("script"), function(scriptNode) { scriptNode.nodeValue = ""; scriptNode.removeAttribute("src"); return true; }); this._removeNodes(doc.getElementsByTagName("noscript")); }, /** * Check if this node has only whitespace and a single element with given tag * Returns false if the DIV node contains non-empty text nodes * or if it contains no element with given tag or more than 1 element. * * @param Element * @param string tag of child element **/ _hasSingleTagInsideElement: function(element, tag) { // There should be exactly 1 element child with given tag if (element.children.length != 1 || element.children[0].tagName !== tag) { return false; } // And there should be no text nodes with real content return !this._someNode(element.childNodes, function(node) { return node.nodeType === this.TEXT_NODE && this.REGEXPS.hasContent.test(node.textContent); }); }, _isElementWithoutContent: function(node) { return node.nodeType === this.ELEMENT_NODE && node.textContent.trim().length == 0 && (node.children.length == 0 || node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length); }, /** * Determine whether element has any children block level elements. * * @param Element */ _hasChildBlockElement: function (element) { return this._someNode(element.childNodes, function(node) { return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 || this._hasChildBlockElement(node); }); }, /*** * Determine if a node qualifies as phrasing content. * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content **/ _isPhrasingContent: function(node) { return node.nodeType === this.TEXT_NODE || this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 || ((node.tagName === "A" || node.tagName === "DEL" || node.tagName === "INS") && this._everyNode(node.childNodes, this._isPhrasingContent)); }, _isWhitespace: function(node) { return (node.nodeType === this.TEXT_NODE && node.textContent.trim().length === 0) || (node.nodeType === this.ELEMENT_NODE && node.tagName === "BR"); }, /** * Get the inner text of a node - cross browser compatibly. * This also strips out any excess whitespace to be found. * * @param Element * @param Boolean normalizeSpaces (default: true) * @return string **/ _getInnerText: function(e, normalizeSpaces) { normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces; var textContent = e.textContent.trim(); if (normalizeSpaces) { return textContent.replace(this.REGEXPS.normalize, " "); } return textContent; }, /** * Get the number of times a string s appears in the node e. * * @param Element * @param string - what to split on. Default is "," * @return number (integer) **/ _getCharCount: function(e, s) { s = s || ","; return this._getInnerText(e).split(s).length - 1; }, /** * Remove the style attribute on every e and under. * TODO: Test if getElementsByTagName(*) is faster. * * @param Element * @return void **/ _cleanStyles: function(e) { if (!e || e.tagName.toLowerCase() === "svg") return; // Remove `style` and deprecated presentational attributes for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) { e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]); } if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) { e.removeAttribute("width"); e.removeAttribute("height"); } var cur = e.firstElementChild; while (cur !== null) { this._cleanStyles(cur); cur = cur.nextElementSibling; } }, /** * Get the density of links as a percentage of the content * This is the amount of text that is inside a link divided by the total text in the node. * * @param Element * @return number (float) **/ _getLinkDensity: function(element) { var textLength = this._getInnerText(element).length; if (textLength === 0) return 0; var linkLength = 0; // XXX implement _reduceNodeList? this._forEachNode(element.getElementsByTagName("a"), function(linkNode) { linkLength += this._getInnerText(linkNode).length; }); return linkLength / textLength; }, /** * Get an elements class/id weight. Uses regular expressions to tell if this * element looks good or bad. * * @param Element * @return number (Integer) **/ _getClassWeight: function(e) { if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) return 0; var weight = 0; // Look for a special classname if (typeof(e.className) === "string" && e.className !== "") { if (this.REGEXPS.negative.test(e.className)) weight -= 25; if (this.REGEXPS.positive.test(e.className)) weight += 25; } // Look for a special ID if (typeof(e.id) === "string" && e.id !== "") { if (this.REGEXPS.negative.test(e.id)) weight -= 25; if (this.REGEXPS.positive.test(e.id)) weight += 25; } return weight; }, /** * Clean a node of all elements of type "tag". * (Unless it's a youtube/vimeo video. People love movies.) * * @param Element * @param string tag to clean * @return void **/ _clean: function(e, tag) { var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1; this._removeNodes(e.getElementsByTagName(tag), function(element) { // Allow youtube and vimeo videos through as people usually want to see those. if (isEmbed) { // First, check the elements attributes to see if any of them contain youtube or vimeo for (var i = 0; i < element.attributes.length; i++) { if (this.REGEXPS.videos.test(element.attributes[i].value)) { return false; } } // For embed with tag, check inner HTML as well. if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) { return false; } } return true; }); }, /** * Check if a given node has one of its ancestor tag name matching the * provided one. * @param HTMLElement node * @param String tagName * @param Number maxDepth * @param Function filterFn a filter to invoke to determine whether this node 'counts' * @return Boolean */ _hasAncestorTag: function(node, tagName, maxDepth, filterFn) { maxDepth = maxDepth || 3; tagName = tagName.toUpperCase(); var depth = 0; while (node.parentNode) { if (maxDepth > 0 && depth > maxDepth) return false; if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode))) return true; node = node.parentNode; depth++; } return false; }, /** * Return an object indicating how many rows and columns this table has. */ _getRowAndColumnCount: function(table) { var rows = 0; var columns = 0; var trs = table.getElementsByTagName("tr"); for (var i = 0; i < trs.length; i++) { var rowspan = trs[i].getAttribute("rowspan") || 0; if (rowspan) { rowspan = parseInt(rowspan, 10); } rows += (rowspan || 1); // Now look for column-related info var columnsInThisRow = 0; var cells = trs[i].getElementsByTagName("td"); for (var j = 0; j < cells.length; j++) { var colspan = cells[j].getAttribute("colspan") || 0; if (colspan) { colspan = parseInt(colspan, 10); } columnsInThisRow += (colspan || 1); } columns = Math.max(columns, columnsInThisRow); } return {rows: rows, columns: columns}; }, /** * Look for 'data' (as opposed to 'layout') tables, for which we use * similar checks as * https://dxr.mozilla.org/mozilla-central/rev/71224049c0b52ab190564d3ea0eab089a159a4cf/accessible/html/HTMLTableAccessible.cpp#920 */ _markDataTables: function(root) { var tables = root.getElementsByTagName("table"); for (var i = 0; i < tables.length; i++) { var table = tables[i]; var role = table.getAttribute("role"); if (role == "presentation") { table._readabilityDataTable = false; continue; } var datatable = table.getAttribute("datatable"); if (datatable == "0") { table._readabilityDataTable = false; continue; } var summary = table.getAttribute("summary"); if (summary) { table._readabilityDataTable = true; continue; } var caption = table.getElementsByTagName("caption")[0]; if (caption && caption.childNodes.length > 0) { table._readabilityDataTable = true; continue; } // If the table has a descendant with any of these tags, consider a data table: var dataTableDescendants = ["col", "colgroup", "tfoot", "thead", "th"]; var descendantExists = function(tag) { return !!table.getElementsByTagName(tag)[0]; }; if (dataTableDescendants.some(descendantExists)) { this.log("Data table because found data-y descendant"); table._readabilityDataTable = true; continue; } // Nested tables indicate a layout table: if (table.getElementsByTagName("table")[0]) { table._readabilityDataTable = false; continue; } var sizeInfo = this._getRowAndColumnCount(table); if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) { table._readabilityDataTable = true; continue; } // Now just go by size entirely: table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10; } }, /* convert images and figures that have properties like data-src into images that can be loaded without JS */ _fixLazyImages: function (root) { this._forEachNode(this._getAllNodesWithTag(root, ["img", "picture", "figure"]), function (elem) { // also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580 if ((!elem.src && (!elem.srcset || elem.srcset == "null")) || elem.className.toLowerCase().indexOf("lazy") !== -1) { for (var i = 0; i < elem.attributes.length; i++) { var attr = elem.attributes[i]; if (attr.name === "src" || attr.name === "srcset") { continue; } var copyTo = null; if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) { copyTo = "srcset"; } else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) { copyTo = "src"; } if (copyTo) { //if this is an img or picture, set the attribute directly if (elem.tagName === "IMG" || elem.tagName === "PICTURE") { elem.setAttribute(copyTo, attr.value); } else if (elem.tagName === "FIGURE" && !this._getAllNodesWithTag(elem, ["img", "picture"]).length) { //if the item is a
that does not contain an image or picture, create one and place it inside the figure //see the nytimes-3 testcase for an example var img = this._doc.createElement("img"); img.setAttribute(copyTo, attr.value); elem.appendChild(img); } } } } }); }, /** * Clean an element of all tags of type "tag" if they look fishy. * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc. * * @return void **/ _cleanConditionally: function(e, tag) { if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) return; var isList = tag === "ul" || tag === "ol"; // Gather counts for other typical elements embedded within. // Traverse backwards so we can remove nodes at the same time // without effecting the traversal. // // TODO: Consider taking into account original contentScore here. this._removeNodes(e.getElementsByTagName(tag), function(node) { // First check if this node IS data table, in which case don't remove it. var isDataTable = function(t) { return t._readabilityDataTable; }; if (tag === "table" && isDataTable(node)) { return false; } // Next check if we're inside a data table, in which case don't remove it as well. if (this._hasAncestorTag(node, "table", -1, isDataTable)) { return false; } var weight = this._getClassWeight(node); var contentScore = 0; this.log("Cleaning Conditionally", node); if (weight + contentScore < 0) { return true; } if (this._getCharCount(node, ",") < 10) { // If there are not very many commas, and the number of // non-paragraph elements is more than paragraphs or other // ominous signs, remove the element. var p = node.getElementsByTagName("p").length; var img = node.getElementsByTagName("img").length; var li = node.getElementsByTagName("li").length - 100; var input = node.getElementsByTagName("input").length; var embedCount = 0; var embeds = this._concatNodeLists( node.getElementsByTagName("object"), node.getElementsByTagName("embed"), node.getElementsByTagName("iframe")); for (var i = 0; i < embeds.length; i++) { // If this embed has attribute that matches video regex, don't delete it. for (var j = 0; j < embeds[i].attributes.length; j++) { if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) { return false; } } // For embed with tag, check inner HTML as well. if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) { return false; } embedCount++; } var linkDensity = this._getLinkDensity(node); var contentLength = this._getInnerText(node).length; var haveToRemove = (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) || (!isList && li > p) || (input > Math.floor(p/3)) || (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) || (!isList && weight < 25 && linkDensity > 0.2) || (weight >= 25 && linkDensity > 0.5) || ((embedCount === 1 && contentLength < 75) || embedCount > 1); return haveToRemove; } return false; }); }, /** * Clean out elements that match the specified conditions * * @param Element * @param Function determines whether a node should be removed * @return void **/ _cleanMatchedNodes: function(e, filter) { var endOfSearchMarkerNode = this._getNextNode(e, true); var next = this._getNextNode(e); while (next && next != endOfSearchMarkerNode) { if (filter.call(this, next, next.className + " " + next.id)) { next = this._removeAndGetNext(next); } else { next = this._getNextNode(next); } } }, /** * Clean out spurious headers from an Element. Checks things like classnames and link density. * * @param Element * @return void **/ _cleanHeaders: function(e) { for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) { this._removeNodes(e.getElementsByTagName("h" + headerIndex), function (header) { return this._getClassWeight(header) < 0; }); } }, _flagIsActive: function(flag) { return (this._flags & flag) > 0; }, _removeFlag: function(flag) { this._flags = this._flags & ~flag; }, _isProbablyVisible: function(node) { return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden") && (!node.hasAttribute("aria-hidden") || node.getAttribute("aria-hidden") != "true"); }, /** * Runs readability. * * Workflow: * 1. Prep the document by removing script tags, css, etc. * 2. Build readability's DOM tree. * 3. Grab the article content from the current dom tree. * 4. Replace the current DOM tree with the new one. * 5. Read peacefully. * * @return void **/ parse: function () { // Avoid parsing too large documents, as per configuration option if (this._maxElemsToParse > 0) { var numTags = this._doc.getElementsByTagName("*").length; if (numTags > this._maxElemsToParse) { throw new Error("Aborting parsing document; " + numTags + " elements found"); } } // Remove script tags from the document. this._removeScripts(this._doc); this._prepDocument(); var metadata = this._getArticleMetadata(); this._articleTitle = metadata.title; var articleContent = this._grabArticle(); if (!articleContent) return null; this.log("Grabbed: " + articleContent.innerHTML); this._postProcessContent(articleContent); // If we haven't found an excerpt in the article's metadata, use the article's // first paragraph as the excerpt. This is used for displaying a preview of // the article's content. if (!metadata.excerpt) { var paragraphs = articleContent.getElementsByTagName("p"); if (paragraphs.length > 0) { metadata.excerpt = paragraphs[0].textContent.trim(); } } var textContent = articleContent.textContent; return { title: this._articleTitle, byline: metadata.byline || this._articleByline, dir: this._articleDir, content: articleContent.innerHTML, textContent: textContent, length: textContent.length, excerpt: metadata.excerpt, siteName: metadata.siteName || this._articleSiteName }; } }; if (typeof module === "object") { module.exports = Readability; } liferea-1.13.7/js/gresource.xml000066400000000000000000000004421415350204600163460ustar00rootroot00000000000000 Readability.js htmlview.js liferea-1.13.7/js/htmlview.js000066400000000000000000000056161415350204600160330ustar00rootroot00000000000000/* * @file htmlview.c htmlview reader mode switching and CSS handling * * Copyright (C) 2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** * loadContent() will be run on each internal / Readability.js rendering */ function loadContent(readerEnabled, content) { if (false == readerEnabled) { if (document.location.href === 'liferea://') { console.log('[liferea] reader mode is off'); document.body.innerHTML = decodeURIComponent(content); } else { console.log('[liferea] reader mode off for website'); } } if (true == readerEnabled) { try { console.log('[liferea] enabling reader mode...'); var documentClone = document.cloneNode(true); // When we are internally browsing than we need basic // structure to insert Reader mode content if(document.location.href !== 'liferea://') { content = document.documentElement.innerHTML; document.body.innerHTML = '
'; } else { // Add all content in shadow DOM and split decoration from content // only pass the content to Readability.js content = decodeURIComponent(content); documentClone.body.innerHTML = content; documentClone.getElementById('content').innerHTML = ''; document.body.innerHTML = documentClone.body.innerHTML; documentClone.body.innerHTML = content; documentClone.body.innerHTML = documentClone.getElementById('content').innerHTML; } // Drop Readability.js created
var header = documentClone.getElementsByTagName('header'); if(header.length > 0) header[0].parentNode.removeChild(header[0]); // Show the results var article = new Readability(documentClone).parse(); if (article) document.getElementById('content').innerHTML = article.content if(document.location.href !== 'liferea://') { // Kill all foreign styles var links = document.querySelectorAll('link'); for (var l of links) { l.parentNode.removeChild(l); } var styles = document.querySelectorAll('style'); for (var s of styles) { s.parentNode.removeChild(s); } // FIXME: Add our header } } catch(e) { console.log('[liferea] reader mode failed: '+e); document.documentElement.innerHTML = content; } } } liferea-1.13.7/liferea.convert000066400000000000000000000033701415350204600162260ustar00rootroot00000000000000[net.sf.liferea:/org/gnome/liferea/] browse-inside-application = /apps/liferea/browse-inside-application browse-key-setting = /apps/liferea/browse-key-setting browser = /apps/liferea/browser browser-id = /apps/liferea/browser_id default-view-mode = /apps/liferea/default-view-mode default-update-interval = /apps/liferea/default-update-interval disable-javascript = /apps/liferea/disable-javascript disable-toolbar = /apps/liferea/disable-toolbar last-hpane-pos = /apps/liferea/last-hpane-pos last-vpane-pos = /apps/liferea/last-vpane-pos last-wpane-pos = /apps/liferea/last-wpane-pos last-window-height = /apps/liferea/last-window-height last-window-maximized = /apps/liferea/last-window-maximized last-window-width = /apps/liferea/last-window-width last-window-x = /apps/liferea/last-window-x last-window-y = /apps/liferea/last-window-y last-window-state = /apps/liferea/last-window-state last-zoomlevel = /apps/liferea/last-zoomlevel maxitemcount = /apps/liferea/maxitemcount startup-feed-action = /apps/liferea/startup_feed_action toolbar-style = /apps/liferea/toolbar_style folder-display-mode = /apps/liferea/folder-display-mode folder-display-hide-read = /apps/liferea/folder-display-hide-read reduced-feedlist = /apps/liferea/reduced-feedlist download-tool = /apps/liferea/enclosure-download-tool proxy-detect-mode = /apps/liferea/proxy/detect-mode proxy-host = /apps/liferea/proxy/host proxy-port = /apps/liferea/proxy/port proxy-use-authentication = /apps/liferea/proxy/use_authentication proxy-authentication-user = /apps/liferea/proxy/authentication_user proxy-authentication-password = /apps/liferea/proxy/authentication_password social-bm-site = /apps/liferea/social-bm-site enable-plugins = /apps/liferea/enable-plugins browser-font = /apps/liferea/browser-font liferea-1.13.7/man/000077500000000000000000000000001415350204600137655ustar00rootroot00000000000000liferea-1.13.7/man/Makefile.am000066400000000000000000000000551415350204600160210ustar00rootroot00000000000000EXTRA_DIST = liferea.1 man_MANS = liferea.1 liferea-1.13.7/man/liferea.1000066400000000000000000000112741415350204600154630ustar00rootroot00000000000000.TH LIFEREA "1" "Nov 24, 2021" .SH NAME Liferea \- GTK desktop news aggregator .SH SYNOPSIS .B liferea .RI [\fIOPTIONS\fR] .SH DESCRIPTION \fBLiferea\fP (Linux Feed Reader) is an aggregator for online news feeds. It can be used to maintain a list of subscribed feeds, browse and search through their items and displays their contents. Additionally Liferea allows one to sync subscriptions and read headlines with online accounts of TinyTinyRSS and TheOldReader. .SH OPTIONS Liferea options: .TP .B \-v, \-\-version Print version information and exit. .TP .B \-h, \-\-help Display a option overview and exit. .TP .B \-a, \-\-add\-feed=\fIURI\fR Add a new subscription URI which can be a feed or website URL. .TP .B \-w, \-\-mainwindow\-state=\fISTATE\fR Start Liferea with its mainwindow in STATE: shown, hidden. .TP .B \-p, \-\-disable\-plugins Start with all plugins disabled. .TP .B \-\-debug\-all Print debugging messages of all types. .TP .B \-\-debug\-cache Print debugging messages for the cache handling. .TP .B \-\-debug\-conf Print debugging messages of the configuration handling. .TP .B \-\-debug\-db Print debugging messages of the configuration handling. .TP .B \-\-debug\-gui Print debugging messages of all GUI functions. .TP .B \-\-debug\-html Enables HTML rendering debugging. Each time Liferea renders HTML output it will also dump the generated HTML into $XDG_CACHE_DIR/liferea/output.html. .TP .B \-\-debug\-net Print debugging messages of all network activities and display the HTTP/S User-Agent string. .TP .B \-\-debug\-parsing Print debugging messages of all parsing functions. .TP .B \-\-debug\-performance Print debugging messages when a function takes too long to process. .TP .B \-\-debug\-trace Print debugging messages when entering/leaving functions. .TP .B \-\-debug\-update Print debugging messages of the feed update processing. .TP .B \-\-debug\-vfolder Print debugging messages of the search folder matching. .TP .B \-\-debug\-verbose Print verbose debugging messages. .SH DBUS INTERFACE To allow integration with other programs \fBLiferea\fP provides a DBUS interface for automatic creation of new subscriptions. The script \fBliferea-add-feed\fP is a convenient way to use this interface. Just pass a valid feed URL as parameter and the feed will be added to the feed list. You can also pass non-feed URLs to use feed auto discovery. See the EXAMPLES section. .SH ENVIRONMENT .TP .B http_proxy (for HTTP connections) .RE .B https_proxy (for HTTPS connections) .RS If defined and a proxy is not specified in the \fBLiferea\fP preferences (which uses the proxy settings provided by dconf), their value will be used as proxy URIs. Both are used by many common CLI tools, so make sure to export them in a dedicated subshell. .RE .TP .B LIFEREA_UA_ANONYMOUS If defined, randomizes and anonymizes the default HTTP/S User-Agent string. .RB .TP .B LIFEREA_UA If defined, its value replaces the default HTTP/S User-Agent string. .RB .LP LIFEREA_UA has always precedence over LIFEREA_UA_ANONYMOUS, so the latter can be safely defined in global shell initialization files. .SH EXAMPLES .TP .nf $ http_proxy="http://proxy.example.com:3128" liferea .fi .RE .nf $ http_proxy="http://username:password@proxy.example.com:3128" liferea .fi .RS Will alter each outgoing request to use proxy.example.com on port 3128 as proxy. If DNS resolution does not work, an IP address can be used instead. .RE .TP .nf $ LIFEREA_UA_ANONYMOUS=1 liferea --debug-net .fi Will alter each outgoing HTTP/S request to randomize \fBLiferea\fP's version and hide the operative system in use. In addition, prints the HTTP/S User-Agent and all outgoing network requests. .RB .TP .nf $ LIFEREA_UA="Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0" liferea .fi Will alter each outgoing HTTP/S request to pose as Firefox on Linux. .RB .TP .nf $ liferea-add-feed "feed:https://www.example.com/feed.rss" .fi .RE .nf $ liferea --add-feed "https://www.example.com/feed.rss" .fi .RS Subscribe to "example.com/feed.rss". Remember to supply a valid and correctly escaped feed URL as parameter. Please note that \fBLiferea\fP needs to be running for \fBliferea-add-feed\fP to work. .RB .SH FILES .TP $XDG_CONFIG_DIR/liferea/feedlist.opml Contains the current list of subscriptions. .TP $XDG_CONFIG_DIR/liferea/liferea.css Stylesheet that can be used to override default HTML style. .TP $XDG_DATA_DIR/liferea/liferea.db SQLite3 database with all subscriptions and headlines. .TP $XDG_DATA_DIR/liferea/plugins/ User-installed plugins are stored here. You can either manually put plugins here or use the plugin installer in Liferea. .SH AUTHOR This manual page was written by Lars Windolf . Updated on Nov 24, 2021 by Lorenzo L. Ancora . liferea-1.13.7/net.sf.liferea.gschema.xml.in000066400000000000000000000301111415350204600205460ustar00rootroot00000000000000 false Open links inside of Liferea? If set to true, links clicked will be opened inside of Liferea, otherwise they will be opened in the selected external browser. 1 Selects which key to use to pagedown or go to the next unread item Selects which key to use to pagedown or go to the next unread item. Set to 0 to use space, 1 to use ctrl-space, or 2 to use alt-space. 'mozilla %s' Selects the browser command to use when browser_module is set to manual Selects the browser command to use when browser_module is set to manual. 'gnome' Selects which browser to use to open external links Selects which browser to use to open external links. The choices include "default" and "manual". true Get a confirmation dialog when marking all read If TRUE Liferea will display a confirmation dialog before marking all items of the selected feed as read. 0 The default view mode for feed list nodes. The default view mode for displaying feed list nodes. Possible values: 0=email like 3-pane, 1=wide view 3-pane 0 Default interval for fetching feeds. This value specifies how often Liferea tries to update feeds. The value is given in minutes. When setting the interval always consider the traffic it produces. Setting a value less than 15min almost never makes sense. true Allows to disable Javascript. Allows to disable Javascript. false Disable displaying the toolbar in the Liferea main window Disable displaying the toolbar in the Liferea main window. 0 Height of the itemlist pane in the mainwindow Height of the itemlist pane in the mainwindow. Use 0 to let GTK+ decide the height. 0 Width of the feedlist pane in the mainwindow Width of the feedlist pane in the mainwindow. Use 0 to let GTK+ decide the width. 0 Height of the Liferea main window Height of the Liferea main window. Use 0 to let GTK+ decide on the height. false Mainwindow is maximized when Liferea starts up Determines if the Liferea main window will be maximized at startup. 0 Width of the Liferea main window Width of the Liferea main window. Use 0 to let GTK+ decide on the width. 0 Left position of the Liferea main window Left position of the Liferea main window. 0 Top position of the Liferea main window Top position of the Liferea main window. 0 Last saved stat of the Liferea main window Last saved of the Liferea main window. Controls how Liferea shows the window on next startup. Possible values see src/ui/liferea_shell.h 100 Zoom level of the HTML view Zoom level of the HTML view. (100 = 1:1) '' Node id of the last feed list selection When shutting down Liferea saves the last selected node id here to be restored on startup. 100 Determines the default number of items saved on each feed This value is used to determine how many items are saved in each feed when Liferea exits. Note that marked items are always saved. 0 Determines if subscriptions are to be updated at startup Numeric value determines whether Liferea shall updates all subscriptions at startup (0=yes, otherwise=no). Inverse logic for compatibility reasons. '' Determines the style of the toolbar buttons Determines the style of the toolbar buttons locally, overriding the GNOME settings. Valid values are "both", "both-horiz", "icons", and "text". If empty or not specified, the GNOME settings are used. 1 Determine if folders show all child content. If set to 0 no items are displayed when selecting a folder. If set to 1 all items of all childs are displayed when selecting a folder. true Filter read items when displaying folders. If this option is enabled and folder-display-mode is not 0 when clicking a folder only the unread items of all childs will be displayed. false Filter feeds without unread items from feed list. If this option is enabled the feed list will contain only feeds that have unread items. '' Custom download command. A command used to download enclosures if download-use-custom-command is true. %s in the command will be replaced by the URL. 0 Which tool to download enclosures. This options determines which download tool Liferea uses to download enclosures (0 = steadyflow, 1 = gwget, 2=kget). false Whether to use the custom command or one of the predefined command. Set to true to use the command defined in download-custom-command, false to use the predefined command set in download-tool. 0 Proxy mode. This options determines what kind of proxy will be used. '' Proxy host. This options determines the proxy host. 8080 Proxy port. This options determines the proxy port. false Proxy auth. This options determines if auth is requiered. '' Proxy user. This options determines auth username. '' Proxy password. This options determines auth password. '' Social bookmark site This option determines which social bookmark site use to save links. 0 Width of the itemlist pane in the mainwindow Width of the itemlist pane in the mainwindow. Use 0 to let GTK+ decide the Width. false Enable plugins This options determines if liferea should enable plugins. true Enable intelligent tracking protection This options determines if liferea should enable WebKit's intelligent tracking protection. true Enable reader mode This options toggles reader mode usage in the item view. If enabled Readability.js will be used for filtering content. '' User defined browser-font This option defines which font should be used to render in the browser. If not specified system setting will be used. false Send "Do Not Track" header Configures wether the "DNT" header is to be sent. If enabled sends "DNT: 1", meaning Do Not Track. [ 'state', 'favicon', 'headline', 'enclosure', 'date' ] Item list view column order The column order in the item list view. ['gnome-keyring','media-player','trayicon','plugin-installer','pane'] Active plugins List of active plugins. It contains the "Module" names of the active plugins. See the .plugin file for obtaining the"Module" name of a given plugin. liferea-1.13.7/net.sourceforge.liferea.appdata.xml.in000066400000000000000000000110401415350204600224640ustar00rootroot00000000000000 net.sourceforge.liferea.desktop CC0 GPL-2.0+ Liferea <_summary>RSS feed reader <_p>Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator for online news feeds. It supports a number of different feed formats including RSS/RDF, CDF and Atom. There are many other news readers available, but these others are not available for Linux or require many extra libraries to be installed. Liferea tries to fill this gap by creating a fast, easy to use, easy to install news aggregator for GTK/GNOME. <_p>Distinguishing features:
    <_li>Read articles when offline <_li>Synchronizes with TheOldReader <_li>Synchronizes with TinyTinyRSS <_li>Synchronizes with InoReader <_li>Synchronizes with Reedah <_li>Permanently save headlines in news bins <_li>Match items using search folders <_li>Play Podcasts
https://lzone.de/liferea/screenshots/screenshot2.png https://lzone.de/liferea/screenshots/screenshot3.png https://lzone.de/liferea/screenshots/screenshot4.png https://lzone.de/liferea/screenshots/screenshot5.png https://lzone.de/liferea/screenshots/screenshot6.png https://lzone.de/liferea/screenshots/screenshot7.png https://lzone.de/liferea/screenshots/screenshot8.png https://lzone.de/liferea/screenshots/screenshot9.png https://lzone.de/liferea/ liferea-devel@lists.sf.net none none none none none none none none none none none none none none none none none none none none none none none none none none none
liferea-1.13.7/net.sourceforge.liferea.desktop.in000066400000000000000000000007271415350204600217360ustar00rootroot00000000000000[Desktop Entry] _Name=Liferea _GenericName=Feed Reader _X-GNOME-FullName=Liferea Feed Reader _Comment=Read news feeds and blogs _Keywords=news;feed;aggregator;blog;podcast;syndication;rss;atom Exec=liferea %U DBusActivatable=true Icon=net.sourceforge.liferea StartupNotify=true Terminal=false Type=Application Categories=Network;News; X-Ubuntu-Gettext-Domain=liferea Version=1.1 MimeType=application/rss+xml;application/atom+xml;application/rdf+xml;x-scheme-handler/feed liferea-1.13.7/net.sourceforge.liferea.json000066400000000000000000000025601415350204600206260ustar00rootroot00000000000000{ "app-id":"net.sourceforge.liferea", "runtime":"org.gnome.Platform", "runtime-version":"3.26", "sdk":"org.gnome.Sdk", "command":"liferea", "desktop-file-name-suffix":" (Master)", "cleanup":[ "/include", "/lib/pkgconfig", "/share/pkgconfig", "/share/aclocal", "*.la", "*.a" ], "finish-args":[ "--share=ipc", "--socket=x11", "--socket=wayland", "--share=network", "--filesystem=home", "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro", "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf" ], "modules":[ { "name":"libpeas", "cleanup":[ "/bin/*", "/lib/peas-demo" ], "sources":[ { "type":"archive", "url":"https://download.gnome.org/sources/libpeas/1.22/libpeas-1.22.0.tar.xz", "sha256":"5b2fc0f53962b25bca131a5ec0139e6fef8e254481b6e777975f7a1d2702a962" } ] }, { "name":"liferea", "sources":[ { "type":"git", "url":"https://github.com/lwindolf/liferea.git" } ] } ] } liferea-1.13.7/opml/000077500000000000000000000000001415350204600141615ustar00rootroot00000000000000liferea-1.13.7/opml/Makefile.am000066400000000000000000000007631415350204600162230ustar00rootroot00000000000000EXTRA_DIST = \ feedlist_en.opml \ feedlist_bg.opml \ feedlist_ca.opml \ feedlist_de.opml \ feedlist_es.opml \ feedlist_eu.opml \ feedlist_fr.opml \ feedlist_he.opml \ feedlist_hu.opml \ feedlist_gl.opml \ feedlist_id.opml \ feedlist_it.opml \ feedlist_pl.opml \ feedlist_pt.opml \ feedlist_pt_BR.opml \ feedlist_sk.opml \ feedlist_sv.opml \ feedlist_nl.opml \ feedlist_no.opml \ feedlist_fa.opml lifereadistopmldir = $(datadir)/liferea/opml lifereadistopml_DATA = $(EXTRA_DIST) liferea-1.13.7/opml/feedlist_bg.opml000066400000000000000000000100711415350204600173200ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/opml/feedlist_ca.opml000066400000000000000000000060011415350204600173110ustar00rootroot00000000000000 Canals predeterminats liferea-1.13.7/opml/feedlist_de.opml000066400000000000000000000062351415350204600173270ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/opml/feedlist_en.opml000066400000000000000000000057021415350204600173370ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/opml/feedlist_es.opml000066400000000000000000000053741415350204600173510ustar00rootroot00000000000000 Fuentes por defecto liferea-1.13.7/opml/feedlist_eu.opml000066400000000000000000000055741415350204600173550ustar00rootroot00000000000000 Iturri lehenetsiak liferea-1.13.7/opml/feedlist_fa.opml000066400000000000000000000051231415350204600173200ustar00rootroot00000000000000 لیست خوراک پیشفرض Liferea liferea-1.13.7/opml/feedlist_fr.opml000066400000000000000000000063051415350204600173440ustar00rootroot00000000000000 Liste des flux par défaut de Liferea liferea-1.13.7/opml/feedlist_gl.opml000066400000000000000000000052731415350204600173420ustar00rootroot00000000000000 Lista de fontes predeterminadas do Liferea liferea-1.13.7/opml/feedlist_he.opml000066400000000000000000000266661415350204600173450ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/opml/feedlist_hu.opml000066400000000000000000000051351415350204600173510ustar00rootroot00000000000000 Liferea alapértelmezett hírforráslista liferea-1.13.7/opml/feedlist_id.opml000066400000000000000000000061751415350204600173360ustar00rootroot00000000000000 Daftar Feed Default Liferea liferea-1.13.7/opml/feedlist_it.opml000066400000000000000000000100101415350204600173350ustar00rootroot00000000000000 Elenco notiziari predefiniti liferea-1.13.7/opml/feedlist_nl.opml000066400000000000000000000054311415350204600173450ustar00rootroot00000000000000 Liferea Dutch Feed List liferea-1.13.7/opml/feedlist_no.opml000066400000000000000000000060451415350204600173520ustar00rootroot00000000000000 Liferea eksempelkanaler liferea-1.13.7/opml/feedlist_pl.opml000066400000000000000000000062411415350204600173470ustar00rootroot00000000000000 Przykładowa lista subskrypcji Liferea liferea-1.13.7/opml/feedlist_pt.opml000066400000000000000000000066471415350204600173710ustar00rootroot00000000000000 Lista de Fontes liferea-1.13.7/opml/feedlist_pt_BR.opml000066400000000000000000000055551415350204600177510ustar00rootroot00000000000000 Lista de Fontes de Notícias liferea-1.13.7/opml/feedlist_sk.opml000066400000000000000000000056311415350204600173530ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/opml/feedlist_sv.opml000066400000000000000000000054451415350204600173710ustar00rootroot00000000000000 Liferea Default Feed List liferea-1.13.7/pixmaps/000077500000000000000000000000001415350204600146735ustar00rootroot00000000000000liferea-1.13.7/pixmaps/16x16/000077500000000000000000000000001415350204600154605ustar00rootroot00000000000000liferea-1.13.7/pixmaps/16x16/Makefile.am000066400000000000000000000003001415350204600175050ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = 16x16 context = apps EXTRA_DIST = net.sourceforge.liferea.png lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/16x16/net.sourceforge.liferea.png000066400000000000000000000013711415350204600227060ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs  ~tIME18>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3X/IDAT8˥KHTQgfL:8|TiOƅN.|LBAa]-"aZ&FALcpFIܹ-̫=p~?=Q6'|B׊@UUr(s,k V~ewL# ܡ!J](3nwpq+,33I8Jp{woYv$$'I";SBΠ8ICݜ&5@33LہCS, dbF,HͿa$UH"%NnNSsv{փͮs4T-ҘU9v7F|1I^(f#(}mlLE2K1ڝ,mrsc i.h?wUl!.#.x9'84ՄM)_Bq?~ Hv+ikpi 6xP(S44-ښ:<%L! 0Y}X<"x4>>\^V8Fsa _9ҔIENDB`liferea-1.13.7/pixmaps/22x22/000077500000000000000000000000001415350204600154525ustar00rootroot00000000000000liferea-1.13.7/pixmaps/22x22/Makefile.am000066400000000000000000000003001415350204600174770ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = 22x22 context = apps EXTRA_DIST = net.sourceforge.liferea.png lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/22x22/net.sourceforge.liferea.png000066400000000000000000000016721415350204600227040ustar00rootroot00000000000000PNG  IHDRĴl;sRGBbKGD pHYs  ~tIME my>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XIDAT8˵OI?[hZ++VXW)pQ7oiٸK<=m<C4DeFQh Zxmw<*DI'swfM qy?ϞdžA\Z(J umRW L244T710 DѺ9ʗ] M$$Drse155ՠ^I&fr4M7I'rmA>-u}o׋nl8?DZzCJYPT9En1;FvX[.b},-- 3-4U022Ҡ8{xrwb q_NtS,y;tb(R 6|>_E$6.W)4#DpXst+¥6ȟ l€[!E _;a6@ T6-IENDB`liferea-1.13.7/pixmaps/24x24/000077500000000000000000000000001415350204600154565ustar00rootroot00000000000000liferea-1.13.7/pixmaps/24x24/Makefile.am000066400000000000000000000003001415350204600175030ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = 24x24 context = apps EXTRA_DIST = net.sourceforge.liferea.png lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/24x24/net.sourceforge.liferea.png000066400000000000000000000017161415350204600227070ustar00rootroot00000000000000PNG  IHDRw=sRGBbKGD pHYs  ~tIME->tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XIDATHՕoG^{1K $>@>TԂ[8E=c/=UD5AH@HLK!%Jb`';l\x5ҎfycM\rQn6/Sm,;æ_2 ܨT*Lzc #N300jz{{0 J---5+STԡXlP(`Y) yttt:0|~ F4U)u8LdR45 rZVZOOk,p5v[h]}>^ULrEdsܺNt7lǏtеUPuۓ}8KYe2R1rIBVL垡:mՁ؎$9CMAx+T~FO@2οϧoS\#R L/1ڐ$Pý,fIח;ƋL搫ǎiԜJiIH)ON`FCWՠTYv$σmX"BDQvDsmTdB]jV /5aaFwEP0??Xi5&IB^{iC|SeۉEt8R or{"Ɩ,canQ`brLS8)Dklc*`+ocsxbIENDB`liferea-1.13.7/pixmaps/32x32/000077500000000000000000000000001415350204600154545ustar00rootroot00000000000000liferea-1.13.7/pixmaps/32x32/Makefile.am000066400000000000000000000003001415350204600175010ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = 32x32 context = apps EXTRA_DIST = net.sourceforge.liferea.png lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/32x32/net.sourceforge.liferea.png000066400000000000000000000030121415350204600226740ustar00rootroot00000000000000PNG  IHDR szzsRGBbKGD pHYs  tIME#IDATXoT?`m 1Vcd%" " J-Bv 6UUUP7j+6m*T4@!ĵc0xܱc]vl @Hʑt;sॽw]8{}ʊiY2MBYBm[E"u]muum۶بֿ2FMSml6+637Ol8?8}t}lFX뺄B!|G)p]t@|rfll0 @&QJ).]ĕ3RE p슳C?/^ĉ>|ke6 w{*id^~2}ɮ(u²g"?ɑH3g0<<CCCm ^__Ϯ]FH)< ?BzE{ݿof[Ķ7)3(@CN52{_wh8ضRj{GA4ulf{W;()ƪWc6w342PLD8yK*Al9!acƟt]076pd*7àkXm)HC[DZf |z5m<H)tycWOo0z;Y\P#H t@G+ir={cA<}' w t:]7== X Mxe*Oȭ2 Fq,7?￙"?7@4ð,83s5[mDUnݙd~̞VZc8ŪWaqSY\G3Eu`6R΍G 2r H6;$v\Sd92Z BU}{Z[ZZhjjB)jW۷Qǻ**kyk׮ u&FFF~*l8X/ }t…r=ӆ@~}@oxkn |َ6Ȟ3zm IENDB`liferea-1.13.7/pixmaps/48x48/000077500000000000000000000000001415350204600154725ustar00rootroot00000000000000liferea-1.13.7/pixmaps/48x48/Makefile.am000066400000000000000000000003001415350204600175170ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = 48x48 context = apps EXTRA_DIST = net.sourceforge.liferea.png lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/48x48/net.sourceforge.liferea.png000066400000000000000000000055341415350204600227250ustar00rootroot00000000000000PNG  IHDR00WsRGBbKGD pHYs B(xtIME . '( IDATh[l\yr/$w&RKSWʖN+AP[դR׭kopV`@aҦE}s!;ȱ[뒈XbIKH.Erogϙ9$S-Q3s09as7z/ly[O4Ms u]?]Ϸ߶퍯1瞫F7չR;iJ)RÇrϞ= Ձ@ F)J4-[ܹQii|kѢEu[qM(zD"sˀ}oVnݺMVZ 4u^`ƍk..]kӧ_$HuV.]Dcc#[n%{1::J?555 0111+3 X ]QJחڽ{wlcg5i>Fu)%yGyd2ttt.RWWɓ'ywٱck׮eϞ=={" ގi;E\EAss~z۷B`0< {n0J)FFF`Æ 444fljb;=z7|3G˲PJ!$ 筷*)'O\Z__/ReYtwwS(ؼy3lR)89r>,iiӦiꫯFEE^k WJjn*l޼ySGGOVX,뺛-#Kee]0P(B5.roocǎtC/ihh !@~y#H)04FP>5w\sCӃ8!^7 G7FWUUU,￿ʪ |ju N3*Pߝ0x;uÆa $z͖-[W x7*ZO6e|<~/l!??@jCNS5oY['_5)~3OH$H)Yt{u (J,Yv]כկ??~C'zA3EOezܝY^>ŇSQFDpu}e"(B?w/ /- xMY + B+ h! T_K]m5~22rb>G)8qkɤ~] Gjkk>yI41_)uu5 Q]Qν O躎Zv ;;D" m{yee%{;wʿ,?qte\ޗhu ]@,R4p]XTAʼ% xBxb_WWwgL& B`]l 9O5VsCB K(\)VL!bbꇖpWT)&i|Ejmm;ΆsdCiph(@8d^yBJϣ8Ikkm |h48Kd0L`+tZDc-d%\W(Bl=9W(?ms1=U0t/N(uuuwiR:sW 0 Zh Rì|Q߆RP%٢pO~a6m- ]ԐB266vb`0Xxq```ɫ$r!Ds(4u/9=bA")W=Ԁ;u҅ŃJ1۶eYJ%511 ?K<㺦i]D"˶mO(GIcs:U476O{pIP"^UQ((<&= + S(x<BRH2$qsT.1D15As[UY~{9{{‹% JCz8KG.7#΃XZ`~spK `rrrF﷤ ϟ6JS8_^" /{﮾c  [H,[`JL]#122PC޶***0M]|Es熧g}Db"N"*>| Ea=S+1M0(ZgsA4#$%[̈plc-[NvKW1OPnWJٶm Iq_<) a45F?X~xHEbȾ?c ,[R+XbIpl}q-8!?o"!UeqzNiYTIRC=ӓ ! vڥ?TJ-EO) directory.png, copy of GNOME stock icons (GPLv2) mail-attachment.png liferea.png (news-reader-48.png) Jakub Steiner fl_opml.png OPML icon (Chris Pirillo ) newsbin.png created for Liferea, Lars Windolf emblem-important.png copy of GNOME Tango theme icon by Jakub Steiner (GPLv2) emblem-web.svg copy of GNOME Tango theme icon by Jakub Steiner (GPLv2) 16x16/liferea.png, 22x22/liferea.png, 24x24/liferea.png, 32x32/liferea.png, 48x48/liferea.png and scalable/liferea.svg: Jeff Fortin , creative commons share-alike license (http://creativecommons.org/licenses/by-sa/3.0/) unread.png identical with 16x16/liferea.png (see above) liferea-1.13.7/pixmaps/Makefile.am000066400000000000000000000014711415350204600167320ustar00rootroot00000000000000SUBDIRS = 16x16 22x22 24x24 32x32 48x48 scalable EXTRA_DIST = \ mail-attachment.png \ default.svg \ folder.png \ emblem-important.png \ emblem-web.svg \ unread.png \ folder-saved-search.png \ fl_opml.png \ newsbin.png lifereadistpixdir = $(datadir)/$(PACKAGE)/pixmaps lifereadistpix_DATA = $(EXTRA_DIST) gtk_update_icon_cache = gtk-update-icon-cache -f -t $(datadir)/icons/hicolor install-data-hook: @-if test -z "$(DESTDIR)"; then \ echo "Updating Gtk icon cache."; \ $(gtk_update_icon_cache); \ else \ echo "*** Icon cache not updated. After install, run this:"; \ echo "*** $(gtk_update_icon_cache)"; \ fi uninstall-hook: @-if test -z "$(DESTDIR)"; then \ echo "Updating Gtk icon cache."; \ $(gtk_update_icon_cache); \ fi liferea-1.13.7/pixmaps/default.svg000066400000000000000000000021501415350204600170360ustar00rootroot00000000000000 liferea-1.13.7/pixmaps/emblem-important.png000066400000000000000000000010011415350204600206450ustar00rootroot00000000000000PNG  IHDRabKGD pHYs  ~tIME8>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XDIDATxݒMKQ8$\Fm$" DR''ZjQmE MAՙMAݜ{>{ Q1 f/R>Wb:ATp iqT>e`wĀJumO-|Rzw4)q푰2t 5"$p&iq; 5dz  ?UzwaN}v65,v 9:܇ L p $B<66hIJ zb٨^/mWGXIENDB`liferea-1.13.7/pixmaps/emblem-web.svg000066400000000000000000001227451415350204600174430ustar00rootroot00000000000000 image/svg+xml Jakub Steiner http://jimmac.musichall.cz Web Browser liferea-1.13.7/pixmaps/fl_opml.png000066400000000000000000000014311415350204600170300ustar00rootroot00000000000000PNG  IHDRaIDAT8MKlUwg&(}HqjP(ZHj`#BPm{₍d4QQkV l( %h )-$PFJN;|=.A\;{Gv|3.tE^ Ź(&ɚ#}B;t>;8+O,hB $7)j ^P7.Yx k_aWj2M 0N,@LyƁ*M W{;n+"B1¹|{eYO Pu;[=7>C=hOٶu,l †5I:[SqyrK <맋ܝɳg:Q`8ڂ] PPچ(J^-/^Bks s\ʡVHӿ'};Ԍ[O(!T5 !x 6w+UUU/@r3x+U˹O>DhBt#g?tL04Sy)oąʧm{R <0#8)_鿊E_hKֱdR+Aa|9}Op{t?+Z9/T|eQ>h|EK e?M\q\>⅃~V0/@1 F[e&k5Z7 BIENDB`liferea-1.13.7/pixmaps/folder-saved-search.png000066400000000000000000000017341415350204600212240ustar00rootroot00000000000000PNG  IHDRa pHYs  ~tIME ([8tEXtCommentCreated with The GIMPd%nRIDATxU]L[uA۰d*Le%bELfFEa/wwfL Z8u1cet Jf7^NB{z w|.~OSJ_! aםgOsX̽}Aln\sssdxxƦ&'oɟ Sӿuj޵Wb1ygEmV6]ܷ~^8r7Ǣ[E8V^Z][G7(T$nx)wc۶rOn/@ |:va=_,%.WJ@5===\<? v5MSQ*\lz+:l*.RJ>$r ˁ~h mۊUԨ ZEQ@R\oob]*A111!4GX*d eW⸒)T,  %m<g~1Mᥡ*Hm0yB%k.)Gv|49yt]ס>Ə#Py>}z R:j:@E}pC=v.+1:kk\;#cgk_=k1d5=u+ ߽}Qh;WCH f4f W~zaasʠi R,/.2t$ǻ_%Ld2Yv]QUEH$WeH~y-͡y}PJ qWnO.sss%)[τim@'j ܲ,k6Ɍ%{{{ձ1RT?6 lB hв8N?Y|s+IENDB`liferea-1.13.7/pixmaps/folder.png000066400000000000000000000006151415350204600166560ustar00rootroot00000000000000PNG  IHDRaTIDAT8˥NAYV4v$'ЂG0L)l,@k4KT*c0I؝bvIdI9{.,xL|}A>/*kZxn}<r<e<:=a]jvP\%?TU&!/&@vEis(4@ա"0 DQ@e(ϛ(T-Jt"EDV佢".~aV9Q,@ .D\~ Ok}:-߁18;NS&$t$yO@Ϲ?kNƨK;:7O*o hKtIENDB`liferea-1.13.7/pixmaps/mail-attachment.png000066400000000000000000000005701415350204600204530ustar00rootroot00000000000000PNG  IHDRabKGD-IDATxڵ=n0_rT9ǖii(SD9ƶ&[Jni,PXK*R,{<2En5߯>ܝ3Go?f5}_QQDx<84pa8#EZkApŸucy~ <"B!*\f(8@DRSm(3X!'hۖ"ªfu]3MSðMq:o}Skj(&$IhaY:aETnEn(vúU8ȲOBGw˵ozyXjR,&+0kVeXB$Mp(ZLy}áW*y$'`Ma3]ٷP@EmH* p?̀M\I~!v`YQDd@"6"8 C"(ILos u|M̨mm)a4oۨ#ɾ]ě1{L%`Yb? %2EbIENDB`liferea-1.13.7/pixmaps/scalable/000077500000000000000000000000001415350204600164415ustar00rootroot00000000000000liferea-1.13.7/pixmaps/scalable/Makefile.am000066400000000000000000000003561415350204600205010ustar00rootroot00000000000000themedir = $(datadir)/icons/hicolor size = scalable context = apps EXTRA_DIST = \ net.sourceforge.liferea.svg \ net.sourceforge.liferea-symbolic.svg lifereadistpixdir = $(themedir)/$(size)/$(context) lifereadistpix_DATA = $(EXTRA_DIST) liferea-1.13.7/pixmaps/scalable/net.sourceforge.liferea-symbolic.svg000066400000000000000000000031021415350204600255130ustar00rootroot00000000000000 image/svg+xml Gnome Symbolic Icon Theme Gnome Symbolic Icon Theme liferea-1.13.7/pixmaps/scalable/net.sourceforge.liferea.svg000066400000000000000000001723511415350204600237110ustar00rootroot00000000000000 image/svg+xml News 2005-03-11 Jakub Steiner http://jimmac.musichall.cz/ news usenet new N E W S liferea-1.13.7/pixmaps/unread.png000066400000000000000000000006431415350204600166620ustar00rootroot00000000000000PNG  IHDRa pHYs  ~tIME  )>ͩ>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XIDATxڥ1r0Ef8J]s$ 83vrtZU>..u#$V_/iWxPV54 <1c6xdY6A'л` P@2@.&I@UUdVy/}~N'W 2Hqr>SMpsR^_@R}XDA1Qڵ~۷qR-vK`Ιc`yTc(CDtIENDB`liferea-1.13.7/plugins/000077500000000000000000000000001415350204600146735ustar00rootroot00000000000000liferea-1.13.7/plugins/README.md000066400000000000000000000024221415350204600161520ustar00rootroot00000000000000## Contribute to Plugins! Plugins are usually in Python and it is easier than ever to change core behaviours of Liferea using plugins! To get an idea how simple it is to modify or extend Liferea with a plugin have a look at "bold-unread.py" which grabs a handle to the feed list and changes the visual rendering of feed titles. ## Plugin Tutorial To help you getting started there is a tutorial on writing plugins: - [Part 1: Introduction + Plugin Boiler Plate](https://lzone.de/liferea/blog/Writing%20Liferea%20Plugins%20Tutorial%20Part%201) - [Part 2: Access and modify UI elements](https://lzone.de/liferea/blog/Writing%20Liferea%20Plugins%20Tutorial%20Part%202) - [Part 3: Adding menu elements](https://lzone.de/liferea/blog/Writing%20Liferea%20Plugins%20Tutorial%20Part%203) - [Part 4: Using GTK inspector](https://lzone.de/liferea/blog/Writing%20Liferea%20Plugins%20Tutorial%20Part%204) - [Part 5: Enabling translations for plugins](https://lzone.de/liferea/blog/Writing%20Liferea%20Plugins%20Tutorial%20Part%205) ## Adding 3rd Party Plugins Using the "plugin installer" Liferea allows installing plugins from a curated list of 3rd party (Github) sources. This list is the "plugin-list.json" file you find in this directory. Open a PR to add useful 3rd party plugins you know about! liferea-1.13.7/plugins/bold-unread.plugin000066400000000000000000000011231415350204600203040ustar00rootroot00000000000000[Plugin] Module=bold-unread Loader=python3 Icon=format-text-bold IAge=2 Name=Bold Unread Name[de]=Ungelesene Feeds hervorheben Name[it]=Non letti in grassetto Description=Bold text for subscriptions with unread items in the feedlist Description[de]=Abonnements mit ungelesenen Schlagzeilen werden mit fetter Schrift dargestellt Description[it]=Testo in grassetto per gli abbonamenti con articoli non letti nell'elenco dei notiziari Description[pl]=Pogrubiony tekst dla subskrypcji z nieprzeczytanymi elementami na liście kanałów. Authors=Yanko Kaneti Copyright=Copyright © 2017 Yanko Kaneti liferea-1.13.7/plugins/bold-unread.py000066400000000000000000000032641415350204600174460ustar00rootroot00000000000000""" bold unread plugin Copyright (C) 2017 Yanko Kaneti This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ from gi.repository import GObject, Liferea, Pango def cell_func_bold_unread(column, cell, model, iterator, data): unread_count = model.get(iterator, 3) cell.set_property("weight", Pango.Weight.BOLD if (unread_count[0] > 0) else Pango.Weight.NORMAL) class NrBoldUnreadPlugin(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = 'NrBoldUnreadPlugin' treeview = None ticol = None layout = None shell = GObject.property(type=Liferea.Shell) def do_activate(self): self.treeview = self.shell.lookup("feedlist") self.ticol = self.treeview.get_column(0) area = self.ticol.get_property("cell-area") self.layout = area.get_cells() self.ticol.set_cell_data_func(self.layout[1], cell_func_bold_unread) self.treeview.queue_draw() def do_deactivate(self): self.ticol.set_cell_data_func(self.layout[1], None) self.layout[1].set_property("weight", Pango.Weight.NORMAL) self.treeview.queue_draw() liferea-1.13.7/plugins/getfocus.plugin000066400000000000000000000003751415350204600177370ustar00rootroot00000000000000[Plugin] Module=getfocus Loader=python3 IAge=2 Name=Get focus! Description=An automatic visual "focus mode" for Liferea, that makes the feedlist less prominent by change its opacity. Authors=Paweł Marciniak Copyright=Copyright © 2021 Paweł Marciniak liferea-1.13.7/plugins/getfocus.py000066400000000000000000000110501415350204600170610ustar00rootroot00000000000000""" GetFocus! Liferea plugin Copyright (C) 2021 Paweł Marciniak . """ import pathlib import gettext from gi.repository import GObject, Gtk, Liferea, PeasGtk # Initialize translations for tooltips _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext file_config = 'opacity.conf' def get_path(): return pathlib.Path.joinpath(pathlib.Path.home(), ".config/liferea/plugins/getfocus") class GetFocusPlugin(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = 'GetFocusPlugin' shell = GObject.property(type=Liferea.Shell) feedlist = None opacity = 0.3 enter_event = None leave_event = None def do_activate(self): self.feedlist = self.shell.lookup('feedlist') self.read_opacity_from_file() self.set_opacity_leave(self.feedlist, None) self.leave_event = self.feedlist.connect('leave-notify-event', self.set_opacity_leave) self.enter_event = self.feedlist.connect('enter-notify-event', self.set_opacity_enter) def do_deactivate(self): self.feedlist.disconnect(self.enter_event) self.feedlist.disconnect(self.leave_event) self.set_opacity_enter(self.feedlist, None) def set_opacity_enter(self, widget, event): self.opacity = widget.get_property('opacity') widget.set_property('opacity', 1) def set_opacity_leave(self, widget, event): widget.set_property('opacity', self.opacity) def read_opacity_from_file(self): path = get_path() file_path = path / file_config if file_path.exists(): self.opacity = float(file_path.read_text()) class GetFocusConfigure(GObject.Object, PeasGtk.Configurable): __gtype_name__ = 'GetFocusConfigure' opacity = None feedlist = None opacity_scale = None def do_create_configure_widget(self): """ Setup configuration widget """ margin = 6 shell = Liferea.Shell self.feedlist = shell.lookup('feedlist') self.opacity = self.feedlist.get_property('opacity') grid = Gtk.Grid(column_spacing=10) label = Gtk.Label(_("Opacity:")) label.props.tooltip_text = _("Opacity") label.props.xalign = 0 label.props.margin = margin label.props.expand = False grid.attach(label, 0, 0, 1, 1) adj = Gtk.Adjustment(self.opacity, 0, 1.0, 0.1, 0.2, 0) self.opacity_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=adj) self.opacity_scale.props.margin = 10 self.opacity_scale.add_mark(0, Gtk.PositionType.BOTTOM, _('Min')) self.opacity_scale.add_mark(0.2, Gtk.PositionType.BOTTOM, None) self.opacity_scale.add_mark(0.4, Gtk.PositionType.BOTTOM, None) self.opacity_scale.add_mark(0.6, Gtk.PositionType.BOTTOM, None) self.opacity_scale.add_mark(0.8, Gtk.PositionType.BOTTOM, None) self.opacity_scale.add_mark(1.0, Gtk.PositionType.BOTTOM, _('Max')) self.opacity_scale.set_hexpand(True) self.opacity_scale.set_size_request(300, 10) #width, height self.opacity_scale.connect("value-changed", self.scale_moved) grid.attach(self.opacity_scale, 1, 0, 1, 1) save_button = Gtk.Button(_('Save')) save_button.set_valign(Gtk.Align.CENTER) save_button.connect("clicked", self.save_opacity_to_file) grid.attach(save_button, 2, 0, 1, 1) return grid def scale_moved(self, event): self.opacity = self.opacity_scale.get_value() self.feedlist.set_property('opacity', self.opacity) def save_opacity_to_file(self, widget): path = get_path() path.mkdir(0o700, True, True) file_path = path / file_config file_path.write_text(str(self.opacity)) liferea-1.13.7/plugins/gnome-keyring.plugin000066400000000000000000000015111415350204600206640ustar00rootroot00000000000000[Plugin] Module=gnome-keyring Loader=python3 Icon=security-medium IAge=2 Name=Libsecret Support Name[de]=Unterstützung für Libsecret Name[he]=תמיכה עבור Libsecret Name[it]=Supporto al portachiavi di Libsecret Description=Allow Liferea to use libsecret as password store Description[de]=Erlaubt Liferea Passwörter mit libsecret zu speichern Description[he]=אפשר ליישום Liferea להשתמש בתוכנה libsecret כמחסן סיסמאות Description[it]=Consente all'applicazione di usare come archivio delle password il portachiavi di libsecret. Description[pl]=Pozwala Liferea na używanie libsecret jako magazynu haseł. Authors=Lars Windolf ;Bastian Germann Copyright=Copyright © 2012 Lars Windolf Website=https://lzone.de/liferea/ Help=https://lzone.de/liferea/ liferea-1.13.7/plugins/gnome-keyring.py000066400000000000000000000047751415350204600200350ustar00rootroot00000000000000""" Libsecret Plugin Copyright (C) 2013 Lars Windolf Copyright (C) 2018 Bastian Germann This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ import gi gi.require_version('Peas', '1.0') gi.require_version('PeasGtk', '1.0') gi.require_version('Liferea', '3.0') gi.require_version('Secret', '1') from gi.repository import GObject, Peas, PeasGtk, Gtk, Liferea, Secret LABEL = 'liferea' SCHEMA = Secret.Schema.new( "net.sf.liferea.Secret", Secret.SchemaFlags.NONE, {'id': Secret.SchemaAttributeType.STRING} ) class LibsecretPlugin(GObject.Object, Liferea.AuthActivatable): __gtype_name__ = 'LibsecretPlugin' object = GObject.property(type=GObject.Object) def get_collection(self): service = Secret.Service.get_sync(Secret.ServiceFlags.LOAD_COLLECTIONS, None) colls = service.get_collections() for c in colls: if c.get_label() == LABEL: return c return None def do_deactivate(self): window = self.object def do_query(self, id): coll = self.get_collection() if coll: items = coll.search_sync(None, {'id': id}, Secret.SearchFlags.UNLOCK, None) if items and items[0].load_secret_sync(None): username, password = items[0].get_secret().get_text().split('@@@') Liferea.auth_info_from_store(id, username, password) def do_delete(self, id): Secret.password_clear_sync(SCHEMA, {'id': id}, None) def do_store(self, id, username, password): coll = self.get_collection() if not coll: coll = Secret.Collection.create_sync(None, LABEL, None, Secret.CollectionCreateFlags(0), None) login = '@@@'.join([username, password]) secret = Secret.Value.new(login, len(login), 'text/plain') Secret.Item.create_sync(coll, SCHEMA, {'id': id}, repr(id), secret, Secret.ItemCreateFlags.REPLACE, None) liferea-1.13.7/plugins/headerbar.plugin000066400000000000000000000005541415350204600200340ustar00rootroot00000000000000[Plugin] Module=headerbar Loader=python3 IAge=2 Name=Header Bar Version=0.2 Description=Get a GNOME style header bar for Liferea Description[pl]=Udostępnia Liferea pasek menu w stylu GNOME3. Authors=Lars Windolf Copyright=Copyright © 2018 Lars Windolf Website=https://lzone.de/liferea/ Help=https://lzone.de/liferea/ liferea-1.13.7/plugins/headerbar.py000066400000000000000000000107721415350204600171710ustar00rootroot00000000000000#!/usr/bin/python3 # vim:fileencoding=utf-8:sw=4:et """ Liferea Header Bar Plugin Copyright (C) 2018 Lars Windolf This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ import gettext import gi gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gio, Gtk, Liferea # Initialize translations for tooltips # Fallback to English if gettext module can't find the translations # (That's possible if they are installed in a nontraditional dir) _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext #function borrowed from https://gist.github.com/thorsummoner/230bed5bbd3380bd5949 def bind_accelerator(accelerators, widget, accelerator, signal='clicked'): key, mod = Gtk.accelerator_parse(accelerator) widget.add_accelerator(signal, accelerators, key, mod, Gtk.AccelFlags.VISIBLE) class HeaderBarPlugin(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = "HeaderBarPlugin" hb = None object = GObject.property(type=GObject.Object) shell = GObject.property(type=Liferea.Shell) def __init__(self): GObject.Object.__init__(self) def do_activate(self): self.hb = Gtk.HeaderBar() self.hb.props.show_close_button = True self.hb.set_title("Liferea") # Left side buttons box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) Gtk.StyleContext.add_class(box.get_style_context(), "linked") button = Gtk.Button() button.add(Gtk.Arrow(Gtk.ArrowType.LEFT, Gtk.ShadowType.NONE)) button.set_action_name("app.prev-read-item") button.set_tooltip_text(_("Previous Item")) box.add(button) button = Gtk.Button() button.add(Gtk.Arrow(Gtk.ArrowType.RIGHT, Gtk.ShadowType.NONE)) button.set_action_name("app.next-read-item") button.set_tooltip_text(_("Next Item")) box.add(button) button = Gtk.Button() icon = Gio.ThemedIcon(name="edit-redo-symbolic") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) button.add(image) button.set_action_name("app.next-unread-item") button.set_tooltip_text(_("_Next Unread Item").replace("_", "")) box.add(button) button = Gtk.Button() icon = Gio.ThemedIcon(name="emblem-ok-symbolic") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) button.add(image) button.set_action_name("app.mark-selected-feed-as-read") button.set_tooltip_text(_("_Mark Items Read").replace("_", "")) box.add(button) self.hb.pack_start(box) # Right side buttons button = Gtk.MenuButton() icon = Gio.ThemedIcon(name="open-menu-symbolic") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) builder = self.shell.get_property("builder") button.set_menu_model(builder.get_object("menubar")) button.add(image) #Bind the F10 key to the hamburger menu button window = self.shell.get_window() accelerators = Gtk.AccelGroup() window.add_accel_group(accelerators) bind_accelerator(accelerators, button, 'F10') self.hb.pack_end(button) button = Gtk.Button() icon = Gio.ThemedIcon(name="edit-find-symbolic") image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON) button.add(image) button.set_action_name("app.search-feeds") button.set_tooltip_text(_("Search All Feeds...")) self.hb.pack_end(button) self.shell.lookup("mainwindow").set_titlebar(self.hb) self.shell.lookup("mainwindow").set_show_menubar(False) self.shell.lookup("maintoolbar").set_visible(False) self.hb.show_all() def do_deactivate(self): self.shell.lookup("mainwindow").set_titlebar(None) self.shell.lookup("mainwindow").set_show_menubar(True) self.shell.lookup("maintoolbar").set_visible(True) self.hb = None liferea-1.13.7/plugins/libnotify.plugin000066400000000000000000000013331415350204600201120ustar00rootroot00000000000000[Plugin] Module=libnotify Loader=python3 Icon=preferences-system-notification IAge=2 Name=Popup Notifications Name[de]=Popup-Benachrichtigungen Name[he]=התראות קופצות Name[it]=Notifiche popup Description=Display popups informing about new updates. Description[de]=Zeigt Popup-Benachrichtigungen an wenn Abonnements aktualisiert werden Description[he]=הצג התראות המיידעות על עדכונים חדשים Description[it]=Mostra dei popup che informano sui nuovi aggiornamenti. Description[pl]=Wyświetl powiadomienie informujące o nowych aktualizacjach. Authors=Lars Windolf Copyright=Copyright © 2013 Lars Windolf Website=https://lzone.de/liferea/ Help=https://lzone.de/liferea/ liferea-1.13.7/plugins/libnotify.py000066400000000000000000000045131415350204600172470ustar00rootroot00000000000000""" libnotify Popup Notifications Plugin Copyright (C) 2013-2015 Lars Windolf Copyright (C) 2020 Tasos Sahanidis This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ import gettext import time import gi gi.require_version('Notify', '0.7') from gi.repository import GObject, Liferea, Notify _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext class LibnotifyPlugin(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = 'LibnotifyPlugin' object = GObject.property (type=GObject.Object) shell = GObject.property (type=Liferea.Shell) notification = None notification_title = _("Feed Updates") notification_body = "" notification_icon = "liferea" timestamp = None _handler_id = None def do_activate(self): self.timestamp = 0 Notify.init('Liferea') self._handler_id = self.shell.props.feed_list.connect("node-updated", self.on_node_updated) self.notification = Notify.Notification.new(self.notification_title, self.notification_body, self.notification_icon) def do_deactivate(self): Notify.uninit() self.shell.props.feed_list.disconnect(self._handler_id) def on_node_updated(self, widget, node_title): new_timestamp = time.time() # Only make a new notification every 10 seconds if new_timestamp > self.timestamp + 10: self.notification_body = node_title # Update the timestamp self.timestamp = new_timestamp else: self.notification_body += "\n" + node_title self.notification.update(self.notification_title, self.notification_body, self.notification_icon) self.notification.show() liferea-1.13.7/plugins/media-player.plugin000066400000000000000000000013631415350204600204670ustar00rootroot00000000000000[Plugin] Module=media-player Loader=python3 Icon=audio-x-generic IAge=2 Name=Media Player Name[de]=Media Player Name[he]=נגן מדיה Name[it]=Riproduttore multimediale Description=Play music and videos directly from Liferea Description[de]=Musik und Videos können direkt in Liferea abgespielt werden Description[he]=נגן מוזיקה וסרטונים היישר מתוך Liferea Description[it]=Riproduce musica e video direttamente nell'applicazione. Description[pl]=Odtwarzaj muzykę i filmy bezpośrednio z Liferea. Authors=Lars Windolf ; Simon Kagedal Reimer ; Mozbugbox Copyright=Copyright © 2012 Lars Windolf Website=https://lzone.de/liferea/ Help=https://lzone.de/liferea/ liferea-1.13.7/plugins/media-player.py000066400000000000000000000221501415350204600176160ustar00rootroot00000000000000""" GStreamer based embedded Media Player Copyright (C) 2013 Lars Windolf Copyright (C) 2013 Simon Kagedal Reimer This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ import gi gi.require_version('Gst', '1.0') gi.require_version('Peas', '1.0') gi.require_version('PeasGtk', '1.0') gi.require_version('Liferea', '3.0') from gi.repository import GObject, Peas, PeasGtk, GLib, Gtk, Liferea, Gst GMARGIN = 6 def enum(*sequential, **named): """Create an ENUM data type""" enums = dict(zip(sequential, range(len(sequential))), **named) return type('Enum', (), enums) PlayState = enum("PLAY", "PAUSE", "STOP") class MediaPlayerPlugin(GObject.Object, Liferea.MediaPlayerActivatable): __gtype_name__ = 'MediaPlayerPlugin' object = GObject.property(type=GObject.Object) def __init__(self): Gst.init_check(None) self.IS_GST010 = Gst.version() < (0, 11) playbin = "playbin2" if self.IS_GST010 else "playbin" self.playing = PlayState.STOP self.player = Gst.ElementFactory.make(playbin, "player") if not self.IS_GST010: fakesink = Gst.ElementFactory.make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message::eos", self.on_eos) bus.connect("message::error", self.on_error) self.player.connect("about-to-finish", self.on_finished) self.moving_slider = False def on_error(self, bus, message): self.playing = PlayState.STOP self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() print("Error: {}".format(err), debug) self.updateButtons() def on_eos(self, bus, message): self.playing = PlayState.STOP self.player.set_state(Gst.State.NULL) self.updateButtons() def on_finished(self, player): self.playing = PlayState.STOP self.slider.set_value(0) self.set_label(0) self.updateButtons() def play(self): self.playing = PlayState.PLAY uri = Liferea.enclosure_get_url(self.enclosures[self.pos]) self.player.set_property("uri", uri) self.player.set_state(Gst.State.PLAYING) Liferea.ItemView.select_enclosure(self.pos) self.updateButtons() GObject.timeout_add(1000, self.updateSlider) def stop(self): self.playing = PlayState.STOP self.player.set_state(Gst.State.NULL) self.slider.set_value(0) self.updateButtons() def pause(self): self.playing = PlayState.PAUSE self.player.set_state(Gst.State.PAUSED) self.updateButtons() def playToggled(self, w): self.set_label(0) if(self.playing != PlayState.PLAY): self.play() else: self.pause() def next(self, w): self.stop() self.pos+=1 self.play() def prev(self, w): self.stop() self.pos-=1 self.play() def set_label(self, position): format = "%d:%02d" if self.moving_slider: format = "%s" % format self.label.set_markup (format % (position / 60, position % 60)) def get_player_position(self): """Get the GStreamer player's position in nanoseconds""" try: if self.IS_GST010: return self.player.query_position(Gst.Format.TIME)[2] else: return self.player.query_position(Gst.Format.TIME)[1] except Exception as e: # pipeline must not be ready and does not know position print(e) return 0 def get_player_duration(self): """Get the GStreamer player's duration in nanoseconds""" try: if self.IS_GST010: return self.player.query_duration(Gst.Format.TIME)[2] else: return self.player.query_duration(Gst.Format.TIME)[1] except Exception as e: # pipeline must not be ready and does not know position print(e) return 0 def updateSlider(self): if self.playing != PlayState.PLAY or self.moving_slider: return False # cancel timeout duration = self.get_player_duration() / Gst.SECOND position = self.get_player_position() / Gst.SECOND self.slider.set_range(0, duration) self.slider.set_value(position) self.set_label(position) return True def updateButtons(self): Gtk.Widget.set_sensitive(self.prevButton, (self.pos != 0)) Gtk.Widget.set_sensitive(self.nextButton, (len(self.enclosures) - 1 > self.pos)) if(self.playing != PlayState.PLAY): self.playButtonImage.set_from_icon_name("media-playback-start", Gtk.IconSize.BUTTON) else: self.playButtonImage.set_from_icon_name("media-playback-pause", Gtk.IconSize.BUTTON) def on_slider_change_value(self, widget, scroll, value): self.set_label(value) self.move_to_nanosecs = value * Gst.SECOND return False def on_slider_button_press(self, widget, event): self.moving_slider = True def on_slider_button_release(self, widget, event): self.moving_slider = False # Stop when moving slider to very near the end end_cutoff = self.get_player_duration() - Gst.SECOND if self.move_to_nanosecs > end_cutoff and self.playing == PlayState.PLAY: self.playToggled(None) else: self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, self.move_to_nanosecs) def do_load(self, parentWidget, enclosures): if parentWidget == None: print("ERROR: Could not find media player insertion widget!") # Test whether Media Player widget already exists childList = Gtk.Container.get_children(parentWidget) if len(childList) == 1: # We need to add a media player... vbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, 0) vbox.props.margin = GMARGIN vbox.props.spacing = GMARGIN Gtk.Box.pack_start(parentWidget, vbox, True, True, 0); image = Gtk.Image() image.set_from_icon_name("media-skip-backward", Gtk.IconSize.BUTTON) self.prevButton = Gtk.Button.new() self.prevButton.add(image) self.prevButton.connect("clicked", self.prev) self.prevButton.set_direction(Gtk.TextDirection.LTR) Gtk.Box.pack_start(vbox, self.prevButton, False, False, 0) self.playButtonImage = Gtk.Image() self.playButtonImage.set_from_icon_name("media-playback-start", Gtk.IconSize.BUTTON) self.playButton = Gtk.Button.new() self.playButton.add(self.playButtonImage) self.playButton.connect("clicked", self.playToggled) self.playButton.set_direction(Gtk.TextDirection.LTR) Gtk.Box.pack_start(vbox, self.playButton, False, False, 0) image = Gtk.Image() image.set_from_icon_name("media-skip-forward", Gtk.IconSize.BUTTON) self.nextButton = Gtk.Button.new() self.nextButton.add(image) self.nextButton.connect("clicked", self.next) self.nextButton.set_direction(Gtk.TextDirection.LTR) Gtk.Box.pack_start(vbox, self.nextButton, False, False, 0) self.slider = Gtk.Scale(orientation = Gtk.Orientation.HORIZONTAL) self.slider.set_draw_value(False) self.slider.set_range(0, 100) self.slider.set_increments(1, 10) self.slider.connect("change-value", self.on_slider_change_value) self.slider.connect("button-press-event", self.on_slider_button_press) self.slider.connect("button-release-event", self.on_slider_button_release) self.slider.set_direction(Gtk.TextDirection.LTR) Gtk.Box.pack_start(vbox, self.slider, True, True, 0) self.label = Gtk.Label() self.set_label(0) Gtk.Box.pack_start(vbox, self.label, False, False, 0) Gtk.Widget.show_all(vbox) self.enclosures = enclosures self.pos = 0 self.player.set_state(Gst.State.NULL) # FIXME: Make this configurable? self.on_finished(self.player) #def do_activate(self): #print("=== MediaPlayer activate") def do_deactivate(self): window = self.object liferea-1.13.7/plugins/pane.plugin000066400000000000000000000002721415350204600170370ustar00rootroot00000000000000[Plugin] Module=pane Loader=python3 IAge=2 Name=Pane position workaround Description=Set pane position, workaround. Authors=Paweł Marciniak Copyright=Copyright © 2021 Paweł Marciniak liferea-1.13.7/plugins/pane.py000066400000000000000000000053541415350204600161770ustar00rootroot00000000000000""" Copyright (C) 2021 Paweł Marciniak . """ import pathlib import gettext from threading import Thread from time import sleep from gi.repository import GObject, Gtk, Liferea, PeasGtk # Initialize translations for tooltips _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext FILE_CONFIG = 'pane.conf' def get_path(): return pathlib.Path.joinpath(pathlib.Path.home(), ".config/liferea/plugins/pane") class PaneWorkaroundPlugin(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = 'PaneWorkaroundPlugin' shell = GObject.property(type=Liferea.Shell) normal_pane = None wide_pane = None pos_normal = 199 pos_wide = 561 delay = 0.2 def threaded_set_position(self): sleep(self.delay) self.normal_pane.set_position(self.pos_normal) self.wide_pane.set_position(self.pos_wide) def do_activate(self): self.normal_pane = self.shell.lookup('normalViewPane') self.wide_pane = self.shell.lookup('wideViewPane') self.read_position_from_file() thread = Thread(target = self.threaded_set_position, args = ()) thread.start() def do_deactivate(self): self.normal_pane = self.shell.lookup('normalViewPane') self.wide_pane = self.shell.lookup('wideViewPane') cur_pos_normal = self.normal_pane.get_position() cur_pos_wide = self.wide_pane.get_position() if cur_pos_normal != self.pos_normal or cur_pos_wide != self.pos_wide: self.save_position_to_file(cur_pos_normal, cur_pos_wide) def read_position_from_file(self): path = get_path() file_path = path / FILE_CONFIG if file_path.exists(): data = file_path.read_text() data = data.split(' ') self.pos_normal = int(data[0]) self.pos_wide = int(data[1]) def save_position_to_file(self, normal, wide): path = get_path() path.mkdir(0o700, True, True) file_path = path / FILE_CONFIG file_path.write_text(f'{normal} {wide}') liferea-1.13.7/plugins/plugin-installer.plugin000066400000000000000000000007531415350204600214110ustar00rootroot00000000000000[Plugin] Loader=python3 Module=plugin-installer IAge=3 Name=Plugin Installer Name[de]=Plugin Installer Description=Discover and install useful Liferea extensions more easily. Description[de]=Einfaches Auffinden und Installieren von Liferea Plugins Description[pl]=Odkrywaj i instaluj przydatne rozszerzenia Liferea. Builtin=true Icon=emblem-default Authors=Lars Windolf Copyright=Copyright © 2017 Lars Windolf Website=https://github.com/lwindolf/liferea Version=0.1.0 liferea-1.13.7/plugins/plugin-installer.py000066400000000000000000000412461415350204600205450ustar00rootroot00000000000000# -*- coding: utf-8 -*- # vim: set ts=4 et sw=4 sts=4: # Plugin browser plugin for Liferea # Copyright (C) 2018 Lars Windolf # # 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 . import urllib.request, json import os, sys, glob, shutil, subprocess import gi gi.require_version('Gtk', '3.0') from gi.repository import GObject, Liferea, Gtk, Gio, PeasGtk import gettext _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext class AppActivatable(GObject.Object, Liferea.ShellActivatable): __gtype_name__ = "PluginBrowserAppActivatable" shell = GObject.property(type=Liferea.Shell) def __init__(self): GObject.Object.__init__(self) def do_activate(self): action = Gio.SimpleAction.new ('InstallPlugins', None) action.connect("activate", self._run) self._app = self.shell.get_window().get_application () self._app.add_action (action) toolsmenu = self.shell.get_property("builder").get_object ("tools_menu") toolsmenu.append (_('Plugins'), 'app.InstallPlugins') def do_deactivate(self): self._browser = None self._app.remove_action ('InstallPlugins') self.app = None def _run(self, action, data=None): self._browser = PluginBrowser() self._browser.show_all() class PluginBrowser(Gtk.Window): SCHEMA_ID = "net.sf.liferea.plugins" def __init__(self): Gtk.Window.__init__(self, title=_("Plugin Installer")) # FIXME: using safe XDG paths would be better self.target_dir = os.path.expanduser("~/.local/share/liferea/plugins/") self.schema_dir = os.path.expanduser("~/.local/share/glib-2.0/schemas/") self.set_border_width(10) self.set_default_size(600,300) self._grid = Gtk.Grid() self._grid.set_column_homogeneous(True) self._grid.set_row_homogeneous(True) self._notebook = Gtk.Notebook() self._notebook.append_page(PeasGtk.PluginManager(None), Gtk.Label(_("Activate Plugins"))) self._notebook.append_page(self._grid, Gtk.Label(_("Download Plugins"))) self.add(self._notebook) self._liststore = Gtk.ListStore(bool, str, str, str, str) self._plugin_list = self.fetch_list() for ref in self._plugin_list['plugins']: try: name = next(iter(ref)) installed = False if(os.path.isfile('%s%s.plugin' % (self.target_dir, ref[name]['module'])) or os.path.isdir('%s%s' % (self.target_dir, ref[name]['module']))): installed = True if not 'icon' in ref[name]: ref[name]['icon'] = 'libpeas-plugin' self._liststore.append((installed, ref[name]['icon'], name, ref[name]['category'], ('%s\n%s') % (name, ref[name]['description']))) except: print(_("Bad fields for plugin entry %s") % name) self.current_filter_category = None self.category_filter = self._liststore.filter_new() self.category_filter.set_visible_func(self.category_filter_func) # creating the treeview, making it use the filter as a model, and adding the columns self.treeview = Gtk.TreeView.new_with_model(self.category_filter) self.treeview.get_selection().connect("changed", self.on_selection_changed) for i, column_title in enumerate(["Inst.", "Icon", "Description"]): if column_title == 'Inst.': renderer = Gtk.CellRendererToggle() column = Gtk.TreeViewColumn(column_title, renderer, active=i) elif column_title == 'Icon': renderer = Gtk.CellRendererPixbuf() column = Gtk.TreeViewColumn(column_title, renderer, icon_name=i) else: renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(column_title, renderer, markup=4) self.treeview.append_column(column) column.set_sort_column_id(i) self._categories = Gtk.ListStore(str) for category in [_("All"), _("Advanced"), _("Menu"), _("Notifications")]: self._categories.append([category]) self._catcombo = Gtk.ComboBox.new_with_model(self._categories) renderer_text = Gtk.CellRendererText() self._catcombo.pack_start(renderer_text, True) self._catcombo.add_attribute(renderer_text, "text", 0) self._catcombo.connect("changed", self.on_catcombo_changed) self._catlabel = Gtk.Label(_("Filter by category")) self._catcombo.set_active(0) # Setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row self.scrollable_treelist = Gtk.ScrolledWindow() self.scrollable_treelist.set_vexpand(True) self._installButton = Gtk.Button.new_with_mnemonic(_("_Install")) self._installButton.connect("clicked", self.install) self._installButton.set_sensitive(False) self._uninstallButton = Gtk.Button.new_with_mnemonic(_("_Uninstall")) self._uninstallButton.connect("clicked", self.uninstall) self._uninstallButton.set_sensitive(False) self._grid.attach(self.scrollable_treelist, 0, 0, 8, 10) self._grid.attach_next_to(self._catlabel, self.scrollable_treelist, Gtk.PositionType.TOP, 1, 1) self._grid.attach_next_to(self._catcombo, self._catlabel, Gtk.PositionType.RIGHT, 2, 1) self._grid.attach_next_to(self._installButton, self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1) self._grid.attach_next_to(self._uninstallButton, self._installButton, Gtk.PositionType.RIGHT, 1, 1) self.scrollable_treelist.add(self.treeview) self.show_all() def fetch_list(self): """Fetch list from github project repo and parse JSON""" list_url = "https://raw.githubusercontent.com/lwindolf/liferea/master/plugins/plugin-list.json" data = None req = urllib.request.Request(list_url) resp = urllib.request.urlopen(req).read() return json.loads(resp.decode('utf-8')) def category_filter_func(self, model, iter, data): """Tests if the category in the row is the one in the filter""" if self.current_filter_category is None or self.current_filter_category == "All": return True else: return model[iter][3] == self.current_filter_category def on_catcombo_changed(self, combo): active_row_id = combo.get_active() if active_row_id != -1: self.current_filter_category = ("All", "Advanced", "Menu", "Notifications")[active_row_id] else: self.current_filter_category = None self.category_filter.refilter() def on_selection_changed(self, selection): self.update_buttons() def update_buttons(self): model, treeiter = self.treeview.get_selection().get_selected() if treeiter != None: self._installButton.set_sensitive(model[treeiter][0] == 0) self._uninstallButton.set_sensitive(model[treeiter][0] != 0) def get_plugin_by_name(self, plugin): plugin_info = None # Get infos on selected plugin for ref in self._plugin_list['plugins']: if plugin == next(iter(ref)): plugin_info = ref[plugin] if plugin_info == None: raise Exception("Fatal: Failed to get plugin infos from tree list!") return plugin_info def install(self, path=None, column=None, user_data=None): selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if treeiter != None: model[treeiter][0] = self.install_plugin(self.get_plugin_by_name(model[treeiter][2])) self.update_buttons() def uninstall(self, path=None, column=None, user_data=None): selection = self.treeview.get_selection() model, treeiter = selection.get_selected() if treeiter != None: model[treeiter][0] = self.uninstall_plugin(self.get_plugin_by_name(model[treeiter][2])) self.update_buttons() def show_message(self, message, error = False, buttons = Gtk.ButtonsType.CLOSE): dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.DESTROY_WITH_PARENT, (Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO), buttons, message) response = Gtk.Dialog.run(dialog) Gtk.Widget.destroy(dialog) return response def install_plugin(self, plugin_info): """Fetches github repo for a plugin and tries to install the plugin""" DIR_NAME = "/tmp/liferea-pluginbrowser-%s" % plugin_info['module'] if os.path.isdir(DIR_NAME): shutil.rmtree(DIR_NAME) os.mkdir(DIR_NAME) os.chdir(DIR_NAME) # Check and install dependencies if 'deps' in plugin_info: # First check if package manager is available try: # Run package manager check command p = subprocess.Popen(' '.join(plugin_info['deps']['pkgmgr']['check']), shell=True) p.wait() pkg_mgr_missing = p.returncode except: pkg_mgr_missing = 0 if pkg_mgr_missing != 0: self.show_message(_("Missing package manager '%s'. Cannot check nor install necessary dependencies!") % plugin_info['deps']['pkgmgr']['name'], True) return False try: # For each package run package check command for pkg in plugin_info['deps']['packages']: print("Checking for %s..."%pkg) cmd = plugin_info['deps']['pkgmgr']['checkPkg'][:] cmd.append(pkg) print(" -> %s"%' '.join(cmd)) p = subprocess.Popen(cmd) p.wait() if p.returncode != 0: cmd = plugin_info['deps']['pkgmgr']['installPkg'][:] cmd.append(pkg) response = self.show_message(_("Missing package '%s'. Do you want to install it? (Will run '%s')") % (pkg, ' '.join(cmd)), False, Gtk.ButtonsType.OK_CANCEL) if Gtk.ResponseType.OK != response: return False p = subprocess.Popen(cmd) p.wait() if p.returncode != 0: self.show_message(_("Package installation failed (%s)! Check console output for further problem details!") % sys.exc_info()[0], True) return False except: self.show_message(_("Failed to check plugin dependencies (%s)!") % sys.exc_info()[0], True) return False # Git checkout try: p = subprocess.Popen(["git", "clone", "https://github.com/%s" % plugin_info['source'], "."]) p.wait() # FIXME: error checking except FileNotFoundError: self.show_message(_("Command \"git\" not found, please install it!"), True) return False # Now copy the plugin source, there are 2 variants: # - either there is a subdir named after the module / # - or there is a module file with language extension .py try: src_dir = '%s/%s' % (DIR_NAME, plugin_info['module']) if os.path.isdir(src_dir): print(_("Copying %s to %s") % (src_dir, self.target_dir)) shutil.copytree(src_dir, "%s/%s" % (self.target_dir, plugin_info['module'])) except: self.show_message(_("Failed to copy plugin directory (%s)!") % sys.exc_info()[0], True) return False # FIXME: support other plugin languages besides Python try: src_file = '%s/%s.py' % (DIR_NAME, plugin_info['module']) if os.path.isfile(src_file): shutil.copy(src_file, self.target_dir) except: self.show_message(_("Failed to copy plugin .py file (%s)!") % sys.exc_info()[0], True) return False # Copy .plugin file if it is not inside the plugin source itself try: # Do not copy .plugin file if it is inside plugin source src_file = '%s/%s/%s.plugin' % (DIR_NAME, plugin_info['module'], plugin_info['module']) if not os.path.isfile(src_file): shutil.copy('%s/%s.plugin' % (DIR_NAME, plugin_info['module']), self.target_dir) except: self.show_message(_("Failed to copy .plugin file (%s)!") % sys.exc_info()[0], True) return False # Optional: find and install schemata try: schema_found = False # We allow for schema either at top level or in first subdir for schema_file in glob.iglob('%s/**/*.gschema.xml' % (DIR_NAME), recursive = True): schema_found = True if not os.path.isdir(self.schema_dir): print(_('Creating schema directory %s') % self.schema_dir) os.makedirs(self.schema_dir) print(_('Installing schema %s') % schema_file) shutil.copy(schema_file,self.schema_dir) if schema_found: print(_('Compiling schemas...')) p = subprocess.Popen(["glib-compile-schemas", self.schema_dir]) p.wait() # FIXME: error checking except: self.show_message(_("Failed to install schema files (%s)!") % sys.exc_info()[0], True) return False # Enable plugin (for next restart) try: settings = Gio.Settings.new(self.SCHEMA_ID) current_plugins = settings.get_strv('active-plugins') current_plugins.append(plugin_info['module']) settings.set_strv('active-plugins', current_plugins) except: self.show_message(_("Failed to enable plugin (%s)!") % sys.exc_info()[0], True) return False # Cleanup shutil.rmtree(DIR_NAME) self.show_message(_("Plugin '%s' is now installed. Ensure to restart Liferea!") % plugin_info['module']) return True def uninstall_plugin(self, plugin_info): """Deletes all possible files and directories that might belong to the plugin""" error = False # Remove plugin from active-plugins try: settings = Gio.Settings.new(self.SCHEMA_ID) current_plugins = settings.get_strv('active-plugins') current_plugins.remove(plugin_info['module']) settings.set_strv('active-plugins', current_plugins) except: print(_("Failed to disable plugin (%s)!") % sys.exc_info()[0]) error = True # Drop plugin dir (for directory plugins) src_dir = '%s/%s' % (self.target_dir, plugin_info['module']) try: if os.path.isdir(src_dir): print(_("Deleting '%s'") % src_dir) shutil.rmtree(src_dir) except: print(_("Failed to remove directory '%s' (%s)!") % (src_dir, sys.exc_info()[0])) error = True # Drop plugin source file (for single file plugins) src_file = '%s/%s.py' % (self.target_dir, plugin_info['module']) try: if os.path.isfile(src_file): print("Deleting '%s'" % src_file) os.remove(src_file) except: print(_("Failed to remove .py file!")) error = True # Drop plugin file src_file = '%s/%s/%s.plugin' % (self.target_dir, plugin_info['module'], plugin_info['module']) try: if os.path.isfile(src_file): print(_("Deleting '%s'") % src_file) os.remove(src_file) except: print(_("Failed to remove .plugin file!")) error = True # FIXME: Drop plugin schema files # # Removing schemas does not really work, as the schema files can have # arbitrary names... if error: self.show_message(_("Sorry! Plugin removal failed!."), True) else: self.show_message(_("Plugin was removed. Please restart Liferea once for it to take full effect!."), False) liferea-1.13.7/plugins/plugin-list.json000066400000000000000000000043611415350204600200410ustar00rootroot00000000000000{ "schemaVersion": 1, "plugins":[ { "Floating Statusbar": { "module" : "floating-statusbar", "category" : "Menu", "source" : "whizse/liferea-plugins", "description" : "Regain some screen real estate by replacing the static statusbar with a floating counterpart. Similar to the ones used in web browsers" }}, { "Python Console": { "module" : "python-console", "category" : "Advanced", "source" : "whizse/liferea-plugins", "description" : "Add a Python console option to the 'Tools' menu. Great for debugging." }}, { "No Mark All Read": { "module" : "markallreadinactive", "category" : "Menu", "source" : "whizse/liferea-plugins", "description" : "Disables the menu entry for 'Subscriptions' → 'Mark All As Read' as it is a little bit too easy to hit by mistake." }}, { "Feed Alerts": { "module" : "feedalerts", "category" : "Notifications", "source" : "ExpatPaul/liferea-plugins", "description" : "Configurable feed notifier. Needs to be configured with regex filters in ~/.local/share/liferea/plugins/feedalerts/feedalerts.conf" }}, { "Webkit Inspector": { "module" : "inspector", "category" : "Advanced", "source" : "lwindolf/liferea-webkit2-inspector", "description" : "Press F12 to launch the Webkit2 inspector (web developer tool)." }}, { "Webkit Settings": { "module" : "webkit-setting", "category" : "Advanced", "source" : "mozbugbox/liferea-plugin-studio", "description" : "Change advanced WebKitGtk settings for HTML rendering." }}, { "Custom Accels": { "module" : "accels", "category" : "Advanced", "source" : "mozbugbox/liferea-plugin-studio", "description" : "Editing Accels/shortcuts for Liferea action." }} ] } liferea-1.13.7/plugins/trayicon.plugin000066400000000000000000000015531415350204600177470ustar00rootroot00000000000000[Plugin] Module=trayicon Loader=python3 Icon=preferences-desktop-accessibility IAge=2 Name=Tray Icon (GNOME Classic) Name[de]=Tray Icon (GNOME Klassik) Name[it]=Icona di vassoio (GNOME Classic) Name[he]=סמל מגש (GNOME קלאסי) Description=Display a small system tray icon to hide/unhide Liferea. Description[de]=Zeigt ein kleines Tray Icon an mit dem Liferea schnell angezeigt und wieder versteckt werden kann Description[it]=Mostra una piccola icona nel vassoio di sistema per visualizzare o nascondere l'applicazione. Description[he]=הצג סמל מגש מערכת קטן כדי להסתיר או להציג את Liferea Description[pl]=Wyświetl małą ikonę na pasku powiadamiania, aby ukryć/odkryć Liferea. Authors=Lars Windolf Copyright=Copyright © 2013 Lars Windolf Website=https://lzone.de/liferea/ Help=https://lzone.de/liferea/ liferea-1.13.7/plugins/trayicon.py000066400000000000000000000227501415350204600171030ustar00rootroot00000000000000""" System Tray Icon Plugin Copyright (C) 2013-2020 Lars Windolf This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ import gettext import pathlib from collections import namedtuple import cairo import gi gi.require_version('Liferea', '3.0') from gi.repository import GObject, Gtk, Liferea from gi.repository import Gdk, GdkPixbuf _ = lambda x: x try: t = gettext.translation("liferea") except FileNotFoundError: pass else: _ = t.gettext # Cairo text extents Extents = namedtuple("Extents", [ "x_bearing", "y_bearing", "width", "height", "x_advance", "y_advance" ]) def pixbuf_text(width, height, text, font_size=16, bg_pix=None): """Draw text to pixbuf at lower right corner @width: canvas width @height: canvas height @text: UTF-8 text to draw @font_size: font size @bg_pix: pixbuf to draw as background @return: a new pixbuf with the given canvas width and height """ bg_color = (0.8, 0.8, 0.8, 1) text_color = (0.8, 0.2, 0, 1) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) context = cairo.Context(surface) if bg_pix is not None: bg_w, bg_h = bg_pix.get_width(), bg_pix.get_height() Gdk.cairo_set_source_pixbuf(context, bg_pix, max(width - bg_w, 0)/2, max(height - bg_h, 0)/2) context.paint() #paint the pixbuf context.set_font_size(font_size) context.select_font_face("condensed") # extents: (x_bearing, y_bearing, width, height, x_advance, y_advance) extents = Extents._make(context.text_extents(text)) # draw text with a background color x_off = width - extents.width - 1 y_off = height - extents.height - 1 context.set_source_rgba(*bg_color) context.rectangle(x_off, y_off, extents.width + 2, extents.height + 2) context.fill() x_off = max(width - extents.x_bearing - extents.width, 1) y_off = height - (extents.height + extents.y_bearing) - 1 context.move_to(x_off, y_off) context.set_source_rgba(*text_color) context.show_text(text) pixbuf= Gdk.pixbuf_get_from_surface(surface, 0, 0, width, height) return pixbuf def get_config_path(): """Return data file path""" data_dir = pathlib.Path.joinpath( pathlib.Path.home(), ".config/liferea/plugins/trayicon" ) if not data_dir.exists(): data_dir.mkdir(0o700, True, True) config_path = data_dir / "trayicon.conf" return config_path class TrayiconPlugin (GObject.Object, Liferea.ShellActivatable): __gtype_name__ = 'TrayiconPlugin' object = GObject.property(type=GObject.Object) shell = GObject.property(type=Liferea.Shell) config_path = None read_pix = None unread_pix = None staticon = None menu = None min_enabled = None window = None delete_signal_id = None feedlist_new_items_cb_id = None feedlist = None def do_activate(self): self.read_pix = Liferea.icon_create_from_file("emblem-web.svg") # FIXME: Support a scalable image! self.unread_pix = Liferea.icon_create_from_file("unread.png") self.staticon = Gtk.StatusIcon () self.staticon.connect("activate", self.trayicon_click) self.staticon.connect("popup_menu", self.trayicon_popup) self.staticon.connect("size-changed", self.trayicon_size_changed) self.staticon.set_visible(True) self.trayicon_set_pixbuf(self.read_pix) self.menu = Gtk.Menu() menuitem_toggle = Gtk.MenuItem(_("Show / Hide")) menuitem_close_behavior = Gtk.CheckMenuItem(_("Minimize to tray on close")) menuitem_quit = Gtk.MenuItem(_("Quit")) self.config_path = get_config_path() self.min_enabled = self.get_config() if self.min_enabled == "True": menuitem_close_behavior.set_active(True) else: menuitem_close_behavior.set_active(False) menuitem_toggle.connect("activate", self.trayicon_toggle) menuitem_close_behavior.connect("toggled", self.trayicon_close_behavior) menuitem_quit.connect("activate", self.trayicon_quit) self.menu.append(menuitem_toggle) self.menu.append(menuitem_close_behavior) self.menu.append(menuitem_quit) self.menu.show_all() self.window = self.shell.get_window() self.delete_signal_id = GObject.signal_lookup("delete_event", Gtk.Window) GObject.signal_handlers_block_matched (self.window, GObject.SignalMatchType.ID | GObject.SignalMatchType.DATA, self.delete_signal_id, 0, None, None, None) self.window.connect("delete_event", self.trayicon_close_action) self.window.connect("window-state-event", self.window_state_event_cb) # show the window if it is hidden when starting liferea self.window.deiconify() self.window.show() feedlist = self.shell.props.feed_list self.feedlist_new_items_cb(feedlist) sigid = feedlist.connect("new-items", self.feedlist_new_items_cb) self.feedlist_new_items_cb_id = sigid self.feedlist = feedlist def window_state_event_cb(self, widget, event): "Hide window when minimize" if event.changed_mask & event.new_window_state & Gdk.WindowState.ICONIFIED: self.window.deiconify() self.shell.save_position() self.window.hide() def get_config(self): """Load configuration file""" try: with open(self.config_path, "r") as f: setting = f.readline() if setting == "": setting = "True" except FileNotFoundError: self.save_config("True") setting = "True" return setting def save_config(self, minimize_setting): """Save configuration file""" with open(self.config_path, "w") as f: f.write(minimize_setting) def trayicon_click(self, widget, data = None): # Always show the window on click, as some window managers misbehave. self.shell.show_window() def trayicon_close_action(self, widget, event): self.shell.save_position() if self.min_enabled == "True": self.window.hide() else: Liferea.Application.shutdown() return True def trayicon_close_behavior(self, widget, data = None): if widget.get_active(): self.min_enabled = "True" else: self.min_enabled = "False" self.save_config(self.min_enabled) def trayicon_toggle(self, widget, data = None): self.shell.toggle_visibility() def trayicon_quit(self, widget, data = None): Liferea.Application.shutdown() def trayicon_popup(self, widget, button, time, data = None): self.menu.popup(None, None, self.staticon.position_menu, self.staticon, 3, time) def trayicon_set_pixbuf(self, pix): if pix is None: return icon_size = self.staticon.props.size if icon_size == 0: return if pix.props.height != icon_size: pix = pix.scale_simple(icon_size, icon_size, GdkPixbuf.InterpType.HYPER) if self.staticon.props.pixbuf != pix: self.staticon.props.pixbuf = pix def show_new_count(self, new_count): """display new count on status icon""" pix_size = self.staticon.props.size font_size = max(10, pix_size/4*2) #print(pix_size, font_size, double_figure) pix = pixbuf_text(pix_size, pix_size, "{}".format(new_count), font_size, self.unread_pix) self.staticon.props.pixbuf = pix return pix def feedlist_new_items_cb(self, feedlist=None, new_count=-1): if new_count < 0: if feedlist is None: feedlist = self.shell.props.feed_list new_count = feedlist.get_new_item_count() if new_count > 0: double_figure = min(99, new_count) # show max 2 digit pix = self.show_new_count(double_figure) else: pix = self.read_pix self.trayicon_set_pixbuf(pix) def trayicon_size_changed(self, widget, size): self.feedlist_new_items_cb() return True def do_deactivate(self): self.staticon.set_visible(False) self.window.disconnect_by_func(self.trayicon_close_action) GObject.signal_handlers_unblock_matched (self.window, GObject.SignalMatchType.ID | GObject.SignalMatchType.DATA, self.delete_signal_id, 0, None,None,None) self.window.disconnect_by_func(self.window_state_event_cb) self.feedlist.disconnect(self.feedlist_new_items_cb_id) # unhide the window when deactivating the plugin self.window.deiconify() self.window.show() del self.staticon del self.window del self.menu liferea-1.13.7/po/000077500000000000000000000000001415350204600136305ustar00rootroot00000000000000liferea-1.13.7/po/LINGUAS000066400000000000000000000002051415350204600146520ustar00rootroot00000000000000ar ast be@latin bg ca cs da de el en_GB es eu fi fr gl he hu id it ja ko lt lv mk nl pl pt pt_BR ro ru sk sq sv tr uk vi zh_CN zh_TW liferea-1.13.7/po/POTFILES.in000066400000000000000000000113761415350204600154150ustar00rootroot00000000000000[encoding: UTF-8] # List of source files containing translatable strings. net.sourceforge.liferea.appdata.xml.in net.sourceforge.liferea.desktop.in plugins/getfocus.py plugins/headerbar.py plugins/libnotify.py plugins/plugin-installer.py plugins/trayicon.py src/browser.c src/browser_history.c src/comments.c src/comments.h src/common.c src/common.h src/conf.c src/conf.h src/date.c src/date.h src/db.c src/db.h src/debug.c src/debug.h src/enclosure.c src/export.c src/export.h src/favicon.c src/favicon.h src/feed.c src/feed.h src/feedlist.c src/feedlist.h src/feed_parser.c src/feed_parser.h src/fl_sources/default_source.c src/fl_sources/default_source.h src/fl_sources/dummy_source.c src/fl_sources/dummy_source.h src/fl_sources/google_source.c src/fl_sources/google_source.h src/fl_sources/node_source.c src/fl_sources/node_source.h src/fl_sources/opml_source.c src/fl_sources/opml_source.h src/fl_sources/reedah_source.c src/fl_sources/reedah_source_feed.c src/fl_sources/reedah_source_feed_list.c src/fl_sources/reedah_source_feed_list.h src/fl_sources/reedah_source.h src/fl_sources/theoldreader_source.c src/fl_sources/theoldreader_source_feed.c src/fl_sources/theoldreader_source_feed_list.c src/fl_sources/theoldreader_source_feed_list.h src/fl_sources/theoldreader_source.h src/fl_sources/ttrss_source.c src/fl_sources/ttrss_source_feed.c src/fl_sources/ttrss_source_feed_list.c src/fl_sources/ttrss_source_feed_list.h src/fl_sources/ttrss_source.h src/folder.c src/folder.h src/html.c src/html.h src/item.c src/item.h src/item_history.c src/item_history.h src/itemlist.c src/itemlist.h src/item_loader.c src/item_loader.h src/itemset.c src/itemset.h src/liferea_application.c src/main.c src/metadata.c src/metadata.h src/migrate.c src/net.c src/net.h src/newsbin.c src/newsbin.h src/node.c src/node.h src/node_type.c src/node_type.h src/parsers/atom10.c src/parsers/atom10.h src/parsers/html5_feed.c src/parsers/html5_feed.h src/parsers/ldjson_feed.c src/parsers/ldjson_feed.h src/parsers/ns_admin.c src/parsers/ns_admin.h src/parsers/ns_ag.c src/parsers/ns_ag.h src/parsers/ns_cC.c src/parsers/ns_cC.h src/parsers/ns_content.c src/parsers/ns_content.h src/parsers/ns_dc.c src/parsers/ns_dc.h src/parsers/ns_slash.c src/parsers/ns_slash.h src/parsers/ns_syn.c src/parsers/ns_syn.h src/parsers/rss_channel.c src/parsers/rss_channel.h src/parsers/rss_item.c src/parsers/rss_item.h src/render.c src/render.h src/rule.c src/rule.h src/social.c src/social.h src/subscription.c src/subscription.h src/subscription_icon.c src/subscription_icon.h src/subscription_type.h src/ui/auth_dialog.c src/ui/auth_dialog.h src/ui/browser_tabs.c src/ui/browser_tabs.h src/ui/enclosure_list_view.c src/ui/enclosure_list_view.h src/ui/feed_list_view.c src/ui/feed_list_view.h src/ui/icons.c src/ui/icons.h src/ui/item_list_view.c src/ui/item_list_view.h src/ui/itemview.c src/ui/itemview.h src/ui/liferea_dialog.c src/ui/liferea_dialog.h src/ui/liferea_htmlview.c src/ui/liferea_htmlview.h src/ui/liferea_shell.c src/ui/liferea_shell.c src/ui/liferea_shell.h src/ui/liferea_shell.h src/ui/popup_menu.c src/ui/popup_menu.h src/ui/preferences_dialog.c src/ui/preferences_dialog.h src/ui/rule_editor.c src/ui/rule_editor.h src/ui/search_dialog.c src/ui/search_dialog.h src/ui/search_folder_dialog.c src/ui/search_folder_dialog.h src/ui/subscription_dialog.c src/ui/subscription_dialog.h src/ui/ui_common.c src/ui/ui_dnd.c src/ui/ui_dnd.h src/ui/ui_folder.c src/ui/ui_folder.h src/ui/ui_update.c src/ui/ui_update.h src/update.c src/update.h src/vfolder.c src/vfolder.h src/vfolder_loader.c src/vfolder_loader.h src/webkit/liferea_web_view.c src/webkit/web_extension/liferea_web_extension.c src/webkit/web_extension/web_extension_main.c src/webkit/webkit.c src/xml.c src/xml.h [type: gettext/glade]glade/about.ui [type: gettext/glade]glade/auth.ui [type: gettext/glade]glade/enclosure_handler.ui [type: gettext/glade]glade/liferea_menu.ui [type: gettext/glade]glade/liferea_toolbar.ui [type: gettext/glade]glade/mainwindow.ui [type: gettext/glade]glade/mark_read_dialog.ui [type: gettext/glade]glade/new_folder.ui [type: gettext/glade]glade/new_newsbin.ui [type: gettext/glade]glade/new_subscription.ui [type: gettext/glade]glade/node_source.ui [type: gettext/glade]glade/opml_source.ui [type: gettext/glade]glade/prefs.ui [type: gettext/glade]glade/properties.ui [type: gettext/glade]glade/reedah_source.ui [type: gettext/glade]glade/rename_node.ui [type: gettext/glade]glade/search_folder.ui [type: gettext/glade]glade/search.ui [type: gettext/glade]glade/simple_search.ui [type: gettext/glade]glade/simple_subscription.ui [type: gettext/glade]glade/theoldreader_source.ui [type: gettext/glade]glade/ttrss_source.ui [type: gettext/glade]glade/update_monitor.ui xslt/feed.xml.in xslt/folder.xml.in xslt/item.xml.in xslt/newsbin.xml.in xslt/source.xml.in xslt/vfolder.xml.in liferea-1.13.7/po/ar.po000066400000000000000000003000561415350204600145760ustar00rootroot00000000000000# translation of liferea_trunk_ar.po to Arabic # This file is distributed under the same license as the Lifearea package. # Copyright (C) 2006-2009 Khaled Hosny # Khaled Hosny , 2006, 2007, 2009, 2010, 2013, 2014. msgid "" msgstr "" "Project-Id-Version: liferea_trunk_ar\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-30 12:36+0200\n" "PO-Revision-Date: 2014-10-11 19:28+0200\n" "Last-Translator: Khaled Hosny \n" "Language-Team: Arabic \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 ? 4 : 5;\n" "X-Generator: Virtaal 1.0.0-beta1\n" "X-Project-Style: gnome\n" #: ../net.sourceforge.liferea.appdata.xml.in.h:1 #, fuzzy msgid "RSS feed reader" msgstr "قارئ تلقيمات" #: ../net.sourceforge.liferea.appdata.xml.in.h:2 msgid "" "Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator " "for online news feeds. It supports a number of different feed formats " "including RSS/RDF, CDF and Atom. There are many other news readers " "available, but these others are not available for Linux or require many " "extra libraries to be installed. Liferea tries to fill this gap by creating " "a fast, easy to use, easy to install news aggregator for GTK/GNOME." msgstr "" "لايفريا هو مجمّع أخبار لتلقيمات الأخبار. يدعم عددًا من أنساق التلقيمات مثل RSS/" "RDF و CDF و Atom. يوجد العديد من قارئات تلقيمات الأخبار لكنها إما غير متوفرة " "على لينكس أو تتطلب الكثير من المكتبات الإضافية. يحاول لايفريا سد هذه الهوّة " "بتوفير مجمّع أخبار سهل الاستخدام و التثبيت لجنوم." #: ../net.sourceforge.liferea.appdata.xml.in.h:3 msgid "Distinguishing features:" msgstr "الخصائص المميزة:" #: ../net.sourceforge.liferea.appdata.xml.in.h:4 msgid "Read articles when offline" msgstr "قراءة المقالات دون اتصال" #: ../net.sourceforge.liferea.appdata.xml.in.h:5 msgid "Synchronizes with TheOldReader" msgstr "المزامنة مع قارئ TheOldReader" #: ../net.sourceforge.liferea.appdata.xml.in.h:6 msgid "Synchronizes with TinyTinyRSS" msgstr "المزامنة مع TinyTinyRSS" #: ../net.sourceforge.liferea.appdata.xml.in.h:7 msgid "Synchronizes with InoReader" msgstr "المزامنة مع قارئ InoReader" #: ../net.sourceforge.liferea.appdata.xml.in.h:8 msgid "Synchronizes with Reedah" msgstr "المزامنة مع قارئ Reedah" #: ../net.sourceforge.liferea.appdata.xml.in.h:9 msgid "Permanently save headlines in news bins" msgstr "حفظ عناوين الأخبار بشكل دائم في سلة الأخبار" #: ../net.sourceforge.liferea.appdata.xml.in.h:10 msgid "Match items using search folders" msgstr "تجميع العناصر باستخدام مجلدات البحث" #: ../net.sourceforge.liferea.appdata.xml.in.h:11 msgid "Play Podcasts" msgstr "تشغيل التدوين صوتي" #: ../net.sourceforge.liferea.desktop.in.h:1 ../src/liferea_application.c:350 #: ../glade/mainwindow.ui.h:1 msgid "Liferea" msgstr "لايفريا" #: ../net.sourceforge.liferea.desktop.in.h:2 msgid "Feed Reader" msgstr "قارئ تلقيمات" # ARABEYES: keep the html code, this is # intentional #: ../net.sourceforge.liferea.desktop.in.h:3 msgid "Liferea Feed Reader" msgstr "قارئ التلقيمات لايفريا" #: ../net.sourceforge.liferea.desktop.in.h:4 msgid "Read news feeds and blogs" msgstr "اقرأ تلقيمات الأخبار و المدونات" #: ../net.sourceforge.liferea.desktop.in.h:5 #, fuzzy msgid "news;feed;aggregator;blog;podcast;syndication;rss;atom" msgstr "أخبار;تلقيمات;مدونة;تدوين;" #: ../plugins/getfocus.py:93 msgid "Opacity:" msgstr "" #: ../plugins/getfocus.py:94 msgid "Opacity" msgstr "" #: ../plugins/getfocus.py:104 msgid "Min" msgstr "" #: ../plugins/getfocus.py:109 msgid "Max" msgstr "" #: ../plugins/getfocus.py:115 msgid "Save" msgstr "" #: ../plugins/headerbar.py:67 ../glade/liferea_menu.ui.h:20 #: ../glade/liferea_toolbar.ui.h:5 msgid "Previous Item" msgstr "العنصر السابق" #: ../plugins/headerbar.py:73 ../glade/liferea_menu.ui.h:21 #: ../glade/liferea_toolbar.ui.h:6 msgid "Next Item" msgstr "العنصر التالي" #: ../plugins/headerbar.py:81 ../glade/liferea_menu.ui.h:19 #: ../glade/liferea_toolbar.ui.h:7 msgid "_Next Unread Item" msgstr "العنصر غير المقروء ال_تالي" #: ../plugins/headerbar.py:89 ../glade/liferea_menu.ui.h:14 #: ../glade/liferea_toolbar.ui.h:3 msgid "_Mark Items Read" msgstr "_علّم العناصر مقروءة" #: ../plugins/headerbar.py:113 ../glade/liferea_menu.ui.h:40 #: ../glade/liferea_toolbar.ui.h:10 msgid "Search All Feeds..." msgstr "ابحث في كل التلقيمات..." #: ../plugins/libnotify.py:42 #, fuzzy msgid "Feed Updates" msgstr "تحديث التلقيمة" #: ../plugins/plugin-installer.py:54 msgid "Plugins" msgstr "الملحقات" #: ../plugins/plugin-installer.py:69 #, fuzzy msgid "Plugin Installer" msgstr "الملحقات" #: ../plugins/plugin-installer.py:83 #, fuzzy msgid "Activate Plugins" msgstr "الملحقات" #: ../plugins/plugin-installer.py:84 #, fuzzy msgid "Download Plugins" msgstr "_نزّل باستخدام" #: ../plugins/plugin-installer.py:102 #, python-format msgid "Bad fields for plugin entry %s" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "All" msgstr "" #: ../plugins/plugin-installer.py:125 ../glade/properties.ui.h:39 msgid "Advanced" msgstr "متقدّم" #: ../plugins/plugin-installer.py:125 msgid "Menu" msgstr "" #: ../plugins/plugin-installer.py:125 #, fuzzy msgid "Notifications" msgstr "إعدادات التنبيه" #: ../plugins/plugin-installer.py:133 msgid "Filter by category" msgstr "" #: ../plugins/plugin-installer.py:140 msgid "_Install" msgstr "" #: ../plugins/plugin-installer.py:144 msgid "_Uninstall" msgstr "" #: ../plugins/plugin-installer.py:245 #, python-format msgid "" "Missing package manager '%s'. Cannot check nor install necessary " "dependencies!" msgstr "" #: ../plugins/plugin-installer.py:261 #, python-format msgid "Missing package '%s'. Do you want to install it? (Will run '%s')" msgstr "" #: ../plugins/plugin-installer.py:268 #, python-format msgid "" "Package installation failed (%s)! Check console output for further problem " "details!" msgstr "" #: ../plugins/plugin-installer.py:271 #, python-format msgid "Failed to check plugin dependencies (%s)!" msgstr "" #: ../plugins/plugin-installer.py:280 msgid "Command \"git\" not found, please install it!" msgstr "" #: ../plugins/plugin-installer.py:289 #, python-format msgid "Copying %s to %s" msgstr "" #: ../plugins/plugin-installer.py:292 #, python-format msgid "Failed to copy plugin directory (%s)!" msgstr "" #: ../plugins/plugin-installer.py:301 #, python-format msgid "Failed to copy plugin .py file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:311 #, python-format msgid "Failed to copy .plugin file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:322 #, fuzzy, python-format msgid "Creating schema directory %s" msgstr "تعذّر إنشاء مجلّد المخبئية \"%s\"!" #: ../plugins/plugin-installer.py:324 #, python-format msgid "Installing schema %s" msgstr "" #: ../plugins/plugin-installer.py:328 msgid "Compiling schemas..." msgstr "" #: ../plugins/plugin-installer.py:333 #, python-format msgid "Failed to install schema files (%s)!" msgstr "" #: ../plugins/plugin-installer.py:343 #, python-format msgid "Failed to enable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:349 #, python-format msgid "Plugin '%s' is now installed. Ensure to restart Liferea!" msgstr "" #: ../plugins/plugin-installer.py:363 #, python-format msgid "Failed to disable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:370 ../plugins/plugin-installer.py:390 #, fuzzy, python-format msgid "Deleting '%s'" msgstr "يجريِ حذف الخانة" #: ../plugins/plugin-installer.py:373 #, python-format msgid "Failed to remove directory '%s' (%s)!" msgstr "" #: ../plugins/plugin-installer.py:383 msgid "Failed to remove .py file!" msgstr "" #: ../plugins/plugin-installer.py:393 msgid "Failed to remove .plugin file!" msgstr "" #: ../plugins/plugin-installer.py:402 msgid "Sorry! Plugin removal failed!." msgstr "" #: ../plugins/plugin-installer.py:404 msgid "" "Plugin was removed. Please restart Liferea once for it to take full effect!." msgstr "" #: ../plugins/trayicon.py:132 #, fuzzy msgid "Show / Hide" msgstr "اعرض التفاصيل" #: ../plugins/trayicon.py:133 msgid "Minimize to tray on close" msgstr "" #: ../plugins/trayicon.py:134 #, fuzzy msgid "Quit" msgstr "ا_خرج" #: ../src/browser.c:81 ../src/browser.c:98 #, c-format msgid "Browser command failed: %s" msgstr "فشل أمر المتصفح: %s" #: ../src/browser.c:101 ../src/ui/liferea_shell.c:1047 #, c-format msgid "Starting: \"%s\"" msgstr "يبدأ: \"%s\"" #. unauthorized #: ../src/comments.c:118 msgid "Authorization Error" msgstr "خطأ في الاستيثاق" #: ../src/common.c:66 #, c-format msgid "Cannot create cache directory \"%s\"!" msgstr "تعذّر إنشاء مجلّد المخبئية \"%s\"!" #: ../src/conf.c:182 msgid "" "Your version of WebKitGTK+ doesn't support changing the proxy settings from " "Liferea. The system's default proxy settings will be used." msgstr "" #. translation hint: date format for today, reorder format codes as necessary #: ../src/date.c:133 msgid "Today %l:%M %p" msgstr "اليوم %l:%M %p" #. translation hint: date format for yesterday, reorder format codes as necessary #: ../src/date.c:142 msgid "Yesterday %l:%M %p" msgstr "بالأمس %l:%M %p" #. translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary #: ../src/date.c:154 msgid "%a %l:%M %p" msgstr "%a %l:%M %p" #. translation hint: date format for dates older than a week but from this year, reorder format codes as necessary #: ../src/date.c:162 msgid "%b %d %l:%M %p" msgstr "%d %b %l:%M %p" #. translation hint: date format for dates from the last years, reorder format codes as necessary #: ../src/date.c:165 msgid "%b %d %Y" msgstr "%d %b %Y" #: ../src/enclosure.c:201 #, c-format msgid "\"%s\" is not a valid enclosure type config file!" msgstr "\"%s\" ليس ملف إعداد أنواع مغلّفات سليم!" #: ../src/enclosure.c:292 #, fuzzy msgid "" "You have not configured a download tool yet! Please do so in the " "'Enclosures' tab in Tools/Preferences." msgstr "" "لم تضبط أداة للتنزيلات، يمكنك ضبطها من لسان 'التنزيل' في أدوات ← التفضيلات." #: ../src/enclosure.c:311 #, fuzzy, c-format msgid "" "Command failed: \n" "\n" "%s\n" "\n" " Please check whether the configured download tool is installed and working " "correctly! You can change it in the 'Download' tab in Tools/Preferences." msgstr "" "لم تضبط أداة للتنزيلات، يمكنك ضبطها من لسان 'التنزيل' في أدوات ← التفضيلات." #: ../src/export.c:187 #, fuzzy, c-format msgid "Error renaming %s to %s: %s\n" msgstr "خطأ أثناء إعادة تسمية %s إلى %s\n" #: ../src/export.c:409 ../src/export.c:411 #, c-format msgid "XML error while reading OPML file! Could not import \"%s\"!" msgstr "خطأ XML أثناء قراءة ملف OPML! تعذّر استيراد \"%s\"!" #: ../src/export.c:417 ../src/export.c:419 #, c-format msgid "" "Empty document! OPML document \"%s\" should not be empty when importing." msgstr "مستند فارغ! يجب ألا يكون مستند OPML \"%s\" فارغا أثناء التصدير." #: ../src/export.c:440 ../src/export.c:442 #, c-format msgid "\"%s\" is not a valid OPML document! Liferea cannot import this file!" msgstr "‏\"%s\" ليس مستند OPML صالحاً! لا يستطيع لايفريا استيراد هذا الملف!" #: ../src/export.c:461 msgid "Imported feed list" msgstr "قائمة التلقيمات المستوردة" #: ../src/export.c:473 msgid "Import Feed List" msgstr "استورد قائمة التلقيمات" #: ../src/export.c:473 msgid "Import" msgstr "استورد" #: ../src/export.c:473 ../src/export.c:490 ../src/fl_sources/opml_source.c:379 msgid "OPML Files" msgstr "ملفات OPML" #: ../src/export.c:481 msgid "Error while exporting feed list!" msgstr "خطأ أثناء تصدير قائمة التلقيمات!" #: ../src/export.c:483 msgid "Feed List exported!" msgstr "صُدّرت قائمة التلقيمات!" #: ../src/export.c:490 msgid "Export Feed List" msgstr "صدّر قائمة التلقيمات" #: ../src/export.c:490 msgid "Export" msgstr "صدّر" #: ../src/feed_parser.c:201 msgid "Empty document!" msgstr "مستند فارغ!" #: ../src/feed_parser.c:210 msgid "Invalid XML!" msgstr "‏XML غير صحيح!" #: ../src/fl_sources/default_source.c:139 ../glade/new_subscription.ui.h:1 #: ../glade/simple_subscription.ui.h:1 msgid "New Subscription" msgstr "اشتراك جديد" #: ../src/fl_sources/google_source.c:111 msgid "Google Reader" msgstr "قارئ جوجل" #: ../src/fl_sources/node_source.c:334 msgid "No feed list source types found!" msgstr "لم يُعثر على أنواع مصادر قائمة التلقيمات!" #: ../src/fl_sources/node_source.c:363 msgid "Source Type" msgstr "نوع المصدر" #: ../src/fl_sources/node_source.c:414 #, c-format msgid "Login for '%s' has not yet completed! Please wait until login is done." msgstr "لم يكتمل الولوج إلى '%s'، من فضلك انتظر اكتماله." #. FIXME: something is not perfect, because if you immediately #. remove the subscription tree afterwards there is a double free #: ../src/fl_sources/node_source.c:580 #, c-format msgid "The '%s' subscription was successfully converted to local feeds!" msgstr "حُوِّلَ اشتراك '%s' بنجاح إلى تلقيمات محلية." #: ../src/fl_sources/opml_source.c:319 msgid "Planet, BlogRoll, OPML" msgstr "Planet, BlogRoll, OPML" #: ../src/fl_sources/opml_source.c:379 msgid "Choose OPML File" msgstr "اختر ملف OPML" #: ../src/fl_sources/opml_source.c:379 ../src/ui/subscription_dialog.c:355 msgid "_Open" msgstr "ا_فتح" #: ../src/fl_sources/opml_source.h:28 msgid "New OPML Subscription" msgstr "اشتراك OPML جديد" #: ../src/fl_sources/reedah_source.c:103 #: ../src/fl_sources/theoldreader_source.c:104 msgid "Login failed!" msgstr "فشل الولوج!" #: ../src/fl_sources/reedah_source.c:313 msgid "Reedah" msgstr "قارئ Reedah" #: ../src/fl_sources/reedah_source_feed.c:154 msgid "Could not parse JSON returned by Reedah API!" msgstr "تعذر تحليل البيانات المجلوبة من واجهة Reedah البرمجية!" #: ../src/fl_sources/theoldreader_source.c:311 msgid "TheOldReader" msgstr "قارئ TheOldReader" #: ../src/fl_sources/ttrss_source.c:227 ../src/fl_sources/ttrss_source.c:293 msgid "TinyTinyRSS HTTP API not reachable!" msgstr "تعذر الوصول إلى واجهة TinyTinyRSS البرمجية عبر HTTP!" #: ../src/fl_sources/ttrss_source.c:234 msgid "" "TinyTinyRSS subscribing to feed failed! Check if you really passed a feed " "URL!" msgstr "" "فشل الاشتراك في تلقيمة TinyTinyRSS! تأكد من أنك أعطيت مسار التلقيمة فعلًا." #: ../src/fl_sources/ttrss_source.c:300 msgid "TinyTinyRSS unsubscribing feed failed!" msgstr "فشل إلغاء الاشتراك في تلقيمة TinyTinyRSS!" #: ../src/fl_sources/ttrss_source.c:318 #, c-format msgid "" "This TinyTinyRSS version does not support removing feeds. Upgrade to version " "%s or later!" msgstr "" "لا تدعم إصدارة TinyTinyRSS هذه إزالة التلقيمات. من فضلك رقِّ إلى الإصدارة %s " "أو أحدث." #: ../src/fl_sources/ttrss_source.c:471 msgid "Tiny Tiny RSS" msgstr "Tiny Tiny RSS" #: ../src/fl_sources/ttrss_source_feed.c:150 msgid "Could not parse JSON returned by TinyTinyRSS API!" msgstr "تعذر تحليل البيانات المجلوبة من واجهة TinyTinyRSS البرمجية!" #. if we don't find a feed with unread items do nothing #: ../src/itemlist.c:395 msgid "There are no unread items" msgstr "لا توجد عناصر غير مقروءة" #: ../src/liferea_application.c:280 msgid "" "Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'" msgstr "" "ابدأ لايفريا والنافذة الرئيسية في الحالة STATE، و التي قد تكون `shown' أو " "`hidden'" #: ../src/liferea_application.c:280 msgid "STATE" msgstr "STATE" #: ../src/liferea_application.c:281 msgid "Show version information and exit" msgstr "" #: ../src/liferea_application.c:282 msgid "Add a new subscription" msgstr "أضف اشتراكا جديدا" #: ../src/liferea_application.c:282 msgid "uri" msgstr "" #: ../src/liferea_application.c:283 msgid "Start with all plugins disabled" msgstr "" #: ../src/liferea_application.c:288 msgid "Print debugging messages of all types" msgstr "" #: ../src/liferea_application.c:289 msgid "Print debugging messages for the cache handling" msgstr "" #: ../src/liferea_application.c:290 msgid "Print debugging messages for the configuration handling" msgstr "" #: ../src/liferea_application.c:291 msgid "Print debugging messages of the database handling" msgstr "" #: ../src/liferea_application.c:292 msgid "Print debugging messages of all GUI functions" msgstr "" #: ../src/liferea_application.c:293 msgid "" "Enables HTML rendering debugging. Each time Liferea renders HTML output it " "will also dump the generated HTML into ~/.cache/liferea/output.html" msgstr "" #: ../src/liferea_application.c:294 msgid "Print debugging messages of all network activity" msgstr "" #: ../src/liferea_application.c:295 msgid "Print debugging messages of all parsing functions" msgstr "" #: ../src/liferea_application.c:296 msgid "Print debugging messages when a function takes too long to process" msgstr "" #: ../src/liferea_application.c:297 msgid "Print debugging messages when entering/leaving functions" msgstr "" #: ../src/liferea_application.c:298 msgid "Print debugging messages of the feed update processing" msgstr "" #: ../src/liferea_application.c:299 msgid "Print debugging messages of the search folder matching" msgstr "" #: ../src/liferea_application.c:300 msgid "Print verbose debugging messages" msgstr "" #: ../src/liferea_application.c:305 ../src/liferea_application.c:306 msgid "Print debugging messages for the given topic" msgstr "" #. Some libsoup transport errors #: ../src/net.c:437 msgid "The update request was cancelled" msgstr "أُلغي طلب التحديث" #: ../src/net.c:438 msgid "Unable to resolve destination host name" msgstr "تعذّر الوصول إلى المستضيف المقصود" #: ../src/net.c:439 msgid "Unable to resolve proxy host name" msgstr "تعذّر الوصول إلى الوسيط" #: ../src/net.c:440 msgid "Unable to connect to remote host" msgstr "تعذّر الاتصال بالمستضيف البعيد" #: ../src/net.c:441 msgid "Unable to connect to proxy" msgstr "تعذّر الاتصال بالوسيط" #: ../src/net.c:442 msgid "" "SSL/TLS negotiation failed. Possible outdated or unsupported encryption " "algorithm. Check your operating system settings." msgstr "" #. http 3xx redirection #: ../src/net.c:445 msgid "The resource moved permanently to a new location" msgstr "انتقل المورد إلى مكان جديد بشكل دائم" #. http 4xx client error #: ../src/net.c:448 msgid "" "You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box" msgstr "" "غير مرخّص لك بتنزيل هذه التلقيمة. رجاء حدّث اسم مستخدمك وكلمة سرّك في صندوق " "حوار خصائص التلقيمة" #: ../src/net.c:450 msgid "Payment required" msgstr "الدّفع مطلوب" #: ../src/net.c:451 msgid "You're not allowed to access this resource" msgstr "غير مسموح لك بالوصول إلى هذا المورد" #: ../src/net.c:452 msgid "Resource Not Found" msgstr "المورِد غير موجود" #: ../src/net.c:453 msgid "Method Not Allowed" msgstr "الطريقة غير مسموحة" #: ../src/net.c:454 msgid "Not Acceptable" msgstr "غير مقبول" #: ../src/net.c:455 msgid "Proxy authentication required" msgstr "استيثاق الوسيط مطلوب" #: ../src/net.c:456 msgid "Request timed out" msgstr "انتهت مهلة الطلب" #: ../src/net.c:457 #, fuzzy msgid "" "The webserver indicates this feed is discontinued. It's no longer available. " "Liferea won't update it anymore but you can still access the cached " "headlines." msgstr "" "توقّفت هذه التلقيمة. لم تعد متوفّرة. لن يستطيع لايفريا التحديث بعد الآن لكن " "يظل بإمكانك الوصول إلى العناوين الرئيسية المخبّأة." #: ../src/net.c:462 msgid "There was an internal error in the update process" msgstr "حدث خطأ داخلي أثناء عملية التحديث" #: ../src/net.c:464 msgid "Feed not available: Server requested unsupported redirection!" msgstr "التلقيم غير متوفّر: طلب الخادم إعادة توجيه غير مدعوم!" #: ../src/net.c:466 msgid "Client Error" msgstr "خطأ عميل" #: ../src/net.c:468 msgid "Server Error" msgstr "خطأ خادوم" #: ../src/net.c:470 msgid "An unknown networking error happened!" msgstr "حدث خطأ شبكي مجهول." #: ../src/parsers/atom10.c:239 msgid "Website" msgstr "الموقع" #: ../src/parsers/ns_ag.c:70 msgid "%b %d %H:%M" msgstr "%b %d %H:%M" #. in-memory check function feedlist.opml rule id rule menu label positive menu option negative menu option has param #. ======================================================================================================================================================================================== #: ../src/rule.c:229 msgid "Item" msgstr "عنصر" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does contain" msgstr "يحتوي" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does not contain" msgstr "لا يحتوي" #: ../src/rule.c:230 msgid "Item title" msgstr "عنوان العنصر" #: ../src/rule.c:231 msgid "Item body" msgstr "متن العنصر" #: ../src/rule.c:232 msgid "Read status" msgstr "حالة القراءة" #: ../src/rule.c:232 msgid "is unread" msgstr "غير مقروء" #: ../src/rule.c:232 msgid "is read" msgstr "مقروء" #: ../src/rule.c:233 msgid "Flag status" msgstr "حالة الشارة" #: ../src/rule.c:233 msgid "is flagged" msgstr "مؤشّر" #: ../src/rule.c:233 msgid "is unflagged" msgstr "غير مؤشّر" #: ../src/rule.c:234 msgid "Podcast" msgstr "تدوين صوتي" #: ../src/rule.c:234 msgid "included" msgstr "مُدْرج" #: ../src/rule.c:234 msgid "not included" msgstr "غير مُدْرج" #: ../src/rule.c:235 msgid "Category" msgstr "الفئة" #: ../src/rule.c:235 msgid "is set" msgstr "ضُبِطَ" #: ../src/rule.c:235 msgid "is not set" msgstr "لم يضبط" #: ../src/rule.c:236 msgid "Feed title" msgstr "عنوان التلقيمة" #: ../src/rule.c:237 #, fuzzy msgid "Feed source" msgstr "مصدر التلقيمة" #: ../src/rule.c:238 msgid "Parent folder title" msgstr "" #: ../src/subscription.c:108 #, c-format msgid "Subscription \"%s\" is already being updated!" msgstr "يجري تحديث الاشتراك \"%s\" بالفعل!" #: ../src/subscription.c:113 #, c-format msgid "" "The subscription \"%s\" was discontinued. Liferea won't update it anymore!" msgstr "أُوقِف الاشتراك \"%s\". لن يقوم لايفريا بالتحديث بعد الآن!" #: ../src/subscription.c:188 #, c-format msgid "The URL of \"%s\" has changed permanently and was updated" msgstr "تغير عنوان \"%s\" بشكل دائم وتم تحديثه" #: ../src/subscription.c:204 #, c-format msgid "\"%s\" is discontinued. Liferea won't updated it anymore!" msgstr "توقّف \"%s\". لن يحدّثه لايفريا بعد الآن!" #: ../src/subscription.c:208 #, c-format msgid "\"%s\" has not changed since last update" msgstr "لم يتغيّر \"%s\" منذ آخر تحديث" #: ../src/subscription.c:221 ../src/subscription.c:296 #, fuzzy, c-format msgid "Updating (%d / %d) ..." msgstr "يجري تحديث..." #: ../src/subscription.c:298 #, fuzzy, c-format msgid "Updating '%s'..." msgstr "يجري تحديث..." #: ../src/ui/auth_dialog.c:114 #, c-format msgid "Enter the username and password for \"%s\" (%s):" msgstr "أدخل اسم المستخدم وكلمة السر لـ \"%s\" (%s):" #: ../src/ui/auth_dialog.c:116 msgid "Unknown source" msgstr "مصدر مجهول" #: ../src/ui/browser_tabs.c:262 msgid "Untitled" msgstr "بدون عنوان" #: ../src/ui/enclosure_list_view.c:168 msgid "Attachments" msgstr "مرفقات" #. The following literals are the enclosure list size units #: ../src/ui/enclosure_list_view.c:260 msgid " Bytes" msgstr " بايت" #: ../src/ui/enclosure_list_view.c:263 msgid "kB" msgstr "ك.بايت" #: ../src/ui/enclosure_list_view.c:267 msgid "MB" msgstr "م.ب" #: ../src/ui/enclosure_list_view.c:271 msgid "GB" msgstr "ج.بايت" #: ../src/ui/enclosure_list_view.c:275 #, c-format msgid "%d%s" msgstr "%d%s" #. update list title #: ../src/ui/enclosure_list_view.c:313 #, c-format msgid "%d attachment" msgid_plural "%d attachments" msgstr[0] "لا مرفقات" msgstr[1] "مُرفق واحد" msgstr[2] "مُرفقين" msgstr[3] "%d مُرفقات" msgstr[4] "%d مُرفقا" msgstr[5] "%d مُرفق" #: ../src/ui/enclosure_list_view.c:402 ../src/ui/subscription_dialog.c:355 msgid "Choose File" msgstr "اختر ملفا" #: ../src/ui/enclosure_list_view.c:463 #, c-format msgid "File Extension .%s" msgstr "امتداد الملف .%s" #: ../src/ui/feed_list_view.c:432 msgid "Liferea is in offline mode. No update possible." msgstr "لايفريا في وضع دون اتصال. لا يمكن التحديث." #: ../src/ui/feed_list_view.c:478 #, fuzzy msgid "all feeds" msgstr "ابحث في كل التلقيمات" #: ../src/ui/feed_list_view.c:479 #, fuzzy, c-format msgid "Mark %s as read ?" msgstr "علّم الكل مقروء" #: ../src/ui/feed_list_view.c:483 #, fuzzy, c-format msgid "Are you sure you want to mark all items in %s as read ?" msgstr "أواثق من رغبتك في حذف \"%s\"؟" #: ../src/ui/feed_list_view.c:621 msgid "(Empty)" msgstr "(فارغ)" #: ../src/ui/feed_list_view.c:832 #, c-format msgid "" "%s\n" "Rebuilding" msgstr "" "%s\n" "يُعيد البناء" #: ../src/ui/feed_list_view.c:901 msgid "Deleting entry" msgstr "يجريِ حذف الخانة" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\" and its contents?" msgstr "أواثق من رغبتك في حذف \"%s\" ومحتوياته؟" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\"?" msgstr "أواثق من رغبتك في حذف \"%s\"؟" #: ../src/ui/feed_list_view.c:911 ../src/ui/feed_list_view.c:965 msgid "_Cancel" msgstr "أ_لغِ" #: ../src/ui/feed_list_view.c:912 ../src/ui/popup_menu.c:335 msgid "_Delete" msgstr "ا_حذف" #: ../src/ui/feed_list_view.c:914 msgid "Deletion Confirmation" msgstr "تأكيد الحذف" #: ../src/ui/feed_list_view.c:953 #, c-format msgid "" "Are you sure that you want to add a new subscription with URL \"%s\"? " "Another subscription with the same URL already exists (\"%s\")." msgstr "" #: ../src/ui/feed_list_view.c:966 msgid "_Add" msgstr "" #: ../src/ui/feed_list_view.c:968 #, fuzzy msgid "Adding Duplicate Subscription Confirmation" msgstr "تأكيد الحذف" #: ../src/ui/icons.c:54 #, c-format msgid "Couldn't find pixmap file: %s" msgstr "تعذّر العثور على ملف الصورة: %s" #: ../src/ui/item_list_view.c:115 msgid "This item has no link specified!" msgstr "لا وصلة محددة لهذا العنصر!" #: ../src/ui/item_list_view.c:482 msgid "*** No title ***" msgstr "*** بلا عنوان ***" #: ../src/ui/item_list_view.c:486 msgid " important " msgstr "" #: ../src/ui/item_list_view.c:849 msgid "Headline" msgstr "عنوان رئيسي" #: ../src/ui/item_list_view.c:871 msgid "Date" msgstr "التاريخ" #: ../src/ui/item_list_view.c:1038 msgid "You must select a feed to delete its items!" msgstr "يجب أن تختار تلقيمة لحذف عناصرها!" #: ../src/ui/item_list_view.c:1054 ../src/ui/item_list_view.c:1132 #: ../src/ui/item_list_view.c:1147 msgid "No item has been selected" msgstr "لم تختر أية عناصر" #: ../src/ui/liferea_shell.c:408 #, c-format msgid " (%d new)" msgid_plural " (%d new)" msgstr[0] " (لا جديد)" msgstr[1] " (واحد جديد)" msgstr[2] " (اثنان جديدان)" msgstr[3] " (%d جديدة)" msgstr[4] " (%d جديدا)" msgstr[5] " (%d جديد)" #: ../src/ui/liferea_shell.c:413 #, c-format msgid "%d unread%s" msgid_plural "%d unread%s" msgstr[0] "%.0sلا غير مقروء%s" msgstr[1] "%.0sواحد غير مقروء%s" msgstr[2] "%.0sاثنان غير مقروءان%s" msgstr[3] "%d غير مقروءة%s" msgstr[4] "%d غير مقروء%s" msgstr[5] "%d غير مقروء%s" #: ../src/ui/liferea_shell.c:754 msgid "Help Topics" msgstr "مواضيع المساعدة" #: ../src/ui/liferea_shell.c:760 msgid "Quick Reference" msgstr "مرجع سريع" #: ../src/ui/liferea_shell.c:766 msgid "FAQ" msgstr "أسئلة شائعة" #: ../src/ui/liferea_shell.c:1044 #, fuzzy, c-format msgid "Email command failed: %s" msgstr "فشل أمر المتصفح: %s" #: ../src/ui/popup_menu.c:102 ../glade/liferea_menu.ui.h:25 msgid "Open In _Tab" msgstr "افتح _لسان" #: ../src/ui/popup_menu.c:106 ../glade/liferea_menu.ui.h:26 msgid "_Open In Browser" msgstr "ا_فتح في المتصفح" #: ../src/ui/popup_menu.c:110 ../glade/liferea_menu.ui.h:27 msgid "Open In _External Browser" msgstr "افتح في متصفح _خارجي" #: ../src/ui/popup_menu.c:115 msgid "Email The Author" msgstr "" #: ../src/ui/popup_menu.c:140 msgid "Copy to News Bin" msgstr "انسخ لسلّة الأخبار" #: ../src/ui/popup_menu.c:148 #, c-format msgid "_Bookmark at %s" msgstr "_علّم في %s" #: ../src/ui/popup_menu.c:154 msgid "Copy Item _Location" msgstr "انسخ م_كان العنصر" #: ../src/ui/popup_menu.c:163 ../glade/liferea_menu.ui.h:22 msgid "Toggle _Read Status" msgstr "بدّل حالة ال_قراءة" #: ../src/ui/popup_menu.c:167 ../glade/liferea_menu.ui.h:23 msgid "Toggle Item _Flag" msgstr "بدّل _شارة العنصر" #: ../src/ui/popup_menu.c:171 msgid "R_emove Item" msgstr "أ_زل العنصر" #: ../src/ui/popup_menu.c:200 msgid "Open Enclosure..." msgstr "افتح مغلّفا..." #: ../src/ui/popup_menu.c:201 msgid "Save As..." msgstr "احفظ ك‍..." #: ../src/ui/popup_menu.c:202 msgid "Copy Link Location" msgstr "انسخ موقع الوصلة" #: ../src/ui/popup_menu.c:280 ../glade/liferea_menu.ui.h:13 msgid "_Update" msgstr "_حدّث" #: ../src/ui/popup_menu.c:282 msgid "_Update Folder" msgstr "_حدّث المجلّد" #: ../src/ui/popup_menu.c:292 msgid "New _Subscription..." msgstr "اشتراك _جديد..." #: ../src/ui/popup_menu.c:295 ../glade/liferea_menu.ui.h:5 msgid "New _Folder..." msgstr "_مجلّد جديد..." #: ../src/ui/popup_menu.c:298 ../glade/liferea_menu.ui.h:6 msgid "New S_earch Folder..." msgstr "مجلّد _بحث جديد..." #: ../src/ui/popup_menu.c:299 msgid "New S_ource..." msgstr "م_صدر جديد..." #: ../src/ui/popup_menu.c:300 ../glade/liferea_menu.ui.h:8 msgid "New _News Bin..." msgstr "_سلّة أخبار جديدة..." #: ../src/ui/popup_menu.c:303 msgid "_New" msgstr "_جديد" #: ../src/ui/popup_menu.c:312 msgid "Sort Feeds" msgstr "رتّب التلقيمات" #: ../src/ui/popup_menu.c:320 msgid "_Mark All As Read" msgstr "_علّم الكل مقروء" #: ../src/ui/popup_menu.c:327 msgid "_Rebuild" msgstr "أعد الب_ناء" #: ../src/ui/popup_menu.c:336 ../glade/liferea_menu.ui.h:17 msgid "_Properties" msgstr "ال_خصائص" #: ../src/ui/popup_menu.c:343 msgid "Convert To Local Subscriptions..." msgstr "حوّل إلى اشتراكات محلية..." #: ../src/ui/preferences_dialog.c:84 msgid "GNOME default" msgstr "افتراضي جنوم" #: ../src/ui/preferences_dialog.c:85 msgid "Text below icons" msgstr "نص تحت الأيقونات" #: ../src/ui/preferences_dialog.c:86 msgid "Text beside icons" msgstr "نص بجانب الأيقونات" #: ../src/ui/preferences_dialog.c:87 msgid "Icons only" msgstr "أيقونات فقط" #: ../src/ui/preferences_dialog.c:88 msgid "Text only" msgstr "نص فقط" #: ../src/ui/preferences_dialog.c:96 ../src/ui/subscription_dialog.c:43 msgid "minutes" msgstr "دقائق" #: ../src/ui/preferences_dialog.c:97 ../src/ui/subscription_dialog.c:44 msgid "hours" msgstr "ساعات" #: ../src/ui/preferences_dialog.c:98 ../src/ui/subscription_dialog.c:45 msgid "days" msgstr "أيّام" #: ../src/ui/preferences_dialog.c:103 msgid "Space" msgstr "مسافة" #: ../src/ui/preferences_dialog.c:104 msgid " Space" msgstr " Space" #: ../src/ui/preferences_dialog.c:105 msgid " Space" msgstr " Space" #: ../src/ui/preferences_dialog.c:110 msgid "Normal View" msgstr "المنظور العادي" #: ../src/ui/preferences_dialog.c:111 msgid "Wide View" msgstr "المنظور العريض" #: ../src/ui/preferences_dialog.c:478 msgid "Default Browser" msgstr "المتصفح المبدئي" #: ../src/ui/preferences_dialog.c:480 msgid "Manual" msgstr "يدوي" #: ../src/ui/preferences_dialog.c:740 msgid "Type" msgstr "النوع" #: ../src/ui/preferences_dialog.c:743 msgid "Program" msgstr "البرنامج" #: ../src/ui/search_dialog.c:106 #, fuzzy msgid "Saved Search" msgstr "بحث متقدّم" #: ../src/ui/subscription_dialog.c:427 #, c-format msgid "The provider of this feed suggests an update interval of %d minute." msgid_plural "" "The provider of this feed suggests an update interval of %d minutes." msgstr[0] "مزوّد هذه التلقيمة لا يقترح فترة تحديث." msgstr[1] "مزوّد هذه التلقيمة يقترح فترة تحديث دقيقة واحدة." msgstr[2] "مزوّد هذه التلقيمة يقترح فترة تحديث دقيقتين." msgstr[3] "مزوّد هذه التلقيمة يقترح فترة تحديث %d دقائق." msgstr[4] "مزوّد هذه التلقيمة يقترح فترة تحديث %d دقيقة." msgstr[5] "مزوّد هذه التلقيمة يقترح فترة تحديث %d دقيقة." #: ../src/ui/subscription_dialog.c:431 msgid "This feed specifies no default update interval." msgstr "لا تُحدِّد هذه التلقيمة فترة تحديث افتراضيّة." #: ../src/ui/ui_common.c:206 msgid "All Files" msgstr "كل الملفات" #: ../src/update.c:350 #, c-format msgid "Error opening temp file %s to use for filtering!" msgstr "خطأ أثناء فتح الملف المؤقّت %s لاستخدامه في الترشيح!" #: ../src/update.c:372 #, c-format msgid "%s exited with status %d" msgstr "خرج %s بالحالة %d" #: ../src/update.c:378 ../src/update.c:379 ../src/update.c:493 #, c-format msgid "Error: Could not open pipe \"%s\"" msgstr "خطأ: تعذّر فتح الأنبوب \"%s\"" #. FIXME: maybe setting request->returncode would be better #: ../src/update.c:517 #, c-format msgid "Error: Could not open file \"%s\"" msgstr "خطأ: تعذّر فتح الملف \"%s\"" #: ../src/update.c:523 #, c-format msgid "Error: There is no file \"%s\"" msgstr "خطأ: ليس ثمّة ملف \"%s\"" #: ../src/vfolder.c:54 msgid "New Search Folder" msgstr "مجلّد بحث جديد" #: ../src/webkit/liferea_web_view.c:177 msgid "Open Link In _Tab" msgstr "افتح الوصلة في _لسان" #: ../src/webkit/liferea_web_view.c:178 #, fuzzy msgid "Open Link In Browser" msgstr "ا_فتح الوصلة في المتصفّح" #: ../src/webkit/liferea_web_view.c:179 #, fuzzy msgid "Open Link In External Browser" msgstr "ا_فتح الوصلة في المتصفّح الخارجي" #: ../src/webkit/liferea_web_view.c:185 #, c-format msgid "_Bookmark Link at %s" msgstr "_علّم الوصلة في %s" #: ../src/webkit/liferea_web_view.c:192 msgid "_Copy Link Location" msgstr "ا_نسخ مكان الوصلة" #: ../src/webkit/liferea_web_view.c:195 #, fuzzy msgid "_View Image" msgstr "احف_ظ الصورة ك‍" #: ../src/webkit/liferea_web_view.c:196 msgid "_Copy Image Location" msgstr "انسخ مكان ال_صورة" #: ../src/webkit/liferea_web_view.c:199 msgid "S_ave Link As" msgstr "احفظ الو_صلة ك‍" #: ../src/webkit/liferea_web_view.c:202 msgid "S_ave Image As" msgstr "احف_ظ الصورة ك‍" #: ../src/webkit/liferea_web_view.c:209 msgid "_Subscribe..." msgstr "ا_شترك..." #: ../src/webkit/liferea_web_view.c:213 msgid "_Copy" msgstr "ا_نسخ" #: ../src/webkit/liferea_web_view.c:219 msgid "_Increase Text Size" msgstr "زِ_د حجم النص" #: ../src/webkit/liferea_web_view.c:220 msgid "_Decrease Text Size" msgstr "_قلل حجم النص" #: ../src/webkit/liferea_web_view.c:227 msgid "_Reader Mode" msgstr "" #: ../src/xml.c:426 msgid "[There were more errors. Output was truncated!]" msgstr "[كان هناك المزيد من الأخطاء. بُتِر الخرْج]" #: ../src/xml.c:594 msgid "XML Parser: Could not parse document:\n" msgstr "محلّل XML: لا يمكن تحليل المستند:\n" #: ../glade/about.ui.h:1 msgid "About" msgstr "عن" #: ../glade/about.ui.h:2 msgid "Liferea is a news aggregator for GTK+" msgstr "لايفريا هو مُجمِّع أخبار لجتك+" # ARABEYES: keep the html code, this is # intentional #: ../glade/about.ui.h:3 msgid "Liferea Homepage" msgstr "صفحة لايفريا" #: ../glade/auth.ui.h:1 msgid "Authentication" msgstr "الاستيثاق" #: ../glade/auth.ui.h:3 #, fuzzy, no-c-format msgid "Enter the username and password for \"%s\" (%s)" msgstr "أدخل اسم المستخدم وكلمة السر لـ \"%s\" (%s):" #: ../glade/auth.ui.h:4 ../glade/properties.ui.h:31 msgid "User_name:" msgstr "ا_سم المستخدم:" #: ../glade/auth.ui.h:5 ../glade/properties.ui.h:32 msgid "_Password:" msgstr "_كلمة السر:" #: ../glade/enclosure_handler.ui.h:1 msgid "Open Enclosure" msgstr "افتح مغلّفًا" #: ../glade/enclosure_handler.ui.h:2 msgid "Open an enclosure of type:" msgstr "افتح مغلّفًا من نوع:" #: ../glade/enclosure_handler.ui.h:3 #, fuzzy msgid "" "_What should Liferea do with this enclosure? Please enter the command you " "want to be executed below. The enclosures URL will be supplied as an " "argument for this command:" msgstr "" "ماذا يجب على لايفريا عمله مع هذا المغلّف؟ من فضلك أدخل الأمر الذي تريد تنفيذه " "بأسفل. سيكون مسار الملف معطى لهذا الأمر:" #: ../glade/enclosure_handler.ui.h:4 msgid "_Browse" msgstr "_تصفّح" #: ../glade/enclosure_handler.ui.h:5 msgid "_Do this automatically for enclosures like this from now on." msgstr "ا_فعل هذا تلقائيا مع المغلّفات مثل هذا من الآن فصاعداَ." #: ../glade/liferea_menu.ui.h:1 msgid "_Subscriptions" msgstr "الا_شتراكات" #: ../glade/liferea_menu.ui.h:2 ../glade/liferea_toolbar.ui.h:8 msgid "Update _All" msgstr "حدّث ال_كل" #: ../glade/liferea_menu.ui.h:3 msgid "Mark All As _Read" msgstr "علّم الكل _مقروء" #: ../glade/liferea_menu.ui.h:4 ../glade/liferea_toolbar.ui.h:1 msgid "_New Subscription..." msgstr "اشتراك _جديد..." #: ../glade/liferea_menu.ui.h:7 msgid "New _Source..." msgstr "م_صدر جديد..." #: ../glade/liferea_menu.ui.h:9 msgid "_Import Feed List..." msgstr "ا_ستورد قائمة التلقيمات..." #: ../glade/liferea_menu.ui.h:10 msgid "_Export Feed List..." msgstr "_صدّر قائمة التلقيمات..." #: ../glade/liferea_menu.ui.h:11 msgid "_Quit" msgstr "ا_خرج" #: ../glade/liferea_menu.ui.h:12 msgid "_Feed" msgstr "_تلقيم" #: ../glade/liferea_menu.ui.h:15 msgid "Remove _All Items" msgstr "احذف _كل العناصر" #: ../glade/liferea_menu.ui.h:16 msgid "_Remove" msgstr "ا_حذف" #: ../glade/liferea_menu.ui.h:18 msgid "_Item" msgstr "_عنصر" #: ../glade/liferea_menu.ui.h:24 msgid "R_emove" msgstr "أ_زل" #: ../glade/liferea_menu.ui.h:28 msgid "_View" msgstr "_عرض" #: ../glade/liferea_menu.ui.h:29 msgid "_Fullscreen" msgstr "_ملء الشاشة" #: ../glade/liferea_menu.ui.h:30 msgid "Zoom _In" msgstr "" #: ../glade/liferea_menu.ui.h:31 msgid "Zoom _Out" msgstr "" #: ../glade/liferea_menu.ui.h:32 #, fuzzy msgid "_Normal size" msgstr "المنظور ال_عادي" #: ../glade/liferea_menu.ui.h:33 msgid "_Normal View" msgstr "المنظور ال_عادي" #: ../glade/liferea_menu.ui.h:34 msgid "_Wide View" msgstr "منظور _عريض" #: ../glade/liferea_menu.ui.h:35 msgid "_Reduced Feed List" msgstr "_ضُم قائمة التلقيمات" #: ../glade/liferea_menu.ui.h:36 msgid "_Tools" msgstr "أ_دوات" #: ../glade/liferea_menu.ui.h:37 msgid "_Update Monitor" msgstr "_مراقب التحديثات" #: ../glade/liferea_menu.ui.h:38 msgid "_Preferences" msgstr "ال_تفضيلات" #: ../glade/liferea_menu.ui.h:39 msgid "S_earch" msgstr "_بحث" #: ../glade/liferea_menu.ui.h:41 msgid "_Help" msgstr "_مساعدة" #: ../glade/liferea_menu.ui.h:42 msgid "_Contents" msgstr "الم_حتويات" #: ../glade/liferea_menu.ui.h:43 msgid "_Quick Reference" msgstr "مرجع _سريع" #: ../glade/liferea_menu.ui.h:44 msgid "_FAQ" msgstr "أ_سئلة شائعة" #: ../glade/liferea_menu.ui.h:45 msgid "_About" msgstr "_عن" #: ../glade/liferea_toolbar.ui.h:2 msgid "Adds a subscription to the feed list." msgstr "أضِف اشتراك إلى قائمة التلقيمات." #: ../glade/liferea_toolbar.ui.h:4 msgid "" "Marks all items of the selected feed list node / in the item list as read." msgstr "علّم كل عناصر الاشتراك المحدد مقروءة." #: ../glade/liferea_toolbar.ui.h:9 msgid "Updates all subscriptions." msgstr "حدّث كل الاشتراكات." #: ../glade/liferea_toolbar.ui.h:11 msgid "Show the search dialog." msgstr "اعرض حوار البحث." #: ../glade/mainwindow.ui.h:2 msgid "page 1" msgstr "" #: ../glade/mainwindow.ui.h:3 msgid "page 2" msgstr "" #: ../glade/mainwindow.ui.h:4 ../glade/prefs.ui.h:23 msgid "Headlines" msgstr "رؤوس العناوين" #: ../glade/mark_read_dialog.ui.h:1 #, fuzzy msgid "Mark all as read ?" msgstr "علّم الكل مقروء" #: ../glade/mark_read_dialog.ui.h:2 msgid "Mark all as read" msgstr "علّم الكل مقروء" #: ../glade/mark_read_dialog.ui.h:3 msgid "Do not ask again" msgstr "" #: ../glade/new_folder.ui.h:1 msgid "New Folder" msgstr "مجلّد جديد" #: ../glade/new_folder.ui.h:2 msgid "_Folder name:" msgstr "ا_سم المجلّد:" #: ../glade/new_newsbin.ui.h:1 msgid "Create News Bin" msgstr "أنشئ سلّة أخبار" #: ../glade/new_newsbin.ui.h:2 msgid "_News Bin Name:" msgstr "اسم سلّة الأ_خبار:" #: ../glade/new_subscription.ui.h:2 ../glade/properties.ui.h:11 msgid "Feed Source" msgstr "مصدر التلقيمة" #: ../glade/new_subscription.ui.h:3 ../glade/properties.ui.h:12 msgid "Source Type:" msgstr "نوع المصدر:" #: ../glade/new_subscription.ui.h:4 ../glade/properties.ui.h:13 msgid "_URL" msgstr "_مسار" #: ../glade/new_subscription.ui.h:5 ../glade/properties.ui.h:14 msgid "_Command" msgstr "أ_مر" #: ../glade/new_subscription.ui.h:6 ../glade/properties.ui.h:15 msgid "_Local File" msgstr "ملف _محلّي" #: ../glade/new_subscription.ui.h:7 ../glade/properties.ui.h:16 msgid "Select File..." msgstr "اختر ملفا..." #: ../glade/new_subscription.ui.h:8 ../glade/properties.ui.h:17 msgid "_Source:" msgstr "ال_مصدر:" #: ../glade/new_subscription.ui.h:9 msgid "Download / Postprocessing" msgstr "تنزيل / معالجة بعديّة" #: ../glade/new_subscription.ui.h:10 ../glade/properties.ui.h:30 msgid "_Don't use proxy for download" msgstr "_لا تستخدم وسيط للتنزيل" #: ../glade/new_subscription.ui.h:11 ../glade/properties.ui.h:18 msgid "Use conversion _filter" msgstr "استخدم _مرشّح تحويل" #: ../glade/new_subscription.ui.h:12 msgid "" "Liferea can use external filter plugins in order to access feeds and " "directories in non-supported formats. See the documentation for more " "information." msgstr "" "يستطيع لايفريا استخدام ملحق مرشِّح خارجي للنفاذ إلى أدلّة التلقيمات ذات " "التنسيقات غير المدعومة. راجع الوثائق لمعلومات أكثر." #: ../glade/new_subscription.ui.h:13 ../glade/properties.ui.h:20 msgid "Convert _using:" msgstr "حوّل با_ستخدام:" #: ../glade/node_source.ui.h:1 msgid "Source Selection" msgstr "اختيار المصدر" #: ../glade/node_source.ui.h:2 #, fuzzy msgid "_Select the source type you want to add..." msgstr "اختر نوع المصدر الذي ترغب بإضافته..." #: ../glade/opml_source.ui.h:1 msgid "Add OPML/Planet" msgstr "أضِف OPML/Planet" #: ../glade/opml_source.ui.h:2 msgid "" "Please specify a local file or an URL pointing to a valid OPML feed list." msgstr "من فضلك حدّد ملفا محلّيا أو مسارا لقائمة تلقيمات OPML سليمة." #: ../glade/opml_source.ui.h:3 msgid "_Location" msgstr "ال_مكان" #: ../glade/opml_source.ui.h:4 msgid "_Select File" msgstr "ا_ختر ملفا" #: ../glade/prefs.ui.h:1 msgid "Liferea Preferences" msgstr "تفضيلات لايفريا" #: ../glade/prefs.ui.h:2 msgid "Feed Cache Handling" msgstr "التعامل مع خبيئة التلقيمات" #: ../glade/prefs.ui.h:3 msgid "Default _number of items per feed to save:" msgstr "_عدد العناصر الافتراضي لحفظة في كل تلقيمة:" #: ../glade/prefs.ui.h:4 ../glade/properties.ui.h:27 msgid "0" msgstr "" #: ../glade/prefs.ui.h:5 msgid "Feed Update Settings" msgstr "إعدادات تحديث التلقيمات" #. Feed update interval hint in preference dialog. #: ../glade/prefs.ui.h:7 msgid "" "Note: Please remember to set a reasonable refresh time. Usually it is a " "waste of bandwidth to poll feeds more often than each hour." msgstr "" "ملحوظة: من فضلك تذكّر أن تضبط فترة تحديث معقولة. سحب التحديثات أكثر من مرّة " "كل ساعة مضيعة للتحميل." #: ../glade/prefs.ui.h:8 msgid "_Update all subscriptions at startup." msgstr "ح_دّث كل الاشتراكات عند بدء التشغيل." #: ../glade/prefs.ui.h:9 msgid "Default Feed Refresh _Interval:" msgstr "_فترة تحديث التلقيمات الافتراضيّة:" #: ../glade/prefs.ui.h:10 ../glade/properties.ui.h:6 msgid "1" msgstr "" #: ../glade/prefs.ui.h:11 msgid "Feeds" msgstr "التلقيمات" #: ../glade/prefs.ui.h:12 msgid "Folder Display Settings" msgstr "إعدادات عرض المجلّد" #: ../glade/prefs.ui.h:13 msgid "_Show the items of all child feeds when a folder is selected." msgstr "أ_ظهر العناصر لكل التلقيمات الإبنة عند اختيار مجلّد." #: ../glade/prefs.ui.h:14 msgid "_Hide read items." msgstr "أ_خفِ العناصر المقروءة." #: ../glade/prefs.ui.h:15 msgid "Feed Icons (Favicons)" msgstr "أيقونات التلقيمات (Favicons)" #: ../glade/prefs.ui.h:16 msgid "_Update all favicons now" msgstr "_حدّث كل الأيقونات الآن" #: ../glade/prefs.ui.h:17 msgid "Folders" msgstr "المجلّدات" #: ../glade/prefs.ui.h:18 msgid "Reading Headlines" msgstr "قراءة رؤوس العناوين" #: ../glade/prefs.ui.h:19 msgid "_Skim through articles with:" msgstr "_تصفّع عبر المقالات باستخدام:" #: ../glade/prefs.ui.h:20 msgid "_Default View Mode:" msgstr "و_ضع المنظور الافتراضي:" #: ../glade/prefs.ui.h:21 msgid "Web Integration" msgstr "التكامل مع الوب" #: ../glade/prefs.ui.h:22 msgid "_Post Bookmarks to" msgstr "ا_نشر العلامات في" #: ../glade/prefs.ui.h:24 msgid "Internal Browser Settings" msgstr "إعدادات المتصفّح الداخلي" #: ../glade/prefs.ui.h:25 msgid "Open links in Liferea's _window." msgstr "افتح الوصلات في _نافذة لايفريا." #: ../glade/prefs.ui.h:26 msgid "_Never run external Javascript." msgstr "" #: ../glade/prefs.ui.h:27 msgid "_Enable browser plugins." msgstr "_فعّل ملحقات المتصفح." #: ../glade/prefs.ui.h:28 msgid "External Browser Settings" msgstr "إعدادات المتصفّح الخارجي" #: ../glade/prefs.ui.h:29 msgid "_Browser:" msgstr "ال_متصفح:" #: ../glade/prefs.ui.h:30 msgid "_Manual:" msgstr "ي_دويا:" #: ../glade/prefs.ui.h:32 #, no-c-format msgid "(%s for URL)" msgstr "(%s للمسار)" #: ../glade/prefs.ui.h:33 msgid "Browser" msgstr "المتصفح" #: ../glade/prefs.ui.h:34 msgid "Toolbar Settings" msgstr "إعدادات شريط الأدوات" #: ../glade/prefs.ui.h:35 msgid "_Hide toolbar." msgstr "أخفِ _شريط الأدوات." #: ../glade/prefs.ui.h:36 msgid "Toolbar _button labels:" msgstr "عناوين أ_زرار شريط الأدوات:" #: ../glade/prefs.ui.h:37 msgid "Other" msgstr "" #: ../glade/prefs.ui.h:38 msgid "Ask for confirmation when marking all items as read" msgstr "" #: ../glade/prefs.ui.h:39 msgid "Desktop" msgstr "" #: ../glade/prefs.ui.h:40 msgid "HTTP Proxy Server" msgstr "خادم وكيل HTTP" #: ../glade/prefs.ui.h:41 msgid "_Auto Detect (GNOME or environment)" msgstr "_تعرف آلي (جنوم أو البيئة)" #: ../glade/prefs.ui.h:42 msgid "_No Proxy" msgstr "_لا وسيط" #: ../glade/prefs.ui.h:43 msgid "_Manual Setting:" msgstr "إعدادات _يدويّة:" #: ../glade/prefs.ui.h:44 msgid "Proxy _Host:" msgstr "_مضيف الوسيط:" #: ../glade/prefs.ui.h:45 msgid "Proxy _Port:" msgstr "م_نفذ الوسيط:" #: ../glade/prefs.ui.h:46 msgid "Use Proxy Au_thentication" msgstr "استخدم استيثاق ال_وسيط" #: ../glade/prefs.ui.h:47 msgid "Proxy _Username:" msgstr "اسم م_ستخدم الوسيط:" #: ../glade/prefs.ui.h:48 msgid "Proxy Pass_word:" msgstr "_كلمة سرّ الوسيط:" #: ../glade/prefs.ui.h:49 msgid "" "Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per " "application proxy settings. The system's default proxy settings will be used." msgstr "" #: ../glade/prefs.ui.h:50 msgid "Proxy" msgstr "الوسيط" #: ../glade/prefs.ui.h:51 #, fuzzy msgid "Privacy Settings" msgstr "إعدادات عرض المجلّد" #: ../glade/prefs.ui.h:52 msgid "Tell sites that I do _not want to be tracked." msgstr "" #: ../glade/prefs.ui.h:53 msgid "_Intelligent Tracking Prevention. " msgstr "" #: ../glade/prefs.ui.h:54 msgid "" "This enables the WebKit feature described here." msgstr "" #: ../glade/prefs.ui.h:55 msgid "" "Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or " "higher." msgstr "" #: ../glade/prefs.ui.h:56 msgid "Use _Reader mode." msgstr "" #: ../glade/prefs.ui.h:57 msgid "" "This enables stripping all non-content elements (like scripts, fonts, tracking)" msgstr "" #: ../glade/prefs.ui.h:58 msgid "Privacy" msgstr "" #: ../glade/prefs.ui.h:59 msgid "Downloading Enclosures" msgstr "تنزيل المغلّفات" #: ../glade/prefs.ui.h:60 msgid "_Download using" msgstr "_نزّل باستخدام" #: ../glade/prefs.ui.h:62 #, no-c-format msgid "custom-command %s" msgstr "" #: ../glade/prefs.ui.h:63 msgid "Opening Enclosures" msgstr "فتح المغلّفات" #: ../glade/prefs.ui.h:64 msgid "Enclosures" msgstr "المغلّفات" #: ../glade/properties.ui.h:1 msgid "Subscription Properties" msgstr "خصائص الاشتراك" #: ../glade/properties.ui.h:2 #, fuzzy msgid "Feed _Name" msgstr "ا_سم التلقيمة:" #: ../glade/properties.ui.h:3 #, fuzzy msgid "Update _Interval" msgstr "فترة التحديث" #: ../glade/properties.ui.h:4 msgid "_Use global default update interval." msgstr "ا_ستخدم فترة التحديث الإفتراضية العموميّة." #: ../glade/properties.ui.h:5 msgid "_Feed specific update interval of" msgstr "فترة التحديث الخاصة بال_تلقيمة من" #: ../glade/properties.ui.h:7 msgid "_Don't update this feed automatically." msgstr "_لا تحدّث هذه التلقيمة آلياً." #: ../glade/properties.ui.h:9 #, no-c-format msgid "This feed provider suggests an update interval of %d minutes." msgstr "يقترح مُزوّد التلقيمة فترة تحديث %d من الدقائق." #: ../glade/properties.ui.h:10 msgid "General" msgstr "عام" #: ../glade/properties.ui.h:19 msgid "" "Liferea can use external filter scripts in order to access feeds and " "directories in non-supported formats." msgstr "" "يستطيع لايفريا استخدام ملحق مرشِّح خارجي للنفاذ إلى أدلّة التلقيمات ذات " "التنسيقات غير المدعومة." #: ../glade/properties.ui.h:21 ../xslt/item.xml.in.h:1 msgid "Source" msgstr "المصدر" #: ../glade/properties.ui.h:22 msgid "" "The cache setting controls if the contents of feeds are saved when Liferea " "exits. Marked items are always saved to the cache." msgstr "" "تتحكم إعدادات الذاكرة المخبّأة في ما إذا كانت ستُحفظ محتويات التلقيمات عند " "خروج لايفريا أم لا. ستحفظ العناصر المختارة دائما في الذاكرة المخبّأة." #: ../glade/properties.ui.h:23 msgid "_Default cache settings" msgstr "إعدادات الذاكرة المخبأة الا_فتراضيّة" #: ../glade/properties.ui.h:24 msgid "Di_sable cache" msgstr "_عطّل الذاكرة المخبّأة" #: ../glade/properties.ui.h:25 msgid "_Unlimited cache" msgstr "ذاكرة مخبّأة _غير محدودة" #: ../glade/properties.ui.h:26 msgid "_Number of items to save:" msgstr "_عدد العناصر التي ستُحفظ:" #: ../glade/properties.ui.h:28 msgid "Archive" msgstr "أرشيف" #: ../glade/properties.ui.h:29 msgid "Use HTTP _authentication" msgstr "استخدم ا_ستيثاق HTTP" #: ../glade/properties.ui.h:33 msgid "Download" msgstr "تنزيل" #: ../glade/properties.ui.h:34 msgid "_Automatically download all enclosures of this feed." msgstr "نزّل كل مغلّفات هذه التلقيمة آ_ليّاً." #: ../glade/properties.ui.h:35 msgid "Auto-_load item link in configured browser when selecting articles." msgstr "_حمّل عنصر الوصلة آليا في المتصفّح المحدد عند انتقاء المقالات." #: ../glade/properties.ui.h:36 msgid "Ignore _comment feeds for this subscription." msgstr "تجاهل تلقيمات تعليقات _هذا الاشتراك." #: ../glade/properties.ui.h:37 msgid "_Mark downloaded items as read." msgstr "_علّم العناصر المنزّلة مقروءة." #: ../glade/properties.ui.h:38 msgid "Extract full content from HTML5 and Google AMP" msgstr "" #: ../glade/reedah_source.ui.h:1 msgid "Add Reedah Account" msgstr "أضِف حساب قارئ Reedah" #: ../glade/reedah_source.ui.h:2 msgid "Please enter your Reedah account settings." msgstr "من فضلِك أدخل إعدادات حسابك في Reedah." #: ../glade/reedah_source.ui.h:3 ../glade/theoldreader_source.ui.h:3 #: ../glade/ttrss_source.ui.h:4 msgid "_Password" msgstr "_كلمة السر" #: ../glade/reedah_source.ui.h:4 ../glade/theoldreader_source.ui.h:4 msgid "_Username (Email)" msgstr "ا_سم المستخدم (البريد)" #: ../glade/rename_node.ui.h:1 msgid "Rename" msgstr "أعِد التسمية" #: ../glade/rename_node.ui.h:2 msgid "_New Name:" msgstr "اسم _جديد:" #: ../glade/search_folder.ui.h:1 msgid "Search Folder Properties" msgstr "خصائص مجلّد البحث" #: ../glade/search_folder.ui.h:2 msgid "Search _Name:" msgstr "ا_سم البحث:" #: ../glade/search_folder.ui.h:3 #, fuzzy msgid "Search Rules" msgstr "لا نتائج للبحث" #: ../glade/search_folder.ui.h:4 msgid "Rules" msgstr "" #: ../glade/search_folder.ui.h:5 msgid "All rules for this search folder" msgstr "" #: ../glade/search_folder.ui.h:6 #, fuzzy msgid "Rule Matching" msgstr "أي _قاعدة تطابق" #: ../glade/search_folder.ui.h:7 ../glade/search.ui.h:4 msgid "A_ny Rule Matches" msgstr "أي _قاعدة تطابق" #: ../glade/search_folder.ui.h:8 ../glade/search.ui.h:5 msgid "_All Rules Must Match" msgstr "_كل القواعد يجب أن تطابق" #: ../glade/search_folder.ui.h:9 #, fuzzy msgid "Hide read items" msgstr "أ_خفِ العناصر المقروءة." #: ../glade/search.ui.h:1 msgid "Advanced Search" msgstr "بحث متقدّم" #: ../glade/search.ui.h:2 msgid "_Search Folder..." msgstr "ا_بحث في المجلّد..." #: ../glade/search.ui.h:3 msgid "Find Items that meet the following criteria" msgstr "ابحث عن العناصر التي تُطابق هذه المعايير" #: ../glade/simple_search.ui.h:1 msgid "Search All Feeds" msgstr "ابحث في كل التلقيمات" #: ../glade/simple_search.ui.h:2 msgid "_Advanced..." msgstr "_متقدّم..." #: ../glade/simple_search.ui.h:3 msgid "" "Starts searching for the specified text in all feeds. The search result will " "appear in the item list." msgstr "" "يبدأ البحث عن النصّ المحدد في كل التلقيمات. ستظهر نتيجة البحث في قائمة " "العناصر." #: ../glade/simple_search.ui.h:4 msgid "_Search for:" msgstr "ا_بحث عن:" #: ../glade/simple_search.ui.h:5 msgid "" "Enter a search string Liferea should find either in a items title or in its " "content." msgstr "أدخل نص بحث ليبحث عنه ليعثر عليه لايفريا في عنصر أو مُحتواه." #: ../glade/simple_subscription.ui.h:2 msgid "Advanced..." msgstr "متقدّم..." #: ../glade/simple_subscription.ui.h:3 #, fuzzy msgid "Feed _Source" msgstr "مصدر التلقيمة" #: ../glade/simple_subscription.ui.h:4 msgid "" "Enter a website location to use feed autodiscovery or in case you know it " "the exact feed location." msgstr "" "أدخِل موقع وب لتستخدم الاكتشاف التلقائي أو -في حال إذا كنت تعرفه- موقع " "التلقيمة بالتحديد." #: ../glade/theoldreader_source.ui.h:1 msgid "Add TheOldReader Account" msgstr "أضِف حساب قارئ TheOldReader" #: ../glade/theoldreader_source.ui.h:2 msgid "Please enter your TheOldReader account settings." msgstr "من فضلِك أدخل إعدادات حسابك في قارئ TheOldReader." #: ../glade/ttrss_source.ui.h:1 msgid "Add Tiny Tiny RSS Account" msgstr "أضِف حساب Tiny Tiny RSS" #: ../glade/ttrss_source.ui.h:2 msgid "Please enter your TinyTinyRSS account settings." msgstr "من فضلِك أدخل إعدادات حسابك في InoReader." #: ../glade/ttrss_source.ui.h:3 msgid "_Server URL" msgstr "عنوان ال_خادوم" #: ../glade/ttrss_source.ui.h:5 msgid "_Username" msgstr "ا_سم المستخدم" #: ../glade/update_monitor.ui.h:1 msgid "Update Monitor" msgstr "مراقب التحديثات" #: ../glade/update_monitor.ui.h:2 msgid "Stop All" msgstr "" #: ../glade/update_monitor.ui.h:3 #, fuzzy msgid "_Pending Requests" msgstr "الطلبات المنتظرة" #: ../glade/update_monitor.ui.h:4 #, fuzzy msgid "_Downloading Now" msgstr "يُنزّل الآن" #: ../xslt/feed.xml.in.h:1 msgid "Feed:" msgstr "تلقيمة:" #: ../xslt/feed.xml.in.h:2 ../xslt/source.xml.in.h:1 msgid "Source:" msgstr "المصدر:" #: ../xslt/feed.xml.in.h:3 msgid "Publisher" msgstr "الناشر" #: ../xslt/feed.xml.in.h:4 msgid "Copyright" msgstr "حقوق النشر" #: ../xslt/feed.xml.in.h:5 #, fuzzy msgid "There was a problem when fetching this subscription!" msgstr "" "كانت هناك مشكلة أثناء قراءة هذا الاشتراك. رجاء تحقق من العنوان وخرْج الطرفية." #: ../xslt/feed.xml.in.h:6 #, fuzzy msgid "1. Authentication" msgstr "الاستيثاق" #: ../xslt/feed.xml.in.h:7 #, fuzzy msgid "2. Download" msgstr "تنزيل" #: ../xslt/feed.xml.in.h:8 msgid "3. Feed Discovery" msgstr "" #: ../xslt/feed.xml.in.h:9 msgid "4. Parsing" msgstr "" #: ../xslt/feed.xml.in.h:10 #, fuzzy msgid "Details:" msgstr "التفاصيل" #: ../xslt/feed.xml.in.h:11 msgid "Authentication failed. Please check the credentials and try again!" msgstr "" #: ../xslt/feed.xml.in.h:12 #, fuzzy msgid "There was an error when downloading the feed source:" msgstr "كانت هناك أخطاء أثناء تحليل هذه التلقيمة!" #: ../xslt/feed.xml.in.h:13 #, fuzzy msgid "There was an error when running the feed filter command:" msgstr "كانت هناك أخطاء أثناء تحليل هذه التلقيمة!" #: ../xslt/feed.xml.in.h:14 msgid "" "The source does not point directly to a feed or a webpage with a link to a " "feed!" msgstr "" #: ../xslt/feed.xml.in.h:15 msgid "Sorry, the feed could not be parsed!" msgstr "" #: ../xslt/feed.xml.in.h:16 msgid "You may want to contact the author/webmaster of the feed about this!" msgstr "قد ترغب في التواصل من مع مؤلف\\مسئول التلقيمة عن هذا." #: ../xslt/folder.xml.in.h:1 msgid "Folder:" msgstr "مجلّد:" #: ../xslt/folder.xml.in.h:2 ../xslt/source.xml.in.h:2 msgid "children with" msgstr "أبناء مع" #: ../xslt/folder.xml.in.h:3 ../xslt/source.xml.in.h:3 #: ../xslt/vfolder.xml.in.h:2 msgid "unread headlines" msgstr "رؤوس العناوين لم تقرأ" #: ../xslt/item.xml.in.h:2 msgid "Feed" msgstr "تلقيمة" #: ../xslt/item.xml.in.h:3 msgid "Filed under" msgstr "وُضِع تحت" #: ../xslt/item.xml.in.h:4 msgid "Author" msgstr "المؤلّف" #: ../xslt/item.xml.in.h:5 msgid "Shared by" msgstr "شارَكهُ" #: ../xslt/item.xml.in.h:6 msgid "Via" msgstr "عبر" #: ../xslt/item.xml.in.h:7 msgid "Related" msgstr "متعلق" #: ../xslt/item.xml.in.h:8 msgid "Also posted in" msgstr "منشورة أيضا في" #: ../xslt/item.xml.in.h:9 msgid "Creator" msgstr "المنشيء" #: ../xslt/item.xml.in.h:10 msgid "Coordinates" msgstr "" #: ../xslt/item.xml.in.h:11 msgid "Map" msgstr "" #: ../xslt/item.xml.in.h:12 msgid "View count" msgstr "" #: ../xslt/item.xml.in.h:13 msgid "Rating" msgstr "" #: ../xslt/item.xml.in.h:14 msgid "Comments" msgstr "التعليقات" #: ../xslt/item.xml.in.h:15 msgid "Updating..." msgstr "يجري تحديث..." #: ../xslt/item.xml.in.h:16 msgid "Section" msgstr "القسم" #: ../xslt/item.xml.in.h:17 msgid "Department" msgstr "المصلحة" #: ../xslt/newsbin.xml.in.h:1 msgid "News Bin:" msgstr "سلّة أخبار:" #: ../xslt/newsbin.xml.in.h:2 msgid "" "Add items to this news bin by selecting \"Copy to News Bin\" from the item " "list context menu." msgstr "" "أضِف عناصر لسلّة الأخبار هذه باختيار \"انسخ لسلّة الأخبار\" من قائمة سياق لائحة " "العناصر." #: ../xslt/vfolder.xml.in.h:1 msgid "Search Folder:" msgstr "مجلّد بحث:" #, c-format #~ msgid "\"%s\" is not available" #~ msgstr "‏\"%s\" ليس متوفّرا" #, c-format #~ msgid "\"%s\" updated..." #~ msgstr "حُدِّث \"%s\"..." #~ msgid "" #~ "A network error occurred, or the other end closed the connection " #~ "unexpectedly" #~ msgstr "حدث خطأ شبكي، أو أن الطرف الآخر أنهى الاتصال فجأة" #~ msgid "" #~ "The last update of this subscription failed!
HTTP error code : " #~ msgstr "" #~ "فشل التحديث الأخير لهذا الاشتراك!
رمز خطأ HTTP: " #~ msgid "Parser Error Details" #~ msgstr "تفاصيل خطأ المحلِّل" #~ msgid "There were errors while filtering this feed!" #~ msgstr "كانت هناك أخطاء أثناء ترشيح هذه التلقيمة!" #~ msgid "Filter Error Details" #~ msgstr "تفاصيل خطأ المرشَّح" #~ msgid "" #~ "

Could not detect the type of this feed! Please check if the source " #~ "really points to a resource provided in one of the supported syndication " #~ "formats!

XML Parser Output:
" #~ msgstr "" #~ "

لم أستطع تحديد نوع هذه التلقيمة! رجاء تأكّد من أن المصدر يشير فعلا إلى " #~ "مورِد متوفّر بأحد تنسيقات تجميع الأخبار المدعومة!

خرْج محلّل XML:
" #~ msgid "" #~ "The URL you want Liferea to subscribe to points to a webpage and the auto " #~ "discovery found no feeds on this page. Maybe this webpage just does not " #~ "support feed auto discovery." #~ msgstr "" #~ "العنوان الذي تريد أن يشترك به لايفريا يشير لصفحة وِب ولم يجد الاكتشاف " #~ "التلقائي أية تلقيمات في هذه الصفحة. ربما لا تدعم هذه الصفحة الاكتشاف " #~ "التلقائي للتلقيمات." #~ msgid "Source points to HTML document." #~ msgstr "يشير المصدر إلى مستند HTML." #~ msgid "Could not determine the feed type." #~ msgstr "تعذّر تحديد نوع التلقيمة." #~ msgid "Gone. Resource doesn't exist. Please unsubscribe!" #~ msgstr "ذهب. المورِد غير موجود. رجاء اشترك!" #~ msgid "Updating \"%s\"" #~ msgstr "يجري تحديث \"%s\"" #~ msgid "XML error while reading feed! Feed \"%s\" could not be loaded!" #~ msgstr "خطأ XML أثناء قراءة التلقيمة! لا يمكن تحميل التلقيمة \"%s\"!" #~ msgid "Combined View" #~ msgstr "المنظور المركّب" #~ msgid "_Disable Javascript." #~ msgstr "_عطّل جافاسكريبت." #~ msgid "GUI" #~ msgstr "الواجهة" #, fuzzy #~ msgid "Cancel All" #~ msgstr "ألغِ ال_كل" #~ msgid "Updating favicon for \"%s\"" #~ msgstr "يجري تحديث أيقونة \"%s\"" #~ msgid "Marks read every item of every subscription." #~ msgstr "علّم كل عنصر في كل اشتراك مقروء." #~ msgid "Imports an OPML feed list." #~ msgstr "استورد قائمة تلقيمات OPML." #~ msgid "Exports the feed list as OPML." #~ msgstr "يصدّر قائمة التلقيمات كـ OPML." #~ msgid "Removes all items of the currently selected feed." #~ msgstr "يحذف كل عناصر التلقيمات المُحددة حالياً." #~ msgid "Increases the text size of the item view." #~ msgstr "يزيد حجم النص عند عرض العنصر." #~ msgid "Decreases the text size of the item view." #~ msgstr "يقلل حجم النص عند عرض العنصر." #~ msgid "Show a list of all feeds currently in the update queue" #~ msgstr "اعرض قائمة بكل التلقيمات الموجودة حاليا في طابور التحديث" #~ msgid "Edit Preferences." #~ msgstr "حرّر التفضيلات." #~ msgid "View help for this application." #~ msgstr "اعرض مساعدة هذا التطبيق." #~ msgid "View a list of all Liferea shortcuts." #~ msgstr "اعرض قائمة بكل اختصارات لايفريا." #~ msgid "View the FAQ for this application." #~ msgstr "اعرض الأسئلة الشائعة عن التطبيق." #~ msgid "Shows an about dialog." #~ msgstr "اعرض حوار عن." #~ msgid "Set view mode to mail client mode." #~ msgstr "اضبط نمط العرض كنمط عميل البريد." #~ msgid "Set view mode to use three vertical panes." #~ msgstr "اضبط نمط العرض ليستخدم ثلاث ألواح رأسيّة." #~ msgid "_Combined View" #~ msgstr "منظور مركّ_ب" #~ msgid "Set view mode to two pane mode." #~ msgstr "اضبط نمط العرض إلى نمط اللوحين." #~ msgid "Hide feeds with no unread items." #~ msgstr "أخفِ التلقيمات التي لا تحتوي عناصر غير مقروءة." #~ msgid "Adds a folder to the feed list." #~ msgstr "أضِف مجلّد إلى قائمة التلقيمات." #~ msgid "Adds a new search folder to the feed list." #~ msgstr "أضِف مجلّد بحث جديد إلى قائمة التلقيمات." #~ msgid "Adds a new feed list source." #~ msgstr "أضِف مصدر قائمة تلقيمات جديد." #~ msgid "Adds a new news bin." #~ msgstr "أضِف سلّة أخبار جديدة." #~ msgid "" #~ "Updates the selected subscription or all subscriptions of the selected " #~ "folder." #~ msgstr "يُحدّث الاشتراك المُحدد أو كل الاشتراكات في المجلّد المُحدد." #~ msgid "Opens the property dialog for the selected subscription." #~ msgstr "يفتح حوار خصائص الاشتراك المحدد." #~ msgid "Removes the selected subscription." #~ msgstr "يحذف الاشتراك المحدد." #~ msgid "Toggles the read status of the selected item." #~ msgstr "تبدّل حالة قراءة العناصر المحددة." #~ msgid "Toggles the flag status of the selected item." #~ msgstr "تبدّل حالة شارة العنصر المحدد." #~ msgid "Removes the selected item." #~ msgstr "يحذف العنصر المحدد." #~ msgid "Launches the item's link in a new Liferea browser tab." #~ msgstr "يفتح وصله العنصر في لسان جديد في متصفّح لايفريا." #~ msgid "Launches the item's link in the Liferea item pane." #~ msgstr "يفتح وصله العنصر في عمود العنصر في لايفريا." #~ msgid "Launches the item's link in the configured external browser." #~ msgstr "يفتح وصله العنصر في المتصفّح الخارجي المعدّ." #~ msgid "Browse at full screen" #~ msgstr "تصفح مع ملء الشاشة" #~ msgid "_Work Offline" #~ msgstr "ا_عمل دون اتّصال" #~ msgid "_Update All" #~ msgstr "_حدّث الكل" #~ msgid "_Show Liferea" #~ msgstr "أ_ظهر لايفريا" #~ msgid "InoReader" #~ msgstr "قارئ InoReader" #~ msgid "" #~ "Note: The username and password will be saved to your Liferea feedlist " #~ "file without using encryption." #~ msgstr "" #~ "ملحوظة: سيُحفظ اسم المستخدم وكلمة السر في قائمة تلقيمات لايفريا بدون " #~ "تعمية." #~ msgid "Add Google Reader Account" #~ msgstr "أضِف حساب قارئ جوجل" #~ msgid "Please enter your Google Reader account settings." #~ msgstr "من فضلِك أدخل اعدادات حسابك في قارئ جوجل." #~ msgid "Add InoReader Account" #~ msgstr "أضِف حساب قارئ InoReader" #~ msgid "Please enter your InoReader account settings." #~ msgstr "من فضلِك أدخل إعدادات حسابك في InoReader." #~ msgid "View Headlines" #~ msgstr "اعرض العناوين الرئيسية" #~ msgid "Feed Name" #~ msgstr "اسم التلقيمة" #~ msgid "normal view" #~ msgstr "المنظور العادي" #~ msgid "wide view" #~ msgstr "المنظور العريض" #~ msgid "combined view" #~ msgstr "المنظور المركّب" # ARABEYES: keep the html code, this is # intentional #~ msgid "Liferea, the Linux Feed Reader" #~ msgstr "

لايفريا، قارئ تلقيمات لينكس

" #~ msgid "For more information, please visit https://lzone.de/liferea/" #~ msgstr "للمزيد من المعلومات، طالع https://lzone.de/liferea/" #~ msgid "_Open Link In Browser" #~ msgstr "ا_فتح الوصلة في المتصفّح" #~ msgid "_Open Link In External Browser" #~ msgstr "ا_فتح الوصلة في المتصفّح الخارجي" #~ msgid " " #~ msgstr " " #~ msgid "Create Search Engine Feed" #~ msgstr "أنشئ تلقيمة محرّك بحث" #~ msgid "enter any search string you want" #~ msgstr "ادخل أي عبارة بحث ترغب فيها" #~ msgid "Maximal _Number Of Result Items:" #~ msgstr "ال_عدد الأقصى من عناصر الناتج:" #~ msgid "" #~ "Note: Liferea will generate a feed subscription which is used to query " #~ "the search engine results for the specified search string. You can keep " #~ "this feed permanently and update it like any other subscription." #~ msgstr "" #~ "ملاحظة: سيوّلد لايفريا اشتراك تلقيم سيستخدم للاستعلام عن نتائج محرّك البحث " #~ "لعبارة البحث المحدّدة. يمكنك إبقاء هذه التلقيمة وتحديثها كأي اشتراك آخر." #~ msgid "Browser default" #~ msgstr "افتراضي المتصفح" #~ msgid "Existing window" #~ msgstr "نافذة موجودة" #~ msgid "New window" #~ msgstr "نافذة جديدة" #~ msgid "New tab" #~ msgstr "لسان جديد" #~ msgid "AOL Reader Login" #~ msgstr "ولوج قارئ AOL" #~ msgid "AOL Reader" #~ msgstr "قارئ AOL" #~ msgid "_Open link in:" #~ msgstr "ا_فتح الوصلة في:" #~ msgid "Liferea is now online" #~ msgstr "الآن لايفريا متّصل" #~ msgid "Work Offline" #~ msgstr "اعمل دون اتّصال" #~ msgid "Liferea is now offline" #~ msgstr "الآن لايفريا غير متّصل" #~ msgid "Work Online" #~ msgstr "اعمل باتّصال" #~ msgid "This option allows you to disable subscription updating." #~ msgstr "يسمح لك هذا الخيار بتعطيل تحديث الاشتراك." #~ msgid "Online/Offline Button" #~ msgstr "زر متصّل/غير متّصل" #~ msgid "Integrate with the messaging menu (indicator)" #~ msgstr "ادمج مع قائمة الرسائل" #~ msgid "Terminate instead of minimizing to the messaging menu" #~ msgstr "أنهِ بدلا من التصغير إلى قائمة الرسائل" #~ msgid "Start minimized to the messaging menu" #~ msgstr "ابدأ مصغّرًا في قائمة الرسائل" #~ msgid "%d new item" #~ msgid_plural "%d new items" #~ msgstr[0] "لا عناصر جديدة" #~ msgstr[1] "عنصر جديد" #~ msgstr[2] "عنصران جديدان" #~ msgstr[3] "%d عناصر جديدة" #~ msgstr[4] "%d عنصرا جديدا" #~ msgstr[5] "%d عنصر جديد" #~ msgid "No new items" #~ msgstr "لا عناصر جديدة" #~ msgid "" #~ "%s\n" #~ "%d unread item" #~ msgid_plural "" #~ "%s\n" #~ "%d unread items" #~ msgstr[0] "" #~ "%s\n" #~ "لا عناصر غير مقروءة" #~ msgstr[1] "" #~ "%s\n" #~ "عنصر غير مقروء" #~ msgstr[2] "" #~ "%s\n" #~ "عنصران غير مقروءان" #~ msgstr[3] "" #~ "%s\n" #~ "%d عناصر غير مقروءة" #~ msgstr[4] "" #~ "%s\n" #~ "%d عنصرا غير مقروء" #~ msgstr[5] "" #~ "%s\n" #~ "%d عنصر غير مقروء" #~ msgid "" #~ "%s\n" #~ "No unread items" #~ msgstr "" #~ "%s\n" #~ "لا عناصر غير مقروءة" #~ msgid "Invalid Atom feed: unknown author" #~ msgstr "تلقيمة Atom غير صحيحة: مؤلف مجهول" #~ msgid "AOL Reader login failed!" #~ msgstr "فشل الولوج إلى قارئ AOL!" #~ msgid "" #~ "Integrate the feed list of your AOL Reader account. Liferea will present " #~ "your AOL Reader subscriptions, and will synchronize your feed list and " #~ "reading lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ AOL. سيعرض لايفريا اشتراكات قارئ " #~ "AOL وسيزامن قائمة التلقيمات مع قائمة قراءة AOL." #~ msgid "InoReader login failed!" #~ msgstr "فشل الولوج إلى قارئ InoReader!" #~ msgid "" #~ "Integrate the feed list of your InoReader account. Liferea will present " #~ "your InoReader subscriptions, and will synchronize your feed list and " #~ "reading lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ InoReader. سيعرض لايفريا اشتراكات " #~ "قارئ InoReader وسيزامن قائمة التلقيمات مع قائمة قراءة InoReader." #~ msgid "" #~ "Integrate the feed list of your Google Reader account. Liferea will " #~ "present your Google Reader subscriptions, and will synchronize your feed " #~ "list and reading lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ جوجل. سيعرض لايفريا اشتراكات قارئ " #~ "جوجل وسيزامن قائمة التلقيمات مع قائمة قراءة جوجل." #~ msgid "" #~ "Integrate blogrolls or Planets in your feed list. Liferea will " #~ "automatically add and remove feeds according to the changes of the source " #~ "OPML document" #~ msgstr "" #~ "يدرج blogrolls أو Planets في قائمة تلقيماتك. سيضيف ويحذف لايفريا " #~ "التلقيمات آليا تبعاً للتغيّر في مصدر مستند OPML " #~ msgid "" #~ "Integrate the feed list of your Reedah account. Liferea will present your " #~ "Reedah subscriptions, and will synchronize your feed list and reading " #~ "lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ Reedah. سيعرض لايفريا اشتراكات قارئ " #~ "Reedah وسيزامن قائمة التلقيمات مع قائمة قراءة Reedah." #~ msgid "" #~ "Integrate the feed list of your Tiny Tiny RSS 1.5+ account. Liferea will " #~ "present your tt-rss subscriptions, and will synchronize your feed list " #~ "and reading lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ Tiny Tiny RSS. سيعرض لايفريا " #~ "اشتراكات قارئ Tiny Tiny RSS وسيزامن قائمة التلقيمات مع قائمة قراءة Tiny " #~ "Tiny RSS." #~ msgid "TheOldReader login failed!" #~ msgstr "فشل الولوج إلى قارئ TheOldReader!" #~ msgid "" #~ "Integrate the feed list of your TheOldReader account. Liferea will " #~ "present your TheOldReader subscriptions, and will synchronize your feed " #~ "list and reading lists." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في قارئ TheOldReader. سيعرض لايفريا " #~ "اشتراكات قارئ TheOldReader وسيزامن قائمة التلقيمات مع قائمة قراءة " #~ "TheOldReader." #~ msgid "This feed does not exist anymore!" #~ msgstr "لا وجود لهذه التلقيمة بعد الآن!" #~ msgid "This news entry has no headline" #~ msgstr "مدخلة الأخبار بلا عناوين رئيسية" #~ msgid "Visit" #~ msgstr "زُر" #~ msgid "Open feed" #~ msgstr "افتح التلقيمة" #~ msgid "%s has %d update" #~ msgid_plural "%s has %d updates" #~ msgstr[0] "لا تحديثات%2$.0s في %1$s" #~ msgstr[1] "يوجد تحديث%2$.0s في %1$s" #~ msgstr[2] "يوجد تحديثان%2$.0s في %1$s" #~ msgstr[3] "يوجد %2$d تحديثات في %1$s" #~ msgstr[4] "يوجد %2$d تحديثا في %1$s" #~ msgstr[5] "يوجد %2$d تحديث في %1$s" #~ msgid "_Enforce popup notification for this subscription." #~ msgstr "ا_فرض إشعار قافز لهذا الاشتراك." #~ msgid "_Never do popup notification for this subscription." #~ msgstr "_لا تعرض إشعار قافز لهذا الاشتراك، أبدا." #~ msgid "Show a _popup window with new headlines." #~ msgstr "اعرض نافذة _منبثقة مع العناوين الرئيسية الجديدة." #~ msgid "Show a status _icon in the notification area (system tray)." #~ msgstr "اعرض أ_يقونة حالة في منطقة التنبيه (لوح النظام)." #~ msgid "Show _number of new items in the tray icon." #~ msgstr "اعرض _عدد العناصر الجديدة في أيقونة اللوحة." #~ msgid "T_erminate instead of minimizing to tray icon." #~ msgstr "أ_نهِ بدلا من التصغير إلى أيقونة اللوحة." #~ msgid "_Start in tray icon." #~ msgstr "ابدأ _كأيقونة في اللوحة." #~ msgid "Please enter your tt-rss account settings." #~ msgstr "من فضلِك أدخل إعدادات حسابك في Tiny Tiny RSS." #~ msgid "No comments yet." #~ msgstr "لا تعليقات حتى الآن." #~ msgid "Refresh" #~ msgstr "حدِّث" # ARABEYES: keep the html code, this is # intentional #~ msgid "Liferea - Linux Feed Reader" #~ msgstr "

لايفريا - قارئ تلقيمات لينكس

" #, fuzzy #~ msgid "" #~ "

Welcome to Liferea, a desktop news aggregator for online news " #~ "feeds.

You can add new subscriptions

  • From main menu " #~ "'Subscription' -> 'New Subscription'
  • By dropping feed links " #~ "into the subscription list
  • By right clicking links and choosing " #~ "'Subscribe' within Liferea

" #~ msgstr "" #~ "

مرحبا في لايفريا، مجمّع أخبار لتلقيمات الأخبار " #~ "عالخط.

اللوح الأيسر يحتوي قائمة باشتراكاتك. لإضافة " #~ "اشتراك اختر الاشتراكات -> اشتراك جديد. لتصفّح العناوين الرئيسية لتلقيمة " #~ "اخترها في قائمة التلقيمات وستحمّل العناوين الرئيسية في اللوح الأيمن.

" #, fuzzy #~ msgid "" #~ "Copyright (c) 2003-2012\n" #~ "The Liferea Team\n" #~ msgstr "" #~ "حقوق النشر © 2003-2011\n" #~ "فريق لايفريا\n" #~ msgid "Download and view feeds" #~ msgstr "نزّل واعرض التلقيمات" #~ msgid "You may want to validate the feed using" #~ msgstr "ربما تريد التأكد من سلامة التلقيمة باستخدام" #~ msgid "Launch Item In _Tab" #~ msgstr "افتح العنصر في _لسان" #~ msgid "_Launch Item In Browser" #~ msgstr "ا_فتح العنصر في المتصفّح" #~ msgid "Copy Item _URL to Clipboard" #~ msgstr "انسخ _عنوان العنصر إلى الحافظة" #~ msgid "flag" #~ msgstr "شارة" #~ msgid "bookmark" #~ msgstr "علامة" #~ msgid "comments" #~ msgstr "تعليقات" #~ msgid "Enclosure download FAILED: \"%s\"" #~ msgstr "فشل تنزيل المغلّف: \"%s\"" #~ msgid "Enclosure download finished: \"%s\"" #~ msgstr "انتهى تنزيل المغلّف: \"%s\"" #~ msgid "" #~ "This version of Liferea uses a new cache format and has migrated your " #~ "feed cache. The cache content in %s was not deleted automatically. Please " #~ "remove this directory manually once you are sure migration was successful!" #~ msgstr "" #~ "تستخدم هذه الإصدارة من لايفرريا نسق خبيئة جديدة وقد نقلت خبيئة تلقيماتك " #~ "إلى النسق الجديد. لم يحذف محتوى الخبيئة من %s تلقائيا. من فضلك احذف هذا " #~ "الدليل يدويا عندما تتأكد من نجاح النقل." #, fuzzy #~ msgid "Download FAILED: \"%s\"" #~ msgstr "فشل تنزيل المغلّف: \"%s\"" #, fuzzy #~ msgid "Download finished." #~ msgstr "_نزّل باستخدام" #~ msgid "Choose download directory" #~ msgstr "اختر دليل التنزيل" #~ msgid "" #~ "_Manual:\n" #~ "(%s for URL)" #~ msgstr "" #~ "_يدوي:\n" #~ "(%s للمسار)" #~ msgid "_Save downloads in" #~ msgstr "ا_حفظ التنزيلات في" #~ msgid "_Enable Local LAN Synchronization" #~ msgstr "_فعّل كل مزامنة الشبكة المحلية" #~ msgid "_Service Name" #~ msgstr "اسم ال_خدمة" #~ msgid "Sync" #~ msgstr "زامن" #~ msgid "Downloading Enclosure" #~ msgstr "تنزيل المغلّفات" #~ msgid "_Pass URL and do not download enclosure." #~ msgstr "_مرر المسار ولا تُنزّل المغلف." #~ msgid "" #~ "Jumps to the next unread item. If necessary selects the next feed with " #~ "unread items." #~ msgstr "" #~ "تقفز للعنصر الغِر مقروء التالي. تختار التلقيمة التالية ذات العناصر غير " #~ "المقروءة إذا كان ضرورياً." #~ msgid "Liferea Sync %s@%s" #~ msgstr "مزامنة ليفريا %s@%s" #~ msgid "%d Search Result for \"%s\"" #~ msgid_plural "%d Search Results for \"%s\"" #~ msgstr[0] "%.0sلا نتائج بحث عن \"%s\"" #~ msgstr[1] "%.0sنتيجة بحث واحدة عن \"%s\"" #~ msgstr[2] "%.0sنتيجتي بحث عن \"%s\"" #~ msgstr[3] "%d نتائج بحث عن \"%s\"" #~ msgstr[4] "%d نتيجة بحث عن \"%s\"" #~ msgstr[5] "%d نتيجة بحث عن \"%s\"" #~ msgid "" #~ "The item list now contains all items matching the specified search " #~ "pattern. If you want to save this search result permanently you can click " #~ "the \"Search Folder\" button in the search dialog and Liferea will add a " #~ "search folder to your feed list." #~ msgstr "" #~ "تحتوى قائمة العناصر الآن كل العناصر التي تطابق نمط البحث المحدّد. إذا أردت " #~ "حفظ نتائج البحث بشكل دائم فانقر زر \"مجلّد بحث\" في حوار البحث وسيضيف " #~ "لايفريا مجلّد بحث في قائمة تلقيماتك." #~ msgid "Liferea is unable to display this item's content." #~ msgstr "لا يستطيع لايفريا عرض محتويات هذا العنصر." #~ msgid "

View this item's content.

" #~ msgstr "

اعرض محتويات هذا العنصر.

" #~ msgid " (Website)" #~ msgstr " (الموقع)" #~ msgid "Bloglines" #~ msgstr "Bloglines" #~ msgid "" #~ "Integrate the feed list of your Bloglines account. Liferea will present " #~ "your Bloglines subscription as a read-only subtree in the feed list." #~ msgstr "" #~ "يتكامل مع قائمة تلقيمات حسابك في Bloglines. سيعرض لايفريا اشتراك " #~ "Bloglines كشجرة فرعية للقرائة فقط في قائمة تلقيماتك." #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid "Hook" #~ msgstr "خُطَّاف" #~ msgid "Registered Scripts" #~ msgstr "سكريبتات مسجّلة" #~ msgid "Script Code" #~ msgstr "كود سكريبت" #~ msgid "text/plain" #~ msgstr "نص/صِرف" #~ msgid "" #~ "This option can cause significant delays when loading folders " #~ "containing many feeds." #~ msgstr "" #~ "قد يؤدي هذا الخيار لتأخير ملحوظ عند تحميل مجلّدات تحتوي الكثير من " #~ "التلقيمات." #~ msgid "Downloading Enclosures" #~ msgstr "تنزيل المغلّفات" #~ msgid "Feed Cache Handling" #~ msgstr "التعامل مع ذاكرة التلقيمات المخبّأة" #~ msgid "Feed Name" #~ msgstr "اسم التلقيمة" #~ msgid "Feed Update Settings" #~ msgstr "إعدادات تحديث التلقيمات" #~ msgid "HTTP Proxy Server" #~ msgstr "خادوم وسيط HTTP" #~ msgid "Opening Enclosures" #~ msgstr "فتح المغلّفات" #~ msgid "Reading Headlines" #~ msgstr "قراءة العناوين الرئيسية" #~ msgid "Toolbar Settings" #~ msgstr "إعدادات شريط الأدوات" #~ msgid "Update Interval" #~ msgstr "فترة التحديث" #~ msgid "Web Integration" #~ msgstr "التكامل مع وِب" #~ msgid "Add Script" #~ msgstr "أضف سكريبت" #~ msgid "Create new script" #~ msgstr "أنشئ سكريبت جديد" #~ msgid "Exec Command" #~ msgstr "أمر التنفيذ" #~ msgid "Reuse existing script" #~ msgstr "أعد استخدام سكريبت موجود" #~ msgid "Script Manager" #~ msgstr "مدير السكريبت" #~ msgid "Update status" #~ msgstr "حدّث الحالة" #~ msgid "was updated" #~ msgstr "حُدِّث" #~ msgid "was not updated" #~ msgstr "لم يُحَدّث" #~ msgid "topics_en.html" #~ msgstr "topics_en.html" #~ msgid "reference_en.html" #~ msgstr "reference_en.html" #~ msgid "faq_en.html" #~ msgstr "faq_en.html" #~ msgid "_Properties..." #~ msgstr "ال_خصائص..." #~ msgid "Update out-dated feeds" #~ msgstr "حدّث التلقيمات القديمة" #~ msgid "Force update of all feeds" #~ msgstr "افرض تحديث كل التلقيمات" #~ msgid "No feed update at all" #~ msgstr "لم تُحدّث أي تلقيمات" #~ msgid "feedlist.opml" #~ msgstr "feedlist.opml" #~ msgid "At _startup:" #~ msgstr "عند ال_بدأ:" #~ msgid "link cosmos" #~ msgstr "محيط الوصلة" #~ msgid "_Script Manager" #~ msgstr "مدير ال_سكريبت" #~ msgid "Allows to configure and edit LUA hook scripts" #~ msgstr "يسمح بإعداد وتحرير LUA hook scripts" #~ msgid "Attention Profile" #~ msgstr "لاحة الانتباه" #~ msgid "Presents statistics on your most read categories" #~ msgstr "يعطي إحصائيات عن أكثر الفئات المقروؤة" #~ msgid "Search With ..." #~ msgstr "ابحث باستخدام..." #~ msgid "Count" #~ msgstr "عدد" #~ msgid "(empty)" #~ msgstr "(فارغ)" #~ msgid "startup" #~ msgstr "البدأ" #~ msgid "feed updated" #~ msgstr "حُدِّثت التلقيمة" #~ msgid "feed added" #~ msgstr "أُضِيفت التلقيمة" #~ msgid "item selected" #~ msgstr "حُدِّد العنصر" #~ msgid "feed selected" #~ msgstr "حُدِّدت التلقيمة" #~ msgid "item unselected" #~ msgstr "أُلغِيَ تحديد العنصرِ" #~ msgid "feed unselected" #~ msgstr "أُلغِي تحديد التلقيمة" #~ msgid "shutdown" #~ msgstr "أطفيء" #~ msgid "Sorry, no scripting support available!" #~ msgstr "آسف، لا يتوفّر دعم للسكريبت" #~ msgid "Script Name" #~ msgstr "اسم السكريبت" #~ msgid "No script selected!" #~ msgstr "لم يُنحدد أية سكريبت!" #~ msgid "Create a new search feed." #~ msgstr "أنشيء تلقيمة بحث جديدة." #~ msgid "%s has %d new / updated headline\n" #~ msgid_plural "%s has %d new / updated headlines\n" #~ msgstr[0] "لا عناوين جديدة / مُحدّثة لـ %s %.0s\n" #~ msgstr[1] "‏%s له عنوان %.0sواحد جديد / مُحدّث\n" #~ msgstr[2] "‏%s له %.0sعنوانين جديدين / مُحدّثين\n" #~ msgstr[3] "‏%s له %d عناوين جديدة / مُحدّثة\n" #~ msgstr[4] "‏%s له %d عنوانا جديدا / مُحدّثا\n" #~ msgstr[5] "‏%s له %d عنوان جديد / مُحدّث\n" #~ msgid "Search _Link Cosmos with" #~ msgstr "ابحث في محيط ال_وصلة عن:" #~ msgid "Liferea seems to be running already!" #~ msgstr "يبدو أن لايفريا يعمل بالفعل!" #~ msgid "The orientation of the tray." #~ msgstr "اتجاه اللوحة." #~ msgid "%s" #~ msgstr "%s" #~ msgid "You have to select a feed entry" #~ msgstr "عليك اختيار عنصر تلقيمة" #~ msgid "_Allow Flash in Feeds." #~ msgstr "ا_سمح بالفلاش في التلقيمات" #, fuzzy #~ msgid "FAILED to download enclosure: \"%s\"" #~ msgstr "انتهى تنزيل المغلّف: \"%s\" " #, fuzzy #~ msgid "" #~ "Copyright (c) 2003-2009\n" #~ "Lars Lindner and \n" #~ "Nathan J. Conrad \n" #~ msgstr "" #~ "حقوق النشر © 2003-2007\n" #~ "Lars Lindner و \n" #~ "Nathan J. Conrad \n" #~ msgid "/_Bookmark Link at %s" #~ msgstr "/_علّم الوصلة في %s" #~ msgid "/Toggle _Read Status" #~ msgstr "/بدّل حالة ال_قراءة" #~ msgid "/Toggle Item _Flag" #~ msgstr "/بدّل _شارة العنصر" #~ msgid "/_Preferences" #~ msgstr "/ال_تفضيلات" #~ msgid "/_Quit" #~ msgstr "/ا_خرج" #~ msgid "/Copy Link Location" #~ msgstr "/انسخ موقع الوصلة" #~ msgid "/_Update" #~ msgstr "/_حدّث" #~ msgid "/_New/New _Subscription..." #~ msgstr "/_جديد/ا_شتراك جديد..." #~ msgid "/_New/New _Folder..." #~ msgstr "/_جديد/_مجلّد جديد..." #~ msgid "/_New/New S_earch Folder..." #~ msgstr "/_جديد/مجلّد _بحث جديد..." #~ msgid "/_New/New S_ource..." #~ msgstr "/_جديد/م_صدر جديد..." #~ msgid "/_New/New _News Bin..." #~ msgstr "/_جديد/_سلّة أخبار جديدة..." #~ msgid "" #~ "This program is free software; you can redistribute it and/or modify\n" #~ "it under the terms of the GNU General Public License as\n" #~ "published by the Free Software Foundation; either version 2 of the\n" #~ "License, or (at your option) any later version.\n" #~ "\n" #~ "This program is distributed in the hope that it will be useful,\n" #~ "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" #~ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" #~ "GNU General Public License for more details.\n" #~ "\n" #~ "You should have received a copy of the GNU General Public License\n" #~ "along with this program; if not, write to the Free Software\n" #~ "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, " #~ "USA." #~ msgstr "" #~ "هذا البرنامج برمجية حرة، يمكنك توزيعه و/أو تعديله حسب بنود رخصة جنو " #~ "العمومية كما \n" #~ "نشرتها مؤسسة البرامج الحرة، الإصدار الثاني أو أي إصدار أحدث (حسب رغبتك).\n" #~ "\n" #~ "\n" #~ "\n" #~ "يوزع هذا البرنامج على أمل أن يكون مفيدًا، ولكن دون أية ضمانات، بما في ذلك " #~ "ضمانات \n" #~ "قابلية البرنامج للتسويق أو الملاءمة لغرض معين. انظر نص رخصة جنو " #~ "العمومية \n" #~ "لمزيد من التفاصيل.\n" #~ "\n" #~ "\n" #~ "\n" #~ "من المفترض أنك تلقيت نسخة من رخصة جنو العمومية مع هذا البرنامج؛ إذا لم " #~ "يحدث \n" #~ "هذا فاكتب إلى مؤسسة البرمجيات الحرة\n" #~ "\n" #~ "Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA \n" #~ "02111-1307, USA." #~ msgid "Access Forbidden" #~ msgstr "النفاذ ممنوع" #~ msgid "Feed link auto discovery failed! No feed links found!" #~ msgstr "فشَل الاكتشاف الآلي لوصلة التلقيم! لم يُعثر على أي وصلة تلقيم!" #~ msgid " --help Print this help and exit" #~ msgstr " --help اطبع هذه المساعدة واخرج" #~ msgid " --mainwindow-state=STATE" #~ msgstr " --mainwindow-state=STATE" #~ msgid " Possible topics are: all,cache,conf,db,gui,html" #~ msgstr " المواضيع الممكنة هي: all,cache,conf,db,gui,html" #~ msgid " net,parsing,plugins,trace,update,verbose" #~ msgstr " net,parsing,plugins,trace,update,verbose" #~ msgid "The --mainwindow-state argument must be given a parameter.\n" #~ msgstr "يجب أن يعطى الخيار --mainwindow-state معاملا.\n" #~ msgid "The --session argument must be given a parameter.\n" #~ msgstr "يجب أن يعطى الخيار --session معاملا.\n" #~ msgid "Liferea encountered an unknown argument: %s\n" #~ msgstr "واجه لايفريا معطى مجهول: %s\n" #~ msgid "Cookie for %s has expired!" #~ msgstr "انتهت مدّة سكاكر %s" #~ msgid "Network error" #~ msgstr "خطأ شبكي" #~ msgid "Could not download \"%s\". Will retry in %d seconds." #~ msgstr "تعذّر تنزيل \"%s\". سأعيد المحاولة خلال %d من الثواني." #~ msgid "http://lzone.de/liferea/" #~ msgstr "http://lzone.de/liferea/" #~ msgid "_Limit cache to" #~ msgstr "حُدّ الذاكرة المخبّأة بـ" #~ msgid "items." #~ msgstr "عناصر." liferea-1.13.7/po/ast.po000066400000000000000000002374651415350204600150000ustar00rootroot00000000000000# Asturian translations for Liferea. # Copyright (C) 2009 The Free Software Foundation, Inc. # This file is distributed under the same license as the Liferea package. # Marquinos , 2010. # msgid "" msgstr "" "Project-Id-Version: liferea 1.8-rc1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-30 12:36+0200\n" "PO-Revision-Date: 2013-03-31 17:15+0100\n" "Last-Translator: Iñigo Varela \n" "Language-Team: Asturian \n" "Language: \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" "X-Poedit-Language: Asturian\n" #: ../net.sourceforge.liferea.appdata.xml.in.h:1 #, fuzzy msgid "RSS feed reader" msgstr "Llector de noticies" #: ../net.sourceforge.liferea.appdata.xml.in.h:2 msgid "" "Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator " "for online news feeds. It supports a number of different feed formats " "including RSS/RDF, CDF and Atom. There are many other news readers " "available, but these others are not available for Linux or require many " "extra libraries to be installed. Liferea tries to fill this gap by creating " "a fast, easy to use, easy to install news aggregator for GTK/GNOME." msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:3 msgid "Distinguishing features:" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:4 #, fuzzy msgid "Read articles when offline" msgstr "Liferea ta ensin conexón" #: ../net.sourceforge.liferea.appdata.xml.in.h:5 #, fuzzy msgid "Synchronizes with TheOldReader" msgstr "Sincronización con equipos cercanos" #: ../net.sourceforge.liferea.appdata.xml.in.h:6 #, fuzzy msgid "Synchronizes with TinyTinyRSS" msgstr "Sincronización con equipos cercanos" #: ../net.sourceforge.liferea.appdata.xml.in.h:7 #, fuzzy msgid "Synchronizes with InoReader" msgstr "Sincronización con equipos cercanos" #: ../net.sourceforge.liferea.appdata.xml.in.h:8 #, fuzzy msgid "Synchronizes with Reedah" msgstr "Sincronización con equipos cercanos" #: ../net.sourceforge.liferea.appdata.xml.in.h:9 msgid "Permanently save headlines in news bins" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:10 msgid "Match items using search folders" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:11 #, fuzzy msgid "Play Podcasts" msgstr "Podcast" #: ../net.sourceforge.liferea.desktop.in.h:1 ../src/liferea_application.c:350 #: ../glade/mainwindow.ui.h:1 msgid "Liferea" msgstr "Liferea" #: ../net.sourceforge.liferea.desktop.in.h:2 msgid "Feed Reader" msgstr "Llector de noticies" #: ../net.sourceforge.liferea.desktop.in.h:3 msgid "Liferea Feed Reader" msgstr "Liferea, llector de noticies" #: ../net.sourceforge.liferea.desktop.in.h:4 msgid "Read news feeds and blogs" msgstr "" #: ../net.sourceforge.liferea.desktop.in.h:5 msgid "news;feed;aggregator;blog;podcast;syndication;rss;atom" msgstr "" #: ../plugins/getfocus.py:93 msgid "Opacity:" msgstr "" #: ../plugins/getfocus.py:94 msgid "Opacity" msgstr "" #: ../plugins/getfocus.py:104 msgid "Min" msgstr "" #: ../plugins/getfocus.py:109 msgid "Max" msgstr "" #: ../plugins/getfocus.py:115 msgid "Save" msgstr "" #: ../plugins/headerbar.py:67 ../glade/liferea_menu.ui.h:20 #: ../glade/liferea_toolbar.ui.h:5 msgid "Previous Item" msgstr "" #: ../plugins/headerbar.py:73 ../glade/liferea_menu.ui.h:21 #: ../glade/liferea_toolbar.ui.h:6 #, fuzzy msgid "Next Item" msgstr "Siguiente artículu _nuevu" #: ../plugins/headerbar.py:81 ../glade/liferea_menu.ui.h:19 #: ../glade/liferea_toolbar.ui.h:7 msgid "_Next Unread Item" msgstr "Siguiente artículu _nuevu" #: ../plugins/headerbar.py:89 ../glade/liferea_menu.ui.h:14 #: ../glade/liferea_toolbar.ui.h:3 msgid "_Mark Items Read" msgstr "_Conseñar artículos lleíos" #: ../plugins/headerbar.py:113 ../glade/liferea_menu.ui.h:40 #: ../glade/liferea_toolbar.ui.h:10 msgid "Search All Feeds..." msgstr "Guetar en toles canales..." #: ../plugins/libnotify.py:42 #, fuzzy msgid "Feed Updates" msgstr "Anovar canal" #: ../plugins/plugin-installer.py:54 msgid "Plugins" msgstr "" #: ../plugins/plugin-installer.py:69 msgid "Plugin Installer" msgstr "" #: ../plugins/plugin-installer.py:83 msgid "Activate Plugins" msgstr "" #: ../plugins/plugin-installer.py:84 #, fuzzy msgid "Download Plugins" msgstr "Baxar usan_do" #: ../plugins/plugin-installer.py:102 #, python-format msgid "Bad fields for plugin entry %s" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "All" msgstr "" #: ../plugins/plugin-installer.py:125 ../glade/properties.ui.h:39 msgid "Advanced" msgstr "Avanzáu" #: ../plugins/plugin-installer.py:125 msgid "Menu" msgstr "" #: ../plugins/plugin-installer.py:125 #, fuzzy msgid "Notifications" msgstr "Axustes de notificación" #: ../plugins/plugin-installer.py:133 msgid "Filter by category" msgstr "" #: ../plugins/plugin-installer.py:140 msgid "_Install" msgstr "" #: ../plugins/plugin-installer.py:144 msgid "_Uninstall" msgstr "" #: ../plugins/plugin-installer.py:245 #, python-format msgid "" "Missing package manager '%s'. Cannot check nor install necessary " "dependencies!" msgstr "" #: ../plugins/plugin-installer.py:261 #, python-format msgid "Missing package '%s'. Do you want to install it? (Will run '%s')" msgstr "" #: ../plugins/plugin-installer.py:268 #, python-format msgid "" "Package installation failed (%s)! Check console output for further problem " "details!" msgstr "" #: ../plugins/plugin-installer.py:271 #, python-format msgid "Failed to check plugin dependencies (%s)!" msgstr "" #: ../plugins/plugin-installer.py:280 msgid "Command \"git\" not found, please install it!" msgstr "" #: ../plugins/plugin-installer.py:289 #, python-format msgid "Copying %s to %s" msgstr "" #: ../plugins/plugin-installer.py:292 #, python-format msgid "Failed to copy plugin directory (%s)!" msgstr "" #: ../plugins/plugin-installer.py:301 #, python-format msgid "Failed to copy plugin .py file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:311 #, python-format msgid "Failed to copy .plugin file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:322 #, fuzzy, python-format msgid "Creating schema directory %s" msgstr "¡Nun pudo criase'l direutoriu d'almacenamientu \"%s\"!" #: ../plugins/plugin-installer.py:324 #, python-format msgid "Installing schema %s" msgstr "" #: ../plugins/plugin-installer.py:328 msgid "Compiling schemas..." msgstr "" #: ../plugins/plugin-installer.py:333 #, python-format msgid "Failed to install schema files (%s)!" msgstr "" #: ../plugins/plugin-installer.py:343 #, python-format msgid "Failed to enable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:349 #, python-format msgid "Plugin '%s' is now installed. Ensure to restart Liferea!" msgstr "" #: ../plugins/plugin-installer.py:363 #, python-format msgid "Failed to disable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:370 ../plugins/plugin-installer.py:390 #, fuzzy, python-format msgid "Deleting '%s'" msgstr "Desaniciando entrada" #: ../plugins/plugin-installer.py:373 #, python-format msgid "Failed to remove directory '%s' (%s)!" msgstr "" #: ../plugins/plugin-installer.py:383 msgid "Failed to remove .py file!" msgstr "" #: ../plugins/plugin-installer.py:393 msgid "Failed to remove .plugin file!" msgstr "" #: ../plugins/plugin-installer.py:402 msgid "Sorry! Plugin removal failed!." msgstr "" #: ../plugins/plugin-installer.py:404 msgid "" "Plugin was removed. Please restart Liferea once for it to take full effect!." msgstr "" #: ../plugins/trayicon.py:132 #, fuzzy msgid "Show / Hide" msgstr "Amosar detalles" #: ../plugins/trayicon.py:133 msgid "Minimize to tray on close" msgstr "" #: ../plugins/trayicon.py:134 #, fuzzy msgid "Quit" msgstr "_Colar" #: ../src/browser.c:81 ../src/browser.c:98 #, c-format msgid "Browser command failed: %s" msgstr "Falló al llanzar el restolador: %s" #: ../src/browser.c:101 ../src/ui/liferea_shell.c:1047 #, c-format msgid "Starting: \"%s\"" msgstr "Aniciando: \"%s\"" #. unauthorized #: ../src/comments.c:118 msgid "Authorization Error" msgstr "Fallu n'autorización" #: ../src/common.c:66 #, c-format msgid "Cannot create cache directory \"%s\"!" msgstr "¡Nun pudo criase'l direutoriu d'almacenamientu \"%s\"!" #: ../src/conf.c:182 msgid "" "Your version of WebKitGTK+ doesn't support changing the proxy settings from " "Liferea. The system's default proxy settings will be used." msgstr "" #. translation hint: date format for today, reorder format codes as necessary #: ../src/date.c:133 msgid "Today %l:%M %p" msgstr "Güei %l:%M %p" #. translation hint: date format for yesterday, reorder format codes as necessary #: ../src/date.c:142 msgid "Yesterday %l:%M %p" msgstr "Ayeri %l:%M %p" #. translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary #: ../src/date.c:154 msgid "%a %l:%M %p" msgstr "%a %l:%M %p" #. translation hint: date format for dates older than a week but from this year, reorder format codes as necessary #: ../src/date.c:162 msgid "%b %d %l:%M %p" msgstr "%d %b %l:%M %p" #. translation hint: date format for dates from the last years, reorder format codes as necessary #: ../src/date.c:165 msgid "%b %d %Y" msgstr "%b %d %Y" #: ../src/enclosure.c:201 #, c-format msgid "\"%s\" is not a valid enclosure type config file!" msgstr "\"%s\" nun ye un ficheru de configuración d'axuntos válidu" #: ../src/enclosure.c:292 msgid "" "You have not configured a download tool yet! Please do so in the " "'Enclosures' tab in Tools/Preferences." msgstr "" #: ../src/enclosure.c:311 #, c-format msgid "" "Command failed: \n" "\n" "%s\n" "\n" " Please check whether the configured download tool is installed and working " "correctly! You can change it in the 'Download' tab in Tools/Preferences." msgstr "" #: ../src/export.c:187 #, fuzzy, c-format msgid "Error renaming %s to %s: %s\n" msgstr "Fallu al renomar %s a %s\n" #: ../src/export.c:409 ../src/export.c:411 #, c-format msgid "XML error while reading OPML file! Could not import \"%s\"!" msgstr "XML erróneu nel documentu OPML «%s». Nun foi dable importalu." #: ../src/export.c:417 ../src/export.c:419 #, c-format msgid "" "Empty document! OPML document \"%s\" should not be empty when importing." msgstr "El documentu OPML \"%s\", que se quería importar, taba vacíu." #: ../src/export.c:440 ../src/export.c:442 #, c-format msgid "\"%s\" is not a valid OPML document! Liferea cannot import this file!" msgstr "" "\"%s\" nun ye un documentu OPML válidu. Liferea nun pudo importar el ficheru." #: ../src/export.c:461 msgid "Imported feed list" msgstr "Llista de canales importaos" #: ../src/export.c:473 msgid "Import Feed List" msgstr "Importar llista de canales" #: ../src/export.c:473 msgid "Import" msgstr "Importar" #: ../src/export.c:473 ../src/export.c:490 ../src/fl_sources/opml_source.c:379 msgid "OPML Files" msgstr "Ficheros OPML" #: ../src/export.c:481 msgid "Error while exporting feed list!" msgstr "Ocurrió un fallu mientres s'esportaba la llista de canales." #: ../src/export.c:483 msgid "Feed List exported!" msgstr "La llista de canales esportóse con ésitu." #: ../src/export.c:490 msgid "Export Feed List" msgstr "Esportar llista de canales" #: ../src/export.c:490 msgid "Export" msgstr "Esportar" #: ../src/feed_parser.c:201 msgid "Empty document!" msgstr "Documentu baleru." #: ../src/feed_parser.c:210 msgid "Invalid XML!" msgstr "XML inválidu" #: ../src/fl_sources/default_source.c:139 ../glade/new_subscription.ui.h:1 #: ../glade/simple_subscription.ui.h:1 msgid "New Subscription" msgstr "Nueva canal" #: ../src/fl_sources/google_source.c:111 msgid "Google Reader" msgstr "Llector Google" #: ../src/fl_sources/node_source.c:334 msgid "No feed list source types found!" msgstr "Nun s'atoparon tribes de fontes de llistes de canales." #: ../src/fl_sources/node_source.c:363 msgid "Source Type" msgstr "Triba de fonte" #: ../src/fl_sources/node_source.c:414 #, c-format msgid "Login for '%s' has not yet completed! Please wait until login is done." msgstr "" #. FIXME: something is not perfect, because if you immediately #. remove the subscription tree afterwards there is a double free #: ../src/fl_sources/node_source.c:580 #, c-format msgid "The '%s' subscription was successfully converted to local feeds!" msgstr "" #: ../src/fl_sources/opml_source.c:319 msgid "Planet, BlogRoll, OPML" msgstr "Planet, BlogRoll, OPML" #: ../src/fl_sources/opml_source.c:379 msgid "Choose OPML File" msgstr "Escoyer Ficheru OPML" #: ../src/fl_sources/opml_source.c:379 ../src/ui/subscription_dialog.c:355 msgid "_Open" msgstr "" #: ../src/fl_sources/opml_source.h:28 msgid "New OPML Subscription" msgstr "Nueva soscripción OPML" #: ../src/fl_sources/reedah_source.c:103 #: ../src/fl_sources/theoldreader_source.c:104 #, fuzzy msgid "Login failed!" msgstr "Falló l'accesu a Google Reader" #: ../src/fl_sources/reedah_source.c:313 msgid "Reedah" msgstr "" #: ../src/fl_sources/reedah_source_feed.c:154 msgid "Could not parse JSON returned by Reedah API!" msgstr "" #: ../src/fl_sources/theoldreader_source.c:311 #, fuzzy msgid "TheOldReader" msgstr "Llector de noticies" #: ../src/fl_sources/ttrss_source.c:227 ../src/fl_sources/ttrss_source.c:293 msgid "TinyTinyRSS HTTP API not reachable!" msgstr "" #: ../src/fl_sources/ttrss_source.c:234 msgid "" "TinyTinyRSS subscribing to feed failed! Check if you really passed a feed " "URL!" msgstr "" #: ../src/fl_sources/ttrss_source.c:300 msgid "TinyTinyRSS unsubscribing feed failed!" msgstr "" #: ../src/fl_sources/ttrss_source.c:318 #, c-format msgid "" "This TinyTinyRSS version does not support removing feeds. Upgrade to version " "%s or later!" msgstr "" #: ../src/fl_sources/ttrss_source.c:471 msgid "Tiny Tiny RSS" msgstr "Tiny Tiny RSS" #: ../src/fl_sources/ttrss_source_feed.c:150 msgid "Could not parse JSON returned by TinyTinyRSS API!" msgstr "" #. if we don't find a feed with unread items do nothing #: ../src/itemlist.c:395 msgid "There are no unread items" msgstr "Nun hai artículos ensin lleer " #: ../src/liferea_application.c:280 #, fuzzy msgid "" "Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'" msgstr "" "Aniciar Liferea cola ventana principal nel ESTÁU, que puede ser «shown» " "p'amosala, «iconified» pa dexala na estaya de notificación, o «hidden» " "p'anubrila" #: ../src/liferea_application.c:280 msgid "STATE" msgstr "ESTÁU" #: ../src/liferea_application.c:281 msgid "Show version information and exit" msgstr "Amosar información de la versión y colar" #: ../src/liferea_application.c:282 msgid "Add a new subscription" msgstr "Amestar una nueva soscripción" #: ../src/liferea_application.c:282 msgid "uri" msgstr "uri" #: ../src/liferea_application.c:283 msgid "Start with all plugins disabled" msgstr "" #: ../src/liferea_application.c:288 msgid "Print debugging messages of all types" msgstr "Amosar tolos mensaxes de depuración" #: ../src/liferea_application.c:289 msgid "Print debugging messages for the cache handling" msgstr "" "Amosar mensaxes de depuración tocante a remanar l'área d'almacenamientu" #: ../src/liferea_application.c:290 msgid "Print debugging messages for the configuration handling" msgstr "Amosar mensaxes de depuración tocante a la configuración" #: ../src/liferea_application.c:291 msgid "Print debugging messages of the database handling" msgstr "Amosar mensaxes de depuración tocante a la base de datos" #: ../src/liferea_application.c:292 msgid "Print debugging messages of all GUI functions" msgstr "" "Amosar mensaxes de depuración tocante toles funciones de la interface gráfica" #: ../src/liferea_application.c:293 msgid "" "Enables HTML rendering debugging. Each time Liferea renders HTML output it " "will also dump the generated HTML into ~/.cache/liferea/output.html" msgstr "" "Activa la depuración de la xeneración de HTML. Cada vegada que Liferea " "xenere HTML atroxarálo en ~/.cache/liferea/output.html" #: ../src/liferea_application.c:294 msgid "Print debugging messages of all network activity" msgstr "Amosar mensaxes de depuración tocante a los accesos a la rede" #: ../src/liferea_application.c:295 msgid "Print debugging messages of all parsing functions" msgstr "" "Amosar mensaxes de depuración tocante a toles funciones de procesamientu" #: ../src/liferea_application.c:296 msgid "Print debugging messages when a function takes too long to process" msgstr "" "Amosar mensaxes de depuración cuando una función allánciase más de la cuenta" #: ../src/liferea_application.c:297 msgid "Print debugging messages when entering/leaving functions" msgstr "Amosar mensaxes de depuración al entrar y colar de funciones" #: ../src/liferea_application.c:298 msgid "Print debugging messages of the feed update processing" msgstr "" "Amosar mensaxes de depuración tocante al procesu d'anovamientu de canales" #: ../src/liferea_application.c:299 #, fuzzy msgid "Print debugging messages of the search folder matching" msgstr "" "Amosar mensaxes de depuración tocante a remanar l'área d'almacenamientu" #: ../src/liferea_application.c:300 msgid "Print verbose debugging messages" msgstr "Amosar información estra nos mensaxes de depuración" #: ../src/liferea_application.c:305 ../src/liferea_application.c:306 msgid "Print debugging messages for the given topic" msgstr "Amosar mensaxes de depuración tocante al tema especificáu" #. Some libsoup transport errors #: ../src/net.c:437 msgid "The update request was cancelled" msgstr "L'anovamientu solicitáu encaboxóse" #: ../src/net.c:438 msgid "Unable to resolve destination host name" msgstr "Nun pudo resolvese'l nome del sirvidor remotu" #: ../src/net.c:439 msgid "Unable to resolve proxy host name" msgstr "Nun pudo resolvese'l nome del sirvidor proxy" #: ../src/net.c:440 msgid "Unable to connect to remote host" msgstr "Nun pudo coneutase col sirvidor" #: ../src/net.c:441 msgid "Unable to connect to proxy" msgstr "Nun foi dable coneutar col proxy" #: ../src/net.c:442 msgid "" "SSL/TLS negotiation failed. Possible outdated or unsupported encryption " "algorithm. Check your operating system settings." msgstr "" #. http 3xx redirection #: ../src/net.c:445 msgid "The resource moved permanently to a new location" msgstr "L'elementu solicitáu camudó permanentemente d'allugamientu" #. http 4xx client error #: ../src/net.c:448 msgid "" "You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box" msgstr "" "Nun tas autorizáu pa ver esta canal. Por favor, especifica'l nome d'usuariu " "y la contraseña nel cuadru de propiedaes de la canal." #: ../src/net.c:450 msgid "Payment required" msgstr "Requierse Pagu" #: ../src/net.c:451 msgid "You're not allowed to access this resource" msgstr "Nun tienes permisu p'acceder a esti elementu" #: ../src/net.c:452 msgid "Resource Not Found" msgstr "Recursu non atopáu" #: ../src/net.c:453 msgid "Method Not Allowed" msgstr "Métodu non permitíu" #: ../src/net.c:454 msgid "Not Acceptable" msgstr "Non aceutable" #: ../src/net.c:455 msgid "Proxy authentication required" msgstr "El proxy requier autenticación" #: ../src/net.c:456 msgid "Request timed out" msgstr "Espiró'l tiempu de rempuesta" #: ../src/net.c:457 #, fuzzy msgid "" "The webserver indicates this feed is discontinued. It's no longer available. " "Liferea won't update it anymore but you can still access the cached " "headlines." msgstr "" "Esta canal foi abandonada y ya nun tá algamable. Liferea nun va anovala pero " "puedes siguir accediendo a los sos artículos atroxaos." #: ../src/net.c:462 msgid "There was an internal error in the update process" msgstr "Asocedió un fallu internu durante l'anovamientu" #: ../src/net.c:464 msgid "Feed not available: Server requested unsupported redirection!" msgstr "" "Canal nun disponible: El sirvidor solicitó una redireición desconocida." #: ../src/net.c:466 msgid "Client Error" msgstr "Fallu del veceru" #: ../src/net.c:468 msgid "Server Error" msgstr "Fallu del sirvidor" #: ../src/net.c:470 msgid "An unknown networking error happened!" msgstr "Hebo un fallu de rede desconocíu" #: ../src/parsers/atom10.c:239 msgid "Website" msgstr "Sitiu web" #: ../src/parsers/ns_ag.c:70 msgid "%b %d %H:%M" msgstr "%b %d %H:%M" #. in-memory check function feedlist.opml rule id rule menu label positive menu option negative menu option has param #. ======================================================================================================================================================================================== #: ../src/rule.c:229 msgid "Item" msgstr "L'artículu" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does contain" msgstr "contién" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does not contain" msgstr "nun contién" #: ../src/rule.c:230 msgid "Item title" msgstr "El títulu" #: ../src/rule.c:231 msgid "Item body" msgstr "El cuerpu del artículu" #: ../src/rule.c:232 msgid "Read status" msgstr "L'estáu de llectura" #: ../src/rule.c:232 msgid "is unread" msgstr "ye nuevu" #: ../src/rule.c:232 msgid "is read" msgstr "ye lleíu" #: ../src/rule.c:233 msgid "Flag status" msgstr "La etiqueta" #: ../src/rule.c:233 msgid "is flagged" msgstr "ta activada" #: ../src/rule.c:233 msgid "is unflagged" msgstr "ta desactivada" #: ../src/rule.c:234 msgid "Podcast" msgstr "Podcast" #: ../src/rule.c:234 msgid "included" msgstr "incluyíu" #: ../src/rule.c:234 msgid "not included" msgstr "non incluyíu" #: ../src/rule.c:235 msgid "Category" msgstr "Categoría" #: ../src/rule.c:235 msgid "is set" msgstr "ta afitada" #: ../src/rule.c:235 msgid "is not set" msgstr "nun ta afitada" #: ../src/rule.c:236 msgid "Feed title" msgstr "El títulu de la canal" #: ../src/rule.c:237 #, fuzzy msgid "Feed source" msgstr "Orixe de la canal" #: ../src/rule.c:238 msgid "Parent folder title" msgstr "" #: ../src/subscription.c:108 #, c-format msgid "Subscription \"%s\" is already being updated!" msgstr "La canal \"%s\" yá ta anovándose!" #: ../src/subscription.c:113 #, c-format msgid "" "The subscription \"%s\" was discontinued. Liferea won't update it anymore!" msgstr "La canal \"%s\" yá nun esiste. Liferea nun volverá a anovala." #: ../src/subscription.c:188 #, c-format msgid "The URL of \"%s\" has changed permanently and was updated" msgstr "La URL de \"%s\" camudó permanentemente y foi anovada" #: ../src/subscription.c:204 #, c-format msgid "\"%s\" is discontinued. Liferea won't updated it anymore!" msgstr "\"%s\" Abandonóse. Liferea yá nun va anovala." #: ../src/subscription.c:208 #, c-format msgid "\"%s\" has not changed since last update" msgstr "El conteníu de \"%s\" nun camudó dende'l caberu anovamientu" #: ../src/subscription.c:221 ../src/subscription.c:296 #, fuzzy, c-format msgid "Updating (%d / %d) ..." msgstr "Anovando..." #: ../src/subscription.c:298 #, fuzzy, c-format msgid "Updating '%s'..." msgstr "Anovando..." #: ../src/ui/auth_dialog.c:114 #, c-format msgid "Enter the username and password for \"%s\" (%s):" msgstr "Escribi'l nome d'usuariu y la contraseña pa \"%s\" (%s):" #: ../src/ui/auth_dialog.c:116 msgid "Unknown source" msgstr "Fonte desconocida" #: ../src/ui/browser_tabs.c:262 msgid "Untitled" msgstr "" #: ../src/ui/enclosure_list_view.c:168 msgid "Attachments" msgstr "Axuntos" #. The following literals are the enclosure list size units #: ../src/ui/enclosure_list_view.c:260 msgid " Bytes" msgstr " Bytes" #: ../src/ui/enclosure_list_view.c:263 msgid "kB" msgstr "kB" #: ../src/ui/enclosure_list_view.c:267 msgid "MB" msgstr "MB" #: ../src/ui/enclosure_list_view.c:271 msgid "GB" msgstr "GB" #: ../src/ui/enclosure_list_view.c:275 #, c-format msgid "%d%s" msgstr "%d%s" #. update list title #: ../src/ui/enclosure_list_view.c:313 #, c-format msgid "%d attachment" msgid_plural "%d attachments" msgstr[0] "%d axuntu" msgstr[1] "%d axuntos" #: ../src/ui/enclosure_list_view.c:402 ../src/ui/subscription_dialog.c:355 msgid "Choose File" msgstr "Escoyer Ficheru" #: ../src/ui/enclosure_list_view.c:463 #, c-format msgid "File Extension .%s" msgstr "Estensión de ficheru .%s" #: ../src/ui/feed_list_view.c:432 msgid "Liferea is in offline mode. No update possible." msgstr "Liferea ta trabayando ensin conexón. Nun pueden anovase les fontes" #: ../src/ui/feed_list_view.c:478 #, fuzzy msgid "all feeds" msgstr "Guetar en toles canales" #: ../src/ui/feed_list_view.c:479 #, fuzzy, c-format msgid "Mark %s as read ?" msgstr "Conseñar too como lleíu" #: ../src/ui/feed_list_view.c:483 #, fuzzy, c-format msgid "Are you sure you want to mark all items in %s as read ?" msgstr "¿Daveres quies desaniciar \"%s\"?" #: ../src/ui/feed_list_view.c:621 msgid "(Empty)" msgstr "" #: ../src/ui/feed_list_view.c:832 #, c-format msgid "" "%s\n" "Rebuilding" msgstr "" #: ../src/ui/feed_list_view.c:901 msgid "Deleting entry" msgstr "Desaniciando entrada" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\" and its contents?" msgstr "¿Daveres quies desaniciar \"%s\" y tolos sos conteníos?" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\"?" msgstr "¿Daveres quies desaniciar \"%s\"?" #: ../src/ui/feed_list_view.c:911 ../src/ui/feed_list_view.c:965 #, fuzzy msgid "_Cancel" msgstr "Encaboxar _too" #: ../src/ui/feed_list_view.c:912 ../src/ui/popup_menu.c:335 msgid "_Delete" msgstr "_Desaniciar" #: ../src/ui/feed_list_view.c:914 msgid "Deletion Confirmation" msgstr "Confirmar Desaniciu" #: ../src/ui/feed_list_view.c:953 #, c-format msgid "" "Are you sure that you want to add a new subscription with URL \"%s\"? " "Another subscription with the same URL already exists (\"%s\")." msgstr "" #: ../src/ui/feed_list_view.c:966 msgid "_Add" msgstr "" #: ../src/ui/feed_list_view.c:968 #, fuzzy msgid "Adding Duplicate Subscription Confirmation" msgstr "Confirmar Desaniciu" #: ../src/ui/icons.c:54 #, c-format msgid "Couldn't find pixmap file: %s" msgstr "Nun pudo atopase la imaxe: %s" #: ../src/ui/item_list_view.c:115 msgid "This item has no link specified!" msgstr "¡Esti artículu nun tien un enllaz asignáu!" #: ../src/ui/item_list_view.c:482 msgid "*** No title ***" msgstr "*** Ensin títulu ***" #: ../src/ui/item_list_view.c:486 msgid " important " msgstr "" #: ../src/ui/item_list_view.c:849 msgid "Headline" msgstr "Titular" #: ../src/ui/item_list_view.c:871 msgid "Date" msgstr "Data" #: ../src/ui/item_list_view.c:1038 msgid "You must select a feed to delete its items!" msgstr "¡Seleiciona una canal pa desaniciar los sos artículos!" #: ../src/ui/item_list_view.c:1054 ../src/ui/item_list_view.c:1132 #: ../src/ui/item_list_view.c:1147 msgid "No item has been selected" msgstr "Nun se seleicionó dengún artículu" #: ../src/ui/liferea_shell.c:408 #, c-format msgid " (%d new)" msgid_plural " (%d new)" msgstr[0] " (%d nuevu)" msgstr[1] " (%d nuevos)" #: ../src/ui/liferea_shell.c:413 #, c-format msgid "%d unread%s" msgid_plural "%d unread%s" msgstr[0] "%d non lleíu%s" msgstr[1] "%d non lleíos%s" #: ../src/ui/liferea_shell.c:754 msgid "Help Topics" msgstr "Temes d'aida" #: ../src/ui/liferea_shell.c:760 msgid "Quick Reference" msgstr "Manual de referencia" #: ../src/ui/liferea_shell.c:766 msgid "FAQ" msgstr "FAQ" #: ../src/ui/liferea_shell.c:1044 #, fuzzy, c-format msgid "Email command failed: %s" msgstr "Falló al llanzar el restolador: %s" #: ../src/ui/popup_menu.c:102 ../glade/liferea_menu.ui.h:25 msgid "Open In _Tab" msgstr "" #: ../src/ui/popup_menu.c:106 ../glade/liferea_menu.ui.h:26 #, fuzzy msgid "_Open In Browser" msgstr "_Abrir nel restolador" #: ../src/ui/popup_menu.c:110 ../glade/liferea_menu.ui.h:27 #, fuzzy msgid "Open In _External Browser" msgstr "Opciones del restolador esternu" #: ../src/ui/popup_menu.c:115 msgid "Email The Author" msgstr "" #: ../src/ui/popup_menu.c:140 msgid "Copy to News Bin" msgstr "Copiar a una bandexa" #: ../src/ui/popup_menu.c:148 #, fuzzy, c-format msgid "_Bookmark at %s" msgstr "Crear un marcador del enllaz en %s" #: ../src/ui/popup_menu.c:154 #, fuzzy msgid "Copy Item _Location" msgstr "_Copiar direición de la imaxe" #: ../src/ui/popup_menu.c:163 ../glade/liferea_menu.ui.h:22 msgid "Toggle _Read Status" msgstr "_Camudar ente lleíu y non lleíu" #: ../src/ui/popup_menu.c:167 ../glade/liferea_menu.ui.h:23 msgid "Toggle Item _Flag" msgstr "Asignar o quitar _etiqueta" #: ../src/ui/popup_menu.c:171 msgid "R_emove Item" msgstr "_Desaniciar artículu" #: ../src/ui/popup_menu.c:200 msgid "Open Enclosure..." msgstr "Abrir axuntu..." #: ../src/ui/popup_menu.c:201 msgid "Save As..." msgstr "Guardar como..." #: ../src/ui/popup_menu.c:202 msgid "Copy Link Location" msgstr "_Copiar la direición del enllaz" #: ../src/ui/popup_menu.c:280 ../glade/liferea_menu.ui.h:13 msgid "_Update" msgstr "_Anovar" #: ../src/ui/popup_menu.c:282 msgid "_Update Folder" msgstr "_Anovar Carpeta" #: ../src/ui/popup_menu.c:292 msgid "New _Subscription..." msgstr "Nueva _soscripción" #: ../src/ui/popup_menu.c:295 ../glade/liferea_menu.ui.h:5 msgid "New _Folder..." msgstr "Nueva _carpeta..." #: ../src/ui/popup_menu.c:298 ../glade/liferea_menu.ui.h:6 msgid "New S_earch Folder..." msgstr "Nueva carpeta de _gueta..." #: ../src/ui/popup_menu.c:299 msgid "New S_ource..." msgstr "Nueva f_onte..." #: ../src/ui/popup_menu.c:300 ../glade/liferea_menu.ui.h:8 msgid "New _News Bin..." msgstr "Nueva ban_dexa..." #: ../src/ui/popup_menu.c:303 msgid "_New" msgstr "_Nueva" #: ../src/ui/popup_menu.c:312 msgid "Sort Feeds" msgstr "Ordenar canales" #: ../src/ui/popup_menu.c:320 msgid "_Mark All As Read" msgstr "_Conseñar toos como lleíos" #: ../src/ui/popup_menu.c:327 msgid "_Rebuild" msgstr "" #: ../src/ui/popup_menu.c:336 ../glade/liferea_menu.ui.h:17 msgid "_Properties" msgstr "_Propiedaes" #: ../src/ui/popup_menu.c:343 #, fuzzy msgid "Convert To Local Subscriptions..." msgstr "_Nueva soscripción..." #: ../src/ui/preferences_dialog.c:84 msgid "GNOME default" msgstr "Predetermináu de Gnome" #: ../src/ui/preferences_dialog.c:85 msgid "Text below icons" msgstr "Testu embaxo de los iconos" #: ../src/ui/preferences_dialog.c:86 msgid "Text beside icons" msgstr "Testu xunto a los iconos" #: ../src/ui/preferences_dialog.c:87 msgid "Icons only" msgstr "Namái iconos" #: ../src/ui/preferences_dialog.c:88 msgid "Text only" msgstr "Namái testu" #: ../src/ui/preferences_dialog.c:96 ../src/ui/subscription_dialog.c:43 msgid "minutes" msgstr "minutos" #: ../src/ui/preferences_dialog.c:97 ../src/ui/subscription_dialog.c:44 msgid "hours" msgstr "hores" #: ../src/ui/preferences_dialog.c:98 ../src/ui/subscription_dialog.c:45 msgid "days" msgstr "díes" #: ../src/ui/preferences_dialog.c:103 msgid "Space" msgstr "Espaciu" #: ../src/ui/preferences_dialog.c:104 msgid " Space" msgstr " Espaciu" #: ../src/ui/preferences_dialog.c:105 msgid " Space" msgstr " Espaciu" #: ../src/ui/preferences_dialog.c:110 #, fuzzy msgid "Normal View" msgstr "_Vista normal" #: ../src/ui/preferences_dialog.c:111 #, fuzzy msgid "Wide View" msgstr "_Vista enantada" #: ../src/ui/preferences_dialog.c:478 msgid "Default Browser" msgstr "Restolador predefiníu" #: ../src/ui/preferences_dialog.c:480 msgid "Manual" msgstr "Manual" #: ../src/ui/preferences_dialog.c:740 msgid "Type" msgstr "Triba" #: ../src/ui/preferences_dialog.c:743 msgid "Program" msgstr "Programa" #: ../src/ui/search_dialog.c:106 #, fuzzy msgid "Saved Search" msgstr "Gueta Avanzada" #: ../src/ui/subscription_dialog.c:427 #, c-format msgid "The provider of this feed suggests an update interval of %d minute." msgid_plural "" "The provider of this feed suggests an update interval of %d minutes." msgstr[0] "Esta canal suxer un intervalu d'anovamientu de %d minutu." msgstr[1] "Esta canal suxer un intervalu d'anovamientu de %d minutos." #: ../src/ui/subscription_dialog.c:431 msgid "This feed specifies no default update interval." msgstr "Esta canal nun especifica un intervalu d'anovamientos predetermináu." #: ../src/ui/ui_common.c:206 msgid "All Files" msgstr "Tolos ficheros" #: ../src/update.c:350 #, c-format msgid "Error opening temp file %s to use for filtering!" msgstr "Prodúxose un fallu al abrir el ficheru temporal %s pal filtráu" #: ../src/update.c:372 #, c-format msgid "%s exited with status %d" msgstr "%s finóse col estáu %d" #: ../src/update.c:378 ../src/update.c:379 ../src/update.c:493 #, c-format msgid "Error: Could not open pipe \"%s\"" msgstr "Nun pudo abrise la tubería \"%s\"" #. FIXME: maybe setting request->returncode would be better #: ../src/update.c:517 #, c-format msgid "Error: Could not open file \"%s\"" msgstr "Fallu: Nun pudo abrise'l ficheru \"%s\"" #: ../src/update.c:523 #, c-format msgid "Error: There is no file \"%s\"" msgstr "Fallu: Nun esiste'l ficheru \"%s\"" #: ../src/vfolder.c:54 msgid "New Search Folder" msgstr "Nueva carpeta de gueta" #: ../src/webkit/liferea_web_view.c:177 #, fuzzy msgid "Open Link In _Tab" msgstr "Abrir l'enllaz nuna lli_ngüeta nueva" #: ../src/webkit/liferea_web_view.c:178 #, fuzzy msgid "Open Link In Browser" msgstr "Abrir l'enllaz n_el restolador" #: ../src/webkit/liferea_web_view.c:179 #, fuzzy msgid "Open Link In External Browser" msgstr "Abrir l'enllaz n_el restolador" #: ../src/webkit/liferea_web_view.c:185 #, c-format msgid "_Bookmark Link at %s" msgstr "Crear un marcador del enllaz en %s" #: ../src/webkit/liferea_web_view.c:192 msgid "_Copy Link Location" msgstr "_Copiar direición del enllaz" #: ../src/webkit/liferea_web_view.c:195 #, fuzzy msgid "_View Image" msgstr "G_uarda imaxe como" #: ../src/webkit/liferea_web_view.c:196 msgid "_Copy Image Location" msgstr "_Copiar direición de la imaxe" #: ../src/webkit/liferea_web_view.c:199 msgid "S_ave Link As" msgstr "Guardar enllaz c_omo" #: ../src/webkit/liferea_web_view.c:202 msgid "S_ave Image As" msgstr "G_uarda imaxe como" #: ../src/webkit/liferea_web_view.c:209 msgid "_Subscribe..." msgstr "_Soscribise..." #: ../src/webkit/liferea_web_view.c:213 msgid "_Copy" msgstr "" #: ../src/webkit/liferea_web_view.c:219 msgid "_Increase Text Size" msgstr "_Aumentar el tamañu del testu" #: ../src/webkit/liferea_web_view.c:220 msgid "_Decrease Text Size" msgstr "_Amenorgar el tamañu del testu" #: ../src/webkit/liferea_web_view.c:227 msgid "_Reader Mode" msgstr "" #: ../src/xml.c:426 msgid "[There were more errors. Output was truncated!]" msgstr "[Hai más fallos. ¡La salida truncóse!]" #: ../src/xml.c:594 msgid "XML Parser: Could not parse document:\n" msgstr "XML Parser: Nun pudo procesase'l documentu:\n" #: ../glade/about.ui.h:1 msgid "About" msgstr "Tocante a" #: ../glade/about.ui.h:2 msgid "Liferea is a news aggregator for GTK+" msgstr "Liferea ye un llector de noticies pa GTK+" #: ../glade/about.ui.h:3 msgid "Liferea Homepage" msgstr "Sitiu web de Liferea" #: ../glade/auth.ui.h:1 msgid "Authentication" msgstr "Autenticación" #: ../glade/auth.ui.h:3 #, fuzzy, no-c-format msgid "Enter the username and password for \"%s\" (%s)" msgstr "Escribi'l nome d'usuariu y la contraseña pa \"%s\" (%s):" #: ../glade/auth.ui.h:4 ../glade/properties.ui.h:31 msgid "User_name:" msgstr "_Nome d'usuariu:" #: ../glade/auth.ui.h:5 ../glade/properties.ui.h:32 msgid "_Password:" msgstr "Contraseña:" #: ../glade/enclosure_handler.ui.h:1 #, fuzzy msgid "Open Enclosure" msgstr "Abrir axuntu..." #: ../glade/enclosure_handler.ui.h:2 #, fuzzy msgid "Open an enclosure of type:" msgstr "Baxando un axuntu de triba:" #: ../glade/enclosure_handler.ui.h:3 #, fuzzy msgid "" "_What should Liferea do with this enclosure? Please enter the command you " "want to be executed below. The enclosures URL will be supplied as an " "argument for this command:" msgstr "" "¿Qué tien de facer Liferea con esti axuntu? Escribi debaxo la orde que quies " "executar. El nome del ficheru descargáu va usase como parámetru pa la orde:" #: ../glade/enclosure_handler.ui.h:4 msgid "_Browse" msgstr "Restolar" #: ../glade/enclosure_handler.ui.h:5 msgid "_Do this automatically for enclosures like this from now on." msgstr "Facer lo mesmo _de mou automáticu pa esta triba de ficheros." #: ../glade/liferea_menu.ui.h:1 msgid "_Subscriptions" msgstr "_Soscripciones" #: ../glade/liferea_menu.ui.h:2 ../glade/liferea_toolbar.ui.h:8 msgid "Update _All" msgstr "_Anovar toes" #: ../glade/liferea_menu.ui.h:3 msgid "Mark All As _Read" msgstr "Conseñar _too como lleío" #: ../glade/liferea_menu.ui.h:4 ../glade/liferea_toolbar.ui.h:1 msgid "_New Subscription..." msgstr "_Nueva soscripción..." #: ../glade/liferea_menu.ui.h:7 msgid "New _Source..." msgstr "Nueva _fonte..." #: ../glade/liferea_menu.ui.h:9 msgid "_Import Feed List..." msgstr "_Importar llista de canales..." #: ../glade/liferea_menu.ui.h:10 msgid "_Export Feed List..." msgstr "_Esportar llista de canales..." #: ../glade/liferea_menu.ui.h:11 msgid "_Quit" msgstr "_Colar" #: ../glade/liferea_menu.ui.h:12 msgid "_Feed" msgstr "_Canal" #: ../glade/liferea_menu.ui.h:15 msgid "Remove _All Items" msgstr "Desanici_ar tolos elementos" #: ../glade/liferea_menu.ui.h:16 msgid "_Remove" msgstr "D_esaniciar" #: ../glade/liferea_menu.ui.h:18 msgid "_Item" msgstr "_Elementu" #: ../glade/liferea_menu.ui.h:24 msgid "R_emove" msgstr "D_esaniciar" #: ../glade/liferea_menu.ui.h:28 msgid "_View" msgstr "_Ver" #: ../glade/liferea_menu.ui.h:29 msgid "_Fullscreen" msgstr "" #: ../glade/liferea_menu.ui.h:30 msgid "Zoom _In" msgstr "" #: ../glade/liferea_menu.ui.h:31 msgid "Zoom _Out" msgstr "" #: ../glade/liferea_menu.ui.h:32 #, fuzzy msgid "_Normal size" msgstr "_Vista normal" #: ../glade/liferea_menu.ui.h:33 msgid "_Normal View" msgstr "_Vista normal" #: ../glade/liferea_menu.ui.h:34 msgid "_Wide View" msgstr "_Vista enantada" #: ../glade/liferea_menu.ui.h:35 msgid "_Reduced Feed List" msgstr "Llista de canales _amenorgada" #: ../glade/liferea_menu.ui.h:36 msgid "_Tools" msgstr "_Ferramientes" #: ../glade/liferea_menu.ui.h:37 msgid "_Update Monitor" msgstr "Monitor d'anovamiento_s" #: ../glade/liferea_menu.ui.h:38 msgid "_Preferences" msgstr "_Preferencies" #: ../glade/liferea_menu.ui.h:39 msgid "S_earch" msgstr "Gu_etar" #: ../glade/liferea_menu.ui.h:41 msgid "_Help" msgstr "A_ida" #: ../glade/liferea_menu.ui.h:42 msgid "_Contents" msgstr "_Conteníos" #: ../glade/liferea_menu.ui.h:43 msgid "_Quick Reference" msgstr "_Referencia rápida" #: ../glade/liferea_menu.ui.h:44 msgid "_FAQ" msgstr "_FAQ" #: ../glade/liferea_menu.ui.h:45 msgid "_About" msgstr "_Tocante a" #: ../glade/liferea_toolbar.ui.h:2 msgid "Adds a subscription to the feed list." msgstr "Axuntar una soscripción a la llista de canales." #: ../glade/liferea_toolbar.ui.h:4 msgid "" "Marks all items of the selected feed list node / in the item list as read." msgstr "" "Conseña como lleíos tolos artículos de la canal esbillada o, nel casu d'una " "carpeta de tolos sos canales." #: ../glade/liferea_toolbar.ui.h:9 msgid "Updates all subscriptions." msgstr "Anova toles soscripciones" #: ../glade/liferea_toolbar.ui.h:11 msgid "Show the search dialog." msgstr "Amosar el ventanu de gueta." #: ../glade/mainwindow.ui.h:2 msgid "page 1" msgstr "" #: ../glade/mainwindow.ui.h:3 msgid "page 2" msgstr "" #: ../glade/mainwindow.ui.h:4 ../glade/prefs.ui.h:23 msgid "Headlines" msgstr "Titulares" #: ../glade/mark_read_dialog.ui.h:1 #, fuzzy msgid "Mark all as read ?" msgstr "Conseñar too como lleíu" #: ../glade/mark_read_dialog.ui.h:2 msgid "Mark all as read" msgstr "Conseñar too como lleíu" #: ../glade/mark_read_dialog.ui.h:3 msgid "Do not ask again" msgstr "" #: ../glade/new_folder.ui.h:1 msgid "New Folder" msgstr "Nueva carpeta" #: ../glade/new_folder.ui.h:2 msgid "_Folder name:" msgstr "_Nome de la carpeta:" #: ../glade/new_newsbin.ui.h:1 msgid "Create News Bin" msgstr "Crea una bandexa de noticies" #: ../glade/new_newsbin.ui.h:2 msgid "_News Bin Name:" msgstr "_Nome de la bandexa de noticies:" #: ../glade/new_subscription.ui.h:2 ../glade/properties.ui.h:11 msgid "Feed Source" msgstr "Orixe de la canal" #: ../glade/new_subscription.ui.h:3 ../glade/properties.ui.h:12 msgid "Source Type:" msgstr "Mou d'accesu:" #: ../glade/new_subscription.ui.h:4 ../glade/properties.ui.h:13 msgid "_URL" msgstr "_URL" #: ../glade/new_subscription.ui.h:5 ../glade/properties.ui.h:14 msgid "_Command" msgstr "_Comandu" #: ../glade/new_subscription.ui.h:6 ../glade/properties.ui.h:15 msgid "_Local File" msgstr "Ficheru _llocal" #: ../glade/new_subscription.ui.h:7 ../glade/properties.ui.h:16 msgid "Select File..." msgstr "Restolar Ficheru..." #: ../glade/new_subscription.ui.h:8 ../glade/properties.ui.h:17 msgid "_Source:" msgstr "Orixe:" #: ../glade/new_subscription.ui.h:9 msgid "Download / Postprocessing" msgstr "Baxada / Postprocesamientu" #: ../glade/new_subscription.ui.h:10 ../glade/properties.ui.h:30 msgid "_Don't use proxy for download" msgstr "Nu_n usar proxy pa baxaes" #: ../glade/new_subscription.ui.h:11 ../glade/properties.ui.h:18 msgid "Use conversion _filter" msgstr "Usar _peñera de charra" #: ../glade/new_subscription.ui.h:12 msgid "" "Liferea can use external filter plugins in order to access feeds and " "directories in non-supported formats. See the documentation for more " "information." msgstr "" "Liferea puede usar peñeres esternes p'acceder a canales y direutorios en " "formatos que nun tean sofitaos. Por favor, llee la documentación pa más " "información." #: ../glade/new_subscription.ui.h:13 ../glade/properties.ui.h:20 msgid "Convert _using:" msgstr "Convertir _usando:" #: ../glade/node_source.ui.h:1 msgid "Source Selection" msgstr "Seleición de fonte" #: ../glade/node_source.ui.h:2 #, fuzzy msgid "_Select the source type you want to add..." msgstr "Seleiciona la triba de fonte que quies amestar..." #: ../glade/opml_source.ui.h:1 msgid "Add OPML/Planet" msgstr "Axuntar OPML/Planeta" #: ../glade/opml_source.ui.h:2 msgid "" "Please specify a local file or an URL pointing to a valid OPML feed list." msgstr "" "Indica un ficheru llocal o una URL qu'apunte a una llista de canales en " "formatu OPML." #: ../glade/opml_source.ui.h:3 msgid "_Location" msgstr "A_llugamientu" #: ../glade/opml_source.ui.h:4 msgid "_Select File" msgstr "_Seleicionar Ficheru" #: ../glade/prefs.ui.h:1 msgid "Liferea Preferences" msgstr "Preferencies" #: ../glade/prefs.ui.h:2 msgid "Feed Cache Handling" msgstr "" #: ../glade/prefs.ui.h:3 msgid "Default _number of items per feed to save:" msgstr "_Númberu d'artículos de cada canal que van atroxase:" #: ../glade/prefs.ui.h:4 ../glade/properties.ui.h:27 msgid "0" msgstr "" #: ../glade/prefs.ui.h:5 msgid "Feed Update Settings" msgstr "Axustes d'anovamientu de canal" #. Feed update interval hint in preference dialog. #: ../glade/prefs.ui.h:7 msgid "" "Note: Please remember to set a reasonable refresh time. Usually it is a " "waste of bandwidth to poll feeds more often than each hour." msgstr "" "Nota: Por favor, usa un tiempu razonable. Guetar anovamientos más d'una " "vez por hora ye, casi siempres, un desperdiciu d'anchu de banda." #: ../glade/prefs.ui.h:8 #, fuzzy msgid "_Update all subscriptions at startup." msgstr "Anova toles soscripciones" #: ../glade/prefs.ui.h:9 msgid "Default Feed Refresh _Interval:" msgstr "_Intervalu d'anovamientu predetermináu" #: ../glade/prefs.ui.h:10 ../glade/properties.ui.h:6 msgid "1" msgstr "" #: ../glade/prefs.ui.h:11 msgid "Feeds" msgstr "Canales" #: ../glade/prefs.ui.h:12 #, fuzzy msgid "Folder Display Settings" msgstr "Opciones de presentación de carpetes" #: ../glade/prefs.ui.h:13 msgid "_Show the items of all child feeds when a folder is selected." msgstr "Amo_sar los elementos de toles fontes fíes al seleicionar una carpeta." #: ../glade/prefs.ui.h:14 msgid "_Hide read items." msgstr "_Anubrir elementos lleíos." #: ../glade/prefs.ui.h:15 msgid "Feed Icons (Favicons)" msgstr "Iconos de los canales (Favicons)" #: ../glade/prefs.ui.h:16 msgid "_Update all favicons now" msgstr "_Anovar tolos favicons agora" #: ../glade/prefs.ui.h:17 msgid "Folders" msgstr "Carpetes" #: ../glade/prefs.ui.h:18 msgid "Reading Headlines" msgstr "Leendo titulares" #: ../glade/prefs.ui.h:19 msgid "_Skim through articles with:" msgstr "_Saltar a artículos con:" #: ../glade/prefs.ui.h:20 msgid "_Default View Mode:" msgstr "" #: ../glade/prefs.ui.h:21 msgid "Web Integration" msgstr "Integración web" #: ../glade/prefs.ui.h:22 msgid "_Post Bookmarks to" msgstr "_Unviar los marcadores a" #: ../glade/prefs.ui.h:24 msgid "Internal Browser Settings" msgstr "Axustes del restolador internu" #: ../glade/prefs.ui.h:25 msgid "Open links in Liferea's _window." msgstr "Abrir los enllaces na ventana de Liferea." #: ../glade/prefs.ui.h:26 msgid "_Never run external Javascript." msgstr "" #: ../glade/prefs.ui.h:27 msgid "_Enable browser plugins." msgstr "Habilitar «plugins» del restolador." #: ../glade/prefs.ui.h:28 #, fuzzy msgid "External Browser Settings" msgstr "Opciones del restolador esternu" #: ../glade/prefs.ui.h:29 msgid "_Browser:" msgstr "Restolador:" #: ../glade/prefs.ui.h:30 #, fuzzy msgid "_Manual:" msgstr "Manual" #: ../glade/prefs.ui.h:32 #, no-c-format msgid "(%s for URL)" msgstr "" #: ../glade/prefs.ui.h:33 msgid "Browser" msgstr "Restolador" #: ../glade/prefs.ui.h:34 msgid "Toolbar Settings" msgstr "Axustes de la barra de ferramientes" #: ../glade/prefs.ui.h:35 msgid "_Hide toolbar." msgstr "Anubrir la barra de ferramientes." #: ../glade/prefs.ui.h:36 msgid "Toolbar _button labels:" msgstr "Etiquetes de los _botones de la barra de ferramientes:" #: ../glade/prefs.ui.h:37 msgid "Other" msgstr "" #: ../glade/prefs.ui.h:38 msgid "Ask for confirmation when marking all items as read" msgstr "" #: ../glade/prefs.ui.h:39 msgid "Desktop" msgstr "" #: ../glade/prefs.ui.h:40 msgid "HTTP Proxy Server" msgstr "HTTP Proxy Server" #: ../glade/prefs.ui.h:41 msgid "_Auto Detect (GNOME or environment)" msgstr "_Auto deteición (GNOME o entornu)" #: ../glade/prefs.ui.h:42 msgid "_No Proxy" msgstr "Ensin proxy" #: ../glade/prefs.ui.h:43 msgid "_Manual Setting:" msgstr "Configuración _manual:" #: ../glade/prefs.ui.h:44 msgid "Proxy _Host:" msgstr "Sirvidor proxy:" #: ../glade/prefs.ui.h:45 msgid "Proxy _Port:" msgstr "_Puertu del proxy:" #: ../glade/prefs.ui.h:46 msgid "Use Proxy Au_thentication" msgstr "_Identificase énte'l proxy" #: ../glade/prefs.ui.h:47 msgid "Proxy _Username:" msgstr "Nome d'_usuariu nel proxy:" #: ../glade/prefs.ui.h:48 msgid "Proxy Pass_word:" msgstr "Contraseña del proxy:" #: ../glade/prefs.ui.h:49 msgid "" "Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per " "application proxy settings. The system's default proxy settings will be used." msgstr "" #: ../glade/prefs.ui.h:50 msgid "Proxy" msgstr "Proxy" #: ../glade/prefs.ui.h:51 #, fuzzy msgid "Privacy Settings" msgstr "Opciones de presentación de carpetes" #: ../glade/prefs.ui.h:52 msgid "Tell sites that I do _not want to be tracked." msgstr "" #: ../glade/prefs.ui.h:53 msgid "_Intelligent Tracking Prevention. " msgstr "" #: ../glade/prefs.ui.h:54 msgid "" "This enables the WebKit feature described here." msgstr "" #: ../glade/prefs.ui.h:55 msgid "" "Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or " "higher." msgstr "" #: ../glade/prefs.ui.h:56 msgid "Use _Reader mode." msgstr "" #: ../glade/prefs.ui.h:57 msgid "" "This enables stripping all non-content elements (like scripts, fonts, tracking)" msgstr "" #: ../glade/prefs.ui.h:58 msgid "Privacy" msgstr "" #: ../glade/prefs.ui.h:59 #, fuzzy msgid "Downloading Enclosures" msgstr "Baxando axuntos" #: ../glade/prefs.ui.h:60 msgid "_Download using" msgstr "Baxar usan_do" #: ../glade/prefs.ui.h:62 #, no-c-format msgid "custom-command %s" msgstr "" #: ../glade/prefs.ui.h:63 #, fuzzy msgid "Opening Enclosures" msgstr "Abrir axuntu..." #: ../glade/prefs.ui.h:64 msgid "Enclosures" msgstr "Axuntos" #: ../glade/properties.ui.h:1 msgid "Subscription Properties" msgstr "Propiedaes de la fonte" #: ../glade/properties.ui.h:2 #, fuzzy msgid "Feed _Name" msgstr "_Nome de la canal:" #: ../glade/properties.ui.h:3 #, fuzzy msgid "Update _Interval" msgstr "Intervalu anovamientu" #: ../glade/properties.ui.h:4 msgid "_Use global default update interval." msgstr "_Usar l'intervalu establecíu globalmente." #: ../glade/properties.ui.h:5 msgid "_Feed specific update interval of" msgstr "Afitar un intervalu especí_ficu de" #: ../glade/properties.ui.h:7 msgid "_Don't update this feed automatically." msgstr "Nun anovar automáticamente esta canal" #: ../glade/properties.ui.h:9 #, no-c-format msgid "This feed provider suggests an update interval of %d minutes." msgstr "" "El fornidor d'conteníu d'esta canal suxier un intervalu d'anovación de %d " "minutos." #: ../glade/properties.ui.h:10 msgid "General" msgstr "Xeneral" #: ../glade/properties.ui.h:19 #, fuzzy msgid "" "Liferea can use external filter scripts in order to access feeds and " "directories in non-supported formats." msgstr "" "Liferea puede usar peñeres esternes p'acceder a canales y direutorios en " "formatos que nun tean sofitaos. Por favor, llee la documentación pa más " "información." #: ../glade/properties.ui.h:21 ../xslt/item.xml.in.h:1 msgid "Source" msgstr "Fonte" #: ../glade/properties.ui.h:22 msgid "" "The cache setting controls if the contents of feeds are saved when Liferea " "exits. Marked items are always saved to the cache." msgstr "" "La opción d'almacenaxe remana si los artículos de la canal s'atroxen en " "discu cuando Liferea fina. Ensin importar les opciones, los artículos con " "etiqueta atróxense siempres." #: ../glade/properties.ui.h:23 msgid "_Default cache settings" msgstr "Usar la configuración _xeneral" #: ../glade/properties.ui.h:24 msgid "Di_sable cache" msgstr "_Desactivar almacenamientu" #: ../glade/properties.ui.h:25 msgid "_Unlimited cache" msgstr "Almacenamientu _illimitáu" #: ../glade/properties.ui.h:26 msgid "_Number of items to save:" msgstr "_Númberu d'artículos a atroxar:" #: ../glade/properties.ui.h:28 msgid "Archive" msgstr "Archivu" #: ../glade/properties.ui.h:29 msgid "Use HTTP _authentication" msgstr "Usar _autenticación HTTP" #: ../glade/properties.ui.h:33 msgid "Download" msgstr "Baxar" #: ../glade/properties.ui.h:34 msgid "_Automatically download all enclosures of this feed." msgstr "Baxar _automáticamente tolos axuntos d'esta canal." #: ../glade/properties.ui.h:35 msgid "Auto-_load item link in configured browser when selecting articles." msgstr "" "Cargar automáticamente nel restolador internu l'en_llaz de los artículos " "seleicionaos." #: ../glade/properties.ui.h:36 msgid "Ignore _comment feeds for this subscription." msgstr "Inorar les canales de comentarios d'esta soscripción." #: ../glade/properties.ui.h:37 msgid "_Mark downloaded items as read." msgstr "_Conseñar elementos descargaos como lleíos." #: ../glade/properties.ui.h:38 msgid "Extract full content from HTML5 and Google AMP" msgstr "" #: ../glade/reedah_source.ui.h:1 #, fuzzy msgid "Add Reedah Account" msgstr "Axuntar cuenta de Google Reader" #: ../glade/reedah_source.ui.h:2 #, fuzzy msgid "Please enter your Reedah account settings." msgstr "Por favor escribi los datos de la to cuenta de Google Reader." #: ../glade/reedah_source.ui.h:3 ../glade/theoldreader_source.ui.h:3 #: ../glade/ttrss_source.ui.h:4 msgid "_Password" msgstr "Cont_raseña" #: ../glade/reedah_source.ui.h:4 ../glade/theoldreader_source.ui.h:4 msgid "_Username (Email)" msgstr "Nome d'_usuariu (Email)" #: ../glade/rename_node.ui.h:1 msgid "Rename" msgstr "Renomar" #: ../glade/rename_node.ui.h:2 msgid "_New Name:" msgstr "_Nuevu nome:" #: ../glade/search_folder.ui.h:1 msgid "Search Folder Properties" msgstr "Propiedaes de la carpeta de gueta" #: ../glade/search_folder.ui.h:2 msgid "Search _Name:" msgstr "Guetar _Nome:" #: ../glade/search_folder.ui.h:3 #, fuzzy msgid "Search Rules" msgstr "%d resultáu" #: ../glade/search_folder.ui.h:4 msgid "Rules" msgstr "" #: ../glade/search_folder.ui.h:5 msgid "All rules for this search folder" msgstr "" #: ../glade/search_folder.ui.h:6 #, fuzzy msgid "Rule Matching" msgstr "Cúmpl_ese cualesquier regla" #: ../glade/search_folder.ui.h:7 ../glade/search.ui.h:4 msgid "A_ny Rule Matches" msgstr "Cúmpl_ese cualesquier regla" #: ../glade/search_folder.ui.h:8 ../glade/search.ui.h:5 msgid "_All Rules Must Match" msgstr "_Cúmplense toles regles" #: ../glade/search_folder.ui.h:9 #, fuzzy msgid "Hide read items" msgstr "_Anubrir elementos lleíos." #: ../glade/search.ui.h:1 msgid "Advanced Search" msgstr "Gueta Avanzada" #: ../glade/search.ui.h:2 msgid "_Search Folder..." msgstr "_Guetar carpeta..." #: ../glade/search.ui.h:3 #, fuzzy msgid "Find Items that meet the following criteria" msgstr "Alcontrar artículos colos criterios siguientes" #: ../glade/simple_search.ui.h:1 msgid "Search All Feeds" msgstr "Guetar en toles canales" #: ../glade/simple_search.ui.h:2 msgid "_Advanced..." msgstr "_Avanzáu…" #: ../glade/simple_search.ui.h:3 msgid "" "Starts searching for the specified text in all feeds. The search result will " "appear in the item list." msgstr "" "Gueta'l testu especificáu en tolos canales. El resultáu aparecerá na llista " "d'artículos." #: ../glade/simple_search.ui.h:4 msgid "_Search for:" msgstr "Términu a guetar:" #: ../glade/simple_search.ui.h:5 msgid "" "Enter a search string Liferea should find either in a items title or in its " "content." msgstr "" "Escribi un testu. Liferea va guetalu tanto nos títulos de los artículos como " "nos conteníos." #: ../glade/simple_subscription.ui.h:2 msgid "Advanced..." msgstr "Avanzáu..." #: ../glade/simple_subscription.ui.h:3 #, fuzzy msgid "Feed _Source" msgstr "Orixe de la canal" #: ../glade/simple_subscription.ui.h:4 msgid "" "Enter a website location to use feed autodiscovery or in case you know it " "the exact feed location." msgstr "" "Escribi la direición d'un sitiu web pa usar l'autodescubrimientu o, si la " "conoces, la direición esauta de la canal." #: ../glade/theoldreader_source.ui.h:1 #, fuzzy msgid "Add TheOldReader Account" msgstr "Axuntar cuenta de Google Reader" #: ../glade/theoldreader_source.ui.h:2 #, fuzzy msgid "Please enter your TheOldReader account settings." msgstr "Por favor escribi los datos de la to cuenta de Google Reader." #: ../glade/ttrss_source.ui.h:1 msgid "Add Tiny Tiny RSS Account" msgstr "Amestar cuenta de Tiny Tiny RSS" #: ../glade/ttrss_source.ui.h:2 #, fuzzy msgid "Please enter your TinyTinyRSS account settings." msgstr "Escribi los datos de la to cuenta de Bloglines." #: ../glade/ttrss_source.ui.h:3 msgid "_Server URL" msgstr "_Sirvidor URL" #: ../glade/ttrss_source.ui.h:5 msgid "_Username" msgstr "Nome d'_usuariu" #: ../glade/update_monitor.ui.h:1 msgid "Update Monitor" msgstr "Monitor d'anovamientos" #: ../glade/update_monitor.ui.h:2 msgid "Stop All" msgstr "" #: ../glade/update_monitor.ui.h:3 #, fuzzy msgid "_Pending Requests" msgstr "Solicitúes pendientes" #: ../glade/update_monitor.ui.h:4 #, fuzzy msgid "_Downloading Now" msgstr "Baxando agora" #: ../xslt/feed.xml.in.h:1 msgid "Feed:" msgstr "Canal:" #: ../xslt/feed.xml.in.h:2 ../xslt/source.xml.in.h:1 msgid "Source:" msgstr "Orixe:" #: ../xslt/feed.xml.in.h:3 msgid "Publisher" msgstr "Editor" #: ../xslt/feed.xml.in.h:4 msgid "Copyright" msgstr "Copyright" #: ../xslt/feed.xml.in.h:5 #, fuzzy msgid "There was a problem when fetching this subscription!" msgstr "" "Atopóse un problema al lleer la canal. Por favor, comprueba la URL y la " "salida na consola." #: ../xslt/feed.xml.in.h:6 #, fuzzy msgid "1. Authentication" msgstr "Autenticación" #: ../xslt/feed.xml.in.h:7 #, fuzzy msgid "2. Download" msgstr "Baxar" #: ../xslt/feed.xml.in.h:8 msgid "3. Feed Discovery" msgstr "" #: ../xslt/feed.xml.in.h:9 msgid "4. Parsing" msgstr "" #: ../xslt/feed.xml.in.h:10 #, fuzzy msgid "Details:" msgstr "Detalles" #: ../xslt/feed.xml.in.h:11 msgid "Authentication failed. Please check the credentials and try again!" msgstr "" #: ../xslt/feed.xml.in.h:12 #, fuzzy msgid "There was an error when downloading the feed source:" msgstr "Atopáronse fallos al procesar esta canal." #: ../xslt/feed.xml.in.h:13 #, fuzzy msgid "There was an error when running the feed filter command:" msgstr "Atopáronse fallos al procesar esta canal." #: ../xslt/feed.xml.in.h:14 msgid "" "The source does not point directly to a feed or a webpage with a link to a " "feed!" msgstr "" #: ../xslt/feed.xml.in.h:15 msgid "Sorry, the feed could not be parsed!" msgstr "" #: ../xslt/feed.xml.in.h:16 msgid "You may want to contact the author/webmaster of the feed about this!" msgstr "" #: ../xslt/folder.xml.in.h:1 msgid "Folder:" msgstr "Carpeta:" #: ../xslt/folder.xml.in.h:2 ../xslt/source.xml.in.h:2 msgid "children with" msgstr "fíos con" #: ../xslt/folder.xml.in.h:3 ../xslt/source.xml.in.h:3 #: ../xslt/vfolder.xml.in.h:2 msgid "unread headlines" msgstr "titulares non lleíos" #: ../xslt/item.xml.in.h:2 msgid "Feed" msgstr "Canal" #: ../xslt/item.xml.in.h:3 msgid "Filed under" msgstr "Archiváu en" #: ../xslt/item.xml.in.h:4 msgid "Author" msgstr "Autor" #: ../xslt/item.xml.in.h:5 msgid "Shared by" msgstr "Compartíu por" #: ../xslt/item.xml.in.h:6 msgid "Via" msgstr "Vía" #: ../xslt/item.xml.in.h:7 msgid "Related" msgstr "Rellacionáu" #: ../xslt/item.xml.in.h:8 msgid "Also posted in" msgstr "Tamién escritu en" #: ../xslt/item.xml.in.h:9 msgid "Creator" msgstr "Creador" #: ../xslt/item.xml.in.h:10 msgid "Coordinates" msgstr "" #: ../xslt/item.xml.in.h:11 msgid "Map" msgstr "" #: ../xslt/item.xml.in.h:12 msgid "View count" msgstr "" #: ../xslt/item.xml.in.h:13 msgid "Rating" msgstr "" #: ../xslt/item.xml.in.h:14 msgid "Comments" msgstr "Comentarios" #: ../xslt/item.xml.in.h:15 msgid "Updating..." msgstr "Anovando..." #: ../xslt/item.xml.in.h:16 msgid "Section" msgstr "Seición" #: ../xslt/item.xml.in.h:17 msgid "Department" msgstr "Departamentu" #: ../xslt/newsbin.xml.in.h:1 msgid "News Bin:" msgstr "Bandexa de noticies:" #: ../xslt/newsbin.xml.in.h:2 msgid "" "Add items to this news bin by selecting \"Copy to News Bin\" from the item " "list context menu." msgstr "" "Amestar elementos a esti contenedor de noticies seleicionáu «Copiar al " "contenedor de noticies» dende la llista d'elementos nel menú de contestu." #: ../xslt/vfolder.xml.in.h:1 msgid "Search Folder:" msgstr "Carpeta de Gueta:" #, c-format #~ msgid "\"%s\" is not available" #~ msgstr "\"%s\" nun ta disponible" #, c-format #~ msgid "\"%s\" updated..." #~ msgstr "\"%s\" anovada..." #~ msgid "" #~ "A network error occurred, or the other end closed the connection " #~ "unexpectedly" #~ msgstr "Asocedió un fallu de rede, o el sirvidor remotu peslló la conexón" #~ msgid "" #~ "The last update of this subscription failed!
HTTP error code : " #~ msgstr "" #~ "Falló el caberu anovamientu d'esta fonte

Fallu HTTP : " #~ msgid "Parser Error Details" #~ msgstr "Detalles del fallu d'analís" #~ msgid "There were errors while filtering this feed!" #~ msgstr "Atopáronse fallos al peñerar esta canal." #~ msgid "Filter Error Details" #~ msgstr "Peñerar los detalles de fallos" #~ msgid "" #~ "

Could not detect the type of this feed! Please check if the source " #~ "really points to a resource provided in one of the supported syndication " #~ "formats!

XML Parser Output:
" #~ msgstr "" #~ "

Nun pudo deteutase la triba de la canal. Por favor, comprueba que la " #~ "URL apunta a un documentu con dalgún de los formatos sofitaos.

Salida " #~ "del procesador de XML:
" #~ msgid "" #~ "The URL you want Liferea to subscribe to points to a webpage and the auto " #~ "discovery found no feeds on this page. Maybe this webpage just does not " #~ "support feed auto discovery." #~ msgstr "" #~ "La URL que disti a Liferea apunta a una páxina web, y el mecanismu " #~ "d'autodescubrimientu nun atopó denguna canal de noticies nella. Seique " #~ "esti sitiu nun sofita'l deteutalos automáticamente." #~ msgid "Source points to HTML document." #~ msgstr "La direición apunta a un documentu HTML." #~ msgid "Could not determine the feed type." #~ msgstr "Nun pudo determinase la triba de la canal." #~ msgid "Gone. Resource doesn't exist. Please unsubscribe!" #~ msgstr "Esta fonte yá nun esiste. Por favor, desaníciala." #~ msgid "Updating \"%s\"" #~ msgstr "Anovando: \"%s\"" #~ msgid "XML error while reading feed! Feed \"%s\" could not be loaded!" #~ msgstr "Fallu XML mientres se lleía'l canal \"%s\". ¡Nun se pudo baxar!" #, fuzzy #~ msgid "Combined View" #~ msgstr "Vista _combinada" #~ msgid "_Disable Javascript." #~ msgstr "_Desautivar JavaScript." #~ msgid "GUI" #~ msgstr "Interfaz gráfica" #, fuzzy #~ msgid "Cancel All" #~ msgstr "Encaboxar _too" #~ msgid "Updating favicon for \"%s\"" #~ msgstr "Anovando'l iconu de \"%s\"" #~ msgid "Marks read every item of every subscription." #~ msgstr "Conseña como lleíos tolos artículos de toles soscripciones." #~ msgid "Imports an OPML feed list." #~ msgstr "Importa una llista de canales en formatu OPML." #~ msgid "Exports the feed list as OPML." #~ msgstr "Esporta la llista de canales en formatu OPML." #~ msgid "Removes all items of the currently selected feed." #~ msgstr "Desaniciar tolos artículos de la canal seleccionada." #~ msgid "Increases the text size of the item view." #~ msgstr "Aumenta'l tamañu del testu na vista del artículu." #~ msgid "Decreases the text size of the item view." #~ msgstr "Amenorga'l tamañu del testu de la vista del artículu" #~ msgid "Show a list of all feeds currently in the update queue" #~ msgstr "Amosar llista de tolos canales con anovamientos pendientes" #~ msgid "Edit Preferences." #~ msgstr "Editar Preferencies." #~ msgid "View help for this application." #~ msgstr "Amosa aida pa esta aplicación." #~ msgid "View a list of all Liferea shortcuts." #~ msgstr "Amosar toles combinaciones de tecles de Liferea." #~ msgid "View the FAQ for this application." #~ msgstr "Amosar les entrugues más frecuentes (FAQ) del programa." #~ msgid "Shows an about dialog." #~ msgstr "Amosar el ventanu Tocante a." #~ msgid "Set view mode to mail client mode." #~ msgstr "Visualiza en mou veceru de corréu." #~ msgid "Set view mode to use three vertical panes." #~ msgstr "Afita la visualización usando tres paneles verticales." #~ msgid "_Combined View" #~ msgstr "Vista _combinada" #~ msgid "Set view mode to two pane mode." #~ msgstr "Afita la visualización usando dos paneles." #~ msgid "Hide feeds with no unread items." #~ msgstr "Anubrir los canales ensin artículos nuevos" #~ msgid "Adds a folder to the feed list." #~ msgstr "Axuntar una carpeta a la llista de canales." #~ msgid "Adds a new search folder to the feed list." #~ msgstr "Axuntar una carpeta de gueta a la llista de canales." #~ msgid "Adds a new feed list source." #~ msgstr "Axuntar una nueva fonte de llistes de canales." #~ msgid "Adds a new news bin." #~ msgstr "Axuntar una nueva bandexa de noticies." #~ msgid "" #~ "Updates the selected subscription or all subscriptions of the selected " #~ "folder." #~ msgstr "" #~ "Anovar la canal seleicionáu o, nel casu d'una carpeta, tolos canales que " #~ "caltenga." #~ msgid "Opens the property dialog for the selected subscription." #~ msgstr "Abre'l cuadru de propiedaes de la canal esbillada." #~ msgid "Removes the selected subscription." #~ msgstr "Desaniciar la canal esbillada." #~ msgid "Toggles the read status of the selected item." #~ msgstr "Pasa'l artículu actual de lleíu a non lleíu o viceversa." #~ msgid "Toggles the flag status of the selected item." #~ msgstr "Crea o desanicia una etiqueta nel artículu seleccionáu." #~ msgid "Removes the selected item." #~ msgstr "Desaniciar l'artículu esbilláu." #, fuzzy #~ msgid "Launches the item's link in a new Liferea browser tab." #~ msgstr "Abre l'enllaz del artículu nel restolador." #, fuzzy #~ msgid "Launches the item's link in the Liferea item pane." #~ msgstr "Abre l'enllaz del artículu nel restolador." #, fuzzy #~ msgid "Launches the item's link in the configured external browser." #~ msgstr "Abre l'enllaz del artículu nel restolador." #~ msgid "_Work Offline" #~ msgstr "Trabayar ensin conexón" #~ msgid "_Update All" #~ msgstr "_Anovar too" #~ msgid "_Show Liferea" #~ msgstr "_Amosar Liferea" #, fuzzy #~ msgid "InoReader" #~ msgstr "Llector Google" #~ msgid "" #~ "Note: The username and password will be saved to your Liferea feedlist " #~ "file without using encryption." #~ msgstr "" #~ "Nota: El nome d'usuariu y la contraseña van atroxase en discu, ensin " #~ "cifrar." #~ msgid "Add Google Reader Account" #~ msgstr "Axuntar cuenta de Google Reader" #~ msgid "Please enter your Google Reader account settings." #~ msgstr "Por favor escribi los datos de la to cuenta de Google Reader." #, fuzzy #~ msgid "Add InoReader Account" #~ msgstr "Axuntar cuenta de Google Reader" #, fuzzy #~ msgid "Please enter your InoReader account settings." #~ msgstr "Por favor escribi los datos de la to cuenta de Google Reader." #~ msgid "View Headlines" #~ msgstr "Ver titulares" #~ msgid "Feed Name" #~ msgstr "Nome de la canal" #~ msgid "normal view" #~ msgstr "vista normal" #~ msgid "wide view" #~ msgstr "vista panorámica" #~ msgid "combined view" #~ msgstr "vista combinada" #~ msgid "Liferea, the Linux Feed Reader" #~ msgstr "Liferea, llector de noticies pa Linux" #~ msgid "For more information, please visit https://lzone.de/liferea/" #~ msgstr "Pa más información, visita https://lzone.de/liferea/" #, fuzzy #~ msgid "_Open Link In Browser" #~ msgstr "Abrir l'enllaz n_el restolador" #, fuzzy #~ msgid "_Open Link In External Browser" #~ msgstr "Abrir l'enllaz n_el restolador" #~ msgid " " #~ msgstr " " #~ msgid "Create Search Engine Feed" #~ msgstr "Crear una canal de resultaos de gueta" #~ msgid "enter any search string you want" #~ msgstr "escribi una cadena de testu a guetar" #~ msgid "Maximal _Number Of Result Items:" #~ msgstr "_Númberu máximu de resultaos:" #, fuzzy #~ msgid "" #~ "Note: Liferea will generate a feed subscription which is used to query " #~ "the search engine results for the specified search string. You can keep " #~ "this feed permanently and update it like any other subscription." #~ msgstr "" #~ "Nota: Liferea xenerará una canal que podrá usase pa consultar los " #~ "resultaos de gueta de la cadena especificada. Puedes caltener esta canal " #~ "de mou permanente y anovala como cualisquier otra. " #~ msgid "Liferea is now online" #~ msgstr "Liferea ta coneutáu" #~ msgid "Work Offline" #~ msgstr "Trabayar ensin conexón" #~ msgid "Liferea is now offline" #~ msgstr "Liferea ta ensin conexón" #~ msgid "Work Online" #~ msgstr "Trabayar coneutáu" #~ msgid "This option allows you to disable subscription updating." #~ msgstr "Esta opción permite deshabilitar l'anovamientu de soscripciones." #~ msgid "Browser default" #~ msgstr "Restolador predetermináu" #~ msgid "Existing window" #~ msgstr "Ventana esistente" #~ msgid "New window" #~ msgstr "Nueva ventana" #~ msgid "New tab" #~ msgstr "Nueva llingüeta" #, fuzzy #~ msgid "AOL Reader" #~ msgstr "Llector de noticies" #~ msgid "Online/Offline Button" #~ msgstr "Boton Coneutáu/Desconeutáu" #~ msgid "_Open link in:" #~ msgstr "Abrir enllaz en:" #~ msgid "No comments yet." #~ msgstr "Entá nun hai comentarios." #~ msgid "Refresh" #~ msgstr "Anovar" #~ msgid "Liferea - Linux Feed Reader" #~ msgstr "Liferea, llector de noticies Linux" #~ msgid "" #~ "

Welcome to Liferea, a desktop news aggregator for online news " #~ "feeds.

You can add new subscriptions

  • From main menu " #~ "'Subscription' -> 'New Subscription'
  • By dropping feed links " #~ "into the subscription list
  • By right clicking links and choosing " #~ "'Subscribe' within Liferea

" #~ msgstr "" #~ "

Bienllegáu a Liferea, un llector de noticies en llinia pal " #~ "escritoriu.

Pues amestar soscripciones nueves

  • Dende'l menú " #~ "'Soscripción' -> 'Nueva soscripción'
  • Arrastrando enllaces de " #~ "feeds dientro de la llista de soscripciones
  • Calcando col botón " #~ "derechu y esbillando 'Soscribir' con Liferea

" #, fuzzy #~ msgid "Integrate with the messaging menu (indicator)" #~ msgstr "Aniciar amenorgáu nel menú mensaxería" #, fuzzy #~ msgid "Terminate instead of minimizing to the messaging menu" #~ msgstr "Finar, _en llugar de minimizar na estaya de notificación." #~ msgid "Start minimized to the messaging menu" #~ msgstr "Aniciar amenorgáu nel menú mensaxería" #~ msgid "%d new item" #~ msgid_plural "%d new items" #~ msgstr[0] "%d artículu nuevu" #~ msgstr[1] "%d artículos nuevos" #~ msgid "No new items" #~ msgstr "Nun hai artículos nuevos" #~ msgid "" #~ "%s\n" #~ "%d unread item" #~ msgid_plural "" #~ "%s\n" #~ "%d unread items" #~ msgstr[0] "" #~ "%s\n" #~ "%d elementu ensin lleer" #~ msgstr[1] "" #~ "%s\n" #~ "%d elementos ensin lleer" #~ msgid "" #~ "%s\n" #~ "No unread items" #~ msgstr "" #~ "%s\n" #~ "Nun hai artículos ensin lleer" #~ msgid "Invalid Atom feed: unknown author" #~ msgstr "Canal Atom inválida: autor desconocíu" #~ msgid "" #~ "Integrate the feed list of your Google Reader account. Liferea will " #~ "present your Google Reader subscriptions, and will synchronize your feed " #~ "list and reading lists." #~ msgstr "" #~ "Integra la llista de canales de la cuenta de Google Reader. Liferea " #~ "presentará la soscripción a Google Reader como un subárbol na llista de " #~ "canales y la caltendrala sincronizada." #~ msgid "" #~ "Integrate blogrolls or Planets in your feed list. Liferea will " #~ "automatically add and remove feeds according to the changes of the source " #~ "OPML document" #~ msgstr "" #~ "Incluye blogrolls o Planets na to llista de canales. Liferea axuntará o " #~ "desaniciará canales, d'acordies a los cambeos nel documentu OPML" #, fuzzy #~ msgid "" #~ "Integrate the feed list of your Tiny Tiny RSS 1.5+ account. Liferea will " #~ "present your tt-rss subscriptions, and will synchronize your feed list " #~ "and reading lists." #~ msgstr "" #~ "Integra la llista de canales de la cuenta de Google Reader. Liferea " #~ "presentará la soscripción a Google Reader como un subárbol na llista de " #~ "canales y la caltendrala sincronizada." #~ msgid "This feed does not exist anymore!" #~ msgstr "¡Esta canal yá nun esiste!" #~ msgid "This news entry has no headline" #~ msgstr "Esti artículu nun tien titular" #~ msgid "Visit" #~ msgstr "Visitar" #~ msgid "Open feed" #~ msgstr "Ver la canal" #~ msgid "%s has %d update" #~ msgid_plural "%s has %d updates" #~ msgstr[0] "%s tien %d anovamientu" #~ msgstr[1] "%s tien %d anovamientos" #~ msgid "_Enforce popup notification for this subscription." #~ msgstr "Siempre xenerar notificaciones pa esta soscripción." #~ msgid "_Never do popup notification for this subscription." #~ msgstr "Enxamás xenerar notificaciones pa esta soscripción." #~ msgid "" #~ "Copyright (c) 2003-2012\n" #~ "The Liferea Team\n" #~ msgstr "" #~ "Copyright (c) 2003-2012\n" #~ "The Liferea Team\n" #~ msgid "Show a _popup window with new headlines." #~ msgstr "Amosar una ventana emerxente al recibir nuevos elementos." #~ msgid "Show a status _icon in the notification area (system tray)." #~ msgstr "Amosar un iconu na estaya de notificación (system tray)." #~ msgid "Show _number of new items in the tray icon." #~ msgstr "Amosar el _númberu d'elementos nuevos nel iconu de la bandexa." #~ msgid "T_erminate instead of minimizing to tray icon." #~ msgstr "Finar, _en llugar de minimizar na estaya de notificación." #~ msgid "_Start in tray icon." #~ msgstr "_Aniciar na bandexa d'iconos." #~ msgid "Download and view feeds" #~ msgstr "Baxar y ver noticies" #~ msgid "You may want to validate the feed using" #~ msgstr "Seique quieras validar el feed usáu" #~ msgid "bookmark" #~ msgstr "marcador" #~ msgid "comments" #~ msgstr "comentarios" #~ msgid "flag" #~ msgstr "etiqueta" #~ msgid "Enclosure download FAILED: \"%s\"" #~ msgstr "Falló la descarga del axuntu: «%s»" #~ msgid "Enclosure download finished: \"%s\"" #~ msgstr "Completóse la baxada del axuntu: \"%s\"" #~ msgid "" #~ "This version of Liferea uses a new cache format and has migrated your " #~ "feed cache. The cache content in %s was not deleted automatically. Please " #~ "remove this directory manually once you are sure migration was successful!" #~ msgstr "" #~ "Esta versión de Liferea usa un formatu nuevu d'almacenamientu y treslladó " #~ "a ésti lo que s'atroxara anteriormente. El conteníu del almacén anterior " #~ "(en %s) nun se desanició automáticamente. Por favor, desanicia talu " #~ "direutoriu de mou manual tres d'asegurate que l'anovamientu foi correutu." #~ msgid "" #~ "Jumps to the next unread item. If necessary selects the next feed with " #~ "unread items." #~ msgstr "" #~ "Salta al siguiente artículu nuevu. Si fore necesario, salta a la " #~ "siguiente canal que tenga artículos nuevos." #~ msgid "Download FAILED: \"%s\"" #~ msgstr "Falló la descarga: \"%s\"" #~ msgid "Download finished." #~ msgstr "Descarga finada." #~ msgid "Launch Item In _Tab" #~ msgstr "Abrir l'artículu nuna llingü_eta nueva" #~ msgid "_Launch Item In Browser" #~ msgstr "Abrir l'artículu nel restolador" #~ msgid "Copy Item _URL to Clipboard" #~ msgstr "Copiar la _URL del artículu al cartafueyos" #~ msgid "Choose download directory" #~ msgstr "Escueyi'l direutoriu de descarga" #~ msgid "Liferea Sync %s@%s" #~ msgstr "Sincronización de Liferea %s@%s" #~ msgid "Downloading Enclosure" #~ msgstr "Baxando axuntos" #~ msgid "Sync" #~ msgstr "Sincronizar" #~ msgid "_Enable Local LAN Synchronization" #~ msgstr "Habilitar sincronización c_ola rede llocal" #~ msgid "" #~ "_Manual:\n" #~ "(%s for URL)" #~ msgstr "" #~ "_Manual:\n" #~ "(%s pa URL)" #~ msgid "_Pass URL and do not download enclosure." #~ msgstr "_Pasar URL y non descargar l'axuntu." #~ msgid "_Save downloads in" #~ msgstr "_Guardar baxaes en" #~ msgid "_Service Name" #~ msgstr "Nome de _serviciu" #~ msgid "link cosmos" #~ msgstr "universu d'enllaces" #~ msgid "Print debugging messages for the plugin loading" #~ msgstr "Amosar mensaxes de depuración tocante a la carga de «plugins»" #~ msgid "Liferea seems to be running already!" #~ msgstr "¡Paez que Liferea ya taba executándose!" #~ msgid "Update status" #~ msgstr "Anova l'estáu" #~ msgid "was updated" #~ msgstr "anovóse" #~ msgid "was not updated" #~ msgstr "nun s'anovó" #~ msgid "The orientation of the tray." #~ msgstr "La orientación de la bandexa." #~ msgid "topics_en.html" #~ msgstr "topics_en.html" #~ msgid "reference_en.html" #~ msgstr "reference_en.html" #~ msgid "faq_en.html" #~ msgstr "faq_en.html" #~ msgid "_Script Manager" #~ msgstr "Xestor de guione_s" #~ msgid "Allows to configure and edit LUA hook scripts" #~ msgstr "Permite configurar y editar scripts LUA" #~ msgid "Search With ..." #~ msgstr "Guetar Con ..." #~ msgid "%d Search Result for \"%s\"" #~ msgid_plural "%d Search Results for \"%s\"" #~ msgstr[0] "%d resultáu de guetar \"%s\"" #~ msgstr[1] "%d resultaos de guetar \"%s\"" #~ msgid "" #~ "The item list now contains all items matching the specified search " #~ "pattern. If you want to save this search result permanently you can click " #~ "the \"Search Folder\" button in the search dialog and Liferea will add a " #~ "search folder to your feed list." #~ msgstr "" #~ "La llista d'elementos amuesa toos aquellos que concasen col patrón " #~ "buscáu. Si quies guardar esti resultáu puedes usar el botón \"Carpeta de " #~ "gueta\" na ventada de gueta. Liferea amestará entós una carpeta virtual a " #~ "la to llista de canales." #~ msgid "You have to select a feed entry" #~ msgstr "Tienes d'esbillar una canal" #~ msgid "(empty)" #~ msgstr "(ermu)" #~ msgid "_Properties..." #~ msgstr "_Propiedaes..." #~ msgid "Update out-dated feeds" #~ msgstr "Anovar les canales non actualizaes" #~ msgid "Force update of all feeds" #~ msgstr "Forciar l'anovamientu de toles canales" #~ msgid "No feed update at all" #~ msgstr "Nun hai dengún anovamientu" #~ msgid "startup" #~ msgstr "Aniciar" #~ msgid "feed updated" #~ msgstr "canal anovada" #~ msgid "feed added" #~ msgstr "canal axuntada" #~ msgid "item selected" #~ msgstr "artículu seleicionáu" #~ msgid "feed selected" #~ msgstr "canal seleicionada" #~ msgid "item unselected" #~ msgstr "artículu non-seleicionáu" #~ msgid "feed unselected" #~ msgstr "canal non-seleicionada" #~ msgid "shutdown" #~ msgstr "finar" #~ msgid "Sorry, no scripting support available!" #~ msgstr "Nun hai sofitu pa scripts." #~ msgid "Script Name" #~ msgstr "Nome del guión" #~ msgid "No script selected!" #~ msgstr "Nun hai un script seleicionáu." #~ msgid "Create a new search feed." #~ msgstr "Crear una nueva canal de gueta." #~ msgid "Liferea is unable to display this item's content." #~ msgstr "Liferea nun puede amosar el conteníu d'esti artículu." #~ msgid "

View this item's content.

" #~ msgstr "

Ver el conteníu d'esti artículu.

" #~ msgid "Bloglines" #~ msgstr "Bloglines" #~ msgid "" #~ "Integrate the feed list of your Bloglines account. Liferea will present " #~ "your Bloglines subscription as a read-only subtree in the feed list." #~ msgstr "" #~ "Integrar la llista de canales de la so cuenta en Bloglines. Liferea " #~ "presentará la so soscripción a Bloglines como un subárbol de namái " #~ "llectura na llista de canales." #~ msgid "feedlist.opml" #~ msgstr "feedlist.opml" #~ msgid "%s has %d new / updated headline\n" #~ msgid_plural "%s has %d new / updated headlines\n" #~ msgstr[0] "%s tien %d titular nuevu / anováu\n" #~ msgstr[1] "%s tien %d titulares nuevos / anovaos\n" #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid "Hook" #~ msgstr "Ganchu" #~ msgid "Registered Scripts" #~ msgstr "Scripts rexistraos" #~ msgid "Script Code" #~ msgstr "Códigu del Script" #~ msgid "text/plain" #~ msgstr "testu/planu" #~ msgid "" #~ "This option can cause significant delays when loading folders " #~ "containing many feeds." #~ msgstr "" #~ "Esta opción puede causar allancios significantes al cargar carpetes " #~ "que caltengan munchos canales." #~ msgid "Downloading Enclosures" #~ msgstr "Baxada d'axuntos" #~ msgid "Feed Cache Handling" #~ msgstr "Opciones d'almacenaxe" #~ msgid "Feed Name" #~ msgstr "Nome de la canal" #~ msgid "Feed Update Settings" #~ msgstr "Opciones d'anovamientu de la canal" #~ msgid "HTTP Proxy Server" #~ msgstr "Sirvidor «proxy» HTTP" #~ msgid "Opening Enclosures" #~ msgstr "Apertura d'axuntos" #~ msgid "Reading Headlines" #~ msgstr "Llectura de titulares" #~ msgid "Toolbar Settings" #~ msgstr "Opciones de la barra de ferramientes" #~ msgid "Update Interval" #~ msgstr "Intervalu d'anovación" #~ msgid "Web Integration" #~ msgstr "Integración web" #~ msgid "Add Script" #~ msgstr "Amestar un script" #~ msgid "At _startup:" #~ msgstr "Al aniciar:" #~ msgid "Create new script" #~ msgstr "Crear un script nuevu" #~ msgid "Exec Command" #~ msgstr "Executar una orde" #~ msgid "Reuse existing script" #~ msgstr "Reutilizar un script esistente" #~ msgid "Script Manager" #~ msgstr "Alministrador de scripts" #~ msgid "Search _Link Cosmos with" #~ msgstr "Guetar nel universu d'enll_aces con" #~ msgid "Count" #~ msgstr "Cuntar" #~ msgid "Attention Profile" #~ msgstr "Perfil d'atención" liferea-1.13.7/po/be@latin.po000066400000000000000000002657731415350204600157320ustar00rootroot00000000000000# Belarusian translations for liferea package. # Copyright (C) 2007 THE liferea'S COPYRIGHT HOLDER # This file is distributed under the same license as the liferea package. # , 2007. # msgid "" msgstr "" "Project-Id-Version: liferea 1.2.20\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-30 12:36+0200\n" "PO-Revision-Date: 2007-08-19 13:26+0300\n" "Last-Translator: \n" "Language-Team: Belarusian Latin\n" "Language: \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" #: ../net.sourceforge.liferea.appdata.xml.in.h:1 #, fuzzy msgid "RSS feed reader" msgstr "Hartač RSS" #: ../net.sourceforge.liferea.appdata.xml.in.h:2 msgid "" "Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator " "for online news feeds. It supports a number of different feed formats " "including RSS/RDF, CDF and Atom. There are many other news readers " "available, but these others are not available for Linux or require many " "extra libraries to be installed. Liferea tries to fill this gap by creating " "a fast, easy to use, easy to install news aggregator for GTK/GNOME." msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:3 msgid "Distinguishing features:" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:4 #, fuzzy msgid "Read articles when offline" msgstr "Liferea ciapier adłučany" #: ../net.sourceforge.liferea.appdata.xml.in.h:5 #, fuzzy msgid "Synchronizes with TheOldReader" msgstr "Nazva kanału" #: ../net.sourceforge.liferea.appdata.xml.in.h:6 #, fuzzy msgid "Synchronizes with TinyTinyRSS" msgstr "Nazva kanału" #: ../net.sourceforge.liferea.appdata.xml.in.h:7 #, fuzzy msgid "Synchronizes with InoReader" msgstr "Nazva kanału" #: ../net.sourceforge.liferea.appdata.xml.in.h:8 #, fuzzy msgid "Synchronizes with Reedah" msgstr "Nazva kanału" #: ../net.sourceforge.liferea.appdata.xml.in.h:9 msgid "Permanently save headlines in news bins" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:10 msgid "Match items using search folders" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:11 #, fuzzy msgid "Play Podcasts" msgstr "Podcast" #: ../net.sourceforge.liferea.desktop.in.h:1 ../src/liferea_application.c:350 #: ../glade/mainwindow.ui.h:1 msgid "Liferea" msgstr "Liferea" #: ../net.sourceforge.liferea.desktop.in.h:2 msgid "Feed Reader" msgstr "Hartač RSS" #: ../net.sourceforge.liferea.desktop.in.h:3 msgid "Liferea Feed Reader" msgstr "Hartač RSS Liferea" #: ../net.sourceforge.liferea.desktop.in.h:4 msgid "Read news feeds and blogs" msgstr "" #: ../net.sourceforge.liferea.desktop.in.h:5 msgid "news;feed;aggregator;blog;podcast;syndication;rss;atom" msgstr "" #: ../plugins/getfocus.py:93 msgid "Opacity:" msgstr "" #: ../plugins/getfocus.py:94 msgid "Opacity" msgstr "" #: ../plugins/getfocus.py:104 msgid "Min" msgstr "" #: ../plugins/getfocus.py:109 msgid "Max" msgstr "" #: ../plugins/getfocus.py:115 msgid "Save" msgstr "" #: ../plugins/headerbar.py:67 ../glade/liferea_menu.ui.h:20 #: ../glade/liferea_toolbar.ui.h:5 msgid "Previous Item" msgstr "" #: ../plugins/headerbar.py:73 ../glade/liferea_menu.ui.h:21 #: ../glade/liferea_toolbar.ui.h:6 #, fuzzy msgid "Next Item" msgstr "_Nastupny niečytany element" #: ../plugins/headerbar.py:81 ../glade/liferea_menu.ui.h:19 #: ../glade/liferea_toolbar.ui.h:7 msgid "_Next Unread Item" msgstr "_Nastupny niečytany element" #: ../plugins/headerbar.py:89 ../glade/liferea_menu.ui.h:14 #: ../glade/liferea_toolbar.ui.h:3 #, fuzzy msgid "_Mark Items Read" msgstr "/_Zaznač jak pračytanaje" #: ../plugins/headerbar.py:113 ../glade/liferea_menu.ui.h:40 #: ../glade/liferea_toolbar.ui.h:10 msgid "Search All Feeds..." msgstr "Šukaj va ŭsich kanałach..." #: ../plugins/libnotify.py:42 #, fuzzy msgid "Feed Updates" msgstr "Aktualizacyja kanału" #: ../plugins/plugin-installer.py:54 msgid "Plugins" msgstr "" #: ../plugins/plugin-installer.py:69 msgid "Plugin Installer" msgstr "" #: ../plugins/plugin-installer.py:83 msgid "Activate Plugins" msgstr "" #: ../plugins/plugin-installer.py:84 #, fuzzy msgid "Download Plugins" msgstr "_Zahružaj z dapamohaj" #: ../plugins/plugin-installer.py:102 #, python-format msgid "Bad fields for plugin entry %s" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "All" msgstr "" #: ../plugins/plugin-installer.py:125 ../glade/properties.ui.h:39 msgid "Advanced" msgstr "Admysłovaje" #: ../plugins/plugin-installer.py:125 msgid "Menu" msgstr "" #: ../plugins/plugin-installer.py:125 #, fuzzy msgid "Notifications" msgstr "Nałady nahadvańnia" #: ../plugins/plugin-installer.py:133 msgid "Filter by category" msgstr "" #: ../plugins/plugin-installer.py:140 msgid "_Install" msgstr "" #: ../plugins/plugin-installer.py:144 msgid "_Uninstall" msgstr "" #: ../plugins/plugin-installer.py:245 #, python-format msgid "" "Missing package manager '%s'. Cannot check nor install necessary " "dependencies!" msgstr "" #: ../plugins/plugin-installer.py:261 #, python-format msgid "Missing package '%s'. Do you want to install it? (Will run '%s')" msgstr "" #: ../plugins/plugin-installer.py:268 #, python-format msgid "" "Package installation failed (%s)! Check console output for further problem " "details!" msgstr "" #: ../plugins/plugin-installer.py:271 #, python-format msgid "Failed to check plugin dependencies (%s)!" msgstr "" #: ../plugins/plugin-installer.py:280 msgid "Command \"git\" not found, please install it!" msgstr "" #: ../plugins/plugin-installer.py:289 #, python-format msgid "Copying %s to %s" msgstr "" #: ../plugins/plugin-installer.py:292 #, python-format msgid "Failed to copy plugin directory (%s)!" msgstr "" #: ../plugins/plugin-installer.py:301 #, python-format msgid "Failed to copy plugin .py file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:311 #, python-format msgid "Failed to copy .plugin file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:322 #, fuzzy, python-format msgid "Creating schema directory %s" msgstr "Niemahčyma stvaryć kataloh padručnaj pamiaci \"%s\"!" #: ../plugins/plugin-installer.py:324 #, python-format msgid "Installing schema %s" msgstr "" #: ../plugins/plugin-installer.py:328 msgid "Compiling schemas..." msgstr "" #: ../plugins/plugin-installer.py:333 #, python-format msgid "Failed to install schema files (%s)!" msgstr "" #: ../plugins/plugin-installer.py:343 #, python-format msgid "Failed to enable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:349 #, python-format msgid "Plugin '%s' is now installed. Ensure to restart Liferea!" msgstr "" #: ../plugins/plugin-installer.py:363 #, python-format msgid "Failed to disable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:370 ../plugins/plugin-installer.py:390 #, fuzzy, python-format msgid "Deleting '%s'" msgstr "Vydaleńnie elementu" #: ../plugins/plugin-installer.py:373 #, python-format msgid "Failed to remove directory '%s' (%s)!" msgstr "" #: ../plugins/plugin-installer.py:383 msgid "Failed to remove .py file!" msgstr "" #: ../plugins/plugin-installer.py:393 msgid "Failed to remove .plugin file!" msgstr "" #: ../plugins/plugin-installer.py:402 msgid "Sorry! Plugin removal failed!." msgstr "" #: ../plugins/plugin-installer.py:404 #, fuzzy msgid "" "Plugin was removed. Please restart Liferea once for it to take full effect!." msgstr "Kali łaska, uruchom znoŭ Liferea, kab zadziejničać źmieny." #: ../plugins/trayicon.py:132 #, fuzzy msgid "Show / Hide" msgstr "Pakažy padrabiaznaści" #: ../plugins/trayicon.py:133 msgid "Minimize to tray on close" msgstr "" #: ../plugins/trayicon.py:134 #, fuzzy msgid "Quit" msgstr "_Vyjdzi" #: ../src/browser.c:81 ../src/browser.c:98 #, c-format msgid "Browser command failed: %s" msgstr "Pamyłka zahadu hartača: %s" #: ../src/browser.c:101 ../src/ui/liferea_shell.c:1047 #, c-format msgid "Starting: \"%s\"" msgstr "Startujecca: \"%s\"" #. unauthorized #: ../src/comments.c:118 msgid "Authorization Error" msgstr "Pamyłka aŭtaryzacyi" #: ../src/common.c:66 #, c-format msgid "Cannot create cache directory \"%s\"!" msgstr "Niemahčyma stvaryć kataloh padručnaj pamiaci \"%s\"!" #: ../src/conf.c:182 msgid "" "Your version of WebKitGTK+ doesn't support changing the proxy settings from " "Liferea. The system's default proxy settings will be used." msgstr "" #. translation hint: date format for today, reorder format codes as necessary #: ../src/date.c:133 msgid "Today %l:%M %p" msgstr "Siońnia %l:%M %p" #. translation hint: date format for yesterday, reorder format codes as necessary #: ../src/date.c:142 msgid "Yesterday %l:%M %p" msgstr "Učora %l:%M %p" #. translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary #: ../src/date.c:154 msgid "%a %l:%M %p" msgstr "%a %l:%M %p" #. translation hint: date format for dates older than a week but from this year, reorder format codes as necessary #: ../src/date.c:162 msgid "%b %d %l:%M %p" msgstr "%b %d %l:%M %p" #. translation hint: date format for dates from the last years, reorder format codes as necessary #: ../src/date.c:165 msgid "%b %d %Y" msgstr "%b %d %Y" #: ../src/enclosure.c:201 #, c-format msgid "\"%s\" is not a valid enclosure type config file!" msgstr "\"%s\" nie źjaŭlajecca pravilnym fajłam kanfihuracyi typu dałučeńnia!" #: ../src/enclosure.c:292 msgid "" "You have not configured a download tool yet! Please do so in the " "'Enclosures' tab in Tools/Preferences." msgstr "" #: ../src/enclosure.c:311 #, c-format msgid "" "Command failed: \n" "\n" "%s\n" "\n" " Please check whether the configured download tool is installed and working " "correctly! You can change it in the 'Download' tab in Tools/Preferences." msgstr "" #: ../src/export.c:187 #, fuzzy, c-format msgid "Error renaming %s to %s: %s\n" msgstr "Pamyłka źmieny nazvy %s na %s\n" #: ../src/export.c:409 ../src/export.c:411 #, fuzzy, c-format msgid "XML error while reading OPML file! Could not import \"%s\"!" msgstr "" "Pamyłka XML pry čytańni padručnaha fajłu! Niemahčyma impartavać \"%s\"!" #: ../src/export.c:417 ../src/export.c:419 #, c-format msgid "" "Empty document! OPML document \"%s\" should not be empty when importing." msgstr "" "Pusty dakument! Dakument OPML \"%s\" nie pavinny być pustym pry impartavańni." #: ../src/export.c:440 ../src/export.c:442 #, c-format msgid "\"%s\" is not a valid OPML document! Liferea cannot import this file!" msgstr "" "\"%s\" nie źjaŭlajecca pravilnym dakumentam OPML! Liferea nia moža " "impartavać hety fajł!" #: ../src/export.c:461 msgid "Imported feed list" msgstr "Śpis impartavanych kanałaŭ" #: ../src/export.c:473 msgid "Import Feed List" msgstr "Impartuj śpis kanałaŭ" #: ../src/export.c:473 msgid "Import" msgstr "Impartuj" #: ../src/export.c:473 ../src/export.c:490 ../src/fl_sources/opml_source.c:379 #, fuzzy msgid "OPML Files" msgstr "Abiary fajł OPML" #: ../src/export.c:481 msgid "Error while exporting feed list!" msgstr "Pamyłka pry ekspartavańni śpisu kanałaŭ!" #: ../src/export.c:483 msgid "Feed List exported!" msgstr "Śpis kanałaŭ ekspartavany!" #: ../src/export.c:490 msgid "Export Feed List" msgstr "Ekspartuj śpis kanałaŭ" #: ../src/export.c:490 msgid "Export" msgstr "Ekspartuj" #: ../src/feed_parser.c:201 msgid "Empty document!" msgstr "Pusty dakument!" #: ../src/feed_parser.c:210 msgid "Invalid XML!" msgstr "Niapravilny XML!" #: ../src/fl_sources/default_source.c:139 ../glade/new_subscription.ui.h:1 #: ../glade/simple_subscription.ui.h:1 msgid "New Subscription" msgstr "Novaja padpiska" #: ../src/fl_sources/google_source.c:111 msgid "Google Reader" msgstr "Google Reader" #: ../src/fl_sources/node_source.c:334 msgid "No feed list source types found!" msgstr "Typy krynic dla śpisu kanałaŭ nia znojdzienyja!" #: ../src/fl_sources/node_source.c:363 msgid "Source Type" msgstr "Typ krynicy" #: ../src/fl_sources/node_source.c:414 #, c-format msgid "Login for '%s' has not yet completed! Please wait until login is done." msgstr "" #. FIXME: something is not perfect, because if you immediately #. remove the subscription tree afterwards there is a double free #: ../src/fl_sources/node_source.c:580 #, c-format msgid "The '%s' subscription was successfully converted to local feeds!" msgstr "" #: ../src/fl_sources/opml_source.c:319 msgid "Planet, BlogRoll, OPML" msgstr "Planet, BlogRoll, OPML" #: ../src/fl_sources/opml_source.c:379 msgid "Choose OPML File" msgstr "Abiary fajł OPML" #: ../src/fl_sources/opml_source.c:379 ../src/ui/subscription_dialog.c:355 msgid "_Open" msgstr "" #: ../src/fl_sources/opml_source.h:28 msgid "New OPML Subscription" msgstr "Novaja padpiska OPML" #: ../src/fl_sources/reedah_source.c:103 #: ../src/fl_sources/theoldreader_source.c:104 #, fuzzy msgid "Login failed!" msgstr "Pamyłka ŭruchamleńnia kontu na Google Reader'y!" #: ../src/fl_sources/reedah_source.c:313 msgid "Reedah" msgstr "" #: ../src/fl_sources/reedah_source_feed.c:154 msgid "Could not parse JSON returned by Reedah API!" msgstr "" #: ../src/fl_sources/theoldreader_source.c:311 #, fuzzy msgid "TheOldReader" msgstr "Hartač RSS" #: ../src/fl_sources/ttrss_source.c:227 ../src/fl_sources/ttrss_source.c:293 msgid "TinyTinyRSS HTTP API not reachable!" msgstr "" #: ../src/fl_sources/ttrss_source.c:234 msgid "" "TinyTinyRSS subscribing to feed failed! Check if you really passed a feed " "URL!" msgstr "" #: ../src/fl_sources/ttrss_source.c:300 msgid "TinyTinyRSS unsubscribing feed failed!" msgstr "" #: ../src/fl_sources/ttrss_source.c:318 #, c-format msgid "" "This TinyTinyRSS version does not support removing feeds. Upgrade to version " "%s or later!" msgstr "" #: ../src/fl_sources/ttrss_source.c:471 msgid "Tiny Tiny RSS" msgstr "" #: ../src/fl_sources/ttrss_source_feed.c:150 msgid "Could not parse JSON returned by TinyTinyRSS API!" msgstr "" #. if we don't find a feed with unread items do nothing #: ../src/itemlist.c:395 #, fuzzy msgid "There are no unread items" msgstr "Nia niečytanych elementaŭ " #: ../src/liferea_application.c:280 #, fuzzy msgid "" "Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'" msgstr " STAN moža być `shown', `iconified' albo `hidden'" #: ../src/liferea_application.c:280 msgid "STATE" msgstr "" #: ../src/liferea_application.c:281 #, fuzzy msgid "Show version information and exit" msgstr " --version Pakažy źviestki ab versii i vyjdzi" #: ../src/liferea_application.c:282 #, fuzzy msgid "Add a new subscription" msgstr "Novaja padpiska" #: ../src/liferea_application.c:282 msgid "uri" msgstr "" #: ../src/liferea_application.c:283 msgid "Start with all plugins disabled" msgstr "" #: ../src/liferea_application.c:288 #, fuzzy msgid "Print debugging messages of all types" msgstr " --debug-all Pakazvaj debugavyja paviedamleńni ŭsich typaŭ" #: ../src/liferea_application.c:289 #, fuzzy msgid "Print debugging messages for the cache handling" msgstr "" " --debug-conf Pakazvaj debugavyja paviedamleńni dla pracy z " "kanfihuracyjaj" #: ../src/liferea_application.c:290 #, fuzzy msgid "Print debugging messages for the configuration handling" msgstr "" " --debug-conf Pakazvaj debugavyja paviedamleńni dla pracy z " "kanfihuracyjaj" #: ../src/liferea_application.c:291 #, fuzzy msgid "Print debugging messages of the database handling" msgstr "" " --debug-conf Pakazvaj debugavyja paviedamleńni dla pracy z " "kanfihuracyjaj" #: ../src/liferea_application.c:292 #, fuzzy msgid "Print debugging messages of all GUI functions" msgstr "" " --debug-gui Pakazvaj debugavyja paviedamleńni dla ŭsich funkcyj GUI" #: ../src/liferea_application.c:293 msgid "" "Enables HTML rendering debugging. Each time Liferea renders HTML output it " "will also dump the generated HTML into ~/.cache/liferea/output.html" msgstr "" #: ../src/liferea_application.c:294 #, fuzzy msgid "Print debugging messages of all network activity" msgstr "" " --debug-net Pakazvaj debugavyja paviedamleńni dla dostupu da sietki" #: ../src/liferea_application.c:295 #, fuzzy msgid "Print debugging messages of all parsing functions" msgstr "" " --debug-parsing Pakazvaj debugavyja paviedamleńni dla ŭsich funkcyj " "razboru" #: ../src/liferea_application.c:296 msgid "Print debugging messages when a function takes too long to process" msgstr "" #: ../src/liferea_application.c:297 #, fuzzy msgid "Print debugging messages when entering/leaving functions" msgstr "" " --debug-trace Pakazvaj debugavyja paviedamleńni dla dunkcyj uvachodu/" "vychadu" #: ../src/liferea_application.c:298 #, fuzzy msgid "Print debugging messages of the feed update processing" msgstr "" " --debug-update Pakazvaj debugavyja paviedamleńni dla aktualizacyj kanałaŭ" #: ../src/liferea_application.c:299 #, fuzzy msgid "Print debugging messages of the search folder matching" msgstr "" " --debug-conf Pakazvaj debugavyja paviedamleńni dla pracy z " "kanfihuracyjaj" #: ../src/liferea_application.c:300 #, fuzzy msgid "Print verbose debugging messages" msgstr " --debug-verbose Pakazvaj padrabiaznyja debugavyja paviedamleńni" #: ../src/liferea_application.c:305 ../src/liferea_application.c:306 #, fuzzy msgid "Print debugging messages for the given topic" msgstr "" " --debug- Pakazvaj debugavyja paviedamleńni dla peŭnaj temypamiaćciu" #. Some libsoup transport errors #: ../src/net.c:437 msgid "The update request was cancelled" msgstr "" #: ../src/net.c:438 msgid "Unable to resolve destination host name" msgstr "" #: ../src/net.c:439 msgid "Unable to resolve proxy host name" msgstr "" #: ../src/net.c:440 #, fuzzy msgid "Unable to connect to remote host" msgstr "Pamyłka spałučeńnia z addalenym kamputaram" #: ../src/net.c:441 msgid "Unable to connect to proxy" msgstr "" #: ../src/net.c:442 msgid "" "SSL/TLS negotiation failed. Possible outdated or unsupported encryption " "algorithm. Check your operating system settings." msgstr "" #. http 3xx redirection #: ../src/net.c:445 msgid "The resource moved permanently to a new location" msgstr "" #. http 4xx client error #: ../src/net.c:448 #, fuzzy msgid "" "You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box" msgstr "" "Tabie nie dazvolena čytać hety kanał. Kali łaska, aktualizuj nazvu " "karystalnika j parol u dyjalohavym aknie ŭłaścivaściaŭ kanału." #: ../src/net.c:450 #, fuzzy msgid "Payment required" msgstr "Patrabujecca apłata" #: ../src/net.c:451 msgid "You're not allowed to access this resource" msgstr "" #: ../src/net.c:452 msgid "Resource Not Found" msgstr "Resurs nia znojdzieny" #: ../src/net.c:453 msgid "Method Not Allowed" msgstr "Metad nie dazvoleny" #: ../src/net.c:454 msgid "Not Acceptable" msgstr "Nie pryjmalna" #: ../src/net.c:455 #, fuzzy msgid "Proxy authentication required" msgstr "Patrabujecca aŭtaryzacyja dla proxy" #: ../src/net.c:456 #, fuzzy msgid "Request timed out" msgstr "Skončyŭsia termin čakańnia zapytu" #: ../src/net.c:457 #, fuzzy msgid "" "The webserver indicates this feed is discontinued. It's no longer available. " "Liferea won't update it anymore but you can still access the cached " "headlines." msgstr "" "Hety kanał skončyŭ pracu. Ciapier jon niedastupny. Liferea nia budzie jaho " "aktualizoŭvać, ale ty možaš čytać zahałoŭki z padručnaj pamiaci." #: ../src/net.c:462 msgid "There was an internal error in the update process" msgstr "" #: ../src/net.c:464 msgid "Feed not available: Server requested unsupported redirection!" msgstr "Kanał niedastupny: Server vymahaje niepadtrymanaje pieranakiravańnie!" #: ../src/net.c:466 msgid "Client Error" msgstr "Pamyłka klijenta" #: ../src/net.c:468 msgid "Server Error" msgstr "Pamyłka servera" #: ../src/net.c:470 #, fuzzy msgid "An unknown networking error happened!" msgstr "(adbyłasia nieviadomaja sietkavaja pamyłka)" #: ../src/parsers/atom10.c:239 msgid "Website" msgstr "" #: ../src/parsers/ns_ag.c:70 msgid "%b %d %H:%M" msgstr "%b %d %H:%M" #. in-memory check function feedlist.opml rule id rule menu label positive menu option negative menu option has param #. ======================================================================================================================================================================================== #: ../src/rule.c:229 msgid "Item" msgstr "Element" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does contain" msgstr "źmiaščaje" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does not contain" msgstr "nie źmiaščaje" #: ../src/rule.c:230 msgid "Item title" msgstr "Zahałovak elementu" #: ../src/rule.c:231 msgid "Item body" msgstr "Cieła elementu" #: ../src/rule.c:232 msgid "Read status" msgstr "Stan pračytanaści" #: ../src/rule.c:232 msgid "is unread" msgstr "nie pračytany" #: ../src/rule.c:232 msgid "is read" msgstr "pračytany" #: ../src/rule.c:233 msgid "Flag status" msgstr "Stan ściahu" #: ../src/rule.c:233 msgid "is flagged" msgstr "zaznačany" #: ../src/rule.c:233 msgid "is unflagged" msgstr "nie zaznačany" #: ../src/rule.c:234 msgid "Podcast" msgstr "Podcast" #: ../src/rule.c:234 msgid "included" msgstr "ułučany" #: ../src/rule.c:234 msgid "not included" msgstr "nia ŭłučany" #: ../src/rule.c:235 #, fuzzy msgid "Category" msgstr "Stvaralnik" #: ../src/rule.c:235 #, fuzzy msgid "is set" msgstr "pračytany" #: ../src/rule.c:235 msgid "is not set" msgstr "" #: ../src/rule.c:236 msgid "Feed title" msgstr "Zahałovak kanału" #: ../src/rule.c:237 #, fuzzy msgid "Feed source" msgstr "Krynica kanału" #: ../src/rule.c:238 msgid "Parent folder title" msgstr "" #: ../src/subscription.c:108 #, c-format msgid "Subscription \"%s\" is already being updated!" msgstr "Padpiska \"%s\" užo była aktualizavanaja!" #: ../src/subscription.c:113 #, c-format msgid "" "The subscription \"%s\" was discontinued. Liferea won't update it anymore!" msgstr "" "Padpiska \"%s\" skončyła pracu. Liferea bolš nia budzie jaje aktualizoŭvać!" #: ../src/subscription.c:188 #, c-format msgid "The URL of \"%s\" has changed permanently and was updated" msgstr "Spasyłka na \"%s\" niadaŭna źmianiłasia i była aktualizavanaja" #: ../src/subscription.c:204 #, c-format msgid "\"%s\" is discontinued. Liferea won't updated it anymore!" msgstr "\"%s\" skončyŭ pracu. Liferea bolš nia budzie aktualizoŭvać jaho!" #: ../src/subscription.c:208 #, c-format msgid "\"%s\" has not changed since last update" msgstr "\"%s\" nie źmianiŭsia ad apošniaj aktualizacyi" #: ../src/subscription.c:221 ../src/subscription.c:296 #, fuzzy, c-format msgid "Updating (%d / %d) ..." msgstr "Aktualizacyja..." #: ../src/subscription.c:298 #, fuzzy, c-format msgid "Updating '%s'..." msgstr "Aktualizacyja..." #: ../src/ui/auth_dialog.c:114 #, c-format msgid "Enter the username and password for \"%s\" (%s):" msgstr "Uviadzi nazvu karystalnika j parol dla \"%s\" (%s):" #: ../src/ui/auth_dialog.c:116 msgid "Unknown source" msgstr "Nieviadomaja krynica" #: ../src/ui/browser_tabs.c:262 msgid "Untitled" msgstr "Biaz nazvy" #: ../src/ui/enclosure_list_view.c:168 #, fuzzy msgid "Attachments" msgstr "kamentary" #. The following literals are the enclosure list size units #: ../src/ui/enclosure_list_view.c:260 msgid " Bytes" msgstr "" #: ../src/ui/enclosure_list_view.c:263 msgid "kB" msgstr "" #: ../src/ui/enclosure_list_view.c:267 msgid "MB" msgstr "" #: ../src/ui/enclosure_list_view.c:271 msgid "GB" msgstr "" #: ../src/ui/enclosure_list_view.c:275 #, c-format msgid "%d%s" msgstr "" #. update list title #: ../src/ui/enclosure_list_view.c:313 #, c-format msgid "%d attachment" msgid_plural "%d attachments" msgstr[0] "" msgstr[1] "" msgstr[2] "" #: ../src/ui/enclosure_list_view.c:402 ../src/ui/subscription_dialog.c:355 msgid "Choose File" msgstr "Abiary fajł" #: ../src/ui/enclosure_list_view.c:463 #, fuzzy, c-format msgid "File Extension .%s" msgstr "Pašyreńnie fajłu .%s" #: ../src/ui/feed_list_view.c:432 msgid "Liferea is in offline mode. No update possible." msgstr "Liferea u adłučanym režymie. Aktualizacyja niemahčymaja." #: ../src/ui/feed_list_view.c:478 #, fuzzy msgid "all feeds" msgstr "Šukaj va ŭsich kanałach" #: ../src/ui/feed_list_view.c:479 #, fuzzy, c-format msgid "Mark %s as read ?" msgstr "Zaznač usie jak pračytanyja" #: ../src/ui/feed_list_view.c:483 #, fuzzy, c-format msgid "Are you sure you want to mark all items in %s as read ?" msgstr "Ty ŭpeŭnieny, što chočaš vydalić \"%s\"?" #: ../src/ui/feed_list_view.c:621 msgid "(Empty)" msgstr "" #: ../src/ui/feed_list_view.c:832 #, c-format msgid "" "%s\n" "Rebuilding" msgstr "" #: ../src/ui/feed_list_view.c:901 msgid "Deleting entry" msgstr "Vydaleńnie elementu" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\" and its contents?" msgstr "Ty ŭpeŭnieny, što chočaš vydalić \"%s\" i jaho źmieściva?" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\"?" msgstr "Ty ŭpeŭnieny, što chočaš vydalić \"%s\"?" #: ../src/ui/feed_list_view.c:911 ../src/ui/feed_list_view.c:965 #, fuzzy msgid "_Cancel" msgstr "Anuluj _usie" #: ../src/ui/feed_list_view.c:912 ../src/ui/popup_menu.c:335 #, fuzzy msgid "_Delete" msgstr "/_Vydal" #: ../src/ui/feed_list_view.c:914 msgid "Deletion Confirmation" msgstr "Paćvierdžańnie vydaleńnia" #: ../src/ui/feed_list_view.c:953 #, c-format msgid "" "Are you sure that you want to add a new subscription with URL \"%s\"? " "Another subscription with the same URL already exists (\"%s\")." msgstr "" #: ../src/ui/feed_list_view.c:966 msgid "_Add" msgstr "" #: ../src/ui/feed_list_view.c:968 #, fuzzy msgid "Adding Duplicate Subscription Confirmation" msgstr "Paćvierdžańnie vydaleńnia" #: ../src/ui/icons.c:54 #, c-format msgid "Couldn't find pixmap file: %s" msgstr "Niemahčyma znajści fajł pikselnaj mapy: %s" #: ../src/ui/item_list_view.c:115 msgid "This item has no link specified!" msgstr "Hety element nia maje akreślenaje spasyłki!" #: ../src/ui/item_list_view.c:482 msgid "*** No title ***" msgstr "*** Biaz nazvy ***" #: ../src/ui/item_list_view.c:486 msgid " important " msgstr "" #: ../src/ui/item_list_view.c:849 msgid "Headline" msgstr "Zahałovak" #: ../src/ui/item_list_view.c:871 msgid "Date" msgstr "Data" #: ../src/ui/item_list_view.c:1038 msgid "You must select a feed to delete its items!" msgstr "Ty pavinny abrać kanał, dla jakoha vydalić elementy!" #: ../src/ui/item_list_view.c:1054 ../src/ui/item_list_view.c:1132 #: ../src/ui/item_list_view.c:1147 msgid "No item has been selected" msgstr "Elementy nie zaznačanyja" #: ../src/ui/liferea_shell.c:408 #, c-format msgid " (%d new)" msgid_plural " (%d new)" msgstr[0] " (%d novy)" msgstr[1] " (%d novyja)" msgstr[2] " (%d novych)" #: ../src/ui/liferea_shell.c:413 #, c-format msgid "%d unread%s" msgid_plural "%d unread%s" msgstr[0] "%d niepračytany%s" msgstr[1] "%d niepračytanyja%s" msgstr[2] "%d niepračytanych%s" #: ../src/ui/liferea_shell.c:754 msgid "Help Topics" msgstr "Temy dapamohi" #: ../src/ui/liferea_shell.c:760 msgid "Quick Reference" msgstr "Chutki ahlad" #: ../src/ui/liferea_shell.c:766 msgid "FAQ" msgstr "Pytańni j adkazy" #: ../src/ui/liferea_shell.c:1044 #, fuzzy, c-format msgid "Email command failed: %s" msgstr "Pamyłka zahadu hartača: %s" #: ../src/ui/popup_menu.c:102 ../glade/liferea_menu.ui.h:25 msgid "Open In _Tab" msgstr "" #: ../src/ui/popup_menu.c:106 ../glade/liferea_menu.ui.h:26 #, fuzzy msgid "_Open In Browser" msgstr "_Adčyni ŭ hartačy" #: ../src/ui/popup_menu.c:110 ../glade/liferea_menu.ui.h:27 #, fuzzy msgid "Open In _External Browser" msgstr "Nałady vonkavaha hartača" #: ../src/ui/popup_menu.c:115 msgid "Email The Author" msgstr "" #: ../src/ui/popup_menu.c:140 #, fuzzy msgid "Copy to News Bin" msgstr "/Skapijuj u koš navinaŭ/%s" #: ../src/ui/popup_menu.c:148 #, fuzzy, c-format msgid "_Bookmark at %s" msgstr "/_Zrabi zakładku dla spasyłki na %s" #: ../src/ui/popup_menu.c:154 #, fuzzy msgid "Copy Item _Location" msgstr "/_Skapijuj adras spasyłki" #: ../src/ui/popup_menu.c:163 ../glade/liferea_menu.ui.h:22 msgid "Toggle _Read Status" msgstr "Źmiani _stan pračytanaści" #: ../src/ui/popup_menu.c:167 ../glade/liferea_menu.ui.h:23 msgid "Toggle Item _Flag" msgstr "Źmiani ś_ciah elementu" #: ../src/ui/popup_menu.c:171 #, fuzzy msgid "R_emove Item" msgstr "/_Vydal element" #: ../src/ui/popup_menu.c:200 #, fuzzy msgid "Open Enclosure..." msgstr "/Adčyni dałučeńnie..." #: ../src/ui/popup_menu.c:201 #, fuzzy msgid "Save As..." msgstr "/Zapišy jak..." #: ../src/ui/popup_menu.c:202 #, fuzzy msgid "Copy Link Location" msgstr "/_Skapijuj adras spasyłki" #: ../src/ui/popup_menu.c:280 ../glade/liferea_menu.ui.h:13 msgid "_Update" msgstr "_Aktualizuj" #: ../src/ui/popup_menu.c:282 #, fuzzy msgid "_Update Folder" msgstr "/_Aktualizuj kataloh" #: ../src/ui/popup_menu.c:292 #, fuzzy msgid "New _Subscription..." msgstr "_Novaja padpiska..." #: ../src/ui/popup_menu.c:295 ../glade/liferea_menu.ui.h:5 msgid "New _Folder..." msgstr "Novy _kataloh..." #: ../src/ui/popup_menu.c:298 ../glade/liferea_menu.ui.h:6 msgid "New S_earch Folder..." msgstr "Novy kataloh _pošuku..." #: ../src/ui/popup_menu.c:299 #, fuzzy msgid "New S_ource..." msgstr "Novaja _krynica..." #: ../src/ui/popup_menu.c:300 ../glade/liferea_menu.ui.h:8 msgid "New _News Bin..." msgstr "Novy _koš navinaŭ..." #: ../src/ui/popup_menu.c:303 #, fuzzy msgid "_New" msgstr "/_Novy" #: ../src/ui/popup_menu.c:312 #, fuzzy msgid "Sort Feeds" msgstr "Impartuj śpis kanałaŭ" #: ../src/ui/popup_menu.c:320 #, fuzzy msgid "_Mark All As Read" msgstr "/_Zaznač usie jak pračytanyja" #: ../src/ui/popup_menu.c:327 msgid "_Rebuild" msgstr "" #: ../src/ui/popup_menu.c:336 ../glade/liferea_menu.ui.h:17 #, fuzzy msgid "_Properties" msgstr "_Ułaścivaści..." #: ../src/ui/popup_menu.c:343 #, fuzzy msgid "Convert To Local Subscriptions..." msgstr "_Novaja padpiska..." #: ../src/ui/preferences_dialog.c:84 msgid "GNOME default" msgstr "zmoŭčany dla GNOME'a" #: ../src/ui/preferences_dialog.c:85 msgid "Text below icons" msgstr "Tekst pad ikonami" #: ../src/ui/preferences_dialog.c:86 msgid "Text beside icons" msgstr "Tekst pa bakoch ikonaŭ" #: ../src/ui/preferences_dialog.c:87 msgid "Icons only" msgstr "Tolki ikony" #: ../src/ui/preferences_dialog.c:88 msgid "Text only" msgstr "Tolki tekst" #: ../src/ui/preferences_dialog.c:96 ../src/ui/subscription_dialog.c:43 msgid "minutes" msgstr "chvilin" #: ../src/ui/preferences_dialog.c:97 ../src/ui/subscription_dialog.c:44 msgid "hours" msgstr "hadzin" #: ../src/ui/preferences_dialog.c:98 ../src/ui/subscription_dialog.c:45 msgid "days" msgstr "dzion" #: ../src/ui/preferences_dialog.c:103 msgid "Space" msgstr "Prabieł" #: ../src/ui/preferences_dialog.c:104 msgid " Space" msgstr " Prabieł" #: ../src/ui/preferences_dialog.c:105 msgid " Space" msgstr " Prabieł" #: ../src/ui/preferences_dialog.c:110 #, fuzzy msgid "Normal View" msgstr "_Zvyčajny vyhlad" #: ../src/ui/preferences_dialog.c:111 #, fuzzy msgid "Wide View" msgstr "Š_yroki vyhlad" #: ../src/ui/preferences_dialog.c:478 #, fuzzy msgid "Default Browser" msgstr "Zmoŭčany hartač GNOME'a" #: ../src/ui/preferences_dialog.c:480 msgid "Manual" msgstr "Admysłovy" #: ../src/ui/preferences_dialog.c:740 msgid "Type" msgstr "Typ" #: ../src/ui/preferences_dialog.c:743 msgid "Program" msgstr "Prahrama" #: ../src/ui/search_dialog.c:106 #, fuzzy msgid "Saved Search" msgstr "Admysłovaje" #: ../src/ui/subscription_dialog.c:427 #, c-format msgid "The provider of this feed suggests an update interval of %d minute." msgid_plural "" "The provider of this feed suggests an update interval of %d minutes." msgstr[0] "Vydaviec hetaha kanału raić intervał aktualizacyi ŭ %d chvilinu." msgstr[1] "Vydaviec hetaha kanału raić intervał aktualizacyi ŭ %d chvilin." msgstr[2] "Vydaviec hetaha kanału raić intervał aktualizacyi ŭ %d chvilin." #: ../src/ui/subscription_dialog.c:431 msgid "This feed specifies no default update interval." msgstr "Dla hetaha kanału nie akreśleny zmoŭčany intervał aktualizacyi." #: ../src/ui/ui_common.c:206 #, fuzzy msgid "All Files" msgstr "_Lakalny fajł" #: ../src/update.c:350 #, c-format msgid "Error opening temp file %s to use for filtering!" msgstr "Pamyłka adčynieńnia časovaha fajłu %s dziela filtracyi!" #: ../src/update.c:372 #, c-format msgid "%s exited with status %d" msgstr "%s vyjšaŭ sa statusam %d" #: ../src/update.c:378 ../src/update.c:379 ../src/update.c:493 #, c-format msgid "Error: Could not open pipe \"%s\"" msgstr "Pamyłka: Niemahčyma adčynić trubu \"%s\"" #. FIXME: maybe setting request->returncode would be better #: ../src/update.c:517 #, c-format msgid "Error: Could not open file \"%s\"" msgstr "Pamyłka: Niemahčyma adčynić fajł \"%s\"" #: ../src/update.c:523 #, c-format msgid "Error: There is no file \"%s\"" msgstr "Pamyłka: Fajł \"%s\" nie isnuje" #: ../src/vfolder.c:54 msgid "New Search Folder" msgstr "Novy kataloh pošuku" #: ../src/webkit/liferea_web_view.c:177 #, fuzzy msgid "Open Link In _Tab" msgstr "/Adčyni spasyłku ŭ _kartcy" #: ../src/webkit/liferea_web_view.c:178 #, fuzzy msgid "Open Link In Browser" msgstr "/_Adčyni spasyłku ŭ hartačy" #: ../src/webkit/liferea_web_view.c:179 #, fuzzy msgid "Open Link In External Browser" msgstr "/_Adčyni spasyłku ŭ hartačy" #: ../src/webkit/liferea_web_view.c:185 #, fuzzy, c-format msgid "_Bookmark Link at %s" msgstr "/_Zrabi zakładku dla spasyłki na %s" #: ../src/webkit/liferea_web_view.c:192 #, fuzzy msgid "_Copy Link Location" msgstr "/_Skapijuj adras spasyłki" #: ../src/webkit/liferea_web_view.c:195 #, fuzzy msgid "_View Image" msgstr "_Vyhlad" #: ../src/webkit/liferea_web_view.c:196 #, fuzzy msgid "_Copy Image Location" msgstr "/_Skapijuj adras spasyłki" #: ../src/webkit/liferea_web_view.c:199 #, fuzzy msgid "S_ave Link As" msgstr "/Zapišy jak..." #: ../src/webkit/liferea_web_view.c:202 msgid "S_ave Image As" msgstr "" #: ../src/webkit/liferea_web_view.c:209 #, fuzzy msgid "_Subscribe..." msgstr "/_Padpišysia..." #: ../src/webkit/liferea_web_view.c:213 msgid "_Copy" msgstr "" #: ../src/webkit/liferea_web_view.c:219 msgid "_Increase Text Size" msgstr "Pa_vialič pamier tekstu" #: ../src/webkit/liferea_web_view.c:220 msgid "_Decrease Text Size" msgstr "Pa_mienš pamier tekstu" #: ../src/webkit/liferea_web_view.c:227 msgid "_Reader Mode" msgstr "" #: ../src/xml.c:426 msgid "[There were more errors. Output was truncated!]" msgstr "[Byli jašče pamyłki. Vyjście abatnutaje!]" #: ../src/xml.c:594 msgid "XML Parser: Could not parse document:\n" msgstr "Raźbiralnik XML: Niemahčyma razabrać dakument:\n" #: ../glade/about.ui.h:1 msgid "About" msgstr "Ab prahramie" #: ../glade/about.ui.h:2 msgid "Liferea is a news aggregator for GTK+" msgstr "Liferea - heta ahrehatar navinaŭ dziela GTK+" #: ../glade/about.ui.h:3 #, fuzzy msgid "Liferea Homepage" msgstr "Hartač RSS Liferea" #: ../glade/auth.ui.h:1 msgid "Authentication" msgstr "Aŭtaryzacyja" #: ../glade/auth.ui.h:3 #, fuzzy, no-c-format msgid "Enter the username and password for \"%s\" (%s)" msgstr "Uviadzi nazvu karystalnika j parol dla \"%s\" (%s):" #: ../glade/auth.ui.h:4 ../glade/properties.ui.h:31 msgid "User_name:" msgstr "_Nazva karystalnika:" #: ../glade/auth.ui.h:5 ../glade/properties.ui.h:32 msgid "_Password:" msgstr "_Parol:" #: ../glade/enclosure_handler.ui.h:1 #, fuzzy msgid "Open Enclosure" msgstr "/Adčyni dałučeńnie..." #: ../glade/enclosure_handler.ui.h:2 #, fuzzy msgid "Open an enclosure of type:" msgstr "Zahruzka dałučeńnia typu:" #: ../glade/enclosure_handler.ui.h:3 #, fuzzy msgid "" "_What should Liferea do with this enclosure? Please enter the command you " "want to be executed below. The enclosures URL will be supplied as an " "argument for this command:" msgstr "" "Što Liferea musić zrabić z hetym dałučeńniem? Uviadzi zahad, jaki ty chočaš " "vykanać, nižej. Zahružany fajł budzie pieradadzieny zahadu jak arhument:" #: ../glade/enclosure_handler.ui.h:4 msgid "_Browse" msgstr "_Ahladaj" #: ../glade/enclosure_handler.ui.h:5 #, fuzzy msgid "_Do this automatically for enclosures like this from now on." msgstr "_Rabi heta aŭtamatyčna nadalej dla padobnych fajłaŭ." #: ../glade/liferea_menu.ui.h:1 msgid "_Subscriptions" msgstr "_Padpiski" #: ../glade/liferea_menu.ui.h:2 ../glade/liferea_toolbar.ui.h:8 msgid "Update _All" msgstr "Aktualizuj _usie" #: ../glade/liferea_menu.ui.h:3 msgid "Mark All As _Read" msgstr "Paznač usie jak _čytanyja" #: ../glade/liferea_menu.ui.h:4 ../glade/liferea_toolbar.ui.h:1 msgid "_New Subscription..." msgstr "_Novaja padpiska..." #: ../glade/liferea_menu.ui.h:7 msgid "New _Source..." msgstr "Novaja _krynica..." #: ../glade/liferea_menu.ui.h:9 msgid "_Import Feed List..." msgstr "_Impartuj śpis kanałaŭ..." #: ../glade/liferea_menu.ui.h:10 msgid "_Export Feed List..." msgstr "_Ekspartuj śpis kanałaŭ..." #: ../glade/liferea_menu.ui.h:11 msgid "_Quit" msgstr "_Vyjdzi" #: ../glade/liferea_menu.ui.h:12 msgid "_Feed" msgstr "Kanał" #: ../glade/liferea_menu.ui.h:15 msgid "Remove _All Items" msgstr "Vydal _usie elementy" #: ../glade/liferea_menu.ui.h:16 msgid "_Remove" msgstr "_Vydal" #: ../glade/liferea_menu.ui.h:18 msgid "_Item" msgstr "_Element" #: ../glade/liferea_menu.ui.h:24 msgid "R_emove" msgstr "_Vydal" #: ../glade/liferea_menu.ui.h:28 msgid "_View" msgstr "_Vyhlad" #: ../glade/liferea_menu.ui.h:29 msgid "_Fullscreen" msgstr "" #: ../glade/liferea_menu.ui.h:30 msgid "Zoom _In" msgstr "" #: ../glade/liferea_menu.ui.h:31 msgid "Zoom _Out" msgstr "" #: ../glade/liferea_menu.ui.h:32 #, fuzzy msgid "_Normal size" msgstr "_Zvyčajny vyhlad" #: ../glade/liferea_menu.ui.h:33 msgid "_Normal View" msgstr "_Zvyčajny vyhlad" #: ../glade/liferea_menu.ui.h:34 msgid "_Wide View" msgstr "Š_yroki vyhlad" #: ../glade/liferea_menu.ui.h:35 msgid "_Reduced Feed List" msgstr "" #: ../glade/liferea_menu.ui.h:36 msgid "_Tools" msgstr "_Pryładździe" #: ../glade/liferea_menu.ui.h:37 msgid "_Update Monitor" msgstr "_Manitor aktualizacyj" #: ../glade/liferea_menu.ui.h:38 msgid "_Preferences" msgstr "_Nałady" #: ../glade/liferea_menu.ui.h:39 #, fuzzy msgid "S_earch" msgstr "_Šukaj" #: ../glade/liferea_menu.ui.h:41 msgid "_Help" msgstr "_Dapamoha" #: ../glade/liferea_menu.ui.h:42 msgid "_Contents" msgstr "_Źmieściva" #: ../glade/liferea_menu.ui.h:43 msgid "_Quick Reference" msgstr "_Chutki ahlad" #: ../glade/liferea_menu.ui.h:44 msgid "_FAQ" msgstr "_Pytańni j adkazy" #: ../glade/liferea_menu.ui.h:45 msgid "_About" msgstr "_Ab prahramie" #: ../glade/liferea_toolbar.ui.h:2 msgid "Adds a subscription to the feed list." msgstr "Dadaje padpisku ŭ śpis kanałaŭ." #: ../glade/liferea_toolbar.ui.h:4 #, fuzzy msgid "" "Marks all items of the selected feed list node / in the item list as read." msgstr "" "Zaznačaje ŭsie elementy abranaje padpiski albo ŭsich padpisak abranaha " "katalohu jak pračytanyja." #: ../glade/liferea_toolbar.ui.h:9 #, fuzzy msgid "Updates all subscriptions." msgstr "Novaja padpiska" #: ../glade/liferea_toolbar.ui.h:11 msgid "Show the search dialog." msgstr "Pakažy dyjalohavaje vakno pošuku." #: ../glade/mainwindow.ui.h:2 msgid "page 1" msgstr "" #: ../glade/mainwindow.ui.h:3 msgid "page 2" msgstr "" #: ../glade/mainwindow.ui.h:4 ../glade/prefs.ui.h:23 msgid "Headlines" msgstr "Zahałoŭki" #: ../glade/mark_read_dialog.ui.h:1 #, fuzzy msgid "Mark all as read ?" msgstr "Zaznač usie jak pračytanyja" #: ../glade/mark_read_dialog.ui.h:2 msgid "Mark all as read" msgstr "Zaznač usie jak pračytanyja" #: ../glade/mark_read_dialog.ui.h:3 msgid "Do not ask again" msgstr "" #: ../glade/new_folder.ui.h:1 msgid "New Folder" msgstr "Novy kataloh" #: ../glade/new_folder.ui.h:2 msgid "_Folder name:" msgstr "Nazva _katalohu:" #: ../glade/new_newsbin.ui.h:1 msgid "Create News Bin" msgstr "Stvary koš navinaŭ" #: ../glade/new_newsbin.ui.h:2 msgid "_News Bin Name:" msgstr "_Nazva dla koša navinaŭ:" #: ../glade/new_subscription.ui.h:2 ../glade/properties.ui.h:11 #, fuzzy msgid "Feed Source" msgstr "Krynica kanału" #: ../glade/new_subscription.ui.h:3 ../glade/properties.ui.h:12 msgid "Source Type:" msgstr "Typ krynicy:" #: ../glade/new_subscription.ui.h:4 ../glade/properties.ui.h:13 msgid "_URL" msgstr "_Spasyłka" #: ../glade/new_subscription.ui.h:5 ../glade/properties.ui.h:14 msgid "_Command" msgstr "_Zahad" #: ../glade/new_subscription.ui.h:6 ../glade/properties.ui.h:15 msgid "_Local File" msgstr "_Lakalny fajł" #: ../glade/new_subscription.ui.h:7 ../glade/properties.ui.h:16 msgid "Select File..." msgstr "Abiary fajł..." #: ../glade/new_subscription.ui.h:8 ../glade/properties.ui.h:17 msgid "_Source:" msgstr "_Krynica:" #: ../glade/new_subscription.ui.h:9 #, fuzzy msgid "Download / Postprocessing" msgstr "Zahruzka / Kancavaja apracoŭka" #: ../glade/new_subscription.ui.h:10 ../glade/properties.ui.h:30 msgid "_Don't use proxy for download" msgstr "_Nie ŭžyvaj proxy dziela zahruzki" #: ../glade/new_subscription.ui.h:11 ../glade/properties.ui.h:18 msgid "Use conversion _filter" msgstr "Užyj _filter kavertacyi" #: ../glade/new_subscription.ui.h:12 msgid "" "Liferea can use external filter plugins in order to access feeds and " "directories in non-supported formats. See the documentation for more " "information." msgstr "" "Liferea moža vykarystoŭvać vonkavyja filtry kanertacyi, kab mahčy čytać " "kanały j katalohi ŭ niepadtrymanym farmacie. Hladzi padrabiaźniejšyja " "źviestki ŭ dakumentacyi." #: ../glade/new_subscription.ui.h:13 ../glade/properties.ui.h:20 msgid "Convert _using:" msgstr "Kanvertuj _praz:" #: ../glade/node_source.ui.h:1 msgid "Source Selection" msgstr "Vybar krynicy" #: ../glade/node_source.ui.h:2 #, fuzzy msgid "_Select the source type you want to add..." msgstr "Abiary typ krynicy, jakuju chočaš dadać..." #: ../glade/opml_source.ui.h:1 msgid "Add OPML/Planet" msgstr "Dadaj OPML/Planet" #: ../glade/opml_source.ui.h:2 msgid "" "Please specify a local file or an URL pointing to a valid OPML feed list." msgstr "" "Akreśl lakalny fajł albo spasyłku na pravilny śpis kanałaŭ u farmacie OPML." #: ../glade/opml_source.ui.h:3 msgid "_Location" msgstr "_Adras" #: ../glade/opml_source.ui.h:4 msgid "_Select File" msgstr "_Abiary fajł" #: ../glade/prefs.ui.h:1 msgid "Liferea Preferences" msgstr "Pieravahi Liferea" #: ../glade/prefs.ui.h:2 msgid "Feed Cache Handling" msgstr "" #: ../glade/prefs.ui.h:3 msgid "Default _number of items per feed to save:" msgstr "Zmoŭčanaja _kolkaść elementaŭ na kanał dziela zapisu:" #: ../glade/prefs.ui.h:4 ../glade/properties.ui.h:27 msgid "0" msgstr "" #: ../glade/prefs.ui.h:5 #, fuzzy msgid "Feed Update Settings" msgstr "Aktualizacyja kanału" #. Feed update interval hint in preference dialog. #: ../glade/prefs.ui.h:7 msgid "" "Note: Please remember to set a reasonable refresh time. Usually it is a " "waste of bandwidth to poll feeds more often than each hour." msgstr "" "Uvaha: Kali łaska, nie zabudźsia akreślić sensoŭny termin pamiž " "aktualizacyjami. Zvyčajna apytvańnie kanałaŭ čaściej za raz na hadzinu " "źjaŭlajecca marnavańniem trafiku." #: ../glade/prefs.ui.h:8 #, fuzzy msgid "_Update all subscriptions at startup." msgstr "Novaja padpiska" #: ../glade/prefs.ui.h:9 msgid "Default Feed Refresh _Interval:" msgstr "Zmoŭčany _intervał aktualizacyi kanału:" #: ../glade/prefs.ui.h:10 ../glade/properties.ui.h:6 msgid "1" msgstr "" #: ../glade/prefs.ui.h:11 msgid "Feeds" msgstr "Kanały" #: ../glade/prefs.ui.h:12 #, fuzzy msgid "Folder Display Settings" msgstr "Nałady pakazu katalohaŭ" #: ../glade/prefs.ui.h:13 msgid "_Show the items of all child feeds when a folder is selected." msgstr "_Pakazvaj elementy dla ŭsich padkanałaŭ pry zaznačeńni katalohu." #: ../glade/prefs.ui.h:14 msgid "_Hide read items." msgstr "_Schavaj pračytanyja elementy." #: ../glade/prefs.ui.h:15 #, fuzzy msgid "Feed Icons (Favicons)" msgstr "Ikony kanałaŭ" #: ../glade/prefs.ui.h:16 msgid "_Update all favicons now" msgstr "_Aktualizuj zaraz usie ikony kanałaŭ" #: ../glade/prefs.ui.h:17 msgid "Folders" msgstr "Katalohi" #: ../glade/prefs.ui.h:18 #, fuzzy msgid "Reading Headlines" msgstr "niečytanyja zahałoŭki" #: ../glade/prefs.ui.h:19 msgid "_Skim through articles with:" msgstr "_Pierachodź pa artykułach z:" #: ../glade/prefs.ui.h:20 msgid "_Default View Mode:" msgstr "" #: ../glade/prefs.ui.h:21 #, fuzzy msgid "Web Integration" msgstr "Aryjentacyja" #: ../glade/prefs.ui.h:22 msgid "_Post Bookmarks to" msgstr "Ź_miaści zakładki na" #: ../glade/prefs.ui.h:24 #, fuzzy msgid "Internal Browser Settings" msgstr "Nałady ŭnutranaha hartača" #: ../glade/prefs.ui.h:25 msgid "Open links in Liferea's _window." msgstr "Adčyniaj spasyłki ŭ _vaknie Liferea." #: ../glade/prefs.ui.h:26 msgid "_Never run external Javascript." msgstr "" #: ../glade/prefs.ui.h:27 #, fuzzy msgid "_Enable browser plugins." msgstr "Nałady vonkavaha hartača" #: ../glade/prefs.ui.h:28 #, fuzzy msgid "External Browser Settings" msgstr "Nałady vonkavaha hartača" #: ../glade/prefs.ui.h:29 msgid "_Browser:" msgstr "_Hartač:" #: ../glade/prefs.ui.h:30 #, fuzzy msgid "_Manual:" msgstr "Admysłovy" #: ../glade/prefs.ui.h:32 #, no-c-format msgid "(%s for URL)" msgstr "" #: ../glade/prefs.ui.h:33 msgid "Browser" msgstr "Hartač" #: ../glade/prefs.ui.h:34 #, fuzzy msgid "Toolbar Settings" msgstr "_Etykiety knopak z paneli pryładździa:" #: ../glade/prefs.ui.h:35 msgid "_Hide toolbar." msgstr "" #: ../glade/prefs.ui.h:36 msgid "Toolbar _button labels:" msgstr "_Etykiety knopak z paneli pryładździa:" #: ../glade/prefs.ui.h:37 msgid "Other" msgstr "" #: ../glade/prefs.ui.h:38 msgid "Ask for confirmation when marking all items as read" msgstr "" #: ../glade/prefs.ui.h:39 msgid "Desktop" msgstr "" #: ../glade/prefs.ui.h:40 msgid "HTTP Proxy Server" msgstr "" #: ../glade/prefs.ui.h:41 msgid "_Auto Detect (GNOME or environment)" msgstr "_Aŭtamatyčna vyznač (GNOME albo asiarodździe)" #: ../glade/prefs.ui.h:42 msgid "_No Proxy" msgstr "_Biaz proxy" #: ../glade/prefs.ui.h:43 msgid "_Manual Setting:" msgstr "_Admysłovaje akreśleńnie:" #: ../glade/prefs.ui.h:44 msgid "Proxy _Host:" msgstr "_Host proxy:" #: ../glade/prefs.ui.h:45 msgid "Proxy _Port:" msgstr "_Port proxy:" #: ../glade/prefs.ui.h:46 msgid "Use Proxy Au_thentication" msgstr "Užyj _aŭtaryzacyju proxy" #: ../glade/prefs.ui.h:47 msgid "Proxy _Username:" msgstr "_Nazva karystalnika proxy:" #: ../glade/prefs.ui.h:48 msgid "Proxy Pass_word:" msgstr "Pa_rol proxy:" #: ../glade/prefs.ui.h:49 msgid "" "Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per " "application proxy settings. The system's default proxy settings will be used." msgstr "" #: ../glade/prefs.ui.h:50 msgid "Proxy" msgstr "Proxy" #: ../glade/prefs.ui.h:51 #, fuzzy msgid "Privacy Settings" msgstr "Nałady pakazu katalohaŭ" #: ../glade/prefs.ui.h:52 msgid "Tell sites that I do _not want to be tracked." msgstr "" #: ../glade/prefs.ui.h:53 msgid "_Intelligent Tracking Prevention. " msgstr "" #: ../glade/prefs.ui.h:54 msgid "" "This enables the WebKit feature described here." msgstr "" #: ../glade/prefs.ui.h:55 msgid "" "Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or " "higher." msgstr "" #: ../glade/prefs.ui.h:56 msgid "Use _Reader mode." msgstr "" #: ../glade/prefs.ui.h:57 msgid "" "This enables stripping all non-content elements (like scripts, fonts, tracking)" msgstr "" #: ../glade/prefs.ui.h:58 msgid "Privacy" msgstr "" #: ../glade/prefs.ui.h:59 #, fuzzy msgid "Downloading Enclosures" msgstr "Zahruzka dałučeńnia" #: ../glade/prefs.ui.h:60 msgid "_Download using" msgstr "_Zahružaj z dapamohaj" #: ../glade/prefs.ui.h:62 #, no-c-format msgid "custom-command %s" msgstr "" #: ../glade/prefs.ui.h:63 #, fuzzy msgid "Opening Enclosures" msgstr "/Adčyni dałučeńnie..." #: ../glade/prefs.ui.h:64 msgid "Enclosures" msgstr "Dałučeńni" #: ../glade/properties.ui.h:1 msgid "Subscription Properties" msgstr "Ułaścivaści padpiski" #: ../glade/properties.ui.h:2 #, fuzzy msgid "Feed _Name" msgstr "_Nazva kanału:" #: ../glade/properties.ui.h:3 #, fuzzy msgid "Update _Interval" msgstr "Manitor aktualizacyj" #: ../glade/properties.ui.h:4 msgid "_Use global default update interval." msgstr "_Užyj hlabalny zmoŭčany intervał aktualizacyi." #: ../glade/properties.ui.h:5 msgid "_Feed specific update interval of" msgstr "_Admysłovy intervał aktualizacyi kanału" #: ../glade/properties.ui.h:7 msgid "_Don't update this feed automatically." msgstr "_Nie aktualizuj aŭtamatyčna hety kanał." #: ../glade/properties.ui.h:9 #, no-c-format msgid "This feed provider suggests an update interval of %d minutes." msgstr "Vydaviec hetaha kanału raić intervał aktualizacyi ŭ %d chvilin." #: ../glade/properties.ui.h:10 msgid "General" msgstr "Ahulnaje" #: ../glade/properties.ui.h:19 #, fuzzy msgid "" "Liferea can use external filter scripts in order to access feeds and " "directories in non-supported formats." msgstr "" "Liferea moža vykarystoŭvać vonkavyja filtry kanertacyi, kab mahčy čytać " "kanały j katalohi ŭ niepadtrymanym farmacie. Hladzi padrabiaźniejšyja " "źviestki ŭ dakumentacyi." #: ../glade/properties.ui.h:21 ../xslt/item.xml.in.h:1 msgid "Source" msgstr "Krynica" #: ../glade/properties.ui.h:22 msgid "" "The cache setting controls if the contents of feeds are saved when Liferea " "exits. Marked items are always saved to the cache." msgstr "" "Nałada padručnaj pamiaci akreślaje, ci zapisvajecca źmieściva kanału pry " "vychadzie Liferea. Zaznačanyja elementy zaŭsiody zapisvajucca ŭ padručnaj " "pamiaci." #: ../glade/properties.ui.h:23 msgid "_Default cache settings" msgstr "_Zmoŭčanyja nałady padručnaj pamiaci" #: ../glade/properties.ui.h:24 msgid "Di_sable cache" msgstr "_Adklučy padručnuju pamiać" #: ../glade/properties.ui.h:25 msgid "_Unlimited cache" msgstr "_Nieabmiežavanaja padručnaja pamiać" #: ../glade/properties.ui.h:26 #, fuzzy msgid "_Number of items to save:" msgstr "Zmoŭčanaja _kolkaść elementaŭ na kanał dziela zapisu:" #: ../glade/properties.ui.h:28 msgid "Archive" msgstr "Archiŭ" #: ../glade/properties.ui.h:29 msgid "Use HTTP _authentication" msgstr "Užyj _aŭtaryzacyju HTTP" #: ../glade/properties.ui.h:33 msgid "Download" msgstr "Zahruzi" #: ../glade/properties.ui.h:34 msgid "_Automatically download all enclosures of this feed." msgstr "_Aŭtamatyčna zahružaj usie dałučeńni dla hetaha kanału." #: ../glade/properties.ui.h:35 msgid "Auto-_load item link in configured browser when selecting articles." msgstr "" "Aŭtamatyčna zahružaj spasyłku elementu ŭ skanfihuravanym hartačy pry vybary " "artykułaŭ." #: ../glade/properties.ui.h:36 #, fuzzy msgid "Ignore _comment feeds for this subscription." msgstr "Adčyniaje dyjalohavaje vakno ŭłaścivaściaŭ dla abranaje padpiski." #: ../glade/properties.ui.h:37 #, fuzzy msgid "_Mark downloaded items as read." msgstr "_Zaznač abranaje jak pračytanaje" #: ../glade/properties.ui.h:38 msgid "Extract full content from HTML5 and Google AMP" msgstr "" #: ../glade/reedah_source.ui.h:1 #, fuzzy msgid "Add Reedah Account" msgstr "Dadaj kont na Google Reader'y" #: ../glade/reedah_source.ui.h:2 #, fuzzy msgid "Please enter your Reedah account settings." msgstr "Uviadzi nałady tvajho kontu na Google Reader'y." #: ../glade/reedah_source.ui.h:3 ../glade/theoldreader_source.ui.h:3 #: ../glade/ttrss_source.ui.h:4 msgid "_Password" msgstr "_Parol" #: ../glade/reedah_source.ui.h:4 ../glade/theoldreader_source.ui.h:4 msgid "_Username (Email)" msgstr "_Nazva karystalnika (Email)" #: ../glade/rename_node.ui.h:1 msgid "Rename" msgstr "Pieranazavi" #: ../glade/rename_node.ui.h:2 msgid "_New Name:" msgstr "_Novaja nazva:" #: ../glade/search_folder.ui.h:1 msgid "Search Folder Properties" msgstr "Ułaścivaści katalohu pošuku" #: ../glade/search_folder.ui.h:2 #, fuzzy msgid "Search _Name:" msgstr "_Nazva kanału:" #: ../glade/search_folder.ui.h:3 #, fuzzy msgid "Search Rules" msgstr "%d vynik pošuku dla \"%s\"" #: ../glade/search_folder.ui.h:4 msgid "Rules" msgstr "" #: ../glade/search_folder.ui.h:5 msgid "All rules for this search folder" msgstr "" #: ../glade/search_folder.ui.h:6 msgid "Rule Matching" msgstr "" #: ../glade/search_folder.ui.h:7 ../glade/search.ui.h:4 msgid "A_ny Rule Matches" msgstr "" #: ../glade/search_folder.ui.h:8 ../glade/search.ui.h:5 msgid "_All Rules Must Match" msgstr "" #: ../glade/search_folder.ui.h:9 #, fuzzy msgid "Hide read items" msgstr "_Schavaj pračytanyja elementy." #: ../glade/search.ui.h:1 #, fuzzy msgid "Advanced Search" msgstr "Admysłovaje" #: ../glade/search.ui.h:2 #, fuzzy msgid "_Search Folder..." msgstr "Kataloh pošuku" #: ../glade/search.ui.h:3 msgid "Find Items that meet the following criteria" msgstr "" #: ../glade/simple_search.ui.h:1 msgid "Search All Feeds" msgstr "Šukaj va ŭsich kanałach" #: ../glade/simple_search.ui.h:2 #, fuzzy msgid "_Advanced..." msgstr "Admysłovaje..." #: ../glade/simple_search.ui.h:3 msgid "" "Starts searching for the specified text in all feeds. The search result will " "appear in the item list." msgstr "" "Pačynaje pošuk akreślenaha tekstu va ŭsich kanałach. Vynik pošuku źjavicca ŭ " "śpisie elementaŭ." #: ../glade/simple_search.ui.h:4 msgid "_Search for:" msgstr "_Šukaj:" #: ../glade/simple_search.ui.h:5 msgid "" "Enter a search string Liferea should find either in a items title or in its " "content." msgstr "" "Uviadzi radok pošuku, jaki Liferea musić šukać albo ŭ zahaloŭkach elementaŭ, " "albo ŭ ichnym źmieścivie." #: ../glade/simple_subscription.ui.h:2 msgid "Advanced..." msgstr "Admysłovaje..." #: ../glade/simple_subscription.ui.h:3 #, fuzzy msgid "Feed _Source" msgstr "Krynica kanału" #: ../glade/simple_subscription.ui.h:4 msgid "" "Enter a website location to use feed autodiscovery or in case you know it " "the exact feed location." msgstr "" "Uviadzi adras web-placoŭki, kab užyć aŭtamatyčny pošuk kanałaŭ, albo, kali " "ty viedaješ, dakładny adras kanału." #: ../glade/theoldreader_source.ui.h:1 #, fuzzy msgid "Add TheOldReader Account" msgstr "Dadaj kont na Google Reader'y" #: ../glade/theoldreader_source.ui.h:2 #, fuzzy msgid "Please enter your TheOldReader account settings." msgstr "Uviadzi nałady tvajho kontu na Google Reader'y." #: ../glade/ttrss_source.ui.h:1 #, fuzzy msgid "Add Tiny Tiny RSS Account" msgstr "Dadaj kont na Bloglines" #: ../glade/ttrss_source.ui.h:2 #, fuzzy msgid "Please enter your TinyTinyRSS account settings." msgstr "Uviadzi nałady tvajho kontu na Bloglines." #: ../glade/ttrss_source.ui.h:3 #, fuzzy msgid "_Server URL" msgstr "Pamyłka servera" #: ../glade/ttrss_source.ui.h:5 msgid "_Username" msgstr "_Nazva karystalnika" #: ../glade/update_monitor.ui.h:1 msgid "Update Monitor" msgstr "Manitor aktualizacyj" #: ../glade/update_monitor.ui.h:2 msgid "Stop All" msgstr "" #: ../glade/update_monitor.ui.h:3 #, fuzzy msgid "_Pending Requests" msgstr "Zapyty ŭ čakańni" #: ../glade/update_monitor.ui.h:4 #, fuzzy msgid "_Downloading Now" msgstr "Zahružajecca zaraz" #: ../xslt/feed.xml.in.h:1 msgid "Feed:" msgstr "Kanał:" #: ../xslt/feed.xml.in.h:2 ../xslt/source.xml.in.h:1 msgid "Source:" msgstr "Krynica:" #: ../xslt/feed.xml.in.h:3 msgid "Publisher" msgstr "Apublikavaŭ" #: ../xslt/feed.xml.in.h:4 msgid "Copyright" msgstr "Aŭtarskija pravy" #: ../xslt/feed.xml.in.h:5 #, fuzzy msgid "There was a problem when fetching this subscription!" msgstr "" "Pry čytańni hetaj padpiski ŭźnikli prablemy. Pravier adras spasyłki i " "vyjście ŭ kansoli." #: ../xslt/feed.xml.in.h:6 #, fuzzy msgid "1. Authentication" msgstr "Aŭtaryzacyja" #: ../xslt/feed.xml.in.h:7 #, fuzzy msgid "2. Download" msgstr "Zahruzi" #: ../xslt/feed.xml.in.h:8 msgid "3. Feed Discovery" msgstr "" #: ../xslt/feed.xml.in.h:9 msgid "4. Parsing" msgstr "" #: ../xslt/feed.xml.in.h:10 #, fuzzy msgid "Details:" msgstr "Padrabiaznaści" #: ../xslt/feed.xml.in.h:11 msgid "Authentication failed. Please check the credentials and try again!" msgstr "" #: ../xslt/feed.xml.in.h:12 #, fuzzy msgid "There was an error when downloading the feed source:" msgstr "Pry razbory kanału %s adbylisia pamyłki!" #: ../xslt/feed.xml.in.h:13 #, fuzzy msgid "There was an error when running the feed filter command:" msgstr "Pry razbory kanału %s adbylisia pamyłki!" #: ../xslt/feed.xml.in.h:14 msgid "" "The source does not point directly to a feed or a webpage with a link to a " "feed!" msgstr "" #: ../xslt/feed.xml.in.h:15 msgid "Sorry, the feed could not be parsed!" msgstr "" #: ../xslt/feed.xml.in.h:16 msgid "You may want to contact the author/webmaster of the feed about this!" msgstr "" #: ../xslt/folder.xml.in.h:1 msgid "Folder:" msgstr "Kataloh:" #: ../xslt/folder.xml.in.h:2 ../xslt/source.xml.in.h:2 msgid "children with" msgstr "dzieci z" #: ../xslt/folder.xml.in.h:3 ../xslt/source.xml.in.h:3 #: ../xslt/vfolder.xml.in.h:2 msgid "unread headlines" msgstr "niečytanyja zahałoŭki" #: ../xslt/item.xml.in.h:2 msgid "Feed" msgstr "Kanał" #: ../xslt/item.xml.in.h:3 msgid "Filed under" msgstr "Vykładziena na" #: ../xslt/item.xml.in.h:4 msgid "Author" msgstr "Aŭtar" #: ../xslt/item.xml.in.h:5 msgid "Shared by" msgstr "" #: ../xslt/item.xml.in.h:6 msgid "Via" msgstr "Ad" #: ../xslt/item.xml.in.h:7 msgid "Related" msgstr "Padobnyja" #: ../xslt/item.xml.in.h:8 msgid "Also posted in" msgstr "Taksama napisana ŭ" #: ../xslt/item.xml.in.h:9 msgid "Creator" msgstr "Stvaralnik" #: ../xslt/item.xml.in.h:10 msgid "Coordinates" msgstr "" #: ../xslt/item.xml.in.h:11 msgid "Map" msgstr "" #: ../xslt/item.xml.in.h:12 msgid "View count" msgstr "" #: ../xslt/item.xml.in.h:13 msgid "Rating" msgstr "" #: ../xslt/item.xml.in.h:14 msgid "Comments" msgstr "Kamentary" #: ../xslt/item.xml.in.h:15 msgid "Updating..." msgstr "Aktualizacyja..." #: ../xslt/item.xml.in.h:16 msgid "Section" msgstr "Raździeł" #: ../xslt/item.xml.in.h:17 msgid "Department" msgstr "Addzieł" #: ../xslt/newsbin.xml.in.h:1 msgid "News Bin:" msgstr "Koš navinaŭ:" #: ../xslt/newsbin.xml.in.h:2 msgid "" "Add items to this news bin by selecting \"Copy to News Bin\" from the item " "list context menu." msgstr "" "Dadaj elementy da hetaha košu z navinami, abraŭšy \"Skapijuj u koš navinaŭ\" " "u kantekstnym menu dla śpisu elementaŭ." #: ../xslt/vfolder.xml.in.h:1 msgid "Search Folder:" msgstr "Kataloh pošuku:" #, c-format #~ msgid "\"%s\" is not available" #~ msgstr "\"%s\" niedastupny" #, c-format #~ msgid "\"%s\" updated..." #~ msgstr "\"%s\" aktualizavany..." #~ msgid "" #~ "The last update of this subscription failed!
HTTP error code : " #~ msgstr "" #~ "Apošniaja aktualizacyja hetaj padpiski była niepaśpiachovaj!
Kod " #~ "pamyłki HTTP : " #~ msgid "There were errors while parsing this feed!" #~ msgstr "Pry razbory hetaha kanału adbylisia pamyłki!" #~ msgid "Parser Error Details" #~ msgstr "Padrabiaznaści ab pamyłcy razboru" #~ msgid "There were errors while filtering this feed!" #~ msgstr "Pry filtravańni hetaha kanału adbylisia pamyłki!" #~ msgid "Filter Error Details" #~ msgstr "Padrabiaznaści ab pamyłcy ŭ filtry" #~ msgid "" #~ "

Could not detect the type of this feed! Please check if the source " #~ "really points to a resource provided in one of the supported syndication " #~ "formats!

XML Parser Output:
" #~ msgstr "" #~ "

Niemahčyma vyznačyć typ dla hetaha kanału! Pravier, ci krynica " #~ "sapraŭdy viadzie da resursu ŭ adnym z padtrymanych farmataŭ syndykacyi!Vyjście raźbiralnika XML:

" #~ msgid "" #~ "The URL you want Liferea to subscribe to points to a webpage and the auto " #~ "discovery found no feeds on this page. Maybe this webpage just does not " #~ "support feed auto discovery." #~ msgstr "" #~ "Spasyłka, jakuju ty pieradaŭ Liferea dziela padpiski, pakazvaje na " #~ "sieciŭnuju staronku, i aŭtapošuk nie znajšoŭ kanałaŭ na hetaj staroncy. " #~ "Moža, hetaja sieciŭnaja staronka nie padtrymvaje aŭtapošuku kanałaŭ." #~ msgid "Source points to HTML document." #~ msgstr "Krynica pakazvaje na dakument HTML." #~ msgid "Could not determine the feed type." #~ msgstr "Niemahčyma vyznačyć typ kanału." #~ msgid "Gone. Resource doesn't exist. Please unsubscribe!" #~ msgstr "Źnik. Resurs nie isnuje. Kali łaska, adpišysia!" #~ msgid "Updating \"%s\"" #~ msgstr "Aktualizacyja \"%s\"" #~ msgid "XML error while reading feed! Feed \"%s\" could not be loaded!" #~ msgstr "Pamyłka XML pry čytańni kanału! Niemahčyma zahruzić kanał \"%s\"!" #, fuzzy #~ msgid "Combined View" #~ msgstr "_Kambinavany vyhlad" #~ msgid "_Disable Javascript." #~ msgstr "_Adklučy Javascript." #~ msgid "GUI" #~ msgstr "GUI" #, fuzzy #~ msgid "Cancel All" #~ msgstr "Anuluj _usie" #~ msgid "Updating favicon for \"%s\"" #~ msgstr "Aktualizacyja ikony kanału dla \"%s\"" #~ msgid "Marks read every item of every subscription." #~ msgstr "Paznačaje čytanym kožny element z kožnaje padpiski." #~ msgid "Imports an OPML feed list." #~ msgstr "Impartuje śpis kanałaŭ u farmacie OPML." #~ msgid "Exports the feed list as OPML." #~ msgstr "Ekspartuje śpis kanałaŭ u farmacie OPML." #~ msgid "Removes all items of the currently selected feed." #~ msgstr "Vydalaje ŭsie elementy z dziejna abranaha kanału." #~ msgid "Increases the text size of the item view." #~ msgstr "Pavialičvaje pamier tekstu ŭ vyhladzie elementaŭ." #~ msgid "Decreases the text size of the item view." #~ msgstr "Pamianšaje pamier tekstu ŭ vyhladzie elementaŭ." #~ msgid "Show a list of all feeds currently in the update queue" #~ msgstr "" #~ "Pakažy śpis usich kanałaŭ, što dziejna znachdziacca ŭ čarzie na " #~ "aktualizacyju" #~ msgid "Edit Preferences." #~ msgstr "Redahuj nałady." #~ msgid "View help for this application." #~ msgstr "Pakažy dapamohu dla hetaje aplikacyi." #~ msgid "View a list of all Liferea shortcuts." #~ msgstr "Pakažy śpis usich skarotaŭ Liferea." #~ msgid "View the FAQ for this application." #~ msgstr "Pakažy Pytańni j Adkazy dla hetaje aplikacyi." #~ msgid "Shows an about dialog." #~ msgstr "Pakazvaje dyjalohavaje vakno sa źviestkami ab prahramie." #~ msgid "Set view mode to mail client mode." #~ msgstr "Vyznač režym ahladu paštovaha klijenta." #~ msgid "Set view mode to use three vertical panes." #~ msgstr "Vyznač režym ahladu z tryma vertykalnymi panelami." #~ msgid "_Combined View" #~ msgstr "_Kambinavany vyhlad" #~ msgid "Set view mode to two pane mode." #~ msgstr "Vyznač režym ahladu z dvuma panelami." #, fuzzy #~ msgid "Hide feeds with no unread items." #~ msgstr "Nia niečytanych elementaŭ " #~ msgid "Adds a folder to the feed list." #~ msgstr "Dadaje kataloh u śpis kanałaŭ." #~ msgid "Adds a new search folder to the feed list." #~ msgstr "Dadaje novy kataloh pošuku ŭ śpis kanałaŭ." #~ msgid "Adds a new feed list source." #~ msgstr "Dadaje novuju krynicu śpisu kanałaŭ." #~ msgid "Adds a new news bin." #~ msgstr "Dadaje novy koš navinaŭ." #~ msgid "" #~ "Updates the selected subscription or all subscriptions of the selected " #~ "folder." #~ msgstr "" #~ "Aktualizuje abranuju padpisku albo ŭsie padpiski z abranaha katalohu." #~ msgid "Opens the property dialog for the selected subscription." #~ msgstr "Adčyniaje dyjalohavaje vakno ŭłaścivaściaŭ dla abranaje padpiski." #~ msgid "Removes the selected subscription." #~ msgstr "Vydalaje abranuju padpisku." #~ msgid "Toggles the read status of the selected item." #~ msgstr "Źmianiaje stan pračytanaści abranaha elementu." #~ msgid "Toggles the flag status of the selected item." #~ msgstr "Źmianiaje stan ściahu dla abranaha elementu." #~ msgid "Removes the selected item." #~ msgstr "Vydalaje abrany element." #, fuzzy #~ msgid "Launches the item's link in a new Liferea browser tab." #~ msgstr "Adčyniaja spasyłku elementu ŭ skanfihuravanym hartačy." #, fuzzy #~ msgid "Launches the item's link in the Liferea item pane." #~ msgstr "Adčyniaja spasyłku elementu ŭ skanfihuravanym hartačy." #, fuzzy #~ msgid "Launches the item's link in the configured external browser." #~ msgstr "Adčyniaja spasyłku elementu ŭ skanfihuravanym hartačy." #~ msgid "_Work Offline" #~ msgstr "_Pracuj adłučana" #, fuzzy #~ msgid "_Update All" #~ msgstr "/_Aktualizuj usie" #, fuzzy #~ msgid "_Show Liferea" #~ msgstr "Liferea" #, fuzzy #~ msgid "InoReader" #~ msgstr "Google Reader" #~ msgid "" #~ "Note: The username and password will be saved to your Liferea feedlist " #~ "file without using encryption." #~ msgstr "" #~ "Uvaha: Nazva karystalnika j parol buduć zachavanyja ŭ fajle śpisu " #~ "kanałaŭ, niezašyfravanyja." #~ msgid "Add Google Reader Account" #~ msgstr "Dadaj kont na Google Reader'y" #~ msgid "Please enter your Google Reader account settings." #~ msgstr "Uviadzi nałady tvajho kontu na Google Reader'y." #, fuzzy #~ msgid "Add InoReader Account" #~ msgstr "Dadaj kont na Google Reader'y" #, fuzzy #~ msgid "Please enter your InoReader account settings." #~ msgstr "Uviadzi nałady tvajho kontu na Google Reader'y." #~ msgid "View Headlines" #~ msgstr "Pakažy zahałoŭki" #, fuzzy #~ msgid "Feed Name" #~ msgstr "_Nazva kanału:" #~ msgid "normal view" #~ msgstr "zvyčajny vyhlad" #~ msgid "wide view" #~ msgstr "šyroki vyhlad" #~ msgid "combined view" #~ msgstr "kambinavany vyhlad" #, fuzzy #~ msgid "Liferea, the Linux Feed Reader" #~ msgstr "Liferea - Čytač kanałaŭ dla Linuksa" #, fuzzy #~ msgid "_Open Link In Browser" #~ msgstr "/_Adčyni spasyłku ŭ hartačy" #, fuzzy #~ msgid "_Open Link In External Browser" #~ msgstr "/_Adčyni spasyłku ŭ hartačy" #~ msgid " " #~ msgstr " " #~ msgid "Create Search Engine Feed" #~ msgstr "Stvary kanał pošukavika" #~ msgid "enter any search string you want" #~ msgstr "uviadzi luby radok dziela pošuku" #~ msgid "Maximal _Number Of Result Items:" #~ msgstr "Maksymalnaja _kolkaść vynikovych elementaŭ:" #, fuzzy #~ msgid "" #~ "Note: Liferea will generate a feed subscription which is used to query " #~ "the search engine results for the specified search string. You can keep " #~ "this feed permanently and update it like any other subscription." #~ msgstr "" #~ "Uvaha: Liferea zhieneruje padpisku na kanał, jakaja vykarystoŭvajecca " #~ "dziela zapytańnia vynikaŭ ad pošukavika na akreśleny radok pošuku. Ty " #~ "možaš pakinuć hety kanał i aktualizoŭvać jaho tak sama, jak i inšyja " #~ "padpiski. " #~ msgid "Liferea is now online" #~ msgstr "Liferea ciapier spałučany" #~ msgid "Work Offline" #~ msgstr "Pracuj adłučana" #~ msgid "Liferea is now offline" #~ msgstr "Liferea ciapier adłučany" #~ msgid "Work Online" #~ msgstr "Pracuj spałučana" #~ msgid "This option allows you to disable subscription updating." #~ msgstr "Hetaja opcyja adklučaje aktualizacyi padpisak." #~ msgid "Browser default" #~ msgstr "Zmoŭčanaje dla hartača" #~ msgid "Existing window" #~ msgstr "Najaŭnaje vakno" #~ msgid "New window" #~ msgstr "Novaje vakno" #~ msgid "New tab" #~ msgstr "Novaja kartka" #, fuzzy #~ msgid "AOL Reader" #~ msgstr "Hartač RSS" #~ msgid "Online/Offline Button" #~ msgstr "Knopka źmieny spałučanaści" #~ msgid "_Open link in:" #~ msgstr "_Adčyni spasyłku ŭ:" #~ msgid "No comments yet." #~ msgstr "Jašče niama kamentaroŭ." #~ msgid "Refresh" #~ msgstr "Aktualizuj" #~ msgid "Liferea - Linux Feed Reader" #~ msgstr "Liferea - Čytač kanałaŭ dla Linuksa" #, fuzzy #~ msgid "" #~ "

Welcome to Liferea, a desktop news aggregator for online news " #~ "feeds.

You can add new subscriptions

  • From main menu " #~ "'Subscription' -> 'New Subscription'
  • By dropping feed links " #~ "into the subscription list
  • By right clicking links and choosing " #~ "'Subscribe' within Liferea

" #~ msgstr "" #~ "

Ciabie vitaje Liferea, ahrehatar navinaŭ dla sieciŭnych " #~ "navinnych kanałaŭ.

Levaja panel źmiaščaje śpis tvaich padpisak. Kab " #~ "dadać padpisku, abiary Kanały -> Novaja padpiska. Kab čytać zahałoŭki " #~ "kanału, abiary jaho ŭ spisie kanałaŭ, i tady zahałoŭki buduć pakazanyja ŭ " #~ "pravaj paneli.

" #, fuzzy #~ msgid "Terminate instead of minimizing to the messaging menu" #~ msgstr "_Zakančvaj pracu zamiest minimizacyi ŭ ikonu traju." #~ msgid "%d new item" #~ msgid_plural "%d new items" #~ msgstr[0] "%d novy element" #~ msgstr[1] "%d novyja elementy" #~ msgstr[2] "%d novych elementaŭ" #~ msgid "No new items" #~ msgstr "Niama novych elementaŭ" #~ msgid "" #~ "%s\n" #~ "%d unread item" #~ msgid_plural "" #~ "%s\n" #~ "%d unread items" #~ msgstr[0] "" #~ "%s\n" #~ "%d niepračytany element" #~ msgstr[1] "" #~ "%s\n" #~ "%d niepračytanyja elementy" #~ msgstr[2] "" #~ "%s\n" #~ "%d niepračytanych elementaŭ" #~ msgid "" #~ "%s\n" #~ "No unread items" #~ msgstr "" #~ "%s\n" #~ "Niama niepračytanych elementaŭ" #~ msgid "Invalid Atom feed: unknown author" #~ msgstr "Niapravilny kanał Atom: nieviadomy aŭtar" #, fuzzy #~ msgid "" #~ "Integrate the feed list of your Google Reader account. Liferea will " #~ "present your Google Reader subscriptions, and will synchronize your feed " #~ "list and reading lists." #~ msgstr "" #~ "Intehruj śpis kanałaŭ z kontam na Google Reader'y. Liferea pakaža tvaju " #~ "padpisku na Google Reader'y ŭ vyhladzie abaronienaha ad źmienaŭ dreva ŭ " #~ "śpisie kanałaŭ." #~ msgid "" #~ "Integrate blogrolls or Planets in your feed list. Liferea will " #~ "automatically add and remove feeds according to the changes of the source " #~ "OPML document" #~ msgstr "" #~ "Intehruj blogroll'y albo Planet'y z tvaim śpisam kanałaŭ. Liferea budzie " #~ "aŭtamatyčna dadavać i vydalać kanały zhodna sa źmienami kryničnaha " #~ "dakumentu OPML." #, fuzzy #~ msgid "" #~ "Integrate the feed list of your Tiny Tiny RSS 1.5+ account. Liferea will " #~ "present your tt-rss subscriptions, and will synchronize your feed list " #~ "and reading lists." #~ msgstr "" #~ "Intehruj śpis kanałaŭ z kontam na Google Reader'y. Liferea pakaža tvaju " #~ "padpisku na Google Reader'y ŭ vyhladzie abaronienaha ad źmienaŭ dreva ŭ " #~ "śpisie kanałaŭ." #~ msgid "This feed does not exist anymore!" #~ msgstr "Hety kanał bolš nie isnuje!" #~ msgid "This news entry has no headline" #~ msgstr "Hety element navinaŭ nia maje zahałoŭka" #~ msgid "Visit" #~ msgstr "Naviedaj" #~ msgid "Open feed" #~ msgstr "Adčyni kanał" #, fuzzy #~ msgid "_Enforce popup notification for this subscription." #~ msgstr "Adčyniaje dyjalohavaje vakno ŭłaścivaściaŭ dla abranaje padpiski." #~ msgid "Show a _popup window with new headlines." #~ msgstr "Pakažy _padručnaje vakno z novymi zahałoŭkami." #~ msgid "Show a status _icon in the notification area (system tray)." #~ msgstr "Pakazvaj _ikonu stanu ŭ abšary nahadvańniaŭ (systemnym trai)." #~ msgid "Show _number of new items in the tray icon." #~ msgstr "Pakazvaj _kolkaść novych elementaŭ na ikonie ŭ trai." #~ msgid "T_erminate instead of minimizing to tray icon." #~ msgstr "_Zakančvaj pracu zamiest minimizacyi ŭ ikonu traju." #~ msgid "Download and view feeds" #~ msgstr "Atrymvaj i ahladaj kanały RSS" #~ msgid "You may want to validate the feed using" #~ msgstr "Ty možaš pravieryć kanał z dapamohaj" #, fuzzy #~ msgid "Launch Item In _Tab" #~ msgstr "/Adčyni element u _kartcy" #, fuzzy #~ msgid "_Launch Item In Browser" #~ msgstr "/_Adčyni element u _hartačy" #, fuzzy #~ msgid "Copy Item _URL to Clipboard" #~ msgstr "/Skapijuj adras elementu ŭ bufer" #~ msgid "flag" #~ msgstr "ściah" #~ msgid "bookmark" #~ msgstr "zakładka" #~ msgid "comments" #~ msgstr "kamentary" #, fuzzy #~ msgid "Enclosure download FAILED: \"%s\"" #~ msgstr "Zahruzka dałučeńnia skončanaja: \"%s\"" #~ msgid "Enclosure download finished: \"%s\"" #~ msgstr "Zahruzka dałučeńnia skončanaja: \"%s\"" #, fuzzy #~ msgid "" #~ "This version of Liferea uses a new cache format and has migrated your " #~ "feed cache. The cache content in %s was not deleted automatically. Please " #~ "remove this directory manually once you are sure migration was successful!" #~ msgstr "" #~ "Hetaja versija Liferea vykarystoŭvaje padručnuju pamiać u novym farmacie, " #~ "tamu tvaje padručnyja fajły byli skanvertavanyja. Padručnyja fajły versii " #~ "1.2 u ~/.liferea_1.2 nie byli vydalenyja aŭtamatyčna. Vydal hety kataloh " #~ "samastojna, jak tolki ŭpeŭniśsia ŭ paśpiachovaści mihracyi!" #, fuzzy #~ msgid "Download FAILED: \"%s\"" #~ msgstr "Zahruzka dałučeńnia skončanaja: \"%s\"" #, fuzzy #~ msgid "Download finished." #~ msgstr "_Zahružaj z dapamohaj" #~ msgid "Choose download directory" #~ msgstr "Abiary kataloh dziela zahruzak" #~ msgid "" #~ "_Manual:\n" #~ "(%s for URL)" #~ msgstr "" #~ "_Admysłovy:\n" #~ "(%s dziela adrasu)" #~ msgid "_Save downloads in" #~ msgstr "_Zapisvaj zahružanaje ŭ" #, fuzzy #~ msgid "_Service Name" #~ msgstr "Nazva skryptu" #~ msgid "Downloading Enclosure" #~ msgstr "Zahruzka dałučeńnia" #~ msgid "" #~ "Jumps to the next unread item. If necessary selects the next feed with " #~ "unread items." #~ msgstr "" #~ "Pierskokvaje da nastupnaha niečytanaha elementu. Kali treba, abiraje " #~ "nastupny kanał ź niečytanymi elementami." #, fuzzy #~ msgid "Print debugging messages for the plugin loading" #~ msgstr "" #~ " --debug- Pakazvaj debugavyja paviedamleńni dla peŭnaj " #~ "temypamiaćciu" #~ msgid "Liferea seems to be running already!" #~ msgstr "Liferea ŭžo pracuje!" #~ msgid "Update status" #~ msgstr "Stan aktualizavanaści" #~ msgid "was updated" #~ msgstr "byŭ aktualizavany" #~ msgid "was not updated" #~ msgstr "nia byŭ aktualizavany" #~ msgid "The orientation of the tray." #~ msgstr "Aryjentacyja traju." #~ msgid "%s" #~ msgstr "%s" #~ msgid "topics_en.html" #~ msgstr "topics_en.html" #~ msgid "reference_en.html" #~ msgstr "reference_en.html" #~ msgid "faq_en.html" #~ msgstr "faq_en.html" #~ msgid "_Script Manager" #~ msgstr "_Kiraŭnik skryptoŭ" #~ msgid "Allows to configure and edit LUA hook scripts" #~ msgstr "Dazvalaje skanfihuravać i adredahavać skrypty LUA" #~ msgid "Search With ..." #~ msgstr "Šukaj u..." #~ msgid "%d Search Result for \"%s\"" #~ msgid_plural "%d Search Results for \"%s\"" #~ msgstr[0] "%d vynik pošuku dla \"%s\"" #~ msgstr[1] "%d vyniki pošuku dla \"%s\"" #~ msgstr[2] "%d vynikaŭ pošuku dla \"%s\"" #~ msgid "" #~ "The item list now contains all items matching the specified search " #~ "pattern. If you want to save this search result permanently you can click " #~ "the \"Search Folder\" button in the search dialog and Liferea will add a " #~ "search folder to your feed list." #~ msgstr "" #~ "Śpis elementaŭ ciapier źmiaščaje ŭsie elementy, što adpaviadajuć " #~ "akreślenamu šablonu pošuku. Kali ty chočaš stała zachavać hety vynik " #~ "pošuku, možaš kliknuć knopku \"Kataloh pošuku\" ŭ dyjalohavym aknie " #~ "pošuku, i tady Liferea dadaść kataloh pošuku ŭ tvoj śpis kanałaŭ." #, fuzzy #~ msgid "Count" #~ msgstr "Kamentary" #~ msgid "You have to select a feed entry" #~ msgstr "Ty pavinny abrać zapis kanału" #~ msgid "(empty)" #~ msgstr "(pusty)" #~ msgid "_Properties..." #~ msgstr "_Ułaścivaści..." #~ msgid "Update out-dated feeds" #~ msgstr "Aktualizuj nieaktualnyja kanały" #~ msgid "Force update of all feeds" #~ msgstr "Prymusova aktualizuj usie kanały" #~ msgid "No feed update at all" #~ msgstr "Naohuł biez aktualizacyj kanału" #~ msgid "startup" #~ msgstr "pačatak" #~ msgid "feed updated" #~ msgstr "kanał aktualizavany" #~ msgid "feed added" #~ msgstr "kanał dadadzieny" #~ msgid "item selected" #~ msgstr "element abrany" #~ msgid "feed selected" #~ msgstr "kanał abrany" #~ msgid "item unselected" #~ msgstr "z elementu źniaty vybar" #~ msgid "feed unselected" #~ msgstr "z kanału źniaty vybar" #~ msgid "shutdown" #~ msgstr "vyklučeńnie" #~ msgid "Sorry, no scripting support available!" #~ msgstr "Padtrymka skryptavańnia adsutničaje!" #~ msgid "Script Name" #~ msgstr "Nazva skryptu" #~ msgid "No script selected!" #~ msgstr "Skrypt nie abrany!" #~ msgid "Create a new search feed." #~ msgstr "Stvary novy kanał pošuku." #~ msgid "Liferea is unable to display this item's content." #~ msgstr "Liferea nia moža pakazać źmieściva hetaha elementu." #~ msgid "

View this item's content.

" #~ msgstr "

Pakažy źmieściva hetaha elementu.

" #~ msgid "Bloglines" #~ msgstr "Bloglines" #~ msgid "" #~ "Integrate the feed list of your Bloglines account. Liferea will present " #~ "your Bloglines subscription as a read-only subtree in the feed list." #~ msgstr "" #~ "Intehruj śpis kanałaŭ z kontam na Bloglines. Liferea pakaža tvaju " #~ "padpisku na Bloglines u vyhladzie abaronienaha ad źmienaŭ dreva ŭ śpisie " #~ "kanałaŭ." #~ msgid "feedlist.opml" #~ msgstr "feedlist.opml" #~ msgid "%s has %d new / updated headline\n" #~ msgid_plural "%s has %d new / updated headlines\n" #~ msgstr[0] "%s maje %d novy / aktualizavany zahałovak\n" #~ msgstr[1] "%s maje %d novyja / aktualizavanyja zahałoŭki\n" #~ msgstr[2] "%s maje %d novych / aktualizavanych zahałoŭkaŭ\n" #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid " " #~ msgstr " " #~ msgid "Hook" #~ msgstr "Kruk" #~ msgid "Registered Scripts" #~ msgstr "Zarehistravanyja skrypty" #~ msgid "Script Code" #~ msgstr "Kod skryptu" #~ msgid "text/plain" #~ msgstr "text/plain" #~ msgid "" #~ "This option can cause significant delays when loading folders " #~ "containing many feeds." #~ msgstr "" #~ "Hetaja opcyja moža pryvieści da zaŭvažnych zatrymak pry zahruzcy " #~ "katalohaŭ, što źmiaščajuć šmat kanałaŭ." #~ msgid "Downloading Enclosures" #~ msgstr "Zahruzka dałučeńniaŭ" #~ msgid "Feed Cache Handling" #~ msgstr "Praca z padručnaj pamiaćciu kanału" #~ msgid "Feed Name" #~ msgstr "Nazva kanału" #~ msgid "Feed Update Settings" #~ msgstr "Nałady aktualizacyi kanału" #~ msgid "HTTP Proxy Server" #~ msgstr "Proxy-server HTTP" #~ msgid "Opening Enclosures" #~ msgstr "Adčynieńnie dałučeńniaŭ" #~ msgid "Reading Headlines" #~ msgstr "Čytańnie zahałoŭkaŭ" #, fuzzy #~ msgid "Toolbar Settings" #~ msgstr "Nałady menu j paneli pryładździa" #~ msgid "Update Interval" #~ msgstr "Intervał aktualizacyi" #~ msgid "Web Integration" #~ msgstr "Intehracyja z Web" #~ msgid "Add Script" #~ msgstr "Dadaj skrypt" #~ msgid "At _startup:" #~ msgstr "Na _pačatku:" #, fuzzy #~ msgid "Attention Profile" #~ msgstr "Pamyłka aŭtaryzacyi" #~ msgid "Create new script" #~ msgstr "Stvary novy skrypt" #~ msgid "Exec Command" #~ msgstr "Vykanaj zahad" #~ msgid "Reuse existing script" #~ msgstr "Znoŭ vykarystaj najaŭny skrypt" #~ msgid "Script Manager" #~ msgstr "Kiraŭnik skryptoŭ" #, fuzzy #~ msgid "Search _Link Cosmos with" #~ msgstr "Šukaj _spasyłku dla" #, fuzzy #~ msgid "FAILED to download enclosure: \"%s\"" #~ msgstr "Zahruzka dałučeńnia skončanaja: \"%s\"" #, fuzzy #~ msgid "" #~ "Copyright (c) 2003-2009\n" #~ "Lars Lindner and \n" #~ "Nathan J. Conrad \n" #~ msgstr "" #~ "Aŭtarskija pravy (c) 2003-2007\n" #~ "Lars Lindner i \n" #~ "Nathan J. Conrad \n" #~ msgid "Rule" #~ msgstr "Praviła" #~ msgid "Liferea Homepage" #~ msgstr "" #~ "Chatniaja staronka Liferea" #~ msgid "" #~ "Code, Patches, Debugging\n" #~ "\n" #~ "James Doherty\n" #~ "Jeremy Messenger\n" #~ "John McKnight\n" #~ "Tomasz Maka\n" #~ "Karl Soderstrom\n" #~ "Christophe Barbe\n" #~ "Juho Snellman\n" #~ "Roshan Revankar\n" #~ "Oliver Feiler\n" #~ "Niklas Morberg\n" #~ "Johannes Schlueter\n" #~ "Pierre Phaneuf\n" #~ "ahmed el-helw\n" #~ "James Bowes\n" #~ "Marc Deslauriers\n" #~ "Amit D. Chaudhary\n" #~ "Christoph Hohmann\n" #~ "Raphael Slinckx\n" #~ "Bjorn Monnens\n" #~ "Thomas de Grenier de Latour\n" #~ "Aristotle Pagaltzis\n" #~ "Norman Jonas\n" #~ "Sebastian Droege\n" #~ "Daniel Gryniewicz\n" #~ "Remi Cardona\n" #~ "Frederic Peters\n" #~ "Don Malcolm\n" #~ "Ed Catmur\n" #~ "Chris Pirillo\n" #~ "Eric Anderson\n" #~ "and many more...\n" #~ "\n" #~ "Code from other projects\n" #~ "\n" #~ "Anders Carlsson (tray icon support)\n" #~ "Philippe Martin, Brion Vibber (favicon support)\n" #~ "Jonathan Blandford (GtkTreeModelFilter)\n" #~ "Kristian Rietveld (GtkTreeModelFilter)\n" #~ "\n" #~ "Included Software\n" #~ "\n" #~ "Liferea uses the XSPF Web Music Player to \n" #~ "allow direct podcast playback. This player was \n" #~ "written by Fabricio Zuardi and can be found \n" #~ "at http://musicplayer.sourceforge.net" #~ msgstr "" #~ "Kadavańnie, patch'y, debugavańnie\n" #~ "\n" #~ "James Doherty\n" #~ "Jeremy Messenger\n" #~ "John McKnight\n" #~ "Tomasz Maka\n" #~ "Karl Soderstrom\n" #~ "Christophe Barbe\n" #~ "Juho Snellman\n" #~ "Roshan Revankar\n" #~ "Oliver Feiler\n" #~ "Niklas Morberg\n" #~ "Johannes Schlueter\n" #~ "Pierre Phaneuf\n" #~ "ahmed el-helw\n" #~ "James Bowes\n" #~ "Marc Deslauriers\n" #~ "Amit D. Chaudhary\n" #~ "Christoph Hohmann\n" #~ "Raphael Slinckx\n" #~ "Bjorn Monnens\n" #~ "Thomas de Grenier de Latour\n" #~ "Aristotle Pagaltzis\n" #~ "Norman Jonas\n" #~ "Sebastian Droege\n" #~ "Daniel Gryniewicz\n" #~ "Remi Cardona\n" #~ "Frederic Peters\n" #~ "Don Malcolm\n" #~ "Ed Catmur\n" #~ "Chris Pirillo\n" #~ "Eric Anderson\n" #~ "i šmat chto inšy...\n" #~ "\n" #~ "Kod ad inšych prajektaŭ\n" #~ "\n" #~ "Anders Carlsson (padtrymka ikony traju)\n" #~ "Philippe Martin, Brion Vibber (padtrymka ikonaŭ kanałaŭ)\n" #~ "Jonathan Blandford (GtkTreeModelFilter)\n" #~ "Kristian Rietveld (GtkTreeModelFilter)\n" #~ "\n" #~ "Ułučanaje prahramnaje zabieśpiačeńnie\n" #~ "\n" #~ "Liferea vykarystoŭvaje Sieciŭny Muzyčny Prajhravalnik XSPF\n" #~ "(XSPF Web Music Player), kab dazvolić prostaje hrańnie\n" #~ "podcastaŭ. Hety prajhravalnik napisaŭ Fabricio Zuardi,\n" #~ "i jaho možna znajści na http://musicplayer.sourceforge.net" #~ msgid "Contributors" #~ msgstr "Udzielniki" #~ msgid "" #~ "Note: Items are added to the search folder if at least one additive rule\n" #~ "matches. They are removed if at least one removing rule matches." #~ msgstr "" #~ "Uvaha: Elementy dadajucca ŭ kataloh pošuku, kali choć adzno dadatnaje\n" #~ "praviła spracoŭvaje. Jany vydalajucca, kali chacia b adno vydalalnaje " #~ "praviła spracoŭvaje." #~ msgid "" #~ "Saves this search as a search folder, which will appear in the feed list." #~ msgstr "" #~ "Zapisvaje hety pošuk jak kataloh pošuku, jaki źjavicca ŭ śpisie kanałaŭ." #~ msgid "Show Menu _And Toolbar" #~ msgstr "Pakazvaj menu _i panel pryładździa" #~ msgid "Show _Menu Only" #~ msgstr "Pakazvaj _tolki menu" #~ msgid "Show _Toolbar Only" #~ msgstr "Pakazvaj tolki _panel pryładździa" #~ msgid "Translation" #~ msgstr "Pierakład" #~ msgid "_Limit cache to" #~ msgstr "_Abmiažuj padručnuju pamiać da" #~ msgid "_Name:" #~ msgstr "_Nazva:" #~ msgid "items." #~ msgstr "elementaŭ." #~ msgid "" #~ "minutes\n" #~ "hours\n" #~ "days" #~ msgstr "" #~ "chvilin\n" #~ "hadzin\n" #~ "dzion" #~ msgid "search" #~ msgstr "pošuk" #~ msgid "Access Forbidden" #~ msgstr "Dostup zabaronieny" #~ msgid "URL is invalid" #~ msgstr "Spasyłka niapravilnaja" #~ msgid "Unsupported network protocol" #~ msgstr "Niepadtrymany sietkavy pratakoł" #~ msgid "Hostname could not be found" #~ msgstr "Niemahčyma znajści nazvu kamputara" #~ msgid "Network connection was refused by the remote host" #~ msgstr "Addaleny kamputar admoviŭ u sietkavym spałučeńni" #~ msgid "Remote host did not finish sending data" #~ msgstr "Addaleny kamputar nia skončyŭ dasyłać źviestki" #~ msgid "Too many HTTP redirects were encountered" #~ msgstr "Adbyłosia nadta šmat pieranakiravańniaŭ HTTP" #~ msgid "Remote host sent an invalid response" #~ msgstr "Addaleny kamputar dasłaŭ niapravilny adkaz" #~ msgid "Webserver's authentication method incompatible with Liferea" #~ msgstr "Metad aŭtaryzacyi na web-servery niesumiaščalny ź Liferea" #~ msgid "" #~ "Unexpected end of character sequence or corrupt UTF-8 encoding! Some " #~ "characters were dropped!" #~ msgstr "" #~ "Niečakany kaniec paśladoŭnaści znakaŭ albo kiepskaje kadavańnie UTF-8! " #~ "Niekatoryja znaki praihnaravanyja!" #~ msgid "Feed link auto discovery failed! No feed links found!" #~ msgstr "" #~ "Aŭtamatyčny pošuk spasyłki na kanał biespaśpiachovy! Spasyłki na kanały " #~ "nia znojdzienyja!" #~ msgid " --help Print this help and exit" #~ msgstr " --help Pakažy hetuju dapamohu j vyjdzi" #~ msgid " --mainwindow-state=STATE" #~ msgstr " --mainwindow-state=STAN" #~ msgid " Start Liferea with its main window in STATE." #~ msgstr " Startuj Liferea z hałoŭnym aknom u STANIE." #~ msgid " Possible topics are: all,cache,conf,db,gui,html" #~ msgstr "" #~ "Mahčymyja temy: \"all\" (usie), \"cache\" (padručnaja pamiać), \"conf" #~ "\" (kanfihuracyja), \"db\" (baza źviestak), \"gui\" (interfejs), \"html" #~ "\" (HTML)" #~ msgid " net,parsing,plugins,trace,update,verbose" #~ msgstr "" #~ "\"net\" (sietka), \"parsing\" (razbor), \"plugins\" (pluginy), \"trace" #~ "\" (trejs), \"update\" (aktualizacyja), \"verbose\" (padrabiaznaść)" #~ msgid "The --mainwindow-state argument must be given a parameter.\n" #~ msgstr "Arhumentu --mainwindow-state treba pieradavać parametar.\n" #~ msgid "The --session argument must be given a parameter.\n" #~ msgstr "Arhumentu --session treba pieradavać parametar.\n" #~ msgid "Liferea encountered an unknown argument: %s\n" #~ msgstr "Liferea napatkała nieviadomy arhument: %s\n" #~ msgid "does match" #~ msgstr "adpaviadaje" #~ msgid "does not match" #~ msgstr "nie adpaviadaje" #~ msgid "Could not download \"%s\". Will retry in %d seconds." #~ msgstr "" #~ "Niemahčyma zahruzić \"%s\". Sproba budzie paŭtoranaja praz %d sekundaŭ." #~ msgid "" #~ "Sorry, I was not able to load any installed browser plugin! Try the --" #~ "debug-plugins option to get debug information!" #~ msgstr "" #~ "Vybačajusia, ale ja nia zmoh zahruzić zainstalavanyja pluginy dla " #~ "hartača. Pasprabuj opcyju --debug-plugins, kab atrymać debugavyja " #~ "źviestki!" #~ msgid "This item does not have a link assigned!" #~ msgstr "Hety element nia maje suadniesienaje spasyłki!" #~ msgid "No link selected!" #~ msgstr "Spasyłka nie zaznačanaja!" #~ msgid "http://lzone.de/liferea/" #~ msgstr "http://lzone.de/liferea/" #~ msgid "Updates all subscriptions. This does not update OCS directories." #~ msgstr "Aktualizoŭvaje ŭsie padpiski. Katalohi OCS nie aktualizoŭvajucca." #~ msgid "/Toggle _Read Status" #~ msgstr "/Źmiani _stan pračytanaści" #~ msgid "/Toggle Item _Flag" #~ msgstr "/Źmiani ś_ciah elementu" #~ msgid "/_Increase Text Size" #~ msgstr "/Pa_vialič pamier tekstu" #~ msgid "/_Decrease Text Size" #~ msgstr "/Pa_mienš pamier tekstu" #~ msgid "/Toggle _Online|Offline" #~ msgstr "/Źmiani _stan spałučanaści" #~ msgid "/_Preferences..." #~ msgstr "/_Nałady..." #~ msgid "/_Show|Hide Window" #~ msgstr "/_Pakažy/Schavaj vakno" #~ msgid "/_Quit" #~ msgstr "/_Vyjdzi" #~ msgid "/_Update" #~ msgstr "/_Aktualizuj" #~ msgid "/_New/New _Subscription..." #~ msgstr "/_Novy/Novaja _padpiska..." #~ msgid "/_New/New _Folder..." #~ msgstr "/_Novy/Novy _kataloh..." #~ msgid "/_New/New S_earch Folder..." #~ msgstr "/_Novy/Novy kataloh p_ošuku..." #~ msgid "/_New/New S_ource..." #~ msgstr "/_Novy/Novaja krynica..." #~ msgid "/_New/New _News Bin..." #~ msgstr "/_Novy/Novy _koš navinaŭ..." #~ msgid "/_Properties..." #~ msgstr "/_Ułaścivaści..." #~ msgid "Cookie for %s has expired!" #~ msgstr "Ciestak dla %s sastareŭ!" #~ msgid "" #~ "Error while reading cache file \"%s\" ! Cache file could not be loaded!" #~ msgstr "" #~ "Pamyłka pry čytańni pdručnaha fajłu \"%s\"! Niemahčyma zahruzić padručny " #~ "fajł!" #~ msgid "" #~ "

XML error while parsing cache file! Feed cache file \"%s\" could not " #~ "be loaded!

" #~ msgstr "" #~ "

Pamyłka XML pry razbory padručnaha fajłu! Niemahčyma zahruzić padručny " #~ "fajł kanału \"%s\"!

" #~ msgid "

\"%s\" is no valid cache file! Cannot read cache file!

" #~ msgstr "" #~ "

\"%s\" nie źjaŭlajecca pravilnym padručnym fajłam! Niemahčyma adčytać " #~ "padručny fajł!

" #~ msgid "There were errors while parsing cache file \"%s\"" #~ msgstr "Pry razbory padručnaha fajłu \"%s\" adbylisia pamyłki" #~ msgid "Feed Loading Settings" #~ msgstr "Nałady zahruzki kanałaŭ" #~ msgid "Optimize for reduced _memory usage." #~ msgstr "Aptymizuj dziela abmiežavanaha ŭžytku _pamiaci." #~ msgid "Optimize for _speed." #~ msgstr "Aptymizuj dziela _chutkaści." #~ msgid "_View Headlines With" #~ msgstr "_Pakazvaj zahałoŭki z dapamohaj" #~ msgid "" #~ "Liferea reuses the GNOME proxy settings. If you use GNOME you can " #~ "change these settings in the GNOME Control Center." #~ msgstr "" #~ "Liferea vykarystoŭvaje nałady proxy ad GNOME'a. Kali ty karystajeśsia " #~ "GNOME'am, ty možaš źmianić hetyja nałady ŭ Centry kiravańnia GNOME'u." #~ msgid "_Enable Proxy" #~ msgstr "_Uklučy proxy" #~ msgid "" #~ " --debug-html Enable HTML debugging (saving to ~/.liferea_1.2/output." #~ "xhtml)" #~ msgstr "" #~ " --debug-html Uklučy debugavańnie HTML (zapis u ~/.liferea_1.2/" #~ "output.html)" #~ msgid " --debug-plugins Print debugging messages when loading plugins" #~ msgstr "" #~ " --debug-plugins Pakazvaj debugavyja paviedamleńni pry zahruzcy pluginaŭ" #~ msgid "_Program" #~ msgstr "_Prahrama" #~ msgid "Update _Selected" #~ msgstr "Aktualizuj _abranaje" #~ msgid "_Delete Selected" #~ msgstr "_Vydal abranaje" #~ msgid "Update only feeds scheduled for updates" #~ msgstr "Aktualizuj tolki kanały, pryznačanyja dla aktualizacyi" #~ msgid "Reset feed update timers (Update no feeds)" #~ msgstr "Abnuli hadzińniki aktualizacyi kanałaŭ (Nie aktualizuj kanały)" #~ msgid "Searching for \"%s\"" #~ msgstr "Pošuk \"%s\"" #~ msgid "Liferea notification" #~ msgstr "Nahadvańnie Liferea" liferea-1.13.7/po/bg.po000066400000000000000000003337001415350204600145660ustar00rootroot00000000000000# Bulgarian translation for Liferea. # Copyright (C) Free Software Foundation, 2004, 2005, 2006. # This file is distributed under the same license as the Liferea package. # Vladimir Petkov , 2004, 2005, 2006. # # msgid "" msgstr "" "Project-Id-Version: Liferea Bulgarian translation\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-30 12:36+0200\n" "PO-Revision-Date: 2006-03-17 12:10+0200\n" "Last-Translator: Vladimir Petkov \n" "Language-Team: Bulgarian \n" "Language: bg\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" #: ../net.sourceforge.liferea.appdata.xml.in.h:1 #, fuzzy msgid "RSS feed reader" msgstr "Временни файлове на емисията" #: ../net.sourceforge.liferea.appdata.xml.in.h:2 msgid "" "Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator " "for online news feeds. It supports a number of different feed formats " "including RSS/RDF, CDF and Atom. There are many other news readers " "available, but these others are not available for Linux or require many " "extra libraries to be installed. Liferea tries to fill this gap by creating " "a fast, easy to use, easy to install news aggregator for GTK/GNOME." msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:3 msgid "Distinguishing features:" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:4 #, fuzzy msgid "Read articles when offline" msgstr "Liferea е в режим „изключен“" #: ../net.sourceforge.liferea.appdata.xml.in.h:5 #, fuzzy msgid "Synchronizes with TheOldReader" msgstr "Име на емисията" #: ../net.sourceforge.liferea.appdata.xml.in.h:6 #, fuzzy msgid "Synchronizes with TinyTinyRSS" msgstr "Име на емисията" #: ../net.sourceforge.liferea.appdata.xml.in.h:7 #, fuzzy msgid "Synchronizes with InoReader" msgstr "Име на емисията" #: ../net.sourceforge.liferea.appdata.xml.in.h:8 #, fuzzy msgid "Synchronizes with Reedah" msgstr "Име на емисията" #: ../net.sourceforge.liferea.appdata.xml.in.h:9 msgid "Permanently save headlines in news bins" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:10 msgid "Match items using search folders" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:11 msgid "Play Podcasts" msgstr "" #: ../net.sourceforge.liferea.desktop.in.h:1 ../src/liferea_application.c:350 #: ../glade/mainwindow.ui.h:1 msgid "Liferea" msgstr "Събирач на RSS емисии (Liferea)" #: ../net.sourceforge.liferea.desktop.in.h:2 #, fuzzy msgid "Feed Reader" msgstr "Временни файлове на емисията" #: ../net.sourceforge.liferea.desktop.in.h:3 #, fuzzy msgid "Liferea Feed Reader" msgstr "Събирач на RSS емисии (Liferea)" #: ../net.sourceforge.liferea.desktop.in.h:4 msgid "Read news feeds and blogs" msgstr "" #: ../net.sourceforge.liferea.desktop.in.h:5 msgid "news;feed;aggregator;blog;podcast;syndication;rss;atom" msgstr "" #: ../plugins/getfocus.py:93 msgid "Opacity:" msgstr "" #: ../plugins/getfocus.py:94 msgid "Opacity" msgstr "" #: ../plugins/getfocus.py:104 msgid "Min" msgstr "" #: ../plugins/getfocus.py:109 msgid "Max" msgstr "" #: ../plugins/getfocus.py:115 msgid "Save" msgstr "" #: ../plugins/headerbar.py:67 ../glade/liferea_menu.ui.h:20 #: ../glade/liferea_toolbar.ui.h:5 msgid "Previous Item" msgstr "" #: ../plugins/headerbar.py:73 ../glade/liferea_menu.ui.h:21 #: ../glade/liferea_toolbar.ui.h:6 #, fuzzy msgid "Next Item" msgstr "_Следващ непрочетен запис" #: ../plugins/headerbar.py:81 ../glade/liferea_menu.ui.h:19 #: ../glade/liferea_toolbar.ui.h:7 msgid "_Next Unread Item" msgstr "_Следващ непрочетен запис" #: ../plugins/headerbar.py:89 ../glade/liferea_menu.ui.h:14 #: ../glade/liferea_toolbar.ui.h:3 #, fuzzy msgid "_Mark Items Read" msgstr "Избиране като прочетен" #: ../plugins/headerbar.py:113 ../glade/liferea_menu.ui.h:40 #: ../glade/liferea_toolbar.ui.h:10 #, fuzzy msgid "Search All Feeds..." msgstr "Търсене във всички емисии" #: ../plugins/libnotify.py:42 #, fuzzy msgid "Feed Updates" msgstr "Временни файлове на емисията" #: ../plugins/plugin-installer.py:54 #, fuzzy msgid "Plugins" msgstr "Нова _приставка" #: ../plugins/plugin-installer.py:69 #, fuzzy msgid "Plugin Installer" msgstr "Нова _приставка" #: ../plugins/plugin-installer.py:83 #, fuzzy msgid "Activate Plugins" msgstr "Нова _приставка" #: ../plugins/plugin-installer.py:84 #, fuzzy msgid "Download Plugins" msgstr "_Чрез програмата" #: ../plugins/plugin-installer.py:102 #, python-format msgid "Bad fields for plugin entry %s" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "All" msgstr "" #: ../plugins/plugin-installer.py:125 ../glade/properties.ui.h:39 msgid "Advanced" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "Menu" msgstr "" #: ../plugins/plugin-installer.py:125 #, fuzzy msgid "Notifications" msgstr "Уведомяване" #: ../plugins/plugin-installer.py:133 msgid "Filter by category" msgstr "" #: ../plugins/plugin-installer.py:140 msgid "_Install" msgstr "" #: ../plugins/plugin-installer.py:144 msgid "_Uninstall" msgstr "" #: ../plugins/plugin-installer.py:245 #, python-format msgid "" "Missing package manager '%s'. Cannot check nor install necessary " "dependencies!" msgstr "" #: ../plugins/plugin-installer.py:261 #, python-format msgid "Missing package '%s'. Do you want to install it? (Will run '%s')" msgstr "" #: ../plugins/plugin-installer.py:268 #, python-format msgid "" "Package installation failed (%s)! Check console output for further problem " "details!" msgstr "" #: ../plugins/plugin-installer.py:271 #, python-format msgid "Failed to check plugin dependencies (%s)!" msgstr "" #: ../plugins/plugin-installer.py:280 msgid "Command \"git\" not found, please install it!" msgstr "" #: ../plugins/plugin-installer.py:289 #, python-format msgid "Copying %s to %s" msgstr "" #: ../plugins/plugin-installer.py:292 #, python-format msgid "Failed to copy plugin directory (%s)!" msgstr "" #: ../plugins/plugin-installer.py:301 #, python-format msgid "Failed to copy plugin .py file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:311 #, python-format msgid "Failed to copy .plugin file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:322 #, fuzzy, python-format msgid "Creating schema directory %s" msgstr "Не може да създаде директорията за временните файлове „%s“!" #: ../plugins/plugin-installer.py:324 #, python-format msgid "Installing schema %s" msgstr "" #: ../plugins/plugin-installer.py:328 msgid "Compiling schemas..." msgstr "" #: ../plugins/plugin-installer.py:333 #, python-format msgid "Failed to install schema files (%s)!" msgstr "" #: ../plugins/plugin-installer.py:343 #, python-format msgid "Failed to enable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:349 #, python-format msgid "Plugin '%s' is now installed. Ensure to restart Liferea!" msgstr "" #: ../plugins/plugin-installer.py:363 #, python-format msgid "Failed to disable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:370 ../plugins/plugin-installer.py:390 #, fuzzy, python-format msgid "Deleting '%s'" msgstr "Изтриване на запис" #: ../plugins/plugin-installer.py:373 #, python-format msgid "Failed to remove directory '%s' (%s)!" msgstr "" #: ../plugins/plugin-installer.py:383 msgid "Failed to remove .py file!" msgstr "" #: ../plugins/plugin-installer.py:393 msgid "Failed to remove .plugin file!" msgstr "" #: ../plugins/plugin-installer.py:402 msgid "Sorry! Plugin removal failed!." msgstr "" #: ../plugins/plugin-installer.py:404 #, fuzzy msgid "" "Plugin was removed. Please restart Liferea once for it to take full effect!." msgstr "Рестартирайте Liferea, за да влязат в сила промените." #: ../plugins/trayicon.py:132 #, fuzzy msgid "Show / Hide" msgstr "Подробности" #: ../plugins/trayicon.py:133 msgid "Minimize to tray on close" msgstr "" #: ../plugins/trayicon.py:134 #, fuzzy msgid "Quit" msgstr "_Спиране на програмата" #: ../src/browser.c:81 ../src/browser.c:98 #, c-format msgid "Browser command failed: %s" msgstr "Командата за стартиране на браузъра е неуспешна: %s" #: ../src/browser.c:101 ../src/ui/liferea_shell.c:1047 #, c-format msgid "Starting: \"%s\"" msgstr "Стартиране: „%s“" #. unauthorized #: ../src/comments.c:118 #, fuzzy msgid "Authorization Error" msgstr "Идентифициране" #: ../src/common.c:66 #, c-format msgid "Cannot create cache directory \"%s\"!" msgstr "Не може да създаде директорията за временните файлове „%s“!" #: ../src/conf.c:182 msgid "" "Your version of WebKitGTK+ doesn't support changing the proxy settings from " "Liferea. The system's default proxy settings will be used." msgstr "" #. translation hint: date format for today, reorder format codes as necessary #: ../src/date.c:133 msgid "Today %l:%M %p" msgstr "" #. translation hint: date format for yesterday, reorder format codes as necessary #: ../src/date.c:142 msgid "Yesterday %l:%M %p" msgstr "" #. translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary #: ../src/date.c:154 msgid "%a %l:%M %p" msgstr "" #. translation hint: date format for dates older than a week but from this year, reorder format codes as necessary #: ../src/date.c:162 msgid "%b %d %l:%M %p" msgstr "" #. translation hint: date format for dates from the last years, reorder format codes as necessary #: ../src/date.c:165 msgid "%b %d %Y" msgstr "" #: ../src/enclosure.c:201 #, c-format msgid "\"%s\" is not a valid enclosure type config file!" msgstr "„%s“ не е валиден тип за файл с настройки за приложенията!" #: ../src/enclosure.c:292 msgid "" "You have not configured a download tool yet! Please do so in the " "'Enclosures' tab in Tools/Preferences." msgstr "" #: ../src/enclosure.c:311 #, c-format msgid "" "Command failed: \n" "\n" "%s\n" "\n" " Please check whether the configured download tool is installed and working " "correctly! You can change it in the 'Download' tab in Tools/Preferences." msgstr "" #: ../src/export.c:187 #, fuzzy, c-format msgid "Error renaming %s to %s: %s\n" msgstr "Грешка при преименуването на %s в %s\n" #: ../src/export.c:409 ../src/export.c:411 #, fuzzy, c-format msgid "XML error while reading OPML file! Could not import \"%s\"!" msgstr "" "Грешка в XML по време на четенето на временния файл! „%s“ не може да бъде " "внесен!" #: ../src/export.c:417 ../src/export.c:419 #, c-format msgid "" "Empty document! OPML document \"%s\" should not be empty when importing." msgstr "" "Празен документ! OPML документа „%s“ не трябва да е празен при внасяне." #: ../src/export.c:440 ../src/export.c:442 #, c-format msgid "\"%s\" is not a valid OPML document! Liferea cannot import this file!" msgstr "„%s“ не е валиден OPML документ! Liferea не може да внесе този файл!" #: ../src/export.c:461 msgid "Imported feed list" msgstr "Списък с внесените емисии" #: ../src/export.c:473 msgid "Import Feed List" msgstr "Вмъкване на списък с емисии" #: ../src/export.c:473 msgid "Import" msgstr "Внасяне" #: ../src/export.c:473 ../src/export.c:490 ../src/fl_sources/opml_source.c:379 #, fuzzy msgid "OPML Files" msgstr "Избор на файл" #: ../src/export.c:481 msgid "Error while exporting feed list!" msgstr "Грешка при изнасянето на списъкът с емисии!" #: ../src/export.c:483 msgid "Feed List exported!" msgstr "Списъкът с емисии е изнесен успешно!" #: ../src/export.c:490 msgid "Export Feed List" msgstr "Изнасяне на списък с емисиите" #: ../src/export.c:490 msgid "Export" msgstr "Изнасяне" #: ../src/feed_parser.c:201 #, fuzzy msgid "Empty document!" msgstr "

Празен документ!

" #: ../src/feed_parser.c:210 #, fuzzy msgid "Invalid XML!" msgstr "

Невалиден XML!

" #: ../src/fl_sources/default_source.c:139 ../glade/new_subscription.ui.h:1 #: ../glade/simple_subscription.ui.h:1 msgid "New Subscription" msgstr "Нова емисия" #: ../src/fl_sources/google_source.c:111 #, fuzzy msgid "Google Reader" msgstr "Временни файлове на емисията" #: ../src/fl_sources/node_source.c:334 msgid "No feed list source types found!" msgstr "" #: ../src/fl_sources/node_source.c:363 #, fuzzy msgid "Source Type" msgstr "Вид на източника:" #: ../src/fl_sources/node_source.c:414 #, c-format msgid "Login for '%s' has not yet completed! Please wait until login is done." msgstr "" #. FIXME: something is not perfect, because if you immediately #. remove the subscription tree afterwards there is a double free #: ../src/fl_sources/node_source.c:580 #, c-format msgid "The '%s' subscription was successfully converted to local feeds!" msgstr "" #: ../src/fl_sources/opml_source.c:319 msgid "Planet, BlogRoll, OPML" msgstr "" #: ../src/fl_sources/opml_source.c:379 #, fuzzy msgid "Choose OPML File" msgstr "Избор на файл" #: ../src/fl_sources/opml_source.c:379 ../src/ui/subscription_dialog.c:355 msgid "_Open" msgstr "" #: ../src/fl_sources/opml_source.h:28 #, fuzzy msgid "New OPML Subscription" msgstr "Нова емисия" #: ../src/fl_sources/reedah_source.c:103 #: ../src/fl_sources/theoldreader_source.c:104 msgid "Login failed!" msgstr "" #: ../src/fl_sources/reedah_source.c:313 msgid "Reedah" msgstr "" #: ../src/fl_sources/reedah_source_feed.c:154 msgid "Could not parse JSON returned by Reedah API!" msgstr "" #: ../src/fl_sources/theoldreader_source.c:311 #, fuzzy msgid "TheOldReader" msgstr "Временни файлове на емисията" #: ../src/fl_sources/ttrss_source.c:227 ../src/fl_sources/ttrss_source.c:293 msgid "TinyTinyRSS HTTP API not reachable!" msgstr "" #: ../src/fl_sources/ttrss_source.c:234 msgid "" "TinyTinyRSS subscribing to feed failed! Check if you really passed a feed " "URL!" msgstr "" #: ../src/fl_sources/ttrss_source.c:300 msgid "TinyTinyRSS unsubscribing feed failed!" msgstr "" #: ../src/fl_sources/ttrss_source.c:318 #, c-format msgid "" "This TinyTinyRSS version does not support removing feeds. Upgrade to version " "%s or later!" msgstr "" #: ../src/fl_sources/ttrss_source.c:471 msgid "Tiny Tiny RSS" msgstr "" #: ../src/fl_sources/ttrss_source_feed.c:150 msgid "Could not parse JSON returned by TinyTinyRSS API!" msgstr "" #. if we don't find a feed with unread items do nothing #: ../src/itemlist.c:395 #, fuzzy msgid "There are no unread items" msgstr "Няма непрочетени записи " #: ../src/liferea_application.c:280 #, fuzzy msgid "" "Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'" msgstr "" " СЪСТОЯНИЕ-то може да е „shown“, „iconified“ или „hidden“" #: ../src/liferea_application.c:280 msgid "STATE" msgstr "" #: ../src/liferea_application.c:281 #, fuzzy msgid "Show version information and exit" msgstr " --version Изписване на информация за версията и напускане" #: ../src/liferea_application.c:282 #, fuzzy msgid "Add a new subscription" msgstr "Нова емисия" #: ../src/liferea_application.c:282 msgid "uri" msgstr "" #: ../src/liferea_application.c:283 msgid "Start with all plugins disabled" msgstr "" #: ../src/liferea_application.c:288 #, fuzzy msgid "Print debugging messages of all types" msgstr "" " --debug-all Отпечатване на всички съобщения за откриване на грешки" #: ../src/liferea_application.c:289 #, fuzzy msgid "Print debugging messages for the cache handling" msgstr "" " --debug-cache Отпечатване на съобщенията за откриване на грешки при " "обработката на кеша" #: ../src/liferea_application.c:290 #, fuzzy msgid "Print debugging messages for the configuration handling" msgstr "" " --debug-conf Отпечатване на съобщенията за откриване на грешки при " "обработката на настройките" #: ../src/liferea_application.c:291 #, fuzzy msgid "Print debugging messages of the database handling" msgstr "" " --debug-cache Отпечатване на съобщенията за откриване на грешки при " "обработката на кеша" #: ../src/liferea_application.c:292 #, fuzzy msgid "Print debugging messages of all GUI functions" msgstr "" " --debug-gui Отпечатване на съобщенията за откриване на грешки във " "всички функции за графичния интерфейс" #: ../src/liferea_application.c:293 msgid "" "Enables HTML rendering debugging. Each time Liferea renders HTML output it " "will also dump the generated HTML into ~/.cache/liferea/output.html" msgstr "" #: ../src/liferea_application.c:294 #, fuzzy msgid "Print debugging messages of all network activity" msgstr "" " --debug-all Отпечатване на всички съобщения за откриване на грешки" #: ../src/liferea_application.c:295 #, fuzzy msgid "Print debugging messages of all parsing functions" msgstr "" " --debug-parsing Отпечатване на съобщенията за откриване на грешки във " "всички функции за синтактичен анализ" #: ../src/liferea_application.c:296 msgid "Print debugging messages when a function takes too long to process" msgstr "" #: ../src/liferea_application.c:297 #, fuzzy msgid "Print debugging messages when entering/leaving functions" msgstr "" " --debug-trace Отпечатване на съобщенията за откриване на грешки при " "влизане и излизане от функции" #: ../src/liferea_application.c:298 #, fuzzy msgid "Print debugging messages of the feed update processing" msgstr "" " --debug-update Отпечатване на съобщенията за откриване на грешки при " "обработката на обновяването на емисиите" #: ../src/liferea_application.c:299 #, fuzzy msgid "Print debugging messages of the search folder matching" msgstr "" " --debug-cache Отпечатване на съобщенията за откриване на грешки при " "обработката на кеша" #: ../src/liferea_application.c:300 #, fuzzy msgid "Print verbose debugging messages" msgstr "" " --debug-verbose Отпечатване на подробни съобщения за откриване на грешки" #: ../src/liferea_application.c:305 ../src/liferea_application.c:306 #, fuzzy msgid "Print debugging messages for the given topic" msgstr "" " --debug-cache Отпечатване на съобщенията за откриване на грешки при " "обработката на кеша" #. Some libsoup transport errors #: ../src/net.c:437 msgid "The update request was cancelled" msgstr "" #: ../src/net.c:438 msgid "Unable to resolve destination host name" msgstr "" #: ../src/net.c:439 msgid "Unable to resolve proxy host name" msgstr "" #: ../src/net.c:440 #, fuzzy msgid "Unable to connect to remote host" msgstr "Грешка при свързването с отдалечения адрес" #: ../src/net.c:441 msgid "Unable to connect to proxy" msgstr "" #: ../src/net.c:442 msgid "" "SSL/TLS negotiation failed. Possible outdated or unsupported encryption " "algorithm. Check your operating system settings." msgstr "" #. http 3xx redirection #: ../src/net.c:445 msgid "The resource moved permanently to a new location" msgstr "" #. http 4xx client error #: ../src/net.c:448 #, fuzzy msgid "" "You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box" msgstr "" "Нямате право да изтеглите тази емисия. Актуализирайте вашето име и парола в " "настройките на емисията." #: ../src/net.c:450 #, fuzzy msgid "Payment required" msgstr "Изисква се заплащане" #: ../src/net.c:451 msgid "You're not allowed to access this resource" msgstr "" #: ../src/net.c:452 msgid "Resource Not Found" msgstr "Ресурсът не е намерен" #: ../src/net.c:453 msgid "Method Not Allowed" msgstr "Непозволен метод" #: ../src/net.c:454 msgid "Not Acceptable" msgstr "Неприемлив" #: ../src/net.c:455 #, fuzzy msgid "Proxy authentication required" msgstr "Нужна е идентификация в сървъра-посредник" #: ../src/net.c:456 #, fuzzy msgid "Request timed out" msgstr "Заявката изтече" #: ../src/net.c:457 #, fuzzy msgid "" "The webserver indicates this feed is discontinued. It's no longer available. " "Liferea won't update it anymore but you can still access the cached " "headlines." msgstr "" "Тази емисия не е продължена. Вече не е налична. Liferea няма да я " "актуализира, но все още можете да преглеждате старите заглавия от кеша." #: ../src/net.c:462 msgid "There was an internal error in the update process" msgstr "" #: ../src/net.c:464 msgid "Feed not available: Server requested unsupported redirection!" msgstr "" "Емисията не е на разположение: Сървърът докладва за неподдържано " "пренасочване!" #: ../src/net.c:466 msgid "Client Error" msgstr "Грешка в клиента" #: ../src/net.c:468 msgid "Server Error" msgstr "Грешка в сървъра" #: ../src/net.c:470 #, fuzzy msgid "An unknown networking error happened!" msgstr "(непозната мрежова грешка)" #: ../src/parsers/atom10.c:239 msgid "Website" msgstr "" #: ../src/parsers/ns_ag.c:70 msgid "%b %d %H:%M" msgstr "" #. in-memory check function feedlist.opml rule id rule menu label positive menu option negative menu option has param #. ======================================================================================================================================================================================== #: ../src/rule.c:229 msgid "Item" msgstr "Запис" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does contain" msgstr "съдържа" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does not contain" msgstr "не съдържа" #: ../src/rule.c:230 msgid "Item title" msgstr "Заглавие на записа" #: ../src/rule.c:231 msgid "Item body" msgstr "Тяло на записа" #: ../src/rule.c:232 msgid "Read status" msgstr "Поставяне на статус на прочетен" #: ../src/rule.c:232 msgid "is unread" msgstr "е непрочетен" #: ../src/rule.c:232 msgid "is read" msgstr "е прочетен" #: ../src/rule.c:233 msgid "Flag status" msgstr "Състояние на флаг" #: ../src/rule.c:233 msgid "is flagged" msgstr "е с флаг" #: ../src/rule.c:233 msgid "is unflagged" msgstr "е без флаг" #: ../src/rule.c:234 msgid "Podcast" msgstr "" #: ../src/rule.c:234 #, fuzzy msgid "included" msgstr "включена фотография" #: ../src/rule.c:234 #, fuzzy msgid "not included" msgstr "включена фотография" #: ../src/rule.c:235 #, fuzzy msgid "Category" msgstr "категория" #: ../src/rule.c:235 #, fuzzy msgid "is set" msgstr "е прочетен" #: ../src/rule.c:235 msgid "is not set" msgstr "" #: ../src/rule.c:236 msgid "Feed title" msgstr "Заглавие на емисията" #: ../src/rule.c:237 #, fuzzy msgid "Feed source" msgstr "Източник на емисията" #: ../src/rule.c:238 msgid "Parent folder title" msgstr "" #: ../src/subscription.c:108 #, fuzzy, c-format msgid "Subscription \"%s\" is already being updated!" msgstr "Тази емисия „%s“ вече беше актуализирана!" #: ../src/subscription.c:113 #, fuzzy, c-format msgid "" "The subscription \"%s\" was discontinued. Liferea won't update it anymore!" msgstr "Емисията „%s“ няма продължение. Liferea няма да я актуализира повече!" #: ../src/subscription.c:188 #, c-format msgid "The URL of \"%s\" has changed permanently and was updated" msgstr "Адресът на „%s“ беше променен за постоянно и затова беше осъвременен" #: ../src/subscription.c:204 #, c-format msgid "\"%s\" is discontinued. Liferea won't updated it anymore!" msgstr "„%s“ не е продължен. Liferea няма да го осъвременява повече!" #: ../src/subscription.c:208 #, c-format msgid "\"%s\" has not changed since last update" msgstr "„%s“ не се промени след последното осъвременяване" #: ../src/subscription.c:221 ../src/subscription.c:296 #, fuzzy, c-format msgid "Updating (%d / %d) ..." msgstr "Актуализиране на „%s“" #: ../src/subscription.c:298 #, fuzzy, c-format msgid "Updating '%s'..." msgstr "Актуализиране на „%s“" #: ../src/ui/auth_dialog.c:114 #, c-format msgid "Enter the username and password for \"%s\" (%s):" msgstr "Въведете потребителското име и паролата за „%s“ (%s):" #: ../src/ui/auth_dialog.c:116 msgid "Unknown source" msgstr "Непознат източник" #: ../src/ui/browser_tabs.c:262 msgid "Untitled" msgstr "Без име" #: ../src/ui/enclosure_list_view.c:168 #, fuzzy msgid "Attachments" msgstr "коментари" #. The following literals are the enclosure list size units #: ../src/ui/enclosure_list_view.c:260 msgid " Bytes" msgstr "" #: ../src/ui/enclosure_list_view.c:263 msgid "kB" msgstr "" #: ../src/ui/enclosure_list_view.c:267 msgid "MB" msgstr "" #: ../src/ui/enclosure_list_view.c:271 msgid "GB" msgstr "" #: ../src/ui/enclosure_list_view.c:275 #, c-format msgid "%d%s" msgstr "" #. update list title #: ../src/ui/enclosure_list_view.c:313 #, c-format msgid "%d attachment" msgid_plural "%d attachments" msgstr[0] "" msgstr[1] "" #: ../src/ui/enclosure_list_view.c:402 ../src/ui/subscription_dialog.c:355 msgid "Choose File" msgstr "Избор на файл" #: ../src/ui/enclosure_list_view.c:463 #, fuzzy, c-format msgid "File Extension .%s" msgstr "Файлово разширение .%s" #: ../src/ui/feed_list_view.c:432 msgid "Liferea is in offline mode. No update possible." msgstr "Liferea е в режим „изключен“. Не е възможно да актуализирате емисиите." #: ../src/ui/feed_list_view.c:478 #, fuzzy msgid "all feeds" msgstr "Търсене във всички емисии." #: ../src/ui/feed_list_view.c:479 #, fuzzy, c-format msgid "Mark %s as read ?" msgstr "Отбелязване на всички като _прочетени" #: ../src/ui/feed_list_view.c:483 #, fuzzy, c-format msgid "Are you sure you want to mark all items in %s as read ?" msgstr "Сигурни ли сте, че искате да изтриете „%s“?" #: ../src/ui/feed_list_view.c:621 msgid "(Empty)" msgstr "" #: ../src/ui/feed_list_view.c:832 #, c-format msgid "" "%s\n" "Rebuilding" msgstr "" #: ../src/ui/feed_list_view.c:901 msgid "Deleting entry" msgstr "Изтриване на запис" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\" and its contents?" msgstr "Сигурни ли сте, че искате да изтриете „%s“ и всички негови записи?" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\"?" msgstr "Сигурни ли сте, че искате да изтриете „%s“?" #: ../src/ui/feed_list_view.c:911 ../src/ui/feed_list_view.c:965 #, fuzzy msgid "_Cancel" msgstr "Актуализиране на _всички" #: ../src/ui/feed_list_view.c:912 ../src/ui/popup_menu.c:335 #, fuzzy msgid "_Delete" msgstr "/_Изтриване" #: ../src/ui/feed_list_view.c:914 msgid "Deletion Confirmation" msgstr "Потвърждаване на изтриването" #: ../src/ui/feed_list_view.c:953 #, c-format msgid "" "Are you sure that you want to add a new subscription with URL \"%s\"? " "Another subscription with the same URL already exists (\"%s\")." msgstr "" #: ../src/ui/feed_list_view.c:966 msgid "_Add" msgstr "" #: ../src/ui/feed_list_view.c:968 #, fuzzy msgid "Adding Duplicate Subscription Confirmation" msgstr "Потвърждаване на изтриването" #: ../src/ui/icons.c:54 #, c-format msgid "Couldn't find pixmap file: %s" msgstr "Файлът с картата с пиксели %s не може да бъде открит." #: ../src/ui/item_list_view.c:115 msgid "This item has no link specified!" msgstr "Този запис няма не съдържа връзка!" #: ../src/ui/item_list_view.c:482 #, fuzzy msgid "*** No title ***" msgstr "[Без заглавие]" #: ../src/ui/item_list_view.c:486 msgid " important " msgstr "" #: ../src/ui/item_list_view.c:849 msgid "Headline" msgstr "Заглавие" #: ../src/ui/item_list_view.c:871 msgid "Date" msgstr "Дата" #: ../src/ui/item_list_view.c:1038 msgid "You must select a feed to delete its items!" msgstr "Трябва да изберете емисия, за да изтриете нейните записи!" #: ../src/ui/item_list_view.c:1054 ../src/ui/item_list_view.c:1132 #: ../src/ui/item_list_view.c:1147 msgid "No item has been selected" msgstr "Няма избран запис" #: ../src/ui/liferea_shell.c:408 #, fuzzy, c-format msgid " (%d new)" msgid_plural " (%d new)" msgstr[0] "%d нов запис" msgstr[1] "%d нови записа" #: ../src/ui/liferea_shell.c:413 #, fuzzy, c-format msgid "%d unread%s" msgid_plural "%d unread%s" msgstr[0] "е непрочетен" msgstr[1] "е непрочетен" #: ../src/ui/liferea_shell.c:754 msgid "Help Topics" msgstr "Раздели в помощта" #: ../src/ui/liferea_shell.c:760 msgid "Quick Reference" msgstr "Бърз справочник" #: ../src/ui/liferea_shell.c:766 msgid "FAQ" msgstr "Често задавани въпроси" #: ../src/ui/liferea_shell.c:1044 #, fuzzy, c-format msgid "Email command failed: %s" msgstr "Командата за стартиране на браузъра е неуспешна: %s" #: ../src/ui/popup_menu.c:102 ../glade/liferea_menu.ui.h:25 msgid "Open In _Tab" msgstr "" #: ../src/ui/popup_menu.c:106 ../glade/liferea_menu.ui.h:26 #, fuzzy msgid "_Open In Browser" msgstr "_Отваряне в браузър" #: ../src/ui/popup_menu.c:110 ../glade/liferea_menu.ui.h:27 #, fuzzy msgid "Open In _External Browser" msgstr "Външен браузър" #: ../src/ui/popup_menu.c:115 msgid "Email The Author" msgstr "" #: ../src/ui/popup_menu.c:140 msgid "Copy to News Bin" msgstr "" #: ../src/ui/popup_menu.c:148 #, c-format msgid "_Bookmark at %s" msgstr "" #: ../src/ui/popup_menu.c:154 #, fuzzy msgid "Copy Item _Location" msgstr "/_Копиране адреса на записа" #: ../src/ui/popup_menu.c:163 ../glade/liferea_menu.ui.h:22 msgid "Toggle _Read Status" msgstr "П_ревключване на състояние: \"прочетено/непрочетено“" #: ../src/ui/popup_menu.c:167 ../glade/liferea_menu.ui.h:23 msgid "Toggle Item _Flag" msgstr "Прикрепяне на флаг към записа" #: ../src/ui/popup_menu.c:171 #, fuzzy msgid "R_emove Item" msgstr "/Пре_махване на запис" #: ../src/ui/popup_menu.c:200 #, fuzzy msgid "Open Enclosure..." msgstr "/Отваряне на приложение..." #: ../src/ui/popup_menu.c:201 #, fuzzy msgid "Save As..." msgstr "/Запазване като..." #: ../src/ui/popup_menu.c:202 #, fuzzy msgid "Copy Link Location" msgstr "/_Копиране адреса на записа" #: ../src/ui/popup_menu.c:280 ../glade/liferea_menu.ui.h:13 #, fuzzy msgid "_Update" msgstr "/_Актуализиране" #: ../src/ui/popup_menu.c:282 #, fuzzy msgid "_Update Folder" msgstr "/_Актуализиране на папка" #: ../src/ui/popup_menu.c:292 #, fuzzy msgid "New _Subscription..." msgstr "_Нова емисия..." #: ../src/ui/popup_menu.c:295 ../glade/liferea_menu.ui.h:5 msgid "New _Folder..." msgstr "Нова _папка..." #: ../src/ui/popup_menu.c:298 ../glade/liferea_menu.ui.h:6 #, fuzzy msgid "New S_earch Folder..." msgstr "Нова _папка..." #: ../src/ui/popup_menu.c:299 #, fuzzy msgid "New S_ource..." msgstr "/_Нов/_Нова папка..." #: ../src/ui/popup_menu.c:300 ../glade/liferea_menu.ui.h:8 #, fuzzy msgid "New _News Bin..." msgstr "/_Нов/Нова _приставка..." #: ../src/ui/popup_menu.c:303 #, fuzzy msgid "_New" msgstr "/_Нов" #: ../src/ui/popup_menu.c:312 #, fuzzy msgid "Sort Feeds" msgstr "Вмъкване на списък с емисии" #: ../src/ui/popup_menu.c:320 #, fuzzy msgid "_Mark All As Read" msgstr "/_Отбелязване на всички като прочетени" #: ../src/ui/popup_menu.c:327 msgid "_Rebuild" msgstr "" #: ../src/ui/popup_menu.c:336 ../glade/liferea_menu.ui.h:17 #, fuzzy msgid "_Properties" msgstr "_Настройки..." #: ../src/ui/popup_menu.c:343 #, fuzzy msgid "Convert To Local Subscriptions..." msgstr "_Нова емисия..." #: ../src/ui/preferences_dialog.c:84 msgid "GNOME default" msgstr "" #: ../src/ui/preferences_dialog.c:85 msgid "Text below icons" msgstr "" #: ../src/ui/preferences_dialog.c:86 msgid "Text beside icons" msgstr "" #: ../src/ui/preferences_dialog.c:87 msgid "Icons only" msgstr "" #: ../src/ui/preferences_dialog.c:88 msgid "Text only" msgstr "" #: ../src/ui/preferences_dialog.c:96 ../src/ui/subscription_dialog.c:43 #, fuzzy msgid "minutes" msgstr "минути." #: ../src/ui/preferences_dialog.c:97 ../src/ui/subscription_dialog.c:44 msgid "hours" msgstr "" #: ../src/ui/preferences_dialog.c:98 ../src/ui/subscription_dialog.c:45 msgid "days" msgstr "" #: ../src/ui/preferences_dialog.c:103 msgid "Space" msgstr "Space" #: ../src/ui/preferences_dialog.c:104 msgid " Space" msgstr " Space" #: ../src/ui/preferences_dialog.c:105 msgid " Space" msgstr " Space" #: ../src/ui/preferences_dialog.c:110 #, fuzzy msgid "Normal View" msgstr "Локален _файл" #: ../src/ui/preferences_dialog.c:111 #, fuzzy msgid "Wide View" msgstr "_Изглед" #: ../src/ui/preferences_dialog.c:478 #, fuzzy msgid "Default Browser" msgstr "Браузър" #: ../src/ui/preferences_dialog.c:480 msgid "Manual" msgstr "Ръчно зададено" #: ../src/ui/preferences_dialog.c:740 msgid "Type" msgstr "Тип" #: ../src/ui/preferences_dialog.c:743 msgid "Program" msgstr "Програма" #: ../src/ui/search_dialog.c:106 #, fuzzy msgid "Saved Search" msgstr "Търсене с Feedster" #: ../src/ui/subscription_dialog.c:427 #, fuzzy, c-format msgid "The provider of this feed suggests an update interval of %d minute." msgid_plural "" "The provider of this feed suggests an update interval of %d minutes." msgstr[0] "" "Доставчикът на този източник предлага да поставите период за осъвременяване " "от %d минути." msgstr[1] "" "Доставчикът на този източник предлага да поставите период за осъвременяване " "от %d минути." #: ../src/ui/subscription_dialog.c:431 msgid "This feed specifies no default update interval." msgstr "Този източник няма подразбиращ се интервал за осъвременяване" #: ../src/ui/ui_common.c:206 #, fuzzy msgid "All Files" msgstr "Локален _файл" #: ../src/update.c:350 #, c-format msgid "Error opening temp file %s to use for filtering!" msgstr "Грешка при отваряне на временния файл %s за филтриране!" #: ../src/update.c:372 #, c-format msgid "%s exited with status %d" msgstr "%s приключи работа със статус %d" #: ../src/update.c:378 ../src/update.c:379 ../src/update.c:493 #, c-format msgid "Error: Could not open pipe \"%s\"" msgstr "Грешка: Тръбата „%s“ не може да бъде отворена" #. FIXME: maybe setting request->returncode would be better #: ../src/update.c:517 #, c-format msgid "Error: Could not open file \"%s\"" msgstr "Грешка: Файлът „%s“ не може да бъде отворен" #: ../src/update.c:523 #, c-format msgid "Error: There is no file \"%s\"" msgstr "Грешка: Файлът „%s“ не съществува" #: ../src/vfolder.c:54 #, fuzzy msgid "New Search Folder" msgstr "Нова папка" #: ../src/webkit/liferea_web_view.c:177 #, fuzzy msgid "Open Link In _Tab" msgstr "/_Зареждане на връзката в _таб" #: ../src/webkit/liferea_web_view.c:178 #, fuzzy msgid "Open Link In Browser" msgstr "/_Зареждане на връзката в браузър" #: ../src/webkit/liferea_web_view.c:179 #, fuzzy msgid "Open Link In External Browser" msgstr "/_Зареждане на връзката в браузър" #: ../src/webkit/liferea_web_view.c:185 #, c-format msgid "_Bookmark Link at %s" msgstr "" #: ../src/webkit/liferea_web_view.c:192 #, fuzzy msgid "_Copy Link Location" msgstr "/_Копиране адреса на записа" #: ../src/webkit/liferea_web_view.c:195 #, fuzzy msgid "_View Image" msgstr "_Изглед" #: ../src/webkit/liferea_web_view.c:196 #, fuzzy msgid "_Copy Image Location" msgstr "/_Копиране адреса на записа" #: ../src/webkit/liferea_web_view.c:199 #, fuzzy msgid "S_ave Link As" msgstr "/Запазване като..." #: ../src/webkit/liferea_web_view.c:202 msgid "S_ave Image As" msgstr "" #: ../src/webkit/liferea_web_view.c:209 #, fuzzy msgid "_Subscribe..." msgstr "/_Абониране..." #: ../src/webkit/liferea_web_view.c:213 msgid "_Copy" msgstr "" #: ../src/webkit/liferea_web_view.c:219 msgid "_Increase Text Size" msgstr "_Увеличаване на големината на шрифта" #: ../src/webkit/liferea_web_view.c:220 msgid "_Decrease Text Size" msgstr "_Намаляване на големината на шрифта" #: ../src/webkit/liferea_web_view.c:227 msgid "_Reader Mode" msgstr "" #: ../src/xml.c:426 #, fuzzy msgid "[There were more errors. Output was truncated!]" msgstr "[Четенето беше прекъснато!]" #: ../src/xml.c:594 #, fuzzy msgid "XML Parser: Could not parse document:\n" msgstr "" "xmlReadMemory(): Документът не може да бъде анализиран:\n" "%s%s" #: ../glade/about.ui.h:1 msgid "About" msgstr "Относно програмата" #: ../glade/about.ui.h:2 msgid "Liferea is a news aggregator for GTK+" msgstr "Liferea е събирач на RSS емисии за GTK+" #: ../glade/about.ui.h:3 #, fuzzy msgid "Liferea Homepage" msgstr "Събирач на RSS емисии (Liferea)" #: ../glade/auth.ui.h:1 msgid "Authentication" msgstr "Идентифициране" #: ../glade/auth.ui.h:3 #, fuzzy, no-c-format msgid "Enter the username and password for \"%s\" (%s)" msgstr "Въведете потребителското име и паролата за „%s“ (%s):" #: ../glade/auth.ui.h:4 ../glade/properties.ui.h:31 msgid "User_name:" msgstr "Потребителско _име:" #: ../glade/auth.ui.h:5 ../glade/properties.ui.h:32 msgid "_Password:" msgstr "_Парола:" #: ../glade/enclosure_handler.ui.h:1 #, fuzzy msgid "Open Enclosure" msgstr "/Отваряне на приложение..." #: ../glade/enclosure_handler.ui.h:2 #, fuzzy msgid "Open an enclosure of type:" msgstr "Изтегля се приложения от вида:" #: ../glade/enclosure_handler.ui.h:3 #, fuzzy msgid "" "_What should Liferea do with this enclosure? Please enter the command you " "want to be executed below. The enclosures URL will be supplied as an " "argument for this command:" msgstr "" "Какво да прави Liferea с това приложение? Въведете командата, която искате " "да бъде изпълнена, отдолу. Изтегленият файл ще бъде подаден като аргумент на " "следната команда:" #: ../glade/enclosure_handler.ui.h:4 msgid "_Browse" msgstr "_Преглед:" #: ../glade/enclosure_handler.ui.h:5 #, fuzzy msgid "_Do this automatically for enclosures like this from now on." msgstr "" "_Действието да се извършва автоматично за файловете подобни на този от сега " "нататък." #: ../glade/liferea_menu.ui.h:1 #, fuzzy msgid "_Subscriptions" msgstr "Нова емисия" #: ../glade/liferea_menu.ui.h:2 ../glade/liferea_toolbar.ui.h:8 msgid "Update _All" msgstr "Актуализиране на _всички" #: ../glade/liferea_menu.ui.h:3 msgid "Mark All As _Read" msgstr "Отбелязване на всички като _прочетени" #: ../glade/liferea_menu.ui.h:4 ../glade/liferea_toolbar.ui.h:1 msgid "_New Subscription..." msgstr "_Нова емисия..." #: ../glade/liferea_menu.ui.h:7 #, fuzzy msgid "New _Source..." msgstr "/_Нов/_Нова папка..." #: ../glade/liferea_menu.ui.h:9 msgid "_Import Feed List..." msgstr "_Внасяне на списък с емисии..." #: ../glade/liferea_menu.ui.h:10 msgid "_Export Feed List..." msgstr "_Изнасяне на списък с емисии..." #: ../glade/liferea_menu.ui.h:11 msgid "_Quit" msgstr "_Спиране на програмата" #: ../glade/liferea_menu.ui.h:12 #, fuzzy msgid "_Feed" msgstr "_Емисии" #: ../glade/liferea_menu.ui.h:15 #, fuzzy msgid "Remove _All Items" msgstr "Премахване на _всички" #: ../glade/liferea_menu.ui.h:16 #, fuzzy msgid "_Remove" msgstr "Премахване на _всички" #: ../glade/liferea_menu.ui.h:18 #, fuzzy msgid "_Item" msgstr "_Записи" #: ../glade/liferea_menu.ui.h:24 #, fuzzy msgid "R_emove" msgstr "/Пре_махване на запис" #: ../glade/liferea_menu.ui.h:28 msgid "_View" msgstr "_Изглед" #: ../glade/liferea_menu.ui.h:29 msgid "_Fullscreen" msgstr "" #: ../glade/liferea_menu.ui.h:30 msgid "Zoom _In" msgstr "" #: ../glade/liferea_menu.ui.h:31 msgid "Zoom _Out" msgstr "" #: ../glade/liferea_menu.ui.h:32 #, fuzzy msgid "_Normal size" msgstr "Локален _файл" #: ../glade/liferea_menu.ui.h:33 #, fuzzy msgid "_Normal View" msgstr "Локален _файл" #: ../glade/liferea_menu.ui.h:34 #, fuzzy msgid "_Wide View" msgstr "_Изглед" #: ../glade/liferea_menu.ui.h:35 msgid "_Reduced Feed List" msgstr "" #: ../glade/liferea_menu.ui.h:36 msgid "_Tools" msgstr "" #: ../glade/liferea_menu.ui.h:37 #, fuzzy msgid "_Update Monitor" msgstr "/_Актуализиране на папка" #: ../glade/liferea_menu.ui.h:38 msgid "_Preferences" msgstr "_Настройки" #: ../glade/liferea_menu.ui.h:39 #, fuzzy msgid "S_earch" msgstr "Търсене" #: ../glade/liferea_menu.ui.h:41 msgid "_Help" msgstr "_Помощ" #: ../glade/liferea_menu.ui.h:42 msgid "_Contents" msgstr "_Ръководство" #: ../glade/liferea_menu.ui.h:43 msgid "_Quick Reference" msgstr "_Бърз справочник" #: ../glade/liferea_menu.ui.h:44 msgid "_FAQ" msgstr "_Често задавани въпроси" #: ../glade/liferea_menu.ui.h:45 msgid "_About" msgstr "_Относно програмата" #: ../glade/liferea_toolbar.ui.h:2 #, fuzzy msgid "Adds a subscription to the feed list." msgstr "Добавяне на нова емисия." #: ../glade/liferea_toolbar.ui.h:4 #, fuzzy msgid "" "Marks all items of the selected feed list node / in the item list as read." msgstr "Отбелязване като прочетено на всичко в избраната папка." #: ../glade/liferea_toolbar.ui.h:9 #, fuzzy msgid "Updates all subscriptions." msgstr "Добавяне на нова емисия." #: ../glade/liferea_toolbar.ui.h:11 msgid "Show the search dialog." msgstr "Показване на полето за търсене." #: ../glade/mainwindow.ui.h:2 msgid "page 1" msgstr "" #: ../glade/mainwindow.ui.h:3 msgid "page 2" msgstr "" #: ../glade/mainwindow.ui.h:4 ../glade/prefs.ui.h:23 msgid "Headlines" msgstr "Заглавия" #: ../glade/mark_read_dialog.ui.h:1 #, fuzzy msgid "Mark all as read ?" msgstr "Отбелязване на всички като _прочетени" #: ../glade/mark_read_dialog.ui.h:2 #, fuzzy msgid "Mark all as read" msgstr "Отбелязване на всички като _прочетени" #: ../glade/mark_read_dialog.ui.h:3 msgid "Do not ask again" msgstr "" #: ../glade/new_folder.ui.h:1 msgid "New Folder" msgstr "Нова папка" #: ../glade/new_folder.ui.h:2 msgid "_Folder name:" msgstr "_Име на папката:" #: ../glade/new_newsbin.ui.h:1 msgid "Create News Bin" msgstr "" #: ../glade/new_newsbin.ui.h:2 msgid "_News Bin Name:" msgstr "" #: ../glade/new_subscription.ui.h:2 ../glade/properties.ui.h:11 msgid "Feed Source" msgstr "Източник на емисията" #: ../glade/new_subscription.ui.h:3 ../glade/properties.ui.h:12 msgid "Source Type:" msgstr "Вид на източника:" #: ../glade/new_subscription.ui.h:4 ../glade/properties.ui.h:13 msgid "_URL" msgstr "_URL" #: ../glade/new_subscription.ui.h:5 ../glade/properties.ui.h:14 msgid "_Command" msgstr "_Команда" #: ../glade/new_subscription.ui.h:6 ../glade/properties.ui.h:15 msgid "_Local File" msgstr "Локален _файл" #: ../glade/new_subscription.ui.h:7 ../glade/properties.ui.h:16 msgid "Select File..." msgstr "Избиране на файл..." #: ../glade/new_subscription.ui.h:8 ../glade/properties.ui.h:17 msgid "_Source:" msgstr "_Изходен код:" #: ../glade/new_subscription.ui.h:9 #, fuzzy msgid "Download / Postprocessing" msgstr "_Чрез програмата" #: ../glade/new_subscription.ui.h:10 ../glade/properties.ui.h:30 msgid "_Don't use proxy for download" msgstr "" #: ../glade/new_subscription.ui.h:11 ../glade/properties.ui.h:18 msgid "Use conversion _filter" msgstr "Използване на филтър за _конвертиране" #: ../glade/new_subscription.ui.h:12 msgid "" "Liferea can use external filter plugins in order to access feeds and " "directories in non-supported formats. See the documentation for more " "information." msgstr "" "Liferea може да използва външни модули и филтри, за да достига емисии и " "директории в неподдържани формати. Погледнете документацията за повече " "информация." #: ../glade/new_subscription.ui.h:13 ../glade/properties.ui.h:20 msgid "Convert _using:" msgstr "Конвертиране _чрез:" #: ../glade/node_source.ui.h:1 msgid "Source Selection" msgstr "" #: ../glade/node_source.ui.h:2 msgid "_Select the source type you want to add..." msgstr "" #: ../glade/opml_source.ui.h:1 msgid "Add OPML/Planet" msgstr "" #: ../glade/opml_source.ui.h:2 msgid "" "Please specify a local file or an URL pointing to a valid OPML feed list." msgstr "" #: ../glade/opml_source.ui.h:3 #, fuzzy msgid "_Location" msgstr "/_Копиране адреса на записа" #: ../glade/opml_source.ui.h:4 #, fuzzy msgid "_Select File" msgstr "Избиране на файл..." #: ../glade/prefs.ui.h:1 msgid "Liferea Preferences" msgstr "Настройки на Liferea" #: ../glade/prefs.ui.h:2 msgid "Feed Cache Handling" msgstr "" #: ../glade/prefs.ui.h:3 msgid "Default _number of items per feed to save:" msgstr "Стандартен _брой записи, за запазване от източник:" #: ../glade/prefs.ui.h:4 ../glade/properties.ui.h:27 msgid "0" msgstr "" #: ../glade/prefs.ui.h:5 #, fuzzy msgid "Feed Update Settings" msgstr "Временни файлове на емисията" #. Feed update interval hint in preference dialog. #: ../glade/prefs.ui.h:7 #, fuzzy msgid "" "Note: Please remember to set a reasonable refresh time. Usually it is a " "waste of bandwidth to poll feeds more often than each hour." msgstr "" "Забележка: Не пропускайте да настройте разумен период на осъвременяване. " "Не е удобно да настроите емисиите да бъдат проверявани през 15 минути! За да " "изключите автоматичното осъвременяване - поставете за период на " "осъвременяване 0." #: ../glade/prefs.ui.h:8 #, fuzzy msgid "_Update all subscriptions at startup." msgstr "Добавяне на нова емисия." #: ../glade/prefs.ui.h:9 msgid "Default Feed Refresh _Interval:" msgstr "_Период за актуализиране на емисията:" #: ../glade/prefs.ui.h:10 ../glade/properties.ui.h:6 msgid "1" msgstr "" #: ../glade/prefs.ui.h:11 msgid "Feeds" msgstr "Емисии" #: ../glade/prefs.ui.h:12 #, fuzzy msgid "Folder Display Settings" msgstr "Изобразяване на папките" #: ../glade/prefs.ui.h:13 msgid "_Show the items of all child feeds when a folder is selected." msgstr "Показване на всички заглавия от всички под-емисии при избор на папка." #: ../glade/prefs.ui.h:14 msgid "_Hide read items." msgstr "_Скриване на прочетените записи." #: ../glade/prefs.ui.h:15 #, fuzzy msgid "Feed Icons (Favicons)" msgstr "Икони на емисиите" #: ../glade/prefs.ui.h:16 msgid "_Update all favicons now" msgstr "_Актуализиране на всички икони на емисии" #: ../glade/prefs.ui.h:17 msgid "Folders" msgstr "Папки" #: ../glade/prefs.ui.h:18 #, fuzzy msgid "Reading Headlines" msgstr "Заглавия" #: ../glade/prefs.ui.h:19 msgid "_Skim through articles with:" msgstr "Бърз преглед чрез:" #: ../glade/prefs.ui.h:20 msgid "_Default View Mode:" msgstr "" #: ../glade/prefs.ui.h:21 #, fuzzy msgid "Web Integration" msgstr "Ориентация" #: ../glade/prefs.ui.h:22 msgid "_Post Bookmarks to" msgstr "" #: ../glade/prefs.ui.h:24 #, fuzzy msgid "Internal Browser Settings" msgstr "Вътрешен браузър" #: ../glade/prefs.ui.h:25 msgid "Open links in Liferea's _window." msgstr "Отваряне на връзките в _Liferea." #: ../glade/prefs.ui.h:26 msgid "_Never run external Javascript." msgstr "" #: ../glade/prefs.ui.h:27 #, fuzzy msgid "_Enable browser plugins." msgstr "Външен браузър" #: ../glade/prefs.ui.h:28 #, fuzzy msgid "External Browser Settings" msgstr "Външен браузър" #: ../glade/prefs.ui.h:29 msgid "_Browser:" msgstr "_Браузър:" #: ../glade/prefs.ui.h:30 #, fuzzy msgid "_Manual:" msgstr "Ръчно зададено" #: ../glade/prefs.ui.h:32 #, no-c-format msgid "(%s for URL)" msgstr "" #: ../glade/prefs.ui.h:33 msgid "Browser" msgstr "Браузър" #: ../glade/prefs.ui.h:34 #, fuzzy msgid "Toolbar Settings" msgstr "Изобразяване на папките" #: ../glade/prefs.ui.h:35 msgid "_Hide toolbar." msgstr "" #: ../glade/prefs.ui.h:36 msgid "Toolbar _button labels:" msgstr "" #: ../glade/prefs.ui.h:37 msgid "Other" msgstr "" #: ../glade/prefs.ui.h:38 msgid "Ask for confirmation when marking all items as read" msgstr "" #: ../glade/prefs.ui.h:39 msgid "Desktop" msgstr "" #: ../glade/prefs.ui.h:40 msgid "HTTP Proxy Server" msgstr "" #: ../glade/prefs.ui.h:41 msgid "_Auto Detect (GNOME or environment)" msgstr "" #: ../glade/prefs.ui.h:42 #, fuzzy msgid "_No Proxy" msgstr "Сървър-посредник" #: ../glade/prefs.ui.h:43 msgid "_Manual Setting:" msgstr "" #: ../glade/prefs.ui.h:44 msgid "Proxy _Host:" msgstr "_Адрес:" #: ../glade/prefs.ui.h:45 msgid "Proxy _Port:" msgstr "_Порт:" #: ../glade/prefs.ui.h:46 #, fuzzy msgid "Use Proxy Au_thentication" msgstr "Използване на _идентификация" #: ../glade/prefs.ui.h:47 msgid "Proxy _Username:" msgstr "Потребителско _име:" #: ../glade/prefs.ui.h:48 msgid "Proxy Pass_word:" msgstr "_Парола:" #: ../glade/prefs.ui.h:49 msgid "" "Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per " "application proxy settings. The system's default proxy settings will be used." msgstr "" #: ../glade/prefs.ui.h:50 msgid "Proxy" msgstr "Сървър-посредник" #: ../glade/prefs.ui.h:51 #, fuzzy msgid "Privacy Settings" msgstr "Изобразяване на папките" #: ../glade/prefs.ui.h:52 msgid "Tell sites that I do _not want to be tracked." msgstr "" #: ../glade/prefs.ui.h:53 msgid "_Intelligent Tracking Prevention. " msgstr "" #: ../glade/prefs.ui.h:54 msgid "" "This enables the WebKit feature described here." msgstr "" #: ../glade/prefs.ui.h:55 msgid "" "Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or " "higher." msgstr "" #: ../glade/prefs.ui.h:56 msgid "Use _Reader mode." msgstr "" #: ../glade/prefs.ui.h:57 msgid "" "This enables stripping all non-content elements (like scripts, fonts, tracking)" msgstr "" #: ../glade/prefs.ui.h:58 msgid "Privacy" msgstr "" #: ../glade/prefs.ui.h:59 #, fuzzy msgid "Downloading Enclosures" msgstr "Изтегля се приложения" #: ../glade/prefs.ui.h:60 msgid "_Download using" msgstr "_Чрез програмата" #: ../glade/prefs.ui.h:62 #, no-c-format msgid "custom-command %s" msgstr "" #: ../glade/prefs.ui.h:63 #, fuzzy msgid "Opening Enclosures" msgstr "/Отваряне на приложение..." #: ../glade/prefs.ui.h:64 msgid "Enclosures" msgstr "Приложения" #: ../glade/properties.ui.h:1 msgid "Subscription Properties" msgstr "Настройки на емисията" #: ../glade/properties.ui.h:2 #, fuzzy msgid "Feed _Name" msgstr "_Име на емисията:" #: ../glade/properties.ui.h:3 #, fuzzy msgid "Update _Interval" msgstr "/_Актуализиране на папка" #: ../glade/properties.ui.h:4 msgid "_Use global default update interval." msgstr "_Използване на стандартния интервал за осъвременяване." #: ../glade/properties.ui.h:5 msgid "_Feed specific update interval of" msgstr "Специфичен интервал за осъвременяване на _емисия" #: ../glade/properties.ui.h:7 msgid "_Don't update this feed automatically." msgstr "Тези емисии _да не бъдат осъвременявани автоматично." #: ../glade/properties.ui.h:9 #, no-c-format msgid "This feed provider suggests an update interval of %d minutes." msgstr "" "Доставчикът на тази емисия предлага да поставите период за осъвременяване от " "%d минути." #: ../glade/properties.ui.h:10 msgid "General" msgstr "Основен" #: ../glade/properties.ui.h:19 #, fuzzy msgid "" "Liferea can use external filter scripts in order to access feeds and " "directories in non-supported formats." msgstr "" "Liferea може да използва външни модули и филтри, за да достига емисии и " "директории в неподдържани формати. Погледнете документацията за повече " "информация." #: ../glade/properties.ui.h:21 ../xslt/item.xml.in.h:1 #, fuzzy msgid "Source" msgstr "Изходен код:" #: ../glade/properties.ui.h:22 msgid "" "The cache setting controls if the contents of feeds are saved when Liferea " "exits. Marked items are always saved to the cache." msgstr "" "Настройките на временните файлове определят дали съдържанието на емисиите се " "запазва при спирането на Liferea. Избраните емисии винаги се записват в кеша." #: ../glade/properties.ui.h:23 msgid "_Default cache settings" msgstr "_Настройки по подразбиране" #: ../glade/properties.ui.h:24 msgid "Di_sable cache" msgstr "Сп_иране на временните файлове" #: ../glade/properties.ui.h:25 msgid "_Unlimited cache" msgstr "_Неограничени временни файлове" #: ../glade/properties.ui.h:26 #, fuzzy msgid "_Number of items to save:" msgstr "Стандартен _брой записи, за запазване от източник:" #: ../glade/properties.ui.h:28 msgid "Archive" msgstr "" #: ../glade/properties.ui.h:29 msgid "Use HTTP _authentication" msgstr "Използване на HTTP _идентификация" #: ../glade/properties.ui.h:33 #, fuzzy msgid "Download" msgstr "_Чрез програмата" #: ../glade/properties.ui.h:34 msgid "_Automatically download all enclosures of this feed." msgstr "_Автоматично изтегляне на всички приложения към емисията." #: ../glade/properties.ui.h:35 msgid "Auto-_load item link in configured browser when selecting articles." msgstr "" #: ../glade/properties.ui.h:36 #, fuzzy msgid "Ignore _comment feeds for this subscription." msgstr "Отваряне на прозореца за предпочитанията за избраната емисия." #: ../glade/properties.ui.h:37 #, fuzzy msgid "_Mark downloaded items as read." msgstr "_Отбелязване на избраните като прочетени" #: ../glade/properties.ui.h:38 msgid "Extract full content from HTML5 and Google AMP" msgstr "" #: ../glade/reedah_source.ui.h:1 msgid "Add Reedah Account" msgstr "" #: ../glade/reedah_source.ui.h:2 msgid "Please enter your Reedah account settings." msgstr "" #: ../glade/reedah_source.ui.h:3 ../glade/theoldreader_source.ui.h:3 #: ../glade/ttrss_source.ui.h:4 #, fuzzy msgid "_Password" msgstr "_Парола:" #: ../glade/reedah_source.ui.h:4 ../glade/theoldreader_source.ui.h:4 msgid "_Username (Email)" msgstr "" #: ../glade/rename_node.ui.h:1 #, fuzzy msgid "Rename" msgstr "Преименуване на папката" #: ../glade/rename_node.ui.h:2 #, fuzzy msgid "_New Name:" msgstr "_Име:" #: ../glade/search_folder.ui.h:1 #, fuzzy msgid "Search Folder Properties" msgstr "Настройки на виртуалната папка" #: ../glade/search_folder.ui.h:2 #, fuzzy msgid "Search _Name:" msgstr "_Име на емисията:" #: ../glade/search_folder.ui.h:3 #, fuzzy msgid "Search Rules" msgstr "Търсене във всички емисии" #: ../glade/search_folder.ui.h:4 msgid "Rules" msgstr "" #: ../glade/search_folder.ui.h:5 msgid "All rules for this search folder" msgstr "" #: ../glade/search_folder.ui.h:6 msgid "Rule Matching" msgstr "" #: ../glade/search_folder.ui.h:7 ../glade/search.ui.h:4 msgid "A_ny Rule Matches" msgstr "" #: ../glade/search_folder.ui.h:8 ../glade/search.ui.h:5 msgid "_All Rules Must Match" msgstr "" #: ../glade/search_folder.ui.h:9 #, fuzzy msgid "Hide read items" msgstr "_Скриване на прочетените записи." #: ../glade/search.ui.h:1 #, fuzzy msgid "Advanced Search" msgstr "Търсене с Feedster" #: ../glade/search.ui.h:2 #, fuzzy msgid "_Search Folder..." msgstr "Нова _папка..." #: ../glade/search.ui.h:3 msgid "Find Items that meet the following criteria" msgstr "" #: ../glade/simple_search.ui.h:1 msgid "Search All Feeds" msgstr "Търсене във всички емисии" #: ../glade/simple_search.ui.h:2 msgid "_Advanced..." msgstr "" #: ../glade/simple_search.ui.h:3 msgid "" "Starts searching for the specified text in all feeds. The search result will " "appear in the item list." msgstr "" "Стартиране на търсене на указания текст във всички емисии. Резултатът от " "търсенето ще се появи в списъка със записи." #: ../glade/simple_search.ui.h:4 msgid "_Search for:" msgstr "_Търсене за:" #: ../glade/simple_search.ui.h:5 msgid "" "Enter a search string Liferea should find either in a items title or in its " "content." msgstr "" "Въведете низа, който Liferea са търси както в заглавията, така и в " "съдържанието на емисиите." #: ../glade/simple_subscription.ui.h:2 msgid "Advanced..." msgstr "" #: ../glade/simple_subscription.ui.h:3 #, fuzzy msgid "Feed _Source" msgstr "Източник на емисията" #: ../glade/simple_subscription.ui.h:4 msgid "" "Enter a website location to use feed autodiscovery or in case you know it " "the exact feed location." msgstr "" #: ../glade/theoldreader_source.ui.h:1 msgid "Add TheOldReader Account" msgstr "" #: ../glade/theoldreader_source.ui.h:2 msgid "Please enter your TheOldReader account settings." msgstr "" #: ../glade/ttrss_source.ui.h:1 msgid "Add Tiny Tiny RSS Account" msgstr "" #: ../glade/ttrss_source.ui.h:2 msgid "Please enter your TinyTinyRSS account settings." msgstr "" #: ../glade/ttrss_source.ui.h:3 #, fuzzy msgid "_Server URL" msgstr "Грешка в сървъра" #: ../glade/ttrss_source.ui.h:5 #, fuzzy msgid "_Username" msgstr "Потребителско _име:" #: ../glade/update_monitor.ui.h:1 #, fuzzy msgid "Update Monitor" msgstr "/_Актуализиране на папка" #: ../glade/update_monitor.ui.h:2 msgid "Stop All" msgstr "" #: ../glade/update_monitor.ui.h:3 msgid "_Pending Requests" msgstr "" #: ../glade/update_monitor.ui.h:4 #, fuzzy msgid "_Downloading Now" msgstr "Изтегля се приложения" #: ../xslt/feed.xml.in.h:1 msgid "Feed:" msgstr "Емисия:" #: ../xslt/feed.xml.in.h:2 ../xslt/source.xml.in.h:1 msgid "Source:" msgstr "Изходен код:" #: ../xslt/feed.xml.in.h:3 #, fuzzy msgid "Publisher" msgstr "издател" #: ../xslt/feed.xml.in.h:4 #, fuzzy msgid "Copyright" msgstr "авторски права" #: ../xslt/feed.xml.in.h:5 #, fuzzy msgid "There was a problem when fetching this subscription!" msgstr "" "Имаше проблем при прочитането на емисията. Проверете адреса и изходната " "информация в конзолата." #: ../xslt/feed.xml.in.h:6 #, fuzzy msgid "1. Authentication" msgstr "Идентифициране" #: ../xslt/feed.xml.in.h:7 #, fuzzy msgid "2. Download" msgstr "_Чрез програмата" #: ../xslt/feed.xml.in.h:8 msgid "3. Feed Discovery" msgstr "" #: ../xslt/feed.xml.in.h:9 msgid "4. Parsing" msgstr "" #: ../xslt/feed.xml.in.h:10 #, fuzzy msgid "Details:" msgstr "Подробности" #: ../xslt/feed.xml.in.h:11 msgid "Authentication failed. Please check the credentials and try again!" msgstr "" #: ../xslt/feed.xml.in.h:12 #, fuzzy msgid "There was an error when downloading the feed source:" msgstr "Имаше грешки докато се изтегляше тази емисия!" #: ../xslt/feed.xml.in.h:13 #, fuzzy msgid "There was an error when running the feed filter command:" msgstr "Имаше грешки докато се изтегляше тази емисия!" #: ../xslt/feed.xml.in.h:14 msgid "" "The source does not point directly to a feed or a webpage with a link to a " "feed!" msgstr "" #: ../xslt/feed.xml.in.h:15 msgid "Sorry, the feed could not be parsed!" msgstr "" #: ../xslt/feed.xml.in.h:16 msgid "You may want to contact the author/webmaster of the feed about this!" msgstr "" #: ../xslt/folder.xml.in.h:1 #, fuzzy msgid "Folder:" msgstr "Виртуална папка:" #: ../xslt/folder.xml.in.h:2 ../xslt/source.xml.in.h:2 msgid "children with" msgstr "" #: ../xslt/folder.xml.in.h:3 ../xslt/source.xml.in.h:3 #: ../xslt/vfolder.xml.in.h:2 #, fuzzy msgid "unread headlines" msgstr "Заглавия" #: ../xslt/item.xml.in.h:2 msgid "Feed" msgstr "Емисия" #: ../xslt/item.xml.in.h:3 #, fuzzy msgid "Filed under" msgstr "Временни файлове на емисията" #: ../xslt/item.xml.in.h:4 #, fuzzy msgid "Author" msgstr "автор" #: ../xslt/item.xml.in.h:5 msgid "Shared by" msgstr "" #: ../xslt/item.xml.in.h:6 msgid "Via" msgstr "" #: ../xslt/item.xml.in.h:7 msgid "Related" msgstr "" #: ../xslt/item.xml.in.h:8 msgid "Also posted in" msgstr "" #: ../xslt/item.xml.in.h:9 #, fuzzy msgid "Creator" msgstr "създател" #: ../xslt/item.xml.in.h:10 msgid "Coordinates" msgstr "" #: ../xslt/item.xml.in.h:11 msgid "Map" msgstr "" #: ../xslt/item.xml.in.h:12 msgid "View count" msgstr "" #: ../xslt/item.xml.in.h:13 msgid "Rating" msgstr "" #: ../xslt/item.xml.in.h:14 #, fuzzy msgid "Comments" msgstr "коментари" #: ../xslt/item.xml.in.h:15 #, fuzzy msgid "Updating..." msgstr "Актуализиране на „%s“" #: ../xslt/item.xml.in.h:16 msgid "Section" msgstr "" #: ../xslt/item.xml.in.h:17 msgid "Department" msgstr "" #: ../xslt/newsbin.xml.in.h:1 #, fuzzy msgid "News Bin:" msgstr "Нов прозорец" #: ../xslt/newsbin.xml.in.h:2 msgid "" "Add items to this news bin by selecting \"Copy to News Bin\" from the item " "list context menu." msgstr "" #: ../xslt/vfolder.xml.in.h:1 #, fuzzy msgid "Search Folder:" msgstr "Нова папка" #, c-format #~ msgid "\"%s\" is not available" #~ msgstr "„%s“ не е достъпна" #, c-format #~ msgid "\"%s\" updated..." #~ msgstr "„%s“ е актуализирана..." #, fuzzy #~ msgid "" #~ "The last update of this subscription failed!
HTTP error code : " #~ msgstr "" #~ "Последното осъвременяване на тази емисия е неуспешно!
Грешка на " #~ "HTTP %d: %s" #, fuzzy #~ msgid "Parser Error Details" #~ msgstr "Подробности" #~ msgid "There were errors while filtering this feed!" #~ msgstr "Имаше грешки при изтеглянето на тази емисия!" #, fuzzy #~ msgid "Filter Error Details" #~ msgstr "Подробности" #, fuzzy #~ msgid "" #~ "

Could not detect the type of this feed! Please check if the source " #~ "really points to a resource provided in one of the supported syndication " #~ "formats!

XML Parser Output:
" #~ msgstr "" #~ "

Видът на емисията не може да бъде определен. Проверете дали това е " #~ "поддържан от програмата формат!

%s" #, fuzzy #~ msgid "" #~ "The URL you want Liferea to subscribe to points to a webpage and the auto " #~ "discovery found no feeds on this page. Maybe this webpage just does not " #~ "support feed auto discovery." #~ msgstr "" #~ "

Адресът, към който искате да абонирате Liferea, сочи към страница, в " #~ "която системата за автоматично откриване не успя да открие емисии. " #~ "Възможно е тази страница просто да не поддържа автоматично откриване на " #~ "емисии.

" #, fuzzy #~ msgid "Could not determine the feed type." #~ msgstr "

Не може да намери началото на RDF!

" #~ msgid "Gone. Resource doesn't exist. Please unsubscribe!" #~ msgstr "Този ресурс вече не съществува. Изтрийте го!" #~ msgid "Updating \"%s\"" #~ msgstr "Актуализиране на „%s“" #, fuzzy #~ msgid "XML error while reading feed! Feed \"%s\" could not be loaded!" #~ msgstr "" #~ "

Грешка в XML по време на четенето на емисията! Емисията „%s“ не може " #~ "да бъде заредена!

" #, fuzzy #~ msgid "Combined View" #~ msgstr "_Общ изглед" #~ msgid "_Disable Javascript." #~ msgstr "_Забраняване на Javascript." #~ msgid "GUI" #~ msgstr "Интерфейс" #, fuzzy #~ msgid "Cancel All" #~ msgstr "Актуализиране на _всички" #, fuzzy #~ msgid "Updating favicon for \"%s\"" #~ msgstr "Актуализиране иконата на емисията „%s“" #~ msgid "Marks read every item of every subscription." #~ msgstr "Отбелязване на абсолютно всички обекти като прочетени" #~ msgid "Imports an OPML feed list." #~ msgstr "Внасяне на списък с емисии в OPML формат." #, fuzzy #~ msgid "Exports the feed list as OPML." #~ msgstr ">Изнасяне на списък с емисии във формат OPML." #~ msgid "Removes all items of the currently selected feed." #~ msgstr "Премахване на всички записи от текущата емисия." #~ msgid "Increases the text size of the item view." #~ msgstr "Увеличаване на размера на текста на записите." #~ msgid "Decreases the text size of the item view." #~ msgstr "Намаляване на размера на текста, при прегледа на записи." #~ msgid "Edit Preferences." #~ msgstr "Промяна на настройките." #~ msgid "View help for this application." #~ msgstr "Преглед на помощта за тази програма." #~ msgid "View a list of all Liferea shortcuts." #~ msgstr "Преглед на списък с всички бързи клавиши в Liferea." #~ msgid "View the FAQ for this application." #~ msgstr "Преглед на често задаваните въпроси." #~ msgid "Shows an about dialog." #~ msgstr "Показване на диалогов прозорец с информация за програмата." #, fuzzy #~ msgid "_Combined View" #~ msgstr "_Общ изглед" #, fuzzy #~ msgid "Hide feeds with no unread items." #~ msgstr "Няма непрочетени записи " #, fuzzy #~ msgid "Adds a folder to the feed list." #~ msgstr "Добавяне на папка към списъка с емисии." #, fuzzy #~ msgid "Adds a new search folder to the feed list." #~ msgstr "Добавяне на нова виртуална папка към списъка с емисии." #, fuzzy #~ msgid "Adds a new feed list source." #~ msgstr "Добавяне на нова виртуална папка към списъка с емисии." #, fuzzy #~ msgid "Adds a new news bin." #~ msgstr "Добавяне на нова емисия." #~ msgid "" #~ "Updates the selected subscription or all subscriptions of the selected " #~ "folder." #~ msgstr "" #~ "Актуализиране на избраните емисии или на всички емисии от избраната папка." #~ msgid "Opens the property dialog for the selected subscription." #~ msgstr "Отваряне на прозореца за предпочитанията за избраната емисия." #~ msgid "Removes the selected subscription." #~ msgstr "Изтриване на избраната емисия." #~ msgid "Toggles the read status of the selected item." #~ msgstr "" #~ "Превключване на състоянието „прочетено/непрочетено“ на избрания запис." #~ msgid "Toggles the flag status of the selected item." #~ msgstr "Превключване на флага за състоянието на избрания запис." #~ msgid "Removes the selected item." #~ msgstr "Премахване на избрания запис." #, fuzzy #~ msgid "Launches the item's link in a new Liferea browser tab." #~ msgstr "Отваряне на адреса на записа в настроения за това браузър." #, fuzzy #~ msgid "Launches the item's link in the Liferea item pane." #~ msgstr "Отваряне на адреса на записа в настроения за това браузър." #, fuzzy #~ msgid "Launches the item's link in the configured external browser." #~ msgstr "Отваряне на адреса на записа в настроения за това браузър." #~ msgid "_Work Offline" #~ msgstr "Режим: „_изключен“" #, fuzzy #~ msgid "_Update All" #~ msgstr "/_Актуализиране на всички" #, fuzzy #~ msgid "_Show Liferea" #~ msgstr "Събирач на RSS емисии (Liferea)" #, fuzzy #~ msgid "InoReader" #~ msgstr "Временни файлове на емисията" #~ msgid "" #~ "Note: The username and password will be saved to your Liferea feedlist " #~ "file without using encryption." #~ msgstr "" #~ "Бележка: Потребителското име и паролата ще бъдат запазени в списъка с " #~ "емисии без да се използва криптиране." #, fuzzy #~ msgid "View Headlines" #~ msgstr "_Преглед на заглавията чрез" #, fuzzy #~ msgid "Feed Name" #~ msgstr "_Име на емисията:" #, fuzzy #~ msgid "Liferea, the Linux Feed Reader" #~ msgstr "Събирач на RSS емисии (Liferea)" #, fuzzy #~ msgid "_Open Link In Browser" #~ msgstr "/_Зареждане на връзката в браузър" #, fuzzy #~ msgid "_Open Link In External Browser" #~ msgstr "/_Зареждане на връзката в браузър" #~ msgid " " #~ msgstr " " #~ msgid "enter any search string you want" #~ msgstr "въведете дума за търсене" #~ msgid "Maximal _Number Of Result Items:" #~ msgstr "Максимален брой резултати:" #, fuzzy #~ msgid "" #~ "Note: Liferea will generate a feed subscription which is used to query " #~ "the search engine results for the specified search string. You can keep " #~ "this feed permanently and update it like any other subscription." #~ msgstr "" #~ "Бележка: Liferea ще генерира емисия, която ще бъде използвана в заявката " #~ "за търсене във Feedster. Можете да запазите тази емисия за постоянно и да " #~ "я осъвременявате като всяка друга обикновена емисия." #~ msgid "Liferea is now online" #~ msgstr "Liferea е в режим „включен“" #, fuzzy #~ msgid "Work Offline" #~ msgstr "Режим: „_изключен“" #~ msgid "Liferea is now offline" #~ msgstr "Liferea е в режим „изключен“" #, fuzzy #~ msgid "Work Online" #~ msgstr "Режим: „_изключен“" #~ msgid "This option allows you to disable subscription updating." #~ msgstr "Тази настройка ще Ви позволи да спрете актуализирането на емисията." #~ msgid "Browser default" #~ msgstr "Стандартен браузър" #~ msgid "Existing window" #~ msgstr "Съществуващ прозорец" #~ msgid "New window" #~ msgstr "Нов прозорец" #~ msgid "New tab" #~ msgstr "Нов подпрозорец" #, fuzzy #~ msgid "AOL Reader" #~ msgstr "Временни файлове на емисията" #~ msgid "_Open link in:" #~ msgstr "_Отваряне на връзката в:" #, fuzzy #~ msgid "No comments yet." #~ msgstr "коментари" #, fuzzy #~ msgid "Liferea - Linux Feed Reader" #~ msgstr "Събирач на RSS емисии (Liferea)" #, fuzzy #~ msgid "" #~ "

Welcome to Liferea, a desktop news aggregator for online news " #~ "feeds.

You can add new subscriptions

  • From main menu " #~ "'Subscription' -> 'New Subscription'
  • By dropping feed links " #~ "into the subscription list
  • By right clicking links and choosing " #~ "'Subscribe' within Liferea

" #~ msgstr "" #~ "liferea.png\\\" />

Liferea - програма за емисии по RSS

Добре дошли в Liferea, програма " #~ "за събиране на RSS емисии и новини.

Отляво се намера списък с " #~ "Вашите емисии. За да добавите емисия изберете Емисии -> Нова емисия. " #~ "За да разгледате заглавията на дадена емисия, изберете емисията и " #~ "заглавията ще се покажат в дясната част на програмата.

" #~ msgid "%d new item" #~ msgid_plural "%d new items" #~ msgstr[0] "%d нов запис" #~ msgstr[1] "%d нови записа" #~ msgid "No new items" #~ msgstr "Няма нови записи" #~ msgid "" #~ "%s\n" #~ "%d unread item" #~ msgid_plural "" #~ "%s\n" #~ "%d unread items" #~ msgstr[0] "" #~ "%s\n" #~ "%d непрочетен запис" #~ msgstr[1] "" #~ "%s\n" #~ "%d непрочетени записа" #~ msgid "" #~ "%s\n" #~ "No unread items" #~ msgstr "" #~ "%s\n" #~ "Няма непрочетени записи" #~ msgid "Invalid Atom feed: unknown author" #~ msgstr "Невалидна Atom емисия: неизвестен автор" #, fuzzy #~ msgid "This feed does not exist anymore!" #~ msgstr "Този запис не съдържа връзка!" #, fuzzy #~ msgid "%s has %d update" #~ msgid_plural "%s has %d updates" #~ msgstr[0] "%d записа \n" #~ msgstr[1] "%d записа \n" #, fuzzy #~ msgid "_Enforce popup notification for this subscription." #~ msgstr "Отваряне на прозореца за предпочитанията за избраната емисия." #~ msgid "Show a _popup window with new headlines." #~ msgstr "Показване на _изскачащ прозорец с новите заглавия." #~ msgid "Show a status _icon in the notification area (system tray)." #~ msgstr "Показване на _икона в зоната за уведомяване." #, fuzzy #~ msgid "Launch Item In _Tab" #~ msgstr "/Стартиране на записа в _таб" #, fuzzy #~ msgid "_Launch Item In Browser" #~ msgstr "/Стартиране на записа в браузър" #, fuzzy #~ msgid "Copy Item _URL to Clipboard" #~ msgstr "/Копиране на _адреса на записа в буфера за обмен" #~ msgid "comments" #~ msgstr "коментари" #, fuzzy #~ msgid "Enclosure download FAILED: \"%s\"" #~ msgstr "Свалянето на прикачения файл приключи: „%s“" #~ msgid "Enclosure download finished: \"%s\"" #~ msgstr "Свалянето на прикачения файл приключи: „%s“" #, fuzzy #~ msgid "Download FAILED: \"%s\"" #~ msgstr "Свалянето на прикачения файл приключи: „%s“" #, fuzzy #~ msgid "Download finished." #~ msgstr "_Чрез програмата" #~ msgid "Choose download directory" #~ msgstr "Избор на папка за запазване на изтеглянията" #~ msgid "" #~ "_Manual:\n" #~ "(%s for URL)" #~ msgstr "" #~ "Ръчно зададено:\n" #~ "(%s за URL)" #~ msgid "_Save downloads in" #~ msgstr "_Запазване на изтеглянията в" #~ msgid "Downloading Enclosure" #~ msgstr "Изтегля се приложения" #~ msgid "" #~ "Jumps to the next unread item. If necessary selects the next feed with " #~ "unread items." #~ msgstr "" #~ "Прескачане до следващия непрочетен запис. Ако е необходимо избира " #~ "следващата емисия с непрочетени записи." #, fuzzy #~ msgid "Print debugging messages for the plugin loading" #~ msgstr "" #~ " --debug-cache Отпечатване на съобщенията за откриване на грешки при " #~ "обработката на кеша" #~ msgid "Update status" #~ msgstr "Състояние на обновяването" #~ msgid "was updated" #~ msgstr "е обновена" #~ msgid "was not updated" #~ msgstr "не е обновена" #~ msgid "The orientation of the tray." #~ msgstr "Ориентацията на тавата." #~ msgid "%s" #~ msgstr "%s" #~ msgid "topics_en.html" #~ msgstr "topics_en.html" #~ msgid "reference_en.html" #~ msgstr "reference_en.html" #~ msgid "faq_en.html" #~ msgstr "faq_en.html" #, fuzzy #~ msgid "Search With ..." #~ msgstr "Търсене с _Feedster..." #, fuzzy #~ msgid "%d Search Result for \"%s\"" #~ msgid_plural "%d Search Results for \"%s\"" #~ msgstr[0] "Търсене за „%s“" #~ msgstr[1] "Търсене за „%s“" #, fuzzy #~ msgid "" #~ "The item list now contains all items matching the specified search " #~ "pattern. If you want to save this search result permanently you can click " #~ "the \"Search Folder\" button in the search dialog and Liferea will add a " #~ "search folder to your feed list." #~ msgstr "" #~ "%s

%d намерени резултата за „%s“

Списъкът в момента съдържа " #~ "всички съвпадения с указания шаблон за търсене. Ако искате да запазите за " #~ "постоянно този резултат, можете да натиснете бутона за виртуална папка в " #~ "прозореца за търсене и Liferea ще добави виртуална папка с резултатите в " #~ "списъка с емисии." #, fuzzy #~ msgid "Count" #~ msgstr "Относно програмата" #~ msgid "You have to select a feed entry" #~ msgstr "Трябва да изберете запис от някоя емисия" #~ msgid "(empty)" #~ msgstr "(празен)" #~ msgid "_Properties..." #~ msgstr "_Настройки..." #, fuzzy #~ msgid "Update out-dated feeds" #~ msgstr "Актуализиране на всички емисии" #, fuzzy #~ msgid "Force update of all feeds" #~ msgstr "Актуализиране на всички емисии" #, fuzzy #~ msgid "startup" #~ msgstr "При _стартиране:" #, fuzzy #~ msgid "feed updated" #~ msgstr "е обновена" #, fuzzy #~ msgid "feed added" #~ msgstr "Временни файлове на емисията" #, fuzzy #~ msgid "item selected" #~ msgstr "Премахване на _избраните" #, fuzzy #~ msgid "feed selected" #~ msgstr "Премахване на _избраните" #, fuzzy #~ msgid "item unselected" #~ msgstr "Няма избран запис" #, fuzzy #~ msgid "feed unselected" #~ msgstr "Премахване на _избраните" #, fuzzy #~ msgid "No script selected!" #~ msgstr "Няма избран запис" #, fuzzy #~ msgid "Create a new search feed." #~ msgstr "Създаване на поле за търсене от типа Feedster." #~ msgid "Liferea is unable to display this item's content." #~ msgstr "Liferea не може да покаже съдържанието на този запис." #~ msgid "

View this item's content.

" #~ msgstr "

Преглед на съдържанието на този запис.

" #~ msgid "feedlist.opml" #~ msgstr "feedlist_bg.opml" #~ msgid " " #~ msgstr " " #, fuzzy #~ msgid " " #~ msgstr " " #, fuzzy #~ msgid " " #~ msgstr " " #~ msgid "Feed Source" #~ msgstr "Източник на данни" #, fuzzy #~ msgid "Hook" #~ msgstr "%s" #, fuzzy #~ msgid "Registered Scripts" #~ msgstr "Източник на данни" #~ msgid "text/plain" #~ msgstr "text/plain" #~ msgid "" #~ "This option can cause significant delays when loading folders " #~ "containing many feeds." #~ msgstr "" #~ "Това може да причини сериозни забавяния при зареждане на папки, " #~ "съдържащи много емисии." #~ msgid "Downloading Enclosures" #~ msgstr "Изтегляния" #~ msgid "Feed Cache Handling" #~ msgstr "Временни файлове" #~ msgid "Feed Name" #~ msgstr "Име на емисията" #~ msgid "Feed Update Settings" #~ msgstr "Актуализиране на емисиите" #~ msgid "HTTP Proxy Server" #~ msgstr "HTTP сървър-посредник" #~ msgid "Opening Enclosures" #~ msgstr "Отваряне" #~ msgid "Reading Headlines" #~ msgstr "Четене" #, fuzzy #~ msgid "Toolbar Settings" #~ msgstr "Изглед на менюто" #~ msgid "Update Interval" #~ msgstr "Интервал на актуализиране" #, fuzzy #~ msgid "Web Integration" #~ msgstr "Интервал на актуализиране" #~ msgid "At _startup:" #~ msgstr "При _стартиране:" #, fuzzy #~ msgid "Attention Profile" #~ msgstr "Идентифицирането е неуспешно" #, fuzzy #~ msgid "Create new script" #~ msgstr "Добавяне на нова емисия." #, fuzzy #~ msgid "Exec Command" #~ msgstr "_Команда" #~ msgid "http://lzone.de/liferea/" #~ msgstr "http://lzone.de/liferea/" #~ msgid "" #~ "Unexpected end of character sequence or corrupt UTF-8 encoding! Some " #~ "characters were dropped!" #~ msgstr "" #~ "Неочакван край на последователността на знаците или повредено UTF-8 " #~ "кодиране! Някои знаци са пропуснати!" #~ msgid "" #~ "Error while reading cache file \"%s\" ! Cache file could not be loaded!" #~ msgstr "" #~ "Грешка при четенето на временния файл „%s“! Временният файл не може да " #~ "бъде зареден!" #~ msgid "" #~ "

XML error while parsing cache file! Feed cache file \"%s\" could not " #~ "be loaded!

" #~ msgstr "" #~ "

Грешка в XML по време на четенето на временния файл „%s“! Временният " #~ "файл не може да бъде зареден!

" #~ msgid "

\"%s\" is no valid cache file! Cannot read cache file!

" #~ msgstr "" #~ "

„%s“ не е валиден временен файл. Временният файл не може да бъде " #~ "прочетен!

" #~ msgid "There were errors while parsing cache file \"%s\"" #~ msgstr "Получиха се грешки при прегледа на временния файл „%s“" #~ msgid "Access Forbidden" #~ msgstr "Достъпът е забранен" #~ msgid "URL is invalid" #~ msgstr "URL-то е невалидно" #~ msgid "Unsupported network protocol" #~ msgstr "Неподдържан мрежови протокол" #~ msgid "Hostname could not be found" #~ msgstr "Хостът не може да бъде намерен" #~ msgid "Network connection was refused by the remote host" #~ msgstr "Връзката беше отказана от отдалечения хост" #~ msgid "Remote host did not finish sending data" #~ msgstr "Отдалеченият хост не приключи с изпращането на данните" #~ msgid "Too many HTTP redirects were encountered" #~ msgstr "Твърде много HTTP пренасочвания" #~ msgid "Remote host sent an invalid response" #~ msgstr "Отдалеченият хост изпрати невалиден отговор" #~ msgid "Webserver's authentication method incompatible with Liferea" #~ msgstr "Методът за идентификация пред уеб сървъра не е съвместим с Liferea" #~ msgid "Feed link auto discovery failed! No feed links found!" #~ msgstr "Неуспех при автоматичното откриване - не са открити емисии!" #~ msgid "single item view" #~ msgstr "режим за преглеждане на един запис" #~ msgid "label133" #~ msgstr "етикет133" #~ msgid "_Limit cache to" #~ msgstr "_Ограничаване на временните файлове до" #~ msgid "items." #~ msgstr "записа." #~ msgid "Feed Loading Settings" #~ msgstr "" #~ "Оптимизиране на зареждането на емисиите" #~ msgid "Optimize for reduced _memory usage." #~ msgstr "По-малка употреба на пам_ет." #~ msgid "Optimize for _speed." #~ msgstr "По-висока _скорост." #~ msgid "Date Column Settings" #~ msgstr "Изглед на колоната с датата" #~ msgid "Display only _time" #~ msgstr "_Час" #~ msgid "Display _date and time" #~ msgstr "Час и _дата" #~ msgid "_User defined format:" #~ msgstr "Формат, избран от _Вас:" #~ msgid "" #~ "for expert users: specify a time format string, consult the strftime() " #~ "manpage for the format codes" #~ msgstr "" #~ "за напреднали потребители: указване на низ за форматиране на датата. За " #~ "подробности - погледнете страницата от ръководството за функцията " #~ "strftime()" #~ msgid " " #~ msgstr " " #~ msgid "Lower Right" #~ msgstr "Отдолу, вдясно" #~ msgid "Upper Right" #~ msgstr "Отгоре, вдясно" #~ msgid "Upper Left" #~ msgstr "Отгоре, вляво" #~ msgid "Lower Left" #~ msgstr "Отгоре, вляво" #~ msgid "Popup Placement" #~ msgstr "Разположение на изскачащите прозорци" #~ msgid "Show Menu _And Toolbar" #~ msgstr "Менюто и _лентата с инструментите" #~ msgid "Show _Menu Only" #~ msgstr "Само _менюто" #~ msgid "Show _Toolbar Only" #~ msgstr "Само _лентата с инструментите" #~ msgid "" #~ "Liferea reuses the GNOME proxy settings. If you use GNOME you can " #~ "change these settings in the GNOME Control Center." #~ msgstr "" #~ "Liferea използва настройките за сървъра-посредник към GNOME. Ако " #~ "използвате GNOME можете да промените тези настройки в Контролния център " #~ "на GNOME." #~ msgid "_Enable Proxy" #~ msgstr "_Включване на сървър-посредник" #~ msgid "Liferea Homepage" #~ msgstr "" #~ "Страница на Liferea" #~ msgid "" #~ "Copyright (c) 2003-2006\n" #~ "Lars Lindner and \n" #~ "Nathan J. Conrad \n" #~ msgstr "" #~ "Авторски права (c) 2003-2006\n" #~ "Lars Lindner и \n" #~ "Nathan J. Conrad \n" #, fuzzy #~ msgid "" #~ "Code, Patches, Debugging\n" #~ "\n" #~ "James Doherty\n" #~ "Jeremy Messenger\n" #~ "John McKnight\n" #~ "Tomasz Maka\n" #~ "Karl Soderstrom\n" #~ "Christophe Barbe\n" #~ "Juho Snellman\n" #~ "Roshan Revankar\n" #~ "Oliver Feiler\n" #~ "Niklas Morberg\n" #~ "Johannes Schlueter\n" #~ "Pierre Phaneuf\n" #~ "ahmed el-helw\n" #~ "James Bowes\n" #~ "Marc Deslauriers\n" #~ "Amit D. Chaudhary\n" #~ "Christoph Hohmann\n" #~ "Raphael Slinckx\n" #~ "Bjorn Monnens\n" #~ "Aristotle Pagaltzis\n" #~ "Norman Jonas\n" #~ "and many more...\n" #~ "\n" #~ "Code from other projects\n" #~ "\n" #~ "Anders Carlsson (tray icon support)\n" #~ "Philippe Martin, Brion Vibber (favicon support)\n" #~ "Jonathan Blandford (GtkTreeModelFilter)\n" #~ "Kristian Rietveld (GtkTreeModelFilter)\n" #~ "\n" #~ "Included Software\n" #~ "\n" #~ "Liferea uses the XSPF Web Music Player to \n" #~ "allow direct podcast playback. This player was \n" #~ "written by Fabricio Zuardi and can be found \n" #~ "at http://musicplayer.sourceforge.net" #~ msgstr "" #~ "Код, поправки, изчистване на грешки\n" #~ "\n" #~ "James Doherty <...>\n" #~ "Jeremy Messenger \n" #~ "John McKnight <...>\n" #~ "Tomasz Maka \n" #~ "Karl Soderstrom \t\n" #~ "Christophe Barbe \n" #~ "Juho Snellman \n" #~ "Roshan Revankar \n" #~ "Oliver Feiler \n" #~ "Niklas Morberg \n" #~ "Johannes Schlueter \n" #~ "Pierre Phaneuf \n" #~ "ahmed el-helw \n" #~ "James Bowes \n" #~ "Marc Deslauriers\n" #~ "Amit D. Chaudhary \n" #~ "Christoph Hohmann \n" #~ "Raphael Slinckx \n" #~ "and many more...\n" #~ "\n" #~ "Код от други проекти\n" #~ "\n" #~ "Anders Carlsson (tray icon support)\n" #~ "Philippe Martin, Brion Vibber (favicon support)\n" #~ "Jonathan Blandford (GtkTreeModelFilter)\n" #~ "Kristian Rietveld (GtkTreeModelFilter)" #~ msgid "Contributors" #~ msgstr "Сътрудници" #~ msgid "Translation" #~ msgstr "Превод" #~ msgid "Rule" #~ msgstr "Правило" #, fuzzy #~ msgid "" #~ "Note: Items are added to the search folder if at least one additive rule\n" #~ "matches. They are removed if at least one removing rule matches." #~ msgstr "" #~ "Бележка: Записи се добавят във виртуалната папка, ако съвпадат поне с " #~ "едно правило за добавяне. Ако поне един правило за премахване съвпада, те " #~ "се извеждат от нея." #~ msgid "Saves this search as a VFolder, which will appear in the feed list." #~ msgstr "" #~ "Запазване на резултатите от това търсене като виртуална папка, която ще " #~ "се появи в списъка с емисии." #~ msgid "VFolder" #~ msgstr "Виртуална папка" #~ msgid " --help Print this help and exit" #~ msgstr " --help Показване на това съобщение и напускане" #~ msgid " --mainwindow-state=STATE" #~ msgstr " --mainwindow-state=СЪСТОЯНИЕ" #~ msgid " Start Liferea with its main window in STATE." #~ msgstr "" #~ " Стартиране на Liferea с основен прозорец в СЪСТОЯНИЕ-" #~ "то." #~ msgid " --debug-plugins Print debugging messages when loading plugins" #~ msgstr "" #~ " --debug-plugins Отпечатване на съобщенията за откриване на грешки при " #~ "зареждане на приставките" #~ msgid "The --mainwindow-state argument must be given a parameter.\n" #~ msgstr "Опцията --mainwindow-state изисква аргумент.\n" #~ msgid "The --session argument must be given a parameter.\n" #~ msgstr "Опцията --session изисква аргумент.\n" #~ msgid "Liferea encountered an unknown argument: %s\n" #~ msgstr "Liferea се сблъска с непознат аргумент %s\n" #~ msgid "" #~ "Another copy of Liferea was found to be running. Please use it instead. " #~ "If there is no other copy of Liferea running, please delete the \"~/." #~ "liferea/lock\" lock file." #~ msgstr "" #~ "Засечено е друго работещо копие на Liferea. Използвайте него вместо това. " #~ "Ако нямате друго копие на Liferea, изтрийте файла „~/.liferea/lock“." #~ msgid "Modules not supported! (%s)" #~ msgstr "Модулите не се поддържат! (%s)" #~ msgid "Scanning for plugins (%s):" #~ msgstr "Търсене на приставки (%s):" #~ msgid "does match" #~ msgstr "съвпада" #~ msgid "does not match" #~ msgstr "не съвпада" #~ msgid "Could not download \"%s\". Will retry in %d seconds." #~ msgstr "„%s“ не може да бъде свален. След %d секунди ще се опита наново." #~ msgid "Note: Using the subscriptions filter disables drag & drop" #~ msgstr "" #~ "Бележка: Употребата на филтър на емисиите спира поддръжката на изтегляне " #~ "и пускане" #, fuzzy #~ msgid "" #~ "Sorry, I was not able to load any installed browser plugin! Try the --" #~ "debug-plugins option to get debug information!" #~ msgstr "" #~ "За съжаление никакви модули за инсталирани браузъри не можаха да бъдат " #~ "заредени! Използвайте опцията --debug-all, за да получите информация за " #~ "изчистване на грешки!" #, fuzzy #~ msgid "" #~ "
This is an unstable version of Liferea 1.1. It " #~ "should not be used for production yet! If you want to use Liferea " #~ "regularily please download the last stable version from SourceForge!" #~ msgstr "" #~ "
Това е много нестабилна версия на Liferea 1.1. " #~ "Кодът може да е в неизползваемо състояние. Да се използва от " #~ "разработчици! Ако искате да ползвате Liferea за ежедневна работа - " #~ "изтеглете последната стабилна версия от сайта на SourceForge!
" #~ msgid "_Program" #~ msgstr "_Програма" #~ msgid "Updates all subscriptions. This does not update OCS directories." #~ msgstr "" #~ "Актуализиране на всички емисии. OCS директориите не се осъвременяват." #~ msgid "_Search" #~ msgstr "_Търсене" #~ msgid "Update _Selected" #~ msgstr "Актуализиране на _избраната" #~ msgid "_Delete Selected" #~ msgstr "_Изтриване на избраната емисия" #~ msgid "Toggles the item list mode between condensed and normal mode." #~ msgstr "Превключване между общ и нормален изглед за показване на записи." #~ msgid "/Toggle _Read Status" #~ msgstr "/Превключване на състояние: \"прочетено/непрочетено\"" #~ msgid "/Toggle Item _Flag" #~ msgstr "/Прикрепяне на _флаг към записа" #~ msgid "/_Next Unread Item" #~ msgstr "/Следващ непрочетен запис" #~ msgid "/_Increase Text Size" #~ msgstr "/_Увеличаване размера на текста" #~ msgid "/_Decrease Text Size" #~ msgstr "/_Намаляване размера на текста" #~ msgid "/Toggle _Online|Offline" #~ msgstr "/_Режим „изключен/включен“" #~ msgid "/_Preferences..." #~ msgstr "/_Настройки..." #~ msgid "/_Show Window" #~ msgstr "/_Показване на прозорец" #~ msgid "/_Quit" #~ msgstr "/_Спиране на програмата" #~ msgid "/_New/New _Subscription..." #~ msgstr "/_Нов/Нова _емисия..." #, fuzzy #~ msgid "/_New/New _Folder..." #~ msgstr "/_Нов/Нова _виртуална папка..." #, fuzzy #~ msgid "/_New/New S_earch Folder..." #~ msgstr "/_Нов/Нова _виртуална папка..." #, fuzzy #~ msgid "/_New/New S_ource..." #~ msgstr "/_Нов/_Нова папка..." #~ msgid "/_Rename Folder..." #~ msgstr "/П_реименуване на папка..." #~ msgid "/_Delete Folder" #~ msgstr "/_Изтриване на папка" #~ msgid "/_Properties..." #~ msgstr "/_Настройки..." #~ msgid "Update only feeds scheduled for updates" #~ msgstr "Осъвременяване само на емисии, разграфени за осъвременяване" #~ msgid "Reset feed update timers (Update no feeds)" #~ msgstr "Изчистване на хронометрите за осъвременяване на емисиите" #~ msgid "This item's content is invalid." #~ msgstr "Съдържанието на този запис не е валидно." #~ msgid "Cookie for %s has expired!" #~ msgstr "Бисквитката за %s е изтекла!" #~ msgid "Liferea notification" #~ msgstr "Уведомяване" #, fuzzy #~ msgid "" #~ "
You may want to validate the feed using Може да валидирате емисията чрез FeedValidator." #~ msgstr "\">валидатора на емисии." #~ msgid "user defined command" #~ msgstr "потребителска команда" #~ msgid "Item:" #~ msgstr "Запис:" #~ msgid "date" #~ msgstr "дата" #~ msgid "feed generator" #~ msgstr "генератор" #~ msgid "contributors" #~ msgstr "сътрудници" #~ msgid "language" #~ msgstr "език" #~ msgid "feed published on" #~ msgstr "емисия публикувана на" #~ msgid "content last updated" #~ msgstr "съдържанието е осъвременено за последно" #~ msgid "managing editor" #~ msgstr "отговорен редактор" #~ msgid "webmaster" #~ msgstr "отговорник за уеб сървъра" #~ msgid "report errors to" #~ msgstr "докладва за грешка на" #~ msgid "original source" #~ msgstr "оригинален източник" #~ msgid "original time" #~ msgstr "оригинално време" #~ msgid "license" #~ msgstr "лиценз" #~ msgid "type" #~ msgstr "тип" #~ msgid "format" #~ msgstr "формат" #~ msgid "identifier" #~ msgstr "идентификатор" #~ msgid "source" #~ msgstr "изходен код" #~ msgid "coverage" #~ msgstr "покритие" #~ msgid "vfolder" #~ msgstr "виртуална папка" #~ msgid "VFolder:" #~ msgstr "Виртуална папка:" #~ msgid "%d items \n" #~ msgstr "%d записа \n" #, fuzzy #~ msgid "" #~ "" #~ msgstr "" #~ "" #~ msgid "New _VFolder..." #~ msgstr "Нова _виртуална папка..." #~ msgid "Adds a new plugin instance to the feed list." #~ msgstr "Добавяне на нова приставка към списъка с емисии." #, fuzzy #~ msgid "%s - %d unread item" #~ msgid_plural "%s - %d unread items" #~ msgstr[0] "" #~ "%s\n" #~ "%d непрочетен запис" #~ msgstr[1] "" #~ "%s\n" #~ "%d непрочетени записа" #~ msgid "/_New/New F_older..." #~ msgstr "/_Нов/_Нова папка..." #~ msgid "New VFolder" #~ msgstr "Нова виртуална папка" #~ msgid "

Could not find Atom 1.0 header!

" #~ msgstr "

Не може да намери началото на Atom·1.0!

" #~ msgid "internal OCS namespace parsing error!" #~ msgstr "вътрешна грешка в анализатора на пространството от имена OCS!" #~ msgid "no namespace handler for <%s:%s>!\n" #~ msgstr "" #~ "липсва програма за обработка на пространството от имена за <%s:%s>!\n" #~ msgid "

Could not find RDF header!

" #~ msgstr "

Не може да намери началото на RDF!

" #~ msgid "

Could not find OPML header!

" #~ msgstr "Не може да бъде намерена началото на OPML!" #~ msgid "

Could not find Atom/Echo/PIE header!

" #~ msgstr "

Не може да бъде намерено началото на OPML!

" #~ msgid "

Could not find RDF/RSS header!

" #~ msgstr "

Не може да бъде намерена заглавната част на RDF/RSS!

" #~ msgid "" #~ "The included HTML tag is not supported with GtkHTML2.\n" #~ "Included Plugins might not be displayed." #~ msgstr "" #~ "Етикетът за елемент в HTML - не се поддържа от GtkHTML2.\n" #~ "Включените обекти няма да бъдат показани." #~ msgid "" #~ "\n" #~ "Trying to load the Mozilla browser module... Note that this\n" #~ "might not work with every Mozilla version. If you have problems\n" #~ "and Liferea does not start, try to set MOZILLA_FIVE_HOME to\n" #~ "another Mozilla installation or delete the gconf configuration\n" #~ "key /apps/liferea/browser-module!\n" #~ "\n" #~ msgstr "" #~ "\n" #~ "Програмата опитва да зареди приставката за браузъра Mozilla... Възможно " #~ "е\n" #~ "това да не работи с всяка версия на Mozilla. Ако имате проблеми\n" #~ "и Liferea не се стартира, опитайте да зададете MOZILLA_FIVE_HOME към\n" #~ "друга инсталация на Mozilla или изтрийте ключа за настройки в gconf\n" #~ "key·/apps/liferea/browser-module!\n" #~ "\n" #~ msgid "" #~ "Failed to open HTML widget module (%s) specified in configuration!\n" #~ "%s\n" #~ msgstr "" #~ "Неуспех при отварянето на модула за графичен обект за HTML (%s), който е " #~ "указан в настройките!\n" #~ "%s\n" #~ msgid "Htmlview API mismatch!" #~ msgstr "Несъвпадение на Htmlview API!" #~ msgid "Detected module is not a valid htmlview module!" #~ msgstr "Засеченият модул е невалиден модул за htmlview!" #~ msgid "Available browser modules (%s):\n" #~ msgstr "Налични модули за браузъра %s:\n" #~ msgid "Loading configured browser module (%s)!\n" #~ msgstr "Зареждане на настроен модул за браузъра %s!\n" #~ msgid "No browser module configured!\n" #~ msgstr "Няма настроен модул за браузъра!\n" #~ msgid "trying to load browser module %s (%s)\n" #~ msgstr "опит за зареждане на модула за браузъра %s (%s)\n" #~ msgid "internal error! time conversion error! mktime failed!\n" #~ msgstr "" #~ "вътрешна грешка! грешка при преобразуването на датата! mktime е " #~ "неуспешна!\n" #~ msgid "Invalid ISO8601 date format! Ignoring information!\n" #~ msgstr "" #~ "Датата не е във формат ISO8601! Игнориране на информацията !\n" #~ msgid "" #~ "

Could not determine the feed type. Please check that it is a valid type and listed in the supported formats." #~ "

" #~ msgstr "" #~ "

Типът на емисията не може да бъде определен. Проверете дали той е валиден и поддържан формат.

" #~ msgid "Please do a search first!" #~ msgstr "Първо направете търсене!" #~ msgid "You must select a feed entry" #~ msgstr "Трябва да изберете запис" #~ msgid "You must select a feed entry." #~ msgstr "Трябва да изберете запис." #~ msgid "A folder must be selected." #~ msgstr "Трябва да изберете папка." #~ msgid "" #~ "internal error! unknown entry type! cannot display appropriate icon!\n" #~ msgstr "" #~ "вътрешна грешка! непознат тип запис! не може да бъде изобразена подходяща " #~ "икона!\n" #~ msgid "Cannot load HTML widget module (%s)!" #~ msgstr "Неуспех при зареждането на модула за графичния обект за HTML (%s)!" #~ msgid "New Feed" #~ msgstr "Нова емисия" #~ msgid "Next Unread" #~ msgstr "Следващ непрочетен" #~ msgid "Update All" #~ msgstr "Осъвременяване на всички" #~ msgid "Viewing Mode" #~ msgstr "Режим на изгледа" #~ msgid "Switches between 2 and 3 pane mode." #~ msgstr "Превключване между 2 и 3 панела." #~ msgid "URL" #~ msgstr "URL" #~ msgid "" #~ "%s\n" #~ "%d unread item%s\n" #~ "%d unread items" #~ msgstr "" #~ "%s\n" #~ "%d непрочетен запис%s\n" #~ "%d непрочетени записа" liferea-1.13.7/po/ca.po000066400000000000000000002450171415350204600145640ustar00rootroot00000000000000# Catalan translation of Liferea. # Copyright © 2005, 2006 Free Software Foundation, Inc. # This file is distributed under the same license as the liferea package. # Jordi Mallach , 2005. # Gil Forcada , 2007, 2009, 2012, 2013. # msgid "" msgstr "" "Project-Id-Version: liferea 1.10-rc1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-07-30 12:36+0200\n" "PO-Revision-Date: 2013-05-21 12:31+0200\n" "Last-Translator: Gil Forcada \n" "Language-Team: Catalan \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" #: ../net.sourceforge.liferea.appdata.xml.in.h:1 #, fuzzy msgid "RSS feed reader" msgstr "Lector de canals de notícies" #: ../net.sourceforge.liferea.appdata.xml.in.h:2 msgid "" "Liferea is an abbreviation for Linux Feed Reader. It is a news aggregator " "for online news feeds. It supports a number of different feed formats " "including RSS/RDF, CDF and Atom. There are many other news readers " "available, but these others are not available for Linux or require many " "extra libraries to be installed. Liferea tries to fill this gap by creating " "a fast, easy to use, easy to install news aggregator for GTK/GNOME." msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:3 msgid "Distinguishing features:" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:4 #, fuzzy msgid "Read articles when offline" msgstr "El Liferea és fora de línia" #: ../net.sourceforge.liferea.appdata.xml.in.h:5 #, fuzzy msgid "Synchronizes with TheOldReader" msgstr "Sincronització amb els equips propers" #: ../net.sourceforge.liferea.appdata.xml.in.h:6 #, fuzzy msgid "Synchronizes with TinyTinyRSS" msgstr "Sincronització amb els equips propers" #: ../net.sourceforge.liferea.appdata.xml.in.h:7 #, fuzzy msgid "Synchronizes with InoReader" msgstr "Sincronització amb els equips propers" #: ../net.sourceforge.liferea.appdata.xml.in.h:8 #, fuzzy msgid "Synchronizes with Reedah" msgstr "Sincronització amb els equips propers" #: ../net.sourceforge.liferea.appdata.xml.in.h:9 msgid "Permanently save headlines in news bins" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:10 msgid "Match items using search folders" msgstr "" #: ../net.sourceforge.liferea.appdata.xml.in.h:11 #, fuzzy msgid "Play Podcasts" msgstr "Podcast" #: ../net.sourceforge.liferea.desktop.in.h:1 ../src/liferea_application.c:350 #: ../glade/mainwindow.ui.h:1 msgid "Liferea" msgstr "Liferea" #: ../net.sourceforge.liferea.desktop.in.h:2 msgid "Feed Reader" msgstr "Lector de canals de notícies" #: ../net.sourceforge.liferea.desktop.in.h:3 msgid "Liferea Feed Reader" msgstr "Lector de canals de notícies Liferea" #: ../net.sourceforge.liferea.desktop.in.h:4 msgid "Read news feeds and blogs" msgstr "Llegiu canals de notícies i blocs" #: ../net.sourceforge.liferea.desktop.in.h:5 msgid "news;feed;aggregator;blog;podcast;syndication;rss;atom" msgstr "" #: ../plugins/getfocus.py:93 msgid "Opacity:" msgstr "" #: ../plugins/getfocus.py:94 msgid "Opacity" msgstr "" #: ../plugins/getfocus.py:104 msgid "Min" msgstr "" #: ../plugins/getfocus.py:109 msgid "Max" msgstr "" #: ../plugins/getfocus.py:115 msgid "Save" msgstr "" #: ../plugins/headerbar.py:67 ../glade/liferea_menu.ui.h:20 #: ../glade/liferea_toolbar.ui.h:5 msgid "Previous Item" msgstr "Element anterior" #: ../plugins/headerbar.py:73 ../glade/liferea_menu.ui.h:21 #: ../glade/liferea_toolbar.ui.h:6 msgid "Next Item" msgstr "Element següent" #: ../plugins/headerbar.py:81 ../glade/liferea_menu.ui.h:19 #: ../glade/liferea_toolbar.ui.h:7 msgid "_Next Unread Item" msgstr "Element sense llegir _següent" #: ../plugins/headerbar.py:89 ../glade/liferea_menu.ui.h:14 #: ../glade/liferea_toolbar.ui.h:3 msgid "_Mark Items Read" msgstr "_Marca'ls tots com a llegits" #: ../plugins/headerbar.py:113 ../glade/liferea_menu.ui.h:40 #: ../glade/liferea_toolbar.ui.h:10 msgid "Search All Feeds..." msgstr "Cerca a tots els canals..." #: ../plugins/libnotify.py:42 #, fuzzy msgid "Feed Updates" msgstr "Actualització de canal" #: ../plugins/plugin-installer.py:54 msgid "Plugins" msgstr "Connectors" #: ../plugins/plugin-installer.py:69 #, fuzzy msgid "Plugin Installer" msgstr "Connectors" #: ../plugins/plugin-installer.py:83 #, fuzzy msgid "Activate Plugins" msgstr "Connectors" #: ../plugins/plugin-installer.py:84 #, fuzzy msgid "Download Plugins" msgstr "_Baixa-les utilitzant" #: ../plugins/plugin-installer.py:102 #, python-format msgid "Bad fields for plugin entry %s" msgstr "" #: ../plugins/plugin-installer.py:125 msgid "All" msgstr "" #: ../plugins/plugin-installer.py:125 ../glade/properties.ui.h:39 msgid "Advanced" msgstr "Avançat" #: ../plugins/plugin-installer.py:125 msgid "Menu" msgstr "" #: ../plugins/plugin-installer.py:125 #, fuzzy msgid "Notifications" msgstr "Paràmetres de notificació" #: ../plugins/plugin-installer.py:133 msgid "Filter by category" msgstr "" #: ../plugins/plugin-installer.py:140 msgid "_Install" msgstr "" #: ../plugins/plugin-installer.py:144 msgid "_Uninstall" msgstr "" #: ../plugins/plugin-installer.py:245 #, python-format msgid "" "Missing package manager '%s'. Cannot check nor install necessary " "dependencies!" msgstr "" #: ../plugins/plugin-installer.py:261 #, python-format msgid "Missing package '%s'. Do you want to install it? (Will run '%s')" msgstr "" #: ../plugins/plugin-installer.py:268 #, python-format msgid "" "Package installation failed (%s)! Check console output for further problem " "details!" msgstr "" #: ../plugins/plugin-installer.py:271 #, python-format msgid "Failed to check plugin dependencies (%s)!" msgstr "" #: ../plugins/plugin-installer.py:280 msgid "Command \"git\" not found, please install it!" msgstr "" #: ../plugins/plugin-installer.py:289 #, python-format msgid "Copying %s to %s" msgstr "" #: ../plugins/plugin-installer.py:292 #, python-format msgid "Failed to copy plugin directory (%s)!" msgstr "" #: ../plugins/plugin-installer.py:301 #, python-format msgid "Failed to copy plugin .py file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:311 #, python-format msgid "Failed to copy .plugin file (%s)!" msgstr "" #: ../plugins/plugin-installer.py:322 #, fuzzy, python-format msgid "Creating schema directory %s" msgstr "No s'ha pogut crear el directori de memòria cau «%s»" #: ../plugins/plugin-installer.py:324 #, python-format msgid "Installing schema %s" msgstr "" #: ../plugins/plugin-installer.py:328 msgid "Compiling schemas..." msgstr "" #: ../plugins/plugin-installer.py:333 #, python-format msgid "Failed to install schema files (%s)!" msgstr "" #: ../plugins/plugin-installer.py:343 #, python-format msgid "Failed to enable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:349 #, python-format msgid "Plugin '%s' is now installed. Ensure to restart Liferea!" msgstr "" #: ../plugins/plugin-installer.py:363 #, python-format msgid "Failed to disable plugin (%s)!" msgstr "" #: ../plugins/plugin-installer.py:370 ../plugins/plugin-installer.py:390 #, fuzzy, python-format msgid "Deleting '%s'" msgstr "Supressió de l'entrada" #: ../plugins/plugin-installer.py:373 #, python-format msgid "Failed to remove directory '%s' (%s)!" msgstr "" #: ../plugins/plugin-installer.py:383 msgid "Failed to remove .py file!" msgstr "" #: ../plugins/plugin-installer.py:393 msgid "Failed to remove .plugin file!" msgstr "" #: ../plugins/plugin-installer.py:402 msgid "Sorry! Plugin removal failed!." msgstr "" #: ../plugins/plugin-installer.py:404 msgid "" "Plugin was removed. Please restart Liferea once for it to take full effect!." msgstr "" #: ../plugins/trayicon.py:132 #, fuzzy msgid "Show / Hide" msgstr "Mostra els detalls" #: ../plugins/trayicon.py:133 msgid "Minimize to tray on close" msgstr "" #: ../plugins/trayicon.py:134 #, fuzzy msgid "Quit" msgstr "_Surt" #: ../src/browser.c:81 ../src/browser.c:98 #, c-format msgid "Browser command failed: %s" msgstr "Ha fallat l'ordre del navegador: %s" #: ../src/browser.c:101 ../src/ui/liferea_shell.c:1047 #, c-format msgid "Starting: \"%s\"" msgstr "S'està iniciant: «%s»" #. unauthorized #: ../src/comments.c:118 msgid "Authorization Error" msgstr "Error d'autenticació" #: ../src/common.c:66 #, c-format msgid "Cannot create cache directory \"%s\"!" msgstr "No s'ha pogut crear el directori de memòria cau «%s»" #: ../src/conf.c:182 msgid "" "Your version of WebKitGTK+ doesn't support changing the proxy settings from " "Liferea. The system's default proxy settings will be used." msgstr "" #. translation hint: date format for today, reorder format codes as necessary #: ../src/date.c:133 msgid "Today %l:%M %p" msgstr "Avui a les %H:%M" #. translation hint: date format for yesterday, reorder format codes as necessary #: ../src/date.c:142 msgid "Yesterday %l:%M %p" msgstr "Ahir a les %H:%M" #. translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary #: ../src/date.c:154 msgid "%a %l:%M %p" msgstr "%a %H:%M" #. translation hint: date format for dates older than a week but from this year, reorder format codes as necessary #: ../src/date.c:162 msgid "%b %d %l:%M %p" msgstr "%b %d %H:%M" #. translation hint: date format for dates from the last years, reorder format codes as necessary #: ../src/date.c:165 msgid "%b %d %Y" msgstr "%d %b %Y" #: ../src/enclosure.c:201 #, c-format msgid "\"%s\" is not a valid enclosure type config file!" msgstr "«%s» no és un un fitxer de configuració de tipus d'adjunció vàlid" #: ../src/enclosure.c:292 #, fuzzy msgid "" "You have not configured a download tool yet! Please do so in the " "'Enclosures' tab in Tools/Preferences." msgstr "" "Encara no heu configurat cap eina de baixada. Ho podeu fer des de la " "pestanya «Baixada» del diàleg que trobareu a Eines/Preferències." #: ../src/enclosure.c:311 #, fuzzy, c-format msgid "" "Command failed: \n" "\n" "%s\n" "\n" " Please check whether the configured download tool is installed and working " "correctly! You can change it in the 'Download' tab in Tools/Preferences." msgstr "" "Ha fallat l'ordre:\n" "\n" "%s\n" "\n" "Comproveu que l'eina de baixada està instal·lada i funciona correctament. La " "podeu canviar des de la pestanya «Baixada» del diàleg que trobareu a Eines/" "Preferències." #: ../src/export.c:187 #, fuzzy, c-format msgid "Error renaming %s to %s: %s\n" msgstr "S'ha produït un error en canviar el nom de %s a %s\n" #: ../src/export.c:409 ../src/export.c:411 #, c-format msgid "XML error while reading OPML file! Could not import \"%s\"!" msgstr "" "S'ha detectat un error en la sintaxi XML en llegir el fitxer OPML. No s'ha " "pogut importar «%s»" #: ../src/export.c:417 ../src/export.c:419 #, c-format msgid "" "Empty document! OPML document \"%s\" should not be empty when importing." msgstr "" "El document és buit. El document OPML «%s» no hauria d'ésser buit en " "importar-lo." #: ../src/export.c:440 ../src/export.c:442 #, c-format msgid "\"%s\" is not a valid OPML document! Liferea cannot import this file!" msgstr "«%s» no és un document OPML vàlid. El Liferea no el pot importar." #: ../src/export.c:461 msgid "Imported feed list" msgstr "Llista de canals importats" #: ../src/export.c:473 msgid "Import Feed List" msgstr "Importa la llista de canals" #: ../src/export.c:473 msgid "Import" msgstr "Importa" #: ../src/export.c:473 ../src/export.c:490 ../src/fl_sources/opml_source.c:379 msgid "OPML Files" msgstr "Fitxers OPML" #: ../src/export.c:481 msgid "Error while exporting feed list!" msgstr "S'ha produït un error en exportar la llista de canals" #: ../src/export.c:483 msgid "Feed List exported!" msgstr "S'ha exportat la llista de canals" #: ../src/export.c:490 msgid "Export Feed List" msgstr "Exporta la llista de canals" #: ../src/export.c:490 msgid "Export" msgstr "Exporta" #: ../src/feed_parser.c:201 msgid "Empty document!" msgstr "El document és buit" #: ../src/feed_parser.c:210 msgid "Invalid XML!" msgstr "L'XML no és vàlid" #: ../src/fl_sources/default_source.c:139 ../glade/new_subscription.ui.h:1 #: ../glade/simple_subscription.ui.h:1 msgid "New Subscription" msgstr "Subscripció nova" #: ../src/fl_sources/google_source.c:111 msgid "Google Reader" msgstr "Google Reader" #: ../src/fl_sources/node_source.c:334 msgid "No feed list source types found!" msgstr "No s'ha trobat cap tipus de font de llista de canals" #: ../src/fl_sources/node_source.c:363 msgid "Source Type" msgstr "Tipus de font" #: ../src/fl_sources/node_source.c:414 #, c-format msgid "Login for '%s' has not yet completed! Please wait until login is done." msgstr "" #. FIXME: something is not perfect, because if you immediately #. remove the subscription tree afterwards there is a double free #: ../src/fl_sources/node_source.c:580 #, c-format msgid "The '%s' subscription was successfully converted to local feeds!" msgstr "" #: ../src/fl_sources/opml_source.c:319 msgid "Planet, BlogRoll, OPML" msgstr "Planeta, llista de blocs, OPML" #: ../src/fl_sources/opml_source.c:379 msgid "Choose OPML File" msgstr "Escolliu el fitxer OPML" #: ../src/fl_sources/opml_source.c:379 ../src/ui/subscription_dialog.c:355 msgid "_Open" msgstr "" #: ../src/fl_sources/opml_source.h:28 msgid "New OPML Subscription" msgstr "Subscripció a un OPML nou" #: ../src/fl_sources/reedah_source.c:103 #: ../src/fl_sources/theoldreader_source.c:104 #, fuzzy msgid "Login failed!" msgstr "Ha fallat l'entrada al Google Reader" #: ../src/fl_sources/reedah_source.c:313 msgid "Reedah" msgstr "" #: ../src/fl_sources/reedah_source_feed.c:154 #, fuzzy msgid "Could not parse JSON returned by Reedah API!" msgstr "No s'ha pogut analitzar el JSON que ha retornat l'API del tt-rss." #: ../src/fl_sources/theoldreader_source.c:311 #, fuzzy msgid "TheOldReader" msgstr "Lector de canals de notícies" #: ../src/fl_sources/ttrss_source.c:227 ../src/fl_sources/ttrss_source.c:293 msgid "TinyTinyRSS HTTP API not reachable!" msgstr "" #: ../src/fl_sources/ttrss_source.c:234 msgid "" "TinyTinyRSS subscribing to feed failed! Check if you really passed a feed " "URL!" msgstr "" #: ../src/fl_sources/ttrss_source.c:300 msgid "TinyTinyRSS unsubscribing feed failed!" msgstr "" #: ../src/fl_sources/ttrss_source.c:318 #, c-format msgid "" "This TinyTinyRSS version does not support removing feeds. Upgrade to version " "%s or later!" msgstr "" #: ../src/fl_sources/ttrss_source.c:471 msgid "Tiny Tiny RSS" msgstr "Tiny Tiny RSS" #: ../src/fl_sources/ttrss_source_feed.c:150 #, fuzzy msgid "Could not parse JSON returned by TinyTinyRSS API!" msgstr "No s'ha pogut analitzar el JSON que ha retornat l'API del tt-rss." #. if we don't find a feed with unread items do nothing #: ../src/itemlist.c:395 msgid "There are no unread items" msgstr "No hi ha elements sense llegir " #: ../src/liferea_application.c:280 #, fuzzy msgid "" "Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'" msgstr "" "Inicia el Liferea amb la finestra principal en l'«ESTAT» determinat. " "L'«ESTAT» pot ser «shown» (visible), «iconified» (iconificada), o " "«hidden» (amagada)" #: ../src/liferea_application.c:280 msgid "STATE" msgstr "ESTAT" #: ../src/liferea_application.c:281 msgid "Show version information and exit" msgstr "Mostra la informació sobre la versió i surt" #: ../src/liferea_application.c:282 msgid "Add a new subscription" msgstr "Afegeix una subscripció nova" #: ../src/liferea_application.c:282 msgid "uri" msgstr "URI" #: ../src/liferea_application.c:283 msgid "Start with all plugins disabled" msgstr "" #: ../src/liferea_application.c:288 msgid "Print debugging messages of all types" msgstr "Mostra tot tipus de missatges de depuració" #: ../src/liferea_application.c:289 msgid "Print debugging messages for the cache handling" msgstr "Mostra missatges de depuració de la gestió de la memòria cau" #: ../src/liferea_application.c:290 msgid "Print debugging messages for the configuration handling" msgstr "Mostra missatges de depuració de la gestió de la configuració" #: ../src/liferea_application.c:291 msgid "Print debugging messages of the database handling" msgstr "Mostra missatges de depuració de la gestió de la base de dades" #: ../src/liferea_application.c:292 msgid "Print debugging messages of all GUI functions" msgstr "" "Mostra missatges de depuració de totes les funcions de l'interfície gràfica" #: ../src/liferea_application.c:293 msgid "" "Enables HTML rendering debugging. Each time Liferea renders HTML output it " "will also dump the generated HTML into ~/.cache/liferea/output.html" msgstr "" "Habilita la depuració del renderitzat de l'HTML. Cada vegada que el Liferea " "renderitzi HTML també crearà una còpia de l'HTML generat a ~/.cache/liferea/" "output.html" #: ../src/liferea_application.c:294 msgid "Print debugging messages of all network activity" msgstr "Mostra missatges de depuració de tota l'activitat de xarxa" #: ../src/liferea_application.c:295 msgid "Print debugging messages of all parsing functions" msgstr "Mostra missatges de depuració de totes les funcions d'anàlisi" #: ../src/liferea_application.c:296 msgid "Print debugging messages when a function takes too long to process" msgstr "" "Mostra missatges de depuració quan alguna funció tardi massa a processar" #: ../src/liferea_application.c:297 msgid "Print debugging messages when entering/leaving functions" msgstr "Mostra missatges de depuració en entrar/sortir de les funcions" #: ../src/liferea_application.c:298 msgid "Print debugging messages of the feed update processing" msgstr "Mostra missatges de depuració del procés d'actualització dels canals" #: ../src/liferea_application.c:299 msgid "Print debugging messages of the search folder matching" msgstr "" "Mostra missatges de depuració de les coincidències de les carpetes de cerca" #: ../src/liferea_application.c:300 msgid "Print verbose debugging messages" msgstr "Mostra missatges de depuració detallats" #: ../src/liferea_application.c:305 ../src/liferea_application.c:306 msgid "Print debugging messages for the given topic" msgstr "Mostra missatges de depuració del tema seleccionat" #. Some libsoup transport errors #: ../src/net.c:437 msgid "The update request was cancelled" msgstr "S'ha cancel·lat la petició d'actualització" #: ../src/net.c:438 msgid "Unable to resolve destination host name" msgstr "No s'ha pogut resoldre el nom de l'ordinador remot" #: ../src/net.c:439 msgid "Unable to resolve proxy host name" msgstr "No s'ha pogut resoldre el nom del servidor intermediari" #: ../src/net.c:440 msgid "Unable to connect to remote host" msgstr "No s'ha pogut connectar a l'ordinador remot" #: ../src/net.c:441 msgid "Unable to connect to proxy" msgstr "No s'ha pogut connectar al servidor intermediari" #: ../src/net.c:442 msgid "" "SSL/TLS negotiation failed. Possible outdated or unsupported encryption " "algorithm. Check your operating system settings." msgstr "" #. http 3xx redirection #: ../src/net.c:445 msgid "The resource moved permanently to a new location" msgstr "El recurs s'ha migrat de forma permanent a una ubicació nova" #. http 4xx client error #: ../src/net.c:448 msgid "" "You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box" msgstr "" "No esteu autoritzat a baixar-vos aquest canal. Actualitzeu el vostre nom " "d'usuari i la contrasenya en el quadre de diàleg de les propietats del canal" #: ../src/net.c:450 msgid "Payment required" msgstr "Es requereix pagament" #: ../src/net.c:451 msgid "You're not allowed to access this resource" msgstr "No podeu accedir en aquest recurs" #: ../src/net.c:452 msgid "Resource Not Found" msgstr "No s'ha trobat el recurs" #: ../src/net.c:453 msgid "Method Not Allowed" msgstr "No es permet utilitzar aquest mètode" #: ../src/net.c:454 msgid "Not Acceptable" msgstr "No acceptable" #: ../src/net.c:455 msgid "Proxy authentication required" msgstr "Es requereix autenticació a través de servidor intermediari" #: ../src/net.c:456 msgid "Request timed out" msgstr "S'ha esgotat el temps de la sol·licitud" #: ../src/net.c:457 #, fuzzy msgid "" "The webserver indicates this feed is discontinued. It's no longer available. " "Liferea won't update it anymore but you can still access the cached " "headlines." msgstr "" "Aquest canal ja no es manté i no està disponible. El Liferea ja no " "l'actualitzarà més, però encara podeu accedir als titulars que hi ha a la " "memòria cau." #: ../src/net.c:462 msgid "There was an internal error in the update process" msgstr "S'ha produït un error intern en el procés d'actualització" #: ../src/net.c:464 msgid "Feed not available: Server requested unsupported redirection!" msgstr "" "El canal no és disponible: el servidor ha sol·licitat una redirecció no " "implementada" #: ../src/net.c:466 msgid "Client Error" msgstr "Error del client" #: ../src/net.c:468 msgid "Server Error" msgstr "Error del servidor" #: ../src/net.c:470 msgid "An unknown networking error happened!" msgstr "S'ha produït un error de xarxa desconegut" #: ../src/parsers/atom10.c:239 msgid "Website" msgstr "Lloc web" #: ../src/parsers/ns_ag.c:70 msgid "%b %d %H:%M" msgstr "%d %b %H:%M" #. in-memory check function feedlist.opml rule id rule menu label positive menu option negative menu option has param #. ======================================================================================================================================================================================== #: ../src/rule.c:229 msgid "Item" msgstr "Element" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does contain" msgstr "conté" #: ../src/rule.c:229 ../src/rule.c:230 ../src/rule.c:231 ../src/rule.c:236 #: ../src/rule.c:237 ../src/rule.c:238 msgid "does not contain" msgstr "no conté" #: ../src/rule.c:230 msgid "Item title" msgstr "Títol de l'element" #: ../src/rule.c:231 msgid "Item body" msgstr "Cos de l'element" #: ../src/rule.c:232 msgid "Read status" msgstr "Estat de lectura" #: ../src/rule.c:232 msgid "is unread" msgstr "no està llegit" #: ../src/rule.c:232 msgid "is read" msgstr "està llegit" #: ../src/rule.c:233 msgid "Flag status" msgstr "Estat de senyalament" #: ../src/rule.c:233 msgid "is flagged" msgstr "està senyalat" #: ../src/rule.c:233 msgid "is unflagged" msgstr "no està senyalat" #: ../src/rule.c:234 msgid "Podcast" msgstr "Podcast" #: ../src/rule.c:234 msgid "included" msgstr "inclòs" #: ../src/rule.c:234 msgid "not included" msgstr "exclòs" #: ../src/rule.c:235 msgid "Category" msgstr "Categoria" #: ../src/rule.c:235 msgid "is set" msgstr "té" #: ../src/rule.c:235 msgid "is not set" msgstr "no té" #: ../src/rule.c:236 msgid "Feed title" msgstr "Títol del canal" #: ../src/rule.c:237 #, fuzzy msgid "Feed source" msgstr "Font del canal" #: ../src/rule.c:238 msgid "Parent folder title" msgstr "" #: ../src/subscription.c:108 #, c-format msgid "Subscription \"%s\" is already being updated!" msgstr "Ja s'està actualitzant la subscripció «%s»" #: ../src/subscription.c:113 #, c-format msgid "" "The subscription \"%s\" was discontinued. Liferea won't update it anymore!" msgstr "" "Ja no es manté la subscripció «%s». El Liferea ja no l'actualitzarà més." #: ../src/subscription.c:188 #, c-format msgid "The URL of \"%s\" has changed permanently and was updated" msgstr "L'URL de «%s» ha canviat permanentment i s'ha actualitzat" #: ../src/subscription.c:204 #, c-format msgid "\"%s\" is discontinued. Liferea won't updated it anymore!" msgstr "«%s» ja no es manté. El Liferea ja no l'actualitzarà més" #: ../src/subscription.c:208 #, c-format msgid "\"%s\" has not changed since last update" msgstr "«%s» no ha canviat des de l'última actualització" #: ../src/subscription.c:221 ../src/subscription.c:296 #, fuzzy, c-format msgid "Updating (%d / %d) ..." msgstr "S'està actualitzant..." #: ../src/subscription.c:298 #, fuzzy, c-format msgid "Updating '%s'..." msgstr "S'està actualitzant..." #: ../src/ui/auth_dialog.c:114 #, c-format msgid "Enter the username and password for \"%s\" (%s):" msgstr "Introduïu el nom d'usuari i la contrasenya per a «%s» (%s):" #: ../src/ui/auth_dialog.c:116 msgid "Unknown source" msgstr "Font desconeguda" #: ../src/ui/browser_tabs.c:262 msgid "Untitled" msgstr "Sense títol" #: ../src/ui/enclosure_list_view.c:168 msgid "Attachments" msgstr "Adjuncions" #. The following literals are the enclosure list size units #: ../src/ui/enclosure_list_view.c:260 msgid " Bytes" msgstr " Bytes" #: ../src/ui/enclosure_list_view.c:263 msgid "kB" msgstr "kB" #: ../src/ui/enclosure_list_view.c:267 msgid "MB" msgstr "MB" #: ../src/ui/enclosure_list_view.c:271 msgid "GB" msgstr "GB" #: ../src/ui/enclosure_list_view.c:275 #, c-format msgid "%d%s" msgstr "%d%s" #. update list title #: ../src/ui/enclosure_list_view.c:313 #, c-format msgid "%d attachment" msgid_plural "%d attachments" msgstr[0] "%d adjunció" msgstr[1] "%d adjuncions" #: ../src/ui/enclosure_list_view.c:402 ../src/ui/subscription_dialog.c:355 msgid "Choose File" msgstr "Escolliu un fitxer" #: ../src/ui/enclosure_list_view.c:463 #, c-format msgid "File Extension .%s" msgstr "Extensió de fitxer .%s" #: ../src/ui/feed_list_view.c:432 msgid "Liferea is in offline mode. No update possible." msgstr "" "El Liferea és en mode de fora de línia. No és poden fer actualitzacions." #: ../src/ui/feed_list_view.c:478 #, fuzzy msgid "all feeds" msgstr "Cerca a tots els canals" #: ../src/ui/feed_list_view.c:479 #, fuzzy, c-format msgid "Mark %s as read ?" msgstr "Marca'ls tots com a llegits" #: ../src/ui/feed_list_view.c:483 #, fuzzy, c-format msgid "Are you sure you want to mark all items in %s as read ?" msgstr "Segur que voleu suprimir «%s»?" #: ../src/ui/feed_list_view.c:621 msgid "(Empty)" msgstr "(buit)" #: ../src/ui/feed_list_view.c:832 #, c-format msgid "" "%s\n" "Rebuilding" msgstr "" "%s\n" "S'està reconstruint" #: ../src/ui/feed_list_view.c:901 msgid "Deleting entry" msgstr "Supressió de l'entrada" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\" and its contents?" msgstr "Segur que voleu suprimir «%s» i el seus continguts?" #: ../src/ui/feed_list_view.c:902 #, c-format msgid "Are you sure that you want to delete \"%s\"?" msgstr "Segur que voleu suprimir «%s»?" #: ../src/ui/feed_list_view.c:911 ../src/ui/feed_list_view.c:965 #, fuzzy msgid "_Cancel" msgstr "Cancel·la-ho _tot" #: ../src/ui/feed_list_view.c:912 ../src/ui/popup_menu.c:335 msgid "_Delete" msgstr "_Suprimeix" #: ../src/ui/feed_list_view.c:914 msgid "Deletion Confirmation" msgstr "Confirmació de la supressió" #: ../src/ui/feed_list_view.c:953 #, c-format msgid "" "Are you sure that you want to add a new subscription with URL \"%s\"? " "Another subscription with the same URL already exists (\"%s\")." msgstr "" #: ../src/ui/feed_list_view.c:966 msgid "_Add" msgstr "" #: ../src/ui/feed_list_view.c:968 #, fuzzy msgid "Adding Duplicate Subscription Confirmation" msgstr "Confirmació de la supressió" #: ../src/ui/icons.c:54 #, c-format msgid "Couldn't find pixmap file: %s" msgstr "No s'ha pogut trobar el fitxer de mapa de píxels: %s" #: ../src/ui/item_list_view.c:115 msgid "This item has no link specified!" msgstr "Aquest element no te cap enllaç especificat" #: ../src/ui/item_list_view.c:482 msgid "*** No title ***" msgstr "*** Sense títol ***" #: ../src/ui/item_list_view.c:486 msgid " important " msgstr "" #: ../src/ui/item_list_view.c:849 msgid "Headline" msgstr "Titular" #: ../src/ui/item_list_view.c:871 msgid "Date" msgstr "Data" #: ../src/ui/item_list_view.c:1038 msgid "You must select a feed to delete its items!" msgstr "Heu de seleccionar un canal per poder-ne suprimir els seus elements" #: ../src/ui/item_list_view.c:1054 ../src/ui/item_list_view.c:1132 #: ../src/ui/item_list_view.c:1147 msgid "No item has been selected" msgstr "No s'ha seleccionat cap element" #: ../src/ui/liferea_shell.c:408 #, c-format msgid " (%d new)" msgid_plural " (%d new)" msgstr[0] " (%d nova)" msgstr[1] " (%d noves)" #: ../src/ui/liferea_shell.c:413 #, c-format msgid "%d unread%s" msgid_plural "%d unread%s" msgstr[0] "%d sense llegir%s" msgstr[1] "%d sense llegir%s" #: ../src/ui/liferea_shell.c:754 msgid "Help Topics" msgstr "Temes d'ajuda (en anglès)" #: ../src/ui/liferea_shell.c:760 msgid "Quick Reference" msgstr "Referència ràpida (en anglès)" #: ../src/ui/liferea_shell.c:766 msgid "FAQ" msgstr "PMF (en anglès)" #: ../src/ui/liferea_shell.c:1044 #, fuzzy, c-format msgid "Email command failed: %s" msgstr "Ha fallat l'ordre del navegador: %s" #: ../src/ui/popup_menu.c:102 ../glade/liferea_menu.ui.h:25 msgid "Open In _Tab" msgstr "Obre en una _pestanya" #: ../src/ui/popup_menu.c:106 ../glade/liferea_menu.ui.h:26 msgid "_Open In Browser" msgstr "_Obre'l en el navegador" #: ../src/ui/popup_menu.c:110 ../glade/liferea_menu.ui.h:27 msgid "Open In _External Browser" msgstr "Obre'l en un navegador _extern" #: ../src/ui/popup_menu.c:115 msgid "Email The Author" msgstr "" #: ../src/ui/popup_menu.c:140 msgid "Copy to News Bin" msgstr "Copia'l al contenidor de notícies" #: ../src/ui/popup_menu.c:148 #, c-format msgid "_Bookmark at %s" msgstr "_Adreça d'interès a %s" #: ../src/ui/popup_menu.c:154 msgid "Copy Item _Location" msgstr "_Copia la ubicació de l'element" #: ../src/ui/popup_menu.c:163 ../glade/liferea_menu.ui.h:22 msgid "Toggle _Read Status" msgstr "Commuta l'estat de _lectura" #: ../src/ui/popup_menu.c:167 ../glade/liferea_menu.ui.h:23 msgid "Toggle Item _Flag" msgstr "Commuta el _senyalador de l'element" #: ../src/ui/popup_menu.c:171 msgid "R_emove Item" msgstr "_Suprimeix l'element" #: ../src/ui/popup_menu.c:200 msgid "Open Enclosure..." msgstr "Obre l'adjunció..." #: ../src/ui/popup_menu.c:201 msgid "Save As..." msgstr "Anomena i desa..." #: ../src/ui/popup_menu.c:202 msgid "Copy Link Location" msgstr "Copia la ubicació de l'enllaç" #: ../src/ui/popup_menu.c:280 ../glade/liferea_menu.ui.h:13 msgid "_Update" msgstr "_Actualitza" #: ../src/ui/popup_menu.c:282 msgid "_Update Folder" msgstr "_Actualitza la carpeta" #: ../src/ui/popup_menu.c:292 msgid "New _Subscription..." msgstr "Subscripció _nova..." #: ../src/ui/popup_menu.c:295 ../glade/liferea_menu.ui.h:5 msgid "New _Folder..." msgstr "_Carpeta nova..." #: ../src/ui/popup_menu.c:298 ../glade/liferea_menu.ui.h:6 msgid "New S_earch Folder..." msgstr "Carpeta de c_erca nova..." #: ../src/ui/popup_menu.c:299 msgid "New S_ource..." msgstr "_Font nova..." #: ../src/ui/popup_menu.c:300 ../glade/liferea_menu.ui.h:8 msgid "New _News Bin..." msgstr "C_ontenidor de notícies nou..." #: ../src/ui/popup_menu.c:303 msgid "_New" msgstr "_Nou" #: ../src/ui/popup_menu.c:312 msgid "Sort Feeds" msgstr "Ordena els canals" #: ../src/ui/popup_menu.c:320 msgid "_Mark All As Read" msgstr "_Marca'ls tots com a llegits" #: ../src/ui/popup_menu.c:327 msgid "_Rebuild" msgstr "_Reconstrueix" #: ../src/ui/popup_menu.c:336 ../glade/liferea_menu.ui.h:17 msgid "_Properties" msgstr "_Propietats" #: ../src/ui/popup_menu.c:343 #, fuzzy msgid "Convert To Local Subscriptions..." msgstr "Subscripció _nova..." #: ../src/ui/preferences_dialog.c:84 msgid "GNOME default" msgstr "Per defecte del GNOME" #: ../src/ui/preferences_dialog.c:85 msgid "Text below icons" msgstr "Text sota les icones" #: ../src/ui/preferences_dialog.c:86 msgid "Text beside icons" msgstr "Text al costat de les icones" #: ../src/ui/preferences_dialog.c:87 msgid "Icons only" msgstr "Només icones" #: ../src/ui/preferences_dialog.c:88 msgid "Text only" msgstr "Només text" #: ../src/ui/preferences_dialog.c:96 ../src/ui/subscription_dialog.c:43 msgid "minutes" msgstr "minuts" #: ../src/ui/preferences_dialog.c:97 ../src/ui/subscription_dialog.c:44 msgid "hours" msgstr "hores" #: ../src/ui/preferences_dialog.c:98 ../src/ui/subscription_dialog.c:45 msgid "days" msgstr "dies" #: ../src/ui/preferences_dialog.c:103 msgid "Space" msgstr "Tecla espaiadora" #: ../src/ui/preferences_dialog.c:104 msgid " Space" msgstr " tecla espaiadora" #: ../src/ui/preferences_dialog.c:105 msgid " Space" msgstr " tecla espaiadora" #: ../src/ui/preferences_dialog.c:110 msgid "Normal View" msgstr "Visualització normal" #: ../src/ui/preferences_dialog.c:111 msgid "Wide View" msgstr "Visualització àmplia" #: ../src/ui/preferences_dialog.c:478 msgid "Default Browser" msgstr "Navegador predeterminat" #: ../src/ui/preferences_dialog.c:480 msgid "Manual" msgstr "Manual" #: ../src/ui/preferences_dialog.c:740 msgid "Type" msgstr "Tipus" #: ../src/ui/preferences_dialog.c:743 msgid "Program" msgstr "Programa" #: ../src/ui/search_dialog.c:106 #, fuzzy msgid "Saved Search" msgstr "Cerca avançada" #: ../src/ui/subscription_dialog.c:427 #, c-format msgid "The provider of this feed suggests an update interval of %d minute." msgid_plural "" "The provider of this feed suggests an update interval of %d minutes." msgstr[0] "" "El proveïdor d'aquest canal suggereix un interval d'actualització de %d " "minut." msgstr[1] "" "El proveïdor d'aquest canal suggereix un interval d'actualització de %d " "minuts." #: ../src/ui/subscription_dialog.c:431 msgid "This feed specifies no default update interval." msgstr "Aquest canal no especifica cap interval d'actualització recomanat." #: ../src/ui/ui_common.c:206 msgid "All Files" msgstr "Tots els fitxers" #: ../src/update.c:350 #, c-format msgid "Error opening temp file %s to use for filtering!" msgstr "" "S'ha produït un error en obrir el fitxer temporal %s per utilitzar-lo per al " "filtratge" #: ../src/update.c:372 #, c-format msgid "%s exited with status %d" msgstr "%s ha sortit amb l'estat %d" #: ../src/update.c:378 ../src/update.c:379 ../src/update.c:493 #, c-format msgid "Error: Could not open pipe \"%s\"" msgstr "Error: no s'ha pogut obrir el conducte «%s»" #. FIXME: maybe setting request->returncode would be better #: ../src/update.c:517 #, c-format msgid "Error: Could not open file \"%s\"" msgstr "Error: no s'ha pogut obrir el fitxer «%s»" #: ../src/update.c:523 #, c-format msgid "Error: There is no file \"%s\"" msgstr "Error: no hi ha cap fitxer «%s»" #: ../src/vfolder.c:54 msgid "New Search Folder" msgstr "Carpeta de cerca nova" #: ../src/webkit/liferea_web_view.c:177 msgid "Open Link In _Tab" msgstr "Obre l'enllaç en una _pestanya" #: ../src/webkit/liferea_web_view.c:178 #, fuzzy msgid "Open Link In Browser" msgstr "_Obre l'enllaç en el navegador" #: ../src/webkit/liferea_web_view.c:179 #, fuzzy msgid "Open Link In External Browser" msgstr "Obre l'enllaç en el navegador _extern" #: ../src/webkit/liferea_web_view.c:185 #, c-format msgid "_Bookmark Link at %s" msgstr "_Publica l'enllaç a %s" #: ../src/webkit/liferea_web_view.c:192 msgid "_Copy Link Location" msgstr "_Copia la ubicació de l'enllaç" #: ../src/webkit/liferea_web_view.c:195 #, fuzzy msgid "_View Image" msgstr "Anomena i desa la _imatge" #: ../src/webkit/liferea_web_view.c:196 msgid "_Copy Image Location" msgstr "_Copia la ubicació de la imatge" #: ../src/webkit/liferea_web_view.c:199 msgid "S_ave Link As" msgstr "Anomena i desa l'enllaç" #: ../src/webkit/liferea_web_view.c:202 msgid "S_ave Image As" msgstr "Anomena i desa la _imatge" #: ../src/webkit/liferea_web_view.c:209 msgid "_Subscribe..." msgstr "_Subscriu..." #: ../src/webkit/liferea_web_view.c:213 msgid "_Copy" msgstr "" #: ../src/webkit/liferea_web_view.c:219 msgid "_Increase Text Size" msgstr "_Augmenta la mida del text" #: ../src/webkit/liferea_web_view.c:220 msgid "_Decrease Text Size" msgstr "_Redueix la mida del text" #: ../src/webkit/liferea_web_view.c:227 msgid "_Reader Mode" msgstr "" #: ../src/xml.c:426 msgid "[There were more errors. Output was truncated!]" msgstr "[Hi ha hagut altres errors. S'ha truncat la sortida]" #: ../src/xml.c:594 msgid "XML Parser: Could not parse document:\n" msgstr "Analitzador XML: no s'ha pogut analitzar el document:\n" #: ../glade/about.ui.h:1 msgid "About" msgstr "Quant a" #: ../glade/about.ui.h:2 msgid "Liferea is a news aggregator for GTK+" msgstr "El Liferea és un agregador de notícies per a GTK+" #: ../glade/about.ui.h:3 msgid "Liferea Homepage" msgstr "Pàgina inicial del Liferea" #: ../glade/auth.ui.h:1 msgid "Authentication" msgstr "Autenticació" #: ../glade/auth.ui.h:3 #, fuzzy, no-c-format msgid "Enter the username and password for \"%s\" (%s)" msgstr "Introduïu el nom d'usuari i la contrasenya per a «%s» (%s):" #: ../glade/auth.ui.h:4 ../glade/properties.ui.h:31 msgid "User_name:" msgstr "_Nom d'usuari:" #: ../glade/auth.ui.h:5 ../glade/properties.ui.h:32 msgid "_Password:" msgstr "_Contrasenya:" #: ../glade/enclosure_handler.ui.h:1 msgid "Open Enclosure" msgstr "Obre l'adjunció" #: ../glade/enclosure_handler.ui.h:2 msgid "Open an enclosure of type:" msgstr "Obre l'adjunció de tipus:" #: ../glade/enclosure_handler.ui.h:3 #, fuzzy msgid "" "_What should Liferea do with this enclosure? Please enter the command you " "want to be executed below. The enclosures URL will be supplied as an " "argument for this command:" msgstr "" "Què voleu que faci el Liferea amb aquesta adjunció? Escriviu aquí sota " "l'ordre que voleu que s'executi. L'URL de l'adjunt s'utilitzarà com a " "argument d'aquesta ordre:" #: ../glade/enclosure_handler.ui.h:4 msgid "_Browse" msgstr "_Navega" #: ../glade/enclosure_handler.ui.h:5 msgid "_Do this automatically for enclosures like this from now on." msgstr "_Fes el mateix per als tipus de fitxer semblants a partir d'ara." #: ../glade/liferea_menu.ui.h:1 msgid "_Subscriptions" msgstr "_Subscripcions" #: ../glade/liferea_menu.ui.h:2 ../glade/liferea_toolbar.ui.h:8 msgid "Update _All" msgstr "_Actualitza'ls tots" #: ../glade/liferea_menu.ui.h:3 msgid "Mark All As _Read" msgstr "Marca'ls tots com a _llegits" #: ../glade/liferea_menu.ui.h:4 ../glade/liferea_toolbar.ui.h:1 msgid "_New Subscription..." msgstr "Subscripció _nova..." #: ../glade/liferea_menu.ui.h:7 msgid "New _Source..." msgstr "_Font nova..." #: ../glade/liferea_menu.ui.h:9 msgid "_Import Feed List..." msgstr "_Importa una llista de canals..." #: ../glade/liferea_menu.ui.h:10 msgid "_Export Feed List..." msgstr "_Exporta la llista de canals..." #: ../glade/liferea_menu.ui.h:11 msgid "_Quit" msgstr "_Surt" #: ../glade/liferea_menu.ui.h:12 msgid "_Feed" msgstr "_Canal" #: ../glade/liferea_menu.ui.h:15 msgid "Remove _All Items" msgstr "Suprimeix tots els _elements" #: ../glade/liferea_menu.ui.h:16 msgid "_Remove" msgstr "_Suprimeix" #: ../glade/liferea_menu.ui.h:18 msgid "_Item" msgstr "_Element" #: ../glade/liferea_menu.ui.h:24 msgid "R_emove" msgstr "_Suprimeix" #: ../glade/liferea_menu.ui.h:28 msgid "_View" msgstr "_Visualitza" #: ../glade/liferea_menu.ui.h:29 msgid "_Fullscreen" msgstr "_Pantalla completa" #: ../glade/liferea_menu.ui.h:30 msgid "Zoom _In" msgstr "" #: ../glade/liferea_menu.ui.h:31 msgid "Zoom _Out" msgstr "" #: ../glade/liferea_menu.ui.h:32 #, fuzzy msgid "_Normal size" msgstr "Visualització _normal" #: ../glade/liferea_menu.ui.h:33 msgid "_Normal View" msgstr "Visualització _normal" #: ../glade/liferea_menu.ui.h:34 msgid "_Wide View" msgstr "Visualització àmpli_a" #: ../glade/liferea_menu.ui.h:35 msgid "_Reduced Feed List" msgstr "Llista de canals _reduïda" #: ../glade/liferea_menu.ui.h:36 msgid "_Tools" msgstr "E_ines" #: ../glade/liferea_menu.ui.h:37 msgid "_Update Monitor" msgstr "_Monitor d'actualització" #: ../glade/liferea_menu.ui.h:38 msgid "_Preferences" msgstr "_Preferències" #: ../glade/liferea_menu.ui.h:39 msgid "S_earch" msgstr "_Cerca" #: ../glade/liferea_menu.ui.h:41 msgid "_Help" msgstr "A_juda" #: ../glade/liferea_menu.ui.h:42 msgid "_Contents" msgstr "_Continguts" #: ../glade/liferea_menu.ui.h:43 msgid "_Quick Reference" msgstr "_Referència ràpida" #: ../glade/liferea_menu.ui.h:44 msgid "_FAQ" msgstr "_PMF" #: ../glade/liferea_menu.ui.h:45 msgid "_About" msgstr "_Quant a" #: ../glade/liferea_toolbar.ui.h:2 msgid "Adds a subscription to the feed list." msgstr "Afegeix una subscripció a la llista de canals." #: ../glade/liferea_toolbar.ui.h:4 msgid "" "Marks all items of the selected feed list node / in the item list as read." msgstr "" "Marca com a llegits tots els elements de la llista de canal o de la llista " "d'elements seleccionada." #: ../glade/liferea_toolbar.ui.h:9 msgid "Updates all subscriptions." msgstr "Actualitza totes les subscripcions." #: ../glade/liferea_toolbar.ui.h:11 msgid "Show the search dialog." msgstr "Mostra el diàleg de cerca." #: ../glade/mainwindow.ui.h:2 msgid "page 1" msgstr "" #: ../glade/mainwindow.ui.h:3 msgid "page 2" msgstr "" #: ../glade/mainwindow.ui.h:4 ../glade/prefs.ui.h:23 msgid "Headlines" msgstr "Titulars" #: ../glade/mark_read_dialog.ui.h:1 #, fuzzy msgid "Mark all as read ?" msgstr "Marca'ls tots com a llegits" #: ../glade/mark_read_dialog.ui.h:2 msgid "Mark all as read" msgstr "Marca'ls tots com a llegits" #: ../glade/mark_read_dialog.ui.h:3 msgid "Do not ask again" msgstr "" #: ../glade/new_folder.ui.h:1 msgid "New Folder" msgstr "Carpeta nova" #: ../glade/new_folder.ui.h:2 msgid "_Folder name:" msgstr "Nom de la _carpeta:" #: ../glade/new_newsbin.ui.h:1 msgid "Create News Bin" msgstr "Crea un contenidor de notícies" #: ../glade/new_newsbin.ui.h:2 msgid "_News Bin Name:" msgstr "_Nom del contenidor de notícies:" #: ../glade/new_subscription.ui.h:2 ../glade/properties.ui.h:11 msgid "Feed Source" msgstr "Font del canal" #: ../glade/new_subscription.ui.h:3 ../glade/properties.ui.h:12 msgid "Source Type:" msgstr "Tipus de font:" #: ../glade/new_subscription.ui.h:4 ../glade/properties.ui.h:13 msgid "_URL" msgstr "_URL" #: ../glade/new_subscription.ui.h:5 ../glade/properties.ui.h:14 msgid "_Command" msgstr "_Ordre" #: ../glade/new_subscription.ui.h:6 ../glade/properties.ui.h:15 msgid "_Local File" msgstr "Fitxer _local" #: ../glade/new_subscription.ui.h:7 ../glade/properties.ui.h:16 msgid "Select File..." msgstr "Seleccioneu un fitxer..." #: ../glade/new_subscription.ui.h:8 ../glade/properties.ui.h:17 msgid "_Source:" msgstr "_Font:" #: ../glade/new_subscription.ui.h:9 msgid "Download / Postprocessing" msgstr "Baixada / Post processament" #: ../glade/new_subscription.ui.h:10 ../glade/properties.ui.h:30 msgid "_Don't use proxy for download" msgstr "_No utilitzis el servidor intermediari per les baixades" #: ../glade/new_subscription.ui.h:11 ../glade/properties.ui.h:18 msgid "Use conversion _filter" msgstr "_Utilitza un filtre de conversió" #: ../glade/new_subscription.ui.h:12 msgid "" "Liferea can use external filter plugins in order to access feeds and " "directories in non-supported formats. See the documentation for more " "information." msgstr "" "El Liferea pot utilitzar connectors de filtres externs per poder accedir a " "canals i carpetes que utilitzin formats no implementats pel propi Liferea. " "Vegeu la documentació per a més informació." #: ../glade/new_subscription.ui.h:13 ../glade/properties.ui.h:20 msgid "Convert _using:" msgstr "Converteix _utilitzant:" #: ../glade/node_source.ui.h:1 msgid "Source Selection" msgstr "Selecció de la font" #: ../glade/node_source.ui.h:2 #, fuzzy msgid "_Select the source type you want to add..." msgstr "Seleccioneu el tipus de font que voleu afegir..." #: ../glade/opml_source.ui.h:1 msgid "Add OPML/Planet" msgstr "Afegeix un OPML/planeta" #: ../glade/opml_source.ui.h:2 msgid "" "Please specify a local file or an URL pointing to a valid OPML feed list." msgstr "" "Especifiqueu un fitxer local o un URL que apunti a una llista de canals OPML " "vàlida." #: ../glade/opml_source.ui.h:3 msgid "_Location" msgstr "_Ubicació" #: ../glade/opml_source.ui.h:4 msgid "_Select File" msgstr "_Seleccioneu un fitxer" #: ../glade/prefs.ui.h:1 msgid "Liferea Preferences" msgstr "Preferències del Liferea" #: ../glade/prefs.ui.h:2 msgid "Feed Cache Handling" msgstr "Gestió de la memòria cau dels canals" #: ../glade/prefs.ui.h:3 msgid "Default _number of items per feed to save:" msgstr "_Nombre per defecte d'elements a desar per canal:" #: ../glade/prefs.ui.h:4 ../glade/properties.ui.h:27 msgid "0" msgstr "" #: ../glade/prefs.ui.h:5 msgid "Feed Update Settings" msgstr "Paràmetres de l'actualització del canal" #. Feed update interval hint in preference dialog. #: ../glade/prefs.ui.h:7 msgid "" "Note: Please remember to set a reasonable refresh time. Usually it is a " "waste of bandwidth to poll feeds more often than each hour." msgstr "" "Nota: Establiu un temps d'actualització raonable. Normalment no serveix " "de res actualitzar més d'un cop cada hora." #: ../glade/prefs.ui.h:8 msgid "_Update all subscriptions at startup." msgstr "_Actualitza totes les subscripcions en iniciar." #: ../glade/prefs.ui.h:9 msgid "Default Feed Refresh _Interval:" msgstr "_Interval d'actualització per defecte dels canals:" #: ../glade/prefs.ui.h:10 ../glade/properties.ui.h:6 msgid "1" msgstr "" #: ../glade/prefs.ui.h:11 msgid "Feeds" msgstr "Canals" #: ../glade/prefs.ui.h:12 msgid "Folder Display Settings" msgstr "Paràmetres de visualització de les carpetes" #: ../glade/prefs.ui.h:13 msgid "_Show the items of all child feeds when a folder is selected." msgstr "" "_Mostra els elements de tots els canals fills en seleccionar una carpeta." #: ../glade/prefs.ui.h:14 msgid "_Hide read items." msgstr "_Oculta els elements llegits." #: ../glade/prefs.ui.h:15 msgid "Feed Icons (Favicons)" msgstr "Icones dels canals (Favicons)" #: ../glade/prefs.ui.h:16 msgid "_Update all favicons now" msgstr "_Actualitza totes les icones dels canals ara" #: ../glade/prefs.ui.h:17 msgid "Folders" msgstr "Carpetes" #: ../glade/prefs.ui.h:18 msgid "Reading Headlines" msgstr "Lectura de titulars" #: ../glade/prefs.ui.h:19 msgid "_Skim through articles with:" msgstr "_Fulleja els articles amb:" #: ../glade/prefs.ui.h:20 msgid "_Default View Mode:" msgstr "Mode de visualització per _defecte:" #: ../glade/prefs.ui.h:21 msgid "Web Integration" msgstr "Integració web" #: ../glade/prefs.ui.h:22 msgid "_Post Bookmarks to" msgstr "_Publica les adreces d'interès a" #: ../glade/prefs.ui.h:24 msgid "Internal Browser Settings" msgstr "Paràmetres del navegador intern" #: ../glade/prefs.ui.h:25 msgid "Open links in Liferea's _window." msgstr "Obre els enllaços a la _finestra del Liferea." #: ../glade/prefs.ui.h:26 msgid "_Never run external Javascript." msgstr "" #: ../glade/prefs.ui.h:27 msgid "_Enable browser plugins." msgstr "_Habilita els connectors del navegador." #: ../glade/prefs.ui.h:28 msgid "External Browser Settings" msgstr "Paràmetres del navegador extern" #: ../glade/prefs.ui.h:29 msgid "_Browser:" msgstr "_Navegador:" #: ../glade/prefs.ui.h:30 msgid "_Manual:" msgstr "_Manual:" #: ../glade/prefs.ui.h:32 #, no-c-format msgid "(%s for URL)" msgstr "(%s per l'URL)" #: ../glade/prefs.ui.h:33 msgid "Browser" msgstr "Navegador" #: ../glade/prefs.ui.h:34 msgid "Toolbar Settings" msgstr "Paràmetres de la barra d'eines" #: ../glade/prefs.ui.h:35 msgid "_Hide toolbar." msgstr "_Oculta la barra d'eines." #: ../glade/prefs.ui.h:36 msgid "Toolbar _button labels:" msgstr "Etiquetes dels _botons de la barra d'eines:" #: ../glade/prefs.ui.h:37 msgid "Other" msgstr "" #: ../glade/prefs.ui.h:38 msgid "Ask for confirmation when marking all items as read" msgstr "" #: ../glade/prefs.ui.h:39 msgid "Desktop" msgstr "" #: ../glade/prefs.ui.h:40 msgid "HTTP Proxy Server" msgstr "Servidor intermediari HTTP" #: ../glade/prefs.ui.h:41 msgid "_Auto Detect (GNOME or environment)" msgstr "Detecta _automàticament (del GNOME o de l'entorn)" #: ../glade/prefs.ui.h:42 msgid "_No Proxy" msgstr "_Sense servidor intermediari" #: ../glade/prefs.ui.h:43 msgid "_Manual Setting:" msgstr "Paràmetres _manuals:" #: ../glade/prefs.ui.h:44 msgid "Proxy _Host:" msgstr "_Ordinador del servidor intermediari:" #: ../glade/prefs.ui.h:45 msgid "Proxy _Port:" msgstr "_Port del servidor intermediari:" #: ../glade/prefs.ui.h:46 msgid "Use Proxy Au_thentication" msgstr "Utilitza l'autenticació del _servidor intermediari" #: ../glade/prefs.ui.h:47 msgid "Proxy _Username:" msgstr "_Usuari del servidor intermediari:" #: ../glade/prefs.ui.h:48 msgid "Proxy Pass_word:" msgstr "_Contrasenya del servidor intermediari:" #: ../glade/prefs.ui.h:49 msgid "" "Your version of WebKitGTK+ is older than 2.15.3. It doesn't support per " "application proxy settings. The system's default proxy settings will be used." msgstr "" #: ../glade/prefs.ui.h:50 msgid "Proxy" msgstr "Servidor intermediari" #: ../glade/prefs.ui.h:51 #, fuzzy msgid "Privacy Settings" msgstr "Paràmetres de visualització de les carpetes" #: ../glade/prefs.ui.h:52 msgid "Tell sites that I do _not want to be tracked." msgstr "" #: ../glade/prefs.ui.h:53 msgid "_Intelligent Tracking Prevention. " msgstr "" #: ../glade/prefs.ui.h:54 msgid "" "This enables the WebKit feature described here." msgstr "" #: ../glade/prefs.ui.h:55 msgid "" "Intelligent tracking prevention is only available with WebKitGtk+ 2.30 or " "higher." msgstr "" #: ../glade/prefs.ui.h:56 msgid "Use _Reader mode." msgstr "" #: ../glade/prefs.ui.h:57 msgid "" "This enables stripping all non-content elements (like scripts, fonts, tracking)" msgstr "" #: ../glade/prefs.ui.h:58 msgid "Privacy" msgstr "" #: ../glade/prefs.ui.h:59 msgid "Downloading Enclosures" msgstr "S'estan baixant les adjuncions" #: ../glade/prefs.ui.h:60 msgid "_Download using" msgstr "_Baixa-les utilitzant" #: ../glade/prefs.ui.h:62 #, no-c-format msgid "custom-command %s" msgstr "" #: ../glade/prefs.ui.h:63 msgid "Opening Enclosures" msgstr "Obre les adjuncions" #: ../glade/prefs.ui.h:64 msgid "Enclosures" msgstr "Adjuncions" #: ../glade/properties.ui.h:1 msgid "Subscription Properties" msgstr "Propietats de la subscripció" #: ../glade/properties.ui.h:2 #, fuzzy msgid "Feed _Name" msgstr "_Nom del canal:" #: ../glade/properties.ui.h:3 #, fuzzy msgid "Update _Interval" msgstr "Interval d'actualització" #: ../glade/properties.ui.h:4 msgid "_Use global default update interval." msgstr "_Utilitza l'interval d'actualització global per defecte." #: ../glade/properties.ui.h:5 msgid "_Feed specific update interval of" msgstr "Interval d'actualització específic del _canal" #: ../glade/properties.ui.h:7 msgid "_Don't update this feed automatically." msgstr "_No actualitzis aquest canal automàticament." #: ../glade/properties.ui.h:9 #, no-c-format msgid "This feed provider suggests an update interval of %d minutes." msgstr "" "El proveïdor d'aquest canal suggereix un interval d'actualització de %d " "minuts." #: ../glade/properties.ui.h:10 msgid "General" msgstr "General" #: ../glade/properties.ui.h:19 #, fuzzy msgid "" "Liferea can use external filter scripts in order to access feeds and " "directories in non-supported formats." msgstr "" "El Liferea pot utilitzar connectors de filtres externs per poder accedir a " "canals i carpetes que utilitzin formats no implementats pel propi Liferea. " "Vegeu la documentació per a més informació." #: ../glade/properties.ui.h:21 ../xslt/item.xml.in.h:1 msgid "Source" msgstr "Font" #: ../glade/properties.ui.h:22 msgid "" "The cache setting controls if the contents of feeds are saved when Liferea " "exits. Marked items are always saved to the cache." msgstr "" "Els paràmetres de la memòria cau controlen si els continguts dels canals es " "desen quan es surt del Liferea. Els elements marcats es desen sempre a la " "memòria cau." #: ../glade/properties.ui.h:23 msgid "_Default cache settings" msgstr "Paràmetres _per defecte de la memòria cau" #: ../glade/properties.ui.h:24 msgid "Di_sable cache" msgstr "_Inhabilita la memòria cau" #: ../glade/properties.ui.h:25 msgid "_Unlimited cache" msgstr "_Memòria cau il·limitada" #: ../glade/properties.ui.h:26 msgid "_Number of items to save:" msgstr "_Nombre d'elements a desar:" #: ../glade/properties.ui.h:28 msgid "Archive" msgstr "Arxiu" #: ../glade/properties.ui.h:29 msgid "Use HTTP _authentication" msgstr "Utilitza _autenticació HTTP" #: ../glade/properties.ui.h:33 msgid "Download" msgstr "Baixades" #: ../glade/properties.ui.h:34 msgid "_Automatically download all enclosures of this feed." msgstr "_Baixa automàticament totes les adjuncions d'aquest canal." #: ../glade/properties.ui.h:35 msgid "Auto-_load item link in configured browser when selecting articles." msgstr "" "Carrega automàticament _l'enllaç de l'element en el navegador configurat " "quan es seleccionin els articles." #: ../glade/properties.ui.h:36 msgid "Ignore _comment feeds for this subscription." msgstr "Ignora els _comentaris dels canals per aquesta subscripció." #: ../glade/properties.ui.h:37 msgid "_Mark downloaded items as read." msgstr "_Marca els elements baixats com a llegits." #: ../glade/properties.ui.h:38 msgid "Extract full content from HTML5 and Google AMP" msgstr "" #: ../glade/reedah_source.ui.h:1 #, fuzzy msgid "Add Reedah Account" msgstr "Afegeix un compte de Google Reader" #: ../glade/reedah_source.ui.h:2 #, fuzzy msgid "Please enter your Reedah account settings." msgstr "Introduïu les vostres dades del compte de Google Reader." #: ../glade/reedah_source.ui.h:3 ../glade/theoldreader_source.ui.h:3 #: ../glade/ttrss_source.ui.h:4 msgid "_Password" msgstr "_Contrasenya" #: ../glade/reedah_source.ui.h:4 ../glade/theoldreader_source.ui.h:4 msgid "_Username (Email)" msgstr "Nom d'_usuari (correu electrònic)" #: ../glade/rename_node.ui.h:1 msgid "Rename" msgstr "Canvia el nom" #: ../glade/rename_node.ui.h:2 msgid "_New Name:" msgstr "Nom _nou:" #: ../glade/search_folder.ui.h:1 msgid "Search Folder Properties" msgstr "Propietats de la carpeta de cerca" #: ../glade/search_folder.ui.h:2 msgid "Search _Name:" msgstr "_Nom de la cerca:" #: ../glade/search_folder.ui.h:3 #, fuzzy msgid "Search Rules" msgstr "%d resultat de cerca" #: ../glade/search_folder.ui.h:4 msgid "Rules" msgstr "" #: ../glade/search_folder.ui.h:5 msgid "All rules for this search folder" msgstr "" #: ../glade/search_folder.ui.h:6 #, fuzzy msgid "Rule Matching" msgstr "Coincidència de _qualsevol regla" #: ../glade/search_folder.ui.h:7 ../glade/search.ui.h:4 msgid "A_ny Rule Matches" msgstr "Coincidència de _qualsevol regla" #: ../glade/search_folder.ui.h:8 ../glade/search.ui.h:5 msgid "_All Rules Must Match" msgstr "Han de coincidir _totes les regles" #: ../glade/search_folder.ui.h:9 #, fuzzy msgid "Hide read items" msgstr "_Oculta els elements llegits." #: ../glade/search.ui.h:1 msgid "Advanced Search" msgstr "Cerca avançada" #: ../glade/search.ui.h:2 msgid "_Search Folder..." msgstr "Carpeta de _cerca..." #: ../glade/search.ui.h:3 msgid "Find Items that meet the following criteria" msgstr "Cerca elements que coincideixen amb els criteris següents" #: ../glade/simple_search.ui.h:1 msgid "Search All Feeds" msgstr "Cerca a tots els canals" #: ../glade/simple_search.ui.h:2 msgid "_Advanced..." msgstr "_Avançat..." #: ../glade/simple_search.ui.h:3 msgid "" "Starts searching for the specified text in all feeds. The search result will " "appear in the item list." msgstr "" "Inicia la cerca del text especificat a tots els canals. Els resultats de la " "cerca apareixeran a la llista d'elements." #: ../glade/simple_search.ui.h:4 msgid "_Search for:" msgstr "_Cerca:" #: ../glade/simple_search.ui.h:5 msgid "" "Enter a search string Liferea should find either in a items title or in its " "content." msgstr "" "Introduïu una cadena de cerca que el Liferea hauria de trobar en els títols " "d'elements o en el seu contingut." #: ../glade/simple_subscription.ui.h:2 msgid "Advanced..." msgstr "Avançat..." #: ../glade/simple_subscription.ui.h:3 #, fuzzy msgid "Feed _Source" msgstr "Font del canal" #: ../glade/simple_subscription.ui.h:4 msgid "" "Enter a website location to use feed autodiscovery or in case you know it " "the exact feed location." msgstr "" "Introduïu la ubicació d'un lloc web per a utilitzar la descoberta automàtica " "del canal, o en cas que la conegueu, la ubicació exacta del canal." #: ../glade/theoldreader_source.ui.h:1 #, fuzzy msgid "Add TheOldReader Account" msgstr "Afegeix un compte de Google Reader" #: ../glade/theoldreader_source.ui.h:2 #, fuzzy msgid "Please enter your TheOldReader account settings." msgstr "Introduïu les vostres dades del compte de Google Reader." #: ../glade/ttrss_source.ui.h:1 msgid "Add Tiny Tiny RSS Account" msgstr "Afegeix un compte de Tiny Tiny RSS" #: ../glade/ttrss_source.ui.h:2 #, fuzzy msgid "Please enter your TinyTinyRSS account settings." msgstr "Introduïu les dades del compte del tt-rss." #: ../glade/ttrss_source.ui.h:3 msgid "_Server URL" msgstr "URL del _servidor" #: ../glade/ttrss_source.ui.h:5 msgid "_Username" msgstr "Nom d'_usuari" #: ../glade/update_monitor.ui.h:1 msgid "Update Monitor" msgstr "Monitor d'actualitzacions" #: ../glade/update_monitor.ui.h:2 msgid "Stop All" msgstr "" #: ../glade/update_monitor.ui.h:3 #, fuzzy msgid "_Pending Requests" msgstr "Sol·licituds pendents" #: ../glade/update_monitor.ui.h:4 #, fuzzy msgid "_Downloading Now" msgstr "S'està baixant" #: ../xslt/feed.xml.in.h:1 msgid "Feed:" msgstr "Canal:" #: ../xslt/feed.xml.in.h:2 ../xslt/source.xml.in.h:1 msgid "Source:" msgstr "Font:" #: ../xslt/feed.xml.in.h:3 msgid "Publisher" msgstr "Editor" #: ../xslt/feed.xml.in.h:4 msgid "Copyright" msgstr "Copyright" #: ../xslt/feed.xml.in.h:5 #, fuzzy msgid "There was a problem when fetching this subscription!" msgstr "" "S'ha produït un error en llegir aquesta subscripció. Verifiqueu-ne l'URL i " "la sortida del terminal." #: ../xslt/feed.xml.in.h:6 #, fuzzy msgid "1. Authentication" msgstr "Autenticació" #: ../xslt/feed.xml.in.h:7 #, fuzzy msgid "2. Download" msgstr "Baixades" #: ../xslt/feed.xml.in.h:8 msgid "3. Feed Discovery" msgstr "" #: ../xslt/feed.xml.in.h:9 msgid "4. Parsing" msgstr "" #: ../xslt/feed.xml.in.h:10 #, fuzzy msgid "Details:" msgstr "Detalls" #: ../xslt/feed.xml.in.h:11 msgid "Authentication failed. Please check the credentials and try again!" msgstr "" #: ../xslt/feed.xml.in.h:12 #, fuzzy msgid "There was an error when downloading the feed source:" msgstr "S'han produït errors en analitzar aquest canal" #: ../xslt/feed.xml.in.h:13 #, fuzzy msgid "There was an error when running the feed filter command:" msgstr "S'han produït errors en analitzar aquest canal" #: ../xslt/feed.xml.in.h:14 msgid "" "The source does not point directly to a feed or a webpage with a link to a " "feed!" msgstr "" #: ../xslt/feed.xml.in.h:15 msgid "Sorry, the feed could not be parsed!" msgstr "" #: ../xslt/feed.xml.in.h:16 msgid "You may want to contact the author/webmaster of the feed about this!" msgstr "" "Us podeu posar en contacte amb l'autor/administrador del canal per explicar-" "li això." #: ../xslt/folder.xml.in.h:1 msgid "Folder:" msgstr "Carpeta:" #: ../xslt/folder.xml.in.h:2 ../xslt/source.xml.in.h:2 msgid "children with" msgstr "fill amb" #: ../xslt/folder.xml.in.h:3 ../xslt/source.xml.in.h:3 #: ../xslt/vfolder.xml.in.h:2 msgid "unread headlines" msgstr "titulars sense llegir" #: ../xslt/item.xml.in.h:2 msgid "Feed" msgstr "Canal" #: ../xslt/item.xml.in.h:3 msgid "Filed under" msgstr "Arxivat a" #: ../xslt/item.xml.in.h:4 msgid "Author" msgstr "Autor" #: ../xslt/item.xml.in.h:5 msgid "Shared by" msgstr "Compartit per" #: ../xslt/item.xml.in.h:6 msgid "Via" msgstr "A través de" #: ../xslt/item.xml.in.h:7 msgid "Related" msgstr "Relacionats" #: ../xslt/item.xml.in.h:8 msgid "Also posted in" msgstr "Publicat també a" #: ../xslt/item.xml.in.h:9 msgid "Creator" msgstr "Creador" #: ../xslt/item.xml.in.h:10 msgid "Coordinates" msgstr "" #: ../xslt/item.xml.in.h:11 msgid "Map" msgstr "" #: ../xslt/item.xml.in.h:12 msgid "View count" msgstr "" #: ../xslt/item.xml.in.h:13 msgid "Rating" msgstr "" #: ../xslt/item.xml.in.h:14 msgid "Comments" msgstr "Comentaris" #: ../xslt/item.xml.in.h:15 msgid "Updating..." msgstr "S'està actualitzant..." #: ../xslt/item.xml.in.h:16 msgid "Section" msgstr "Secció" #: ../xslt/item.xml.in.h:17 msgid "Department" msgstr "Departament" #: ../xslt/newsbin.xml.in.h:1 msgid "News Bin:" msgstr "Contenidor de notícies:" #: ../xslt/newsbin.xml.in.h:2 msgid "" "Add items to this news bin by selecting \"Copy to News Bin\" from the item " "list context menu." msgstr "" "Podeu afegir elements a aquest contenidor de notícies si seleccioneu «Copia " "al contenidor de notícies» del menú contextual de la llista d'elements." #: ../xslt/vfolder.xml.in.h:1 msgid "Search Folder:" msgstr "Carpeta de cerca:" #, c-format #~ msgid "\"%s\" is not available" #~ msgstr "no està disponible «%s»" #, c-format #~ msgid "\"%s\" updated..." #~ msgstr "s'ha actualitzat «%s»..." #~ msgid "" #~ "A network error occurred, or the other end closed the connection " #~ "unexpectedly" #~ msgstr "" #~ "S'ha produït un error de xarxa o l'altre extrem ha tancat la connexió " #~ "inesperadament" #~ msgid "" #~ "The last update of this subscription failed!
HTTP error code : " #~ msgstr "" #~ "Ha fallat l'última actualització d'aquesta subscripció
Codi " #~ "d'error HTTP : " #~ msgid "Parser Error Details" #~ msgstr "Detalls de l'error de l'analitzador" #~ msgid "There were errors while filtering this feed!" #~ msgstr "S'han produït errors en filtrar aquest canal" #~ msgid "Filter Error Details" #~ msgstr "Detalls de l'error del filtre" #~ msgid "" #~ "

Could not detect the type of this feed! Please check if the source " #~ "really points to a resource provided in one of the supported syndication " #~ "formats!

XML Parser Output:
" #~ msgstr "" #~ "

No s'ha pogut detectar el tipus d'aquest canal. Verifiqueu que la font " #~ "apunti realment a un recurs proporcionat per un dels formats de " #~ "sindicació coneguts.

Sortida de l'analitzador XML:
" #~ msgid "" #~ "The URL you want Liferea to subscribe to points to a webpage and the auto " #~ "discovery found no feeds on this page. Maybe this webpage just does not " #~ "support feed auto discovery." #~ msgstr "" #~ "L'URL de subscripció que heu proporcionat al Liferea apunta a una pàgina " #~ "web i el descobridor automàtic no hi ha trobat cap canal. Potser aquesta " #~ "pàgina web no permet el descobriment automàtic del canal." #~ msgid "Source points to HTML document." #~ msgstr "La font apunta a un document HTML." #~ msgid "Could not determine the feed type." #~ msgstr "No s'ha pogut determinar el tipus de canal." #~ msgid "Gone. Resource doesn't exist. Please unsubscribe!" #~ msgstr "El recurs ja no existeix, cancel·leu la subscripció." #~ msgid "Updating \"%s\"" #~ msgstr "S'està actualitzant «%s»" #~ msgid "XML error while reading feed! Feed \"%s\" could not be loaded!" #~ msgstr "" #~ "S'ha detectat un error en la sintaxi XML en llegir el canal. No s'ha " #~ "pogut carregar el canal «%s»." #~ msgid "Combined View" #~ msgstr "Visualització combinada" #~ msgid "_Disable Javascript." #~ msgstr "_Inhabilita el Javascript." #~ msgid "GUI" #~ msgstr "Interfície gràfica" #, fuzzy #~ msgid "Cancel All" #~ msgstr "Cancel·la-ho _tot" #~ msgid "Updating favicon for \"%s\"" #~ msgstr "S'està actualitzant la icona del canal «%s»" #~ msgid "Marks read every item of every subscription." #~ msgstr "Marca cada element de cada subscripció com a llegit." #~ msgid "Imports an OPML feed list." #~ msgstr "Importa una llista de canals OPML." #~ msgid "Exports the feed list as OPML." #~ msgstr "Exporta la llista de canals com a OPML." #~ msgid "Removes all items of the currently selected feed." #~ msgstr "Suprimeix tots els elements del canal seleccionat." #~ msgid "Increases the text size of the item view." #~ msgstr "Augmenta la mida del text de l'element visualitzat." #~ msgid "Decreases the text size of the item view." #~ msgstr "Redueix la mida del text de l'element visualitzat." #~ msgid "Show a list of all feeds currently in the update queue" #~ msgstr "" #~ "Mostra una llista de tots els canals que hi ha a la cua d'actualització" #~ msgid "Edit Preferences." #~ msgstr "Edita les preferències." #~ msgid "View help for this application." #~ msgstr "Visualitza l'ajuda d'aquesta aplicació." #~ msgid "View a list of all Liferea shortcuts." #~ msgstr "Visualitza una llista de totes les dreceres del Liferea." #~ msgid "View the FAQ for this application." #~ msgstr "Visualitza les PMF d'aquesta aplicació." #~ msgid "Shows an about dialog." #~ msgstr "Mostra el diàleg de quant a." #~ msgid "Set view mode to mail client mode." #~ msgstr "" #~ "Estableix el mode de visualització semblant al mode del client de correu." #~ msgid "Set view mode to use three vertical panes." #~ msgstr "Estableix el mode de visualització a tres quadres verticals." #~ msgid "_Combined View" #~ msgstr "Visualització _combinada" #~ msgid "Set view mode to two pane mode." #~ msgstr "Estableix el mode de visualització a dos quadres." #~ msgid "Hide feeds with no unread items." #~ msgstr "Oculta els canals sense cap element no llegit." #~ msgid "Adds a folder to the feed list." #~ msgstr "Afegeix una carpeta a la llista de canals." #~ msgid "Adds a new search folder to the feed list." #~ msgstr "Afegeix una carpeta de cerca nova a la llista de canals." #~ msgid "Adds a new feed list source." #~ msgstr "Afegeix una font de llista de canals nova." #~ msgid "Adds a new news bin." #~ msgstr "Afegeix un contenidor de notícies nou." #~ msgid "" #~ "Updates the selected subscription or all subscriptions of the selected " #~ "folder." #~ msgstr "" #~ "Actualitza la subscripció seleccionada o totes les subscripcions de la " #~ "carpeta seleccionada." #~ msgid "Opens the property dialog for the selected subscription." #~ msgstr "Obre el diàleg de propietats de la subscripció seleccionada." #~ msgid "Removes the selected subscription." #~ msgstr "Suprimeix la subscripció seleccionada." #~ msgid "Toggles the read status of the selected item." #~ msgstr "Commuta l'estat de lectura de l'element seleccionat." #~ msgid "Toggles the flag status of the selected item." #~ msgstr "Commuta l'estat del senyalador de l'element seleccionat." #~ msgid "Removes the selected item." #~ msgstr "Suprimeix l'element seleccionat." #~ msgid "Launches the item's link in a new Liferea browser tab." #~ msgstr "" #~ "Obre l'enllaç de l'element en una pestanya nova del navegador del Liferea." #~ msgid "Launches the item's link in the Liferea item pane." #~ msgstr "Obre l'enllaç de l'element en el quadre d'elements del Lifera." #~ msgid "Launches the item's link in the configured external browser." #~ msgstr "Obre l'enllaç de l'element en el navegador extern preconfigurat." #~ msgid "Browse at full screen" #~ msgstr "Navega a pantalla completa" #~ msgid "_Work Offline" #~ msgstr "_Treballa fora de línia" #~ msgid "_Update All" #~ msgstr "_Actualitza-ho tot" #~ msgid "_Show Liferea" #~ msgstr "_Mostra el Liferea" #, fuzzy #~ msgid "InoReader" #~ msgstr "Google Reader" #~ msgid "" #~ "Note: The username and password will be saved to your Liferea feedlist " #~ "file without using encryption." #~ msgstr "" #~ "Nota: es desarà, sense xifrar, el nom d'usuari i la contrasenya al " #~ "fitxer de la llista de canals del Liferea." #~ msgid "Add Google Reader Account" #~ msgstr "Afegeix un compte de Google Reader" #~ msgid "Please enter your Google Reader account settings." #~ msgstr "Introduïu les vostres dades del compte de Google Reader." #, fuzzy #~ msgid "Add InoReader Account" #~ msgstr "Afegeix un compte de Google Reader" #, fuzzy #~ msgid "Please enter your InoReader account settings." #~ msgstr "Introduïu les vostres dades del compte de Google Reader." #~ msgid "View Headlines" #~ msgstr "Visualitza els titulars" #~ msgid "Feed Name" #~ msgstr "Nom del canal" #~ msgid "normal view" #~ msgstr "visualització normal" #~ msgid "wide view" #~ msgstr "visualització àmplia" #~ msgid "combined view" #~ msgstr "visualització combinada" #~ msgid "Liferea, the Linux Feed Reader" #~ msgstr "Liferea, el lector de canals per a Linux" #~ msgid "For more information, please visit https://lzone.de/liferea/" #~ msgstr "Per a més informació, visiteu https://lzone.de/liferea/" #~ msgid "_Open Link In Browser" #~ msgstr "_Obre l'enllaç en el navegador" #~ msgid "_Open Link In External Browser" #~ msgstr "Obre l'enllaç en el navegador _extern" #~ msgid " " #~ msgstr " " #~ msgid "Create Search Engine Feed" #~ msgstr "Crea un canal de motor de cerca" #~ msgid "enter any search string you want" #~ msgstr "introduïu la cadena que voleu cercar" #~ msgid "Maximal _Number Of Result Items:" #~ msgstr "_Nombre màxim de resultats:" #~ msgid "" #~ "Note: Liferea will generate a feed subscription which is used to query " #~ "the search engine results for the specified search string. You can keep " #~ "this feed permanently and update it like any other subscription." #~ msgstr "" #~ "Nota: el Liferea generarà un canal de subscripció que es farà servir per " #~ "consultar els resultats del motor de cerca generats a partir de la cadena " #~ "de cerca especificada. Podeu mantenir aquest canal permanentment i " #~ "actualitzar-lo de la mateixa manera que qualsevol altra subscripció." #~ msgid "Liferea is now online" #~ msgstr "El Liferea és en línia" #~ msgid "Work Offline" #~ msgstr "Treballa fora de línia" #~ msgid "Liferea is now offline" #~ msgstr "El Liferea és fora de línia" #~ msgid "Work Online" #~ msgstr "Treballa en línia" #~ msgid "This option allows you to disable subscription updating." #~ msgstr "" #~ "Aquesta opció us permet inhabilitar l'actualització de les subscripcions." #~ msgid "Browser default" #~ msgstr "Per defecte del navegador" #~ msgid "Existing window" #~ msgstr "Finestra existent" #~ msgid "New window" #~ msgstr "Finestra nova" #~ msgid "New tab" #~ msgstr "Pestanya nova" #, fuzzy #~ msgid "AOL Reader" #~ msgstr "Lector de canals de notícies" #~ msgid "Online/Offline Button" #~ msgstr "Botó de en línia/fora de línia" #~ msgid "_Open link in:" #~ msgstr "_Obre l'enllaç a:" #~ msgid "No comments yet." #~ msgstr "Encara no hi ha cap comentari." #~ msgid "Refresh" #~ msgstr "Actualitza" #~ msgid "Liferea - Linux Feed Reader" #~ msgstr "Liferea - Lector de canals per a Linux" #~ msgid "" #~ "

Welcome to Liferea, a desktop news aggregator for online news " #~ "feeds.

You can add new subscriptions

  • From main menu " #~ "'Subscription' -> 'New Subscription'
  • By dropping feed links " #~ "into the subscription list
  • By right clicking links and choosing " #~ "'Subscribe' within Liferea

" #~ msgstr "" #~ "

Us donem la benvinguda al Liferea, un agregador de canals de " #~ "notícies en línia per a l'escriptori.

Podeu afegir subscripcions " #~ "noves

Liferea - Linux 餽流閱讀程式

歡迎使用 Liferea!這是一個桌用新聞匯集器。" #~ "

左邊的窗格包含了你所有訂閱的列表,如果想新增訂閱,請在選單按下 [餽" #~ "流] > 新增訂閱;而如果要瀏覽內文,在標題點選後,即會出現在右側窗格。

(empty)" #~ msgstr "(空)" #~ msgid "_Properties..." #~ msgstr "屬性(_P)..." #, fuzzy #~ msgid "Update out-dated feeds" #~ msgstr "更新所有饋流" #, fuzzy #~ msgid "Force update of all feeds" #~ msgstr "更新所有饋流" #, fuzzy #~ msgid "startup" #~ msgstr "於啟動時(_S):" #, fuzzy #~ msgid "feed updated" #~ msgstr "\"%s\" 更新中" #, fuzzy #~ msgid "feed added" #~ msgstr "饋流快取" #, fuzzy #~ msgid "item selected" #~ msgstr "刪除所選擇的(_S)" #, fuzzy #~ msgid "feed selected" #~ msgstr "刪除所選擇的(_S)" #, fuzzy #~ msgid "item unselected" #~ msgstr "未選擇項目!" #, fuzzy #~ msgid "feed unselected" #~ msgstr "刪除所選擇的(_S)" #, fuzzy #~ msgid "No script selected!" #~ msgstr "未選擇項目!" #, fuzzy #~ msgid "Create a new search feed." #~ msgstr "創造一個 Feedster 搜尋饋流" #, fuzzy #~ msgid "Liferea is unable to display this item's content." #~ msgstr "無法顯示該項目的內容" #, fuzzy #~ msgid "

View this item's content.

" #~ msgstr "

檢視該項目的內文

" #~ msgid "feedlist.opml" #~ msgstr "feedlist.opml" #~ msgid "Feed Source" #~ msgstr "饋流來源" #, fuzzy #~ msgid "Hook" #~ msgstr "規則" #, fuzzy #~ msgid "Registered Scripts" #~ msgstr "饋流來源" #~ msgid "" #~ "This option can cause significant delays when loading folders " #~ "containing many feeds." #~ msgstr "本選項可能會在載入含有大量饋流的目錄時,導致嚴重遲緩的現象。" #~ msgid "Downloading Enclosures" #~ msgstr "下載附件" #~ msgid "Feed Cache Handling" #~ msgstr "饋流快取處理" #~ msgid "Feed Name" #~ msgstr "饋流名稱" #~ msgid "Feed Update Settings" #~ msgstr "饋流更新設定" #~ msgid "HTTP Proxy Server" #~ msgstr "HTTP 代理伺服器" #~ msgid "Opening Enclosures" #~ msgstr "開啟附件" #~ msgid "Reading Headlines" #~ msgstr "檢視標題" #, fuzzy #~ msgid "Toolbar Settings" #~ msgstr "選單設定" #~ msgid "Update Interval" #~ msgstr "更新週期" #, fuzzy #~ msgid "Web Integration" #~ msgstr "更新週期" #~ msgid "At _startup:" #~ msgstr "於啟動時(_S):" #, fuzzy #~ msgid "Attention Profile" #~ msgstr "認證失敗!" #, fuzzy #~ msgid "Create new script" #~ msgstr "新增訂閱" #, fuzzy #~ msgid "Exec Command" #~ msgstr "指令(_C)" #~ msgid "" #~ "Unexpected end of character sequence or corrupt UTF-8 encoding! Some " #~ "characters were dropped!" #~ msgstr "非預期的字元順序或錯誤的 UTF-8 編碼! 某些字元已毀損!" #~ msgid "" #~ "Error while reading cache file \"%s\" ! Cache file could not be loaded!" #~ msgstr "讀入快取檔案 \"%s\"時錯誤! 無法載入快取檔案!" #~ msgid "" #~ "

XML error while parsing cache file! Feed cache file \"%s\" could not " #~ "be loaded!

" #~ msgstr "

分析快取檔案時發生 XML 錯誤! 無法載入快取檔案 \"%s\"!

" #~ msgid "

\"%s\" is no valid cache file! Cannot read cache file!

" #~ msgstr "

\"%s\" 是無效的快取檔案! 無法讀入檔案!

" #~ msgid "There were errors while parsing cache file \"%s\"" #~ msgstr "分析快取檔案 \"%s\" 時出現錯誤!" #~ msgid "Access Forbidden" #~ msgstr "禁用存取" #~ msgid "URL is invalid" #~ msgstr "URL 不合法" #~ msgid "Hostname could not be found" #~ msgstr "無法找到伺服器名稱" #~ msgid "Network connection was refused by the remote host" #~ msgstr "遠端主機拒絕網路連線" #~ msgid "Remote host did not finish sending data" #~ msgstr "遠端主機並未結束傳送資料" #~ msgid "Too many HTTP redirects were encountered" #~ msgstr "太多 HTTP 重導! 放棄。" #~ msgid "Remote host sent an invalid response" #~ msgstr "網站伺服器傳送了一個無效的回應" #~ msgid "Webserver's authentication method incompatible with Liferea" #~ msgstr "網站伺服器的認證方法與 Lifera 不相容" #~ msgid "Feed link auto discovery failed! No feed links found!" #~ msgstr "自動偵測饋流連結失敗! 未找到饋流連結!" #~ msgid "single item view" #~ msgstr "單一項目檢視" #~ msgid "_Limit cache to" #~ msgstr "限制快取到(_L)" #~ msgid "items." #~ msgstr "項目。" #~ msgid "Feed Loading Settings" #~ msgstr "饋流載入設定" #~ msgid "Optimize for reduced _memory usage." #~ msgstr "針對記憶體使用最佳化(_M)。" #~ msgid "Optimize for _speed." #~ msgstr "針對使用效率最佳化(_S)" #~ msgid "Date Column Settings" #~ msgstr "日期欄位設定" #~ msgid "Display only _time" #~ msgstr "只顯示時間(_T)" #~ msgid "Display _date and time" #~ msgstr "顯示日期與時間(_D)" #~ msgid "_User defined format:" #~ msgstr "使用自訂格式:(_U)" #~ msgid "" #~ "for expert users: specify a time format string, consult the strftime() " #~ "manpage for the format codes" #~ msgstr "" #~ "給專家使用者: 指定一個特定的時間格式,請參考 strftime 手冊中的格式碼" #~ msgid "Lower Right" #~ msgstr "右下方" #~ msgid "Upper Right" #~ msgstr "右上方" #~ msgid "Upper Left" #~ msgstr "左上方" #~ msgid "Lower Left" #~ msgstr "左下方" #~ msgid "Popup Placement" #~ msgstr "彈跳視窗擺置" #~ msgid "Show Menu _And Toolbar" #~ msgstr "顯示選單與工具列(_A)" #~ msgid "Show _Menu Only" #~ msgstr "只顯示選單(_M)" #~ msgid "Show _Toolbar Only" #~ msgstr "僅顯示工具列(_T)" #~ msgid "" #~ "Liferea reuses the GNOME proxy settings. If you use GNOME you can " #~ "change these settings in the GNOME Control Center." #~ msgstr "" #~ "Liferea 使用 GNOME 代理伺服器設定。如果您使用 GNOME,您可以在 GNOME 控制中" #~ "心修改。" #~ msgid "_Enable Proxy" #~ msgstr "啟用代理伺服器(_E)" #~ msgid "Liferea Homepage" #~ msgstr "Liferea 首頁" #~ msgid "Contributors" #~ msgstr "貢獻者" #~ msgid "Translation" #~ msgstr "翻譯" #, fuzzy #~ msgid "" #~ "Note: Items are added to the search folder if at least one additive rule\n" #~ "matches. They are removed if at least one removing rule matches." #~ msgstr "" #~ "註: 如果符合任一條規則,項目將加入虛擬目錄中。如果符合任一移除規則,它們將" #~ "會被移除。" #~ msgid "Saves this search as a VFolder, which will appear in the feed list." #~ msgstr "儲存此搜尋為饋流列表上的虛擬目錄,如此一來就會出現於饋流列表。" #~ msgid "VFolder" #~ msgstr "虛擬目錄" #~ msgid " --help Print this help and exit" #~ msgstr " --help 印出此訊息後退出" #~ msgid " --debug-plugins Print debugging messages when loading plugins" #~ msgstr " --debug-plugins 印出所有分析功能的除錯訊息" #~ msgid "The --mainwindow-state argument must be given a parameter.\n" #~ msgstr "--mainwindow-state 選項必須指定參數。\n" #~ msgid "The --session argument must be given a parameter.\n" #~ msgstr "--session 選項必須指定參數。\n" #~ msgid "Liferea encountered an unknown argument: %s\n" #~ msgstr "Lifeare 碰到未知的參數: %s\n" #~ msgid "" #~ "Another copy of Liferea was found to be running. Please use it instead. " #~ "If there is no other copy of Liferea running, please delete the \"~/." #~ "liferea/lock\" lock file." #~ msgstr "" #~ "發現另外一份 Liferea 正在執行。請使用它。如果並沒有其他 Liferea 執行中,請" #~ "刪除 \"~/.liferea/lock\" 檔案。" #~ msgid "does match" #~ msgstr "吻合" #~ msgid "does not match" #~ msgstr "不吻合" #~ msgid "Note: Using the subscriptions filter disables drag & drop" #~ msgstr "註: 使用訂閱過濾將停用拖拉功能!" #, fuzzy #~ msgid "" #~ "Sorry, I was not able to load any installed browser plugin! Try the --" #~ "debug-plugins option to get debug information!" #~ msgstr "抱歉, 無法載入任何瀏覽器模組! 請藉由 --debug-all 選項來取得除錯訊息" #~ msgid "_Program" #~ msgstr "程式(_P)" #~ msgid "Updates all subscriptions. This does not update OCS directories." #~ msgstr "立即升級所有饋流 (除了 OCS 目錄)。" #~ msgid "_Search" #~ msgstr "搜尋(_S)" #~ msgid "Update _Selected" #~ msgstr "更新選擇的饋流(_S)" #~ msgid "_Delete Selected" #~ msgstr "刪除選擇饋流(_D)" #~ msgid "Toggles the item list mode between condensed and normal mode." #~ msgstr "切換項目顯示模式為摘要模式或一般模式" #~ msgid "/Toggle _Read Status" #~ msgstr "/切換閱讀狀態(_R)" #~ msgid "/Toggle Item _Flag" #~ msgstr "/切換項目旗幟(_F)" #~ msgid "/_Next Unread Item" #~ msgstr "/下筆未讀項目(_N)" #~ msgid "/_Increase Text Size" #~ msgstr "/縮小字型(_I)" #~ msgid "/_Decrease Text Size" #~ msgstr "/放大字型(_D)" #~ msgid "/Toggle _Online|Offline" #~ msgstr "/切換線上或離線瀏覽(_O)" #~ msgid "/_Preferences..." #~ msgstr "/偏好設定(_P)" #~ msgid "/_Show Window" #~ msgstr "/顯示視窗(_S)" #~ msgid "/_Quit" #~ msgstr "/離開(_Q)" #~ msgid "/_New/New _Subscription..." #~ msgstr "/新增(_N)/新增訂閱(_S)" #, fuzzy #~ msgid "/_New/New _Folder..." #~ msgstr "/新增(_N)/新增虛擬目錄(_O)" #, fuzzy #~ msgid "/_New/New S_earch Folder..." #~ msgstr "/新增(_N)/新增虛擬目錄(_O)" #, fuzzy #~ msgid "/_New/New S_ource..." #~ msgstr "/新增(_N)/新增目錄(_O)" #~ msgid "/_Rename Folder..." #~ msgstr "/重新命名目錄(_R)" #~ msgid "/_Delete Folder" #~ msgstr "/刪除目錄(_D)" #~ msgid "/_Properties..." #~ msgstr "/屬性(_P)" #~ msgid "Update only feeds scheduled for updates" #~ msgstr "只更新預定更新的饋流" #~ msgid "Reset feed update timers (Update no feeds)" #~ msgstr "重置更新時間表(不更新任何饋流)" #, fuzzy #~ msgid "This item's content is invalid." #~ msgstr "該項目內文有誤" #~ msgid "Cookie for %s has expired!" #~ msgstr "%s 的 cookie 已失效" #~ msgid "Liferea notification" #~ msgstr "Lifeare 通知" #, fuzzy #~ msgid "" #~ "
You may want to validate the feed using Could not find Atom 1.0 header!

" #~ msgstr "

無法找到 Atom 1.0 標頭!

" #~ msgid "internal OCS namespace parsing error!" #~ msgstr "內部 OCS 命名空間分析錯誤!" #~ msgid "no namespace handler for <%s:%s>!\n" #~ msgstr "無 <%s:%s> 之 namespace handler!\n" #~ msgid "

Could not find RDF header!

" #~ msgstr "

無法找到 RDF 標頭!

" #~ msgid "

Could not find OPML header!

" #~ msgstr "

無法找到 OPML 標頭!

" #~ msgid "

Could not find Atom/Echo/PIE header!

" #~ msgstr "

無法找到 Atom/Echo/PIE 標頭!

" #~ msgid "

Could not find RDF/RSS header!

" #~ msgstr "

無法找到 RDF/RSS 標頭!

" #~ msgid "" #~ "The included HTML tag is not supported with GtkHTML2.\n" #~ "Included Plugins might not be displayed." #~ msgstr "" #~ "GtkHTML2 不支援 HTML 標籤。\n" #~ "內含的外掛可能無法顯示。" #~ msgid "" #~ "\n" #~ "Trying to load the Mozilla browser module... Note that this\n" #~ "might not work with every Mozilla version. If you have problems\n" #~ "and Liferea does not start, try to set MOZILLA_FIVE_HOME to\n" #~ "another Mozilla installation or delete the gconf configuration\n" #~ "key /apps/liferea/browser-module!\n" #~ "\n" #~ msgstr "" #~ "\n" #~ "試著載入 Mozilla 瀏覽器模組... 註: 這可能無法在每個 Mozilla \n" #~ "版本中使用。如果您碰到問題而 Liferea 無法啟動,請試著將 \n" #~ "MOZILLA_FIVE_HOME 設定到另外一個 Mozilla 安裝目錄或刪除 gconf\n" #~ " 設定中的 /apps/liferea/browser-module!\n" #~ "\n" #~ msgid "" #~ "Failed to open HTML widget module (%s) specified in configuration!\n" #~ "%s\n" #~ msgstr "" #~ "無法開啟設定中的 HTML 元件模組 (%s)!\n" #~ "%s\n" #~ msgid "Htmlview API mismatch!" #~ msgstr "Htmlview API 版本不一致!" #~ msgid "Detected module is not a valid htmlview module!" #~ msgstr "偵測到的模組並非有效的 htmlview 模組!" #~ msgid "Available browser modules (%s):\n" #~ msgstr "可用的瀏覽器模組 (%s)\n" #~ msgid "Loading configured browser module (%s)!\n" #~ msgstr "載入已設定瀏覽器模組(%s)!\n" #~ msgid "No browser module configured!\n" #~ msgstr "瀏覽器模組未設定!\n" #~ msgid "trying to load browser module %s (%s)\n" #~ msgstr "試著載入瀏覽器模組 %s (%s)\n" #~ msgid "internal error! time conversion error! mktime failed!\n" #~ msgstr "內部錯誤! 時間轉換錯誤! mktime 失敗!\n" #~ msgid "Invalid ISO8601 date format! Ignoring information!\n" #~ msgstr "無效的 ISO8601 日期格式! 忽略 資訊!\n" #~ msgid "" #~ "

Could not determine the feed type. Please check that it is a valid type and listed in the supported formats." #~ "

" #~ msgstr "" #~ "

無法偵測饋流類型。請確定為 有效的" #~ "饋流類型,並為 支援的格式

" #~ msgid "Please do a search first!" #~ msgstr "請先執行搜尋!" #~ msgid "You must select a feed entry." #~ msgstr "您必須選擇一饋流項目!" #~ msgid "New Feed" #~ msgstr "新增饋流" #~ msgid "Next Unread" #~ msgstr "下筆未讀" #~ msgid "Update All" #~ msgstr "更新全部" #~ msgid "Viewing Mode" #~ msgstr "檢視模式" #~ msgid "Switches between 2 and 3 pane mode." #~ msgstr "在兩個與三個窗格的模式作切換" #~ msgid "A folder must be selected." #~ msgstr "目錄必須先選擇。" #~ msgid "URL" #~ msgstr "網址" liferea-1.13.7/src/000077500000000000000000000000001415350204600140015ustar00rootroot00000000000000liferea-1.13.7/src/Makefile.am000066400000000000000000000073701415350204600160440ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in SUBDIRS = parsers ui fl_sources webkit tests . AM_CPPFLAGS = \ -DPACKAGE_DATA_DIR=\""$(datadir)"\" \ -DPACKAGE_LIB_DIR=\""$(pkglibdir)"\" \ -DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \ -DBIN_DIR=\""$(bindir)"\" \ -I$(top_srcdir)/src \ $(PACKAGE_CFLAGS) \ $(INTROSPECTION_CFLAGS) bin_PROGRAMS = liferea bin_SCRIPTS = liferea-add-feed liferea_SOURCES = \ auth.c auth.h \ auth_activatable.c auth_activatable.h \ browser.c browser.h \ browser_history.c browser_history.h \ comments.c comments.h \ common.c common.h \ conf.c conf.h \ date.c date.h \ db.c db.h \ dbus.c dbus.h \ debug.c debug.h \ enclosure.c enclosure.h \ export.c export.h \ favicon.c favicon.h \ feed.c feed.h \ feed_parser.c feed_parser.h \ feedlist.c feedlist.h \ folder.c folder.h \ html.c html.h \ item.c item.h \ item_history.c item_history.h \ item_loader.c item_loader.h \ item_state.c item_state.h \ itemset.c itemset.h \ itemlist.c itemlist.h \ js.c js.h \ json.c json.h \ liferea_application.c liferea_application.h \ metadata.c metadata.h \ migrate.c migrate.h \ net.c net.h \ net_monitor.c net_monitor.h \ newsbin.c newsbin.h \ node.c node.h \ node_type.c node_type.h \ node_view.h \ plugins_engine.c plugins_engine.h \ render.c render.h \ rule.c rule.h \ social.c social.h \ subscription.c subscription.h \ subscription_icon.c subscription_icon.h \ subscription_type.h \ update.c update.h \ main.c \ vfolder.c vfolder.h \ vfolder_loader.c vfolder_loader.h \ xml.c xml.h liferea_LDADD = parsers/libliparsers.a \ fl_sources/libliflsources.a \ ui/libliui.a \ webkit/libwebkit.a \ $(PACKAGE_LIBS) \ $(INTLLIBS) \ $(WEBKIT_LIBS) \ $(INTROSPECTION_LIBS) \ -lm js.h: $(top_srcdir)/js/gresource.xml $(top_srcdir)/js/htmlview.js $(top_srcdir)/js/Readability.js $(main_dep) glib-compile-resources --generate --target=$@ --c-name js --sourcedir=$(top_srcdir)/js $< js.c: $(top_srcdir)/js/gresource.xml $(top_srcdir)/js/htmlview.js $(top_srcdir)/js/Readability.js $(main_dep) glib-compile-resources --generate --target=$@ --c-name js --sourcedir=$(top_srcdir)/js $< js.o: js.c js.h main.o: js.h EXTRA_DIST = $(srcdir)/liferea-add-feed.in DISTCLEANFILES = $(srcdir)/liferea-add-feed AM_INSTALLCHECK_STD_OPTIONS_EXEMPT = liferea-add-feed -include $(INTROSPECTION_MAKEFILE) INTROSPECTION_GIRS = Liferea-3.0.gir Liferea-3.0.gir: liferea$(EXEEXT) INTROSPECTION_SCANNER_ARGS = -I$(top_srcdir)/src --warn-all --accept-unprefixed --identifier-prefix=Liferea --verbose Liferea_3_0_gir_NAMESPACE = Liferea Liferea_3_0_gir_VERSION = 3.0 Liferea_3_0_gir_PROGRAM = $(builddir)/liferea$(EXEEXT) Liferea_3_0_gir_FILES = \ auth.c auth.h \ auth_activatable.c auth_activatable.h \ enclosure.h \ feedlist.c feedlist.h \ item.h \ itemlist.c itemlist.h \ itemset.c itemset.h \ liferea_application.h \ node.h node.c \ node_view.h \ social.c social.h \ subscription_type.h \ ui/browser_tabs.c ui/browser_tabs.h \ ui/icons.c ui/icons.h \ ui/itemview.c ui/itemview.h \ ui/item_list_view.c ui/item_list_view.h \ ui/liferea_htmlview.c ui/liferea_htmlview.h \ ui/liferea_shell.c ui/liferea_shell.h \ ui/liferea_shell_activatable.c ui/liferea_shell_activatable.h \ ui/media_player.c ui/media_player.h \ ui/media_player_activatable.c ui/media_player_activatable.h \ fl_sources/node_source.c fl_sources/node_source.h \ fl_sources/node_source_activatable.c fl_sources/node_source_activatable.h Liferea_3_0_gir_INCLUDES = Gtk-3.0 libxml2-2.0 if HAVE_INTROSPECTION girdir = $(datadir)/liferea/gir-1.0 gir_DATA = $(INTROSPECTION_GIRS) typelibdir = $(libdir)/liferea/girepository-1.0 typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib) CLEANFILES = \ $(srcdir)/js.c $(srcdir)/js.h \ $(gir_DATA) \ $(typelib_DATA) endif liferea-1.13.7/src/auth.c000066400000000000000000000076551415350204600151230ustar00rootroot00000000000000/* * @file auth.c authentication helpers * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "auth.h" #include "auth_activatable.h" #include "plugins_engine.h" #include "subscription.h" #include #include // FIXME: This should be a member of some object! static PeasExtensionSet *extensions = NULL; /*<< Plugin management */ static gint count = 0; /*<< Number of active auth plugins */ static void on_extension_added (PeasExtensionSet *extensions, PeasPluginInfo *info, PeasExtension *exten, gpointer user_data) { peas_extension_call (exten, "activate"); count++; } static void on_extension_removed (PeasExtensionSet *extensions, PeasPluginInfo *info, PeasExtension *exten, gpointer user_data) { peas_extension_call (exten, "deactivate"); count--; } static PeasExtensionSet * liferea_auth_get_extension_set (void) { if (!extensions) { extensions = peas_extension_set_new (PEAS_ENGINE (liferea_plugins_engine_get_default ()), LIFEREA_AUTH_ACTIVATABLE_TYPE, NULL); g_signal_connect (extensions, "extension-added", G_CALLBACK (on_extension_added), NULL); g_signal_connect (extensions, "extension-removed", G_CALLBACK (on_extension_removed), NULL); peas_extension_set_foreach (extensions, on_extension_added, NULL); } return extensions; } static void liferea_auth_info_store_foreach (PeasExtensionSet *set, PeasPluginInfo *info, PeasExtension *exten, gpointer user_data) { subscriptionPtr subscription = (subscriptionPtr)user_data; g_assert (subscription != NULL); g_assert (subscription->node != NULL); g_assert (subscription->updateOptions != NULL); liferea_auth_activatable_store (LIFEREA_AUTH_ACTIVATABLE (exten), subscription->node->id, subscription->updateOptions->username, subscription->updateOptions->password); } void liferea_auth_info_store (gpointer user_data) { peas_extension_set_foreach (liferea_auth_get_extension_set (), liferea_auth_info_store_foreach, user_data); } void liferea_auth_info_from_store (const gchar *id, const gchar *username, const gchar *password) { nodePtr node = node_from_id (id); g_assert (NULL != node->subscription); node->subscription->updateOptions->username = g_strdup (username); node->subscription->updateOptions->password = g_strdup (password); } static void liferea_auth_info_query_foreach (PeasExtensionSet *set, PeasPluginInfo *info, PeasExtension *exten, gpointer data) { liferea_auth_activatable_query (LIFEREA_AUTH_ACTIVATABLE (exten), data); } void liferea_auth_info_query (const gchar *authId) { peas_extension_set_foreach (liferea_auth_get_extension_set (), liferea_auth_info_query_foreach, (gpointer)authId); } gboolean liferea_auth_has_active_store (void) { return (count > 0); } liferea-1.13.7/src/auth.h000066400000000000000000000037311415350204600151170ustar00rootroot00000000000000/* * @file auth.h authentication helpers * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _AUTH_H #define _AUTH_H #include /** * liferea_auth_has_active_store: * * Method to query whether there is an active password store. * * @returns TRUE if there is a password store */ gboolean liferea_auth_has_active_store (void); /** * liferea_auth_info_from_store: * * @param authId a node id * @param username * @param password * * Allow plugins to provide authentication infos */ void liferea_auth_info_from_store (const gchar *authId, const gchar *username, const gchar *password); /** * liferea_auth_info_store: * * @param subscription pointer to a subscription * * Save given authentication info of a given subscription into password store (if available). */ void liferea_auth_info_store (gpointer subscription); /** * liferea_auth_info_query: * * Return auth information for a given node. Each extension able to * supply a user name and password for the given id is to synchronously call * liferea_auth_info_from_store() to set them. * * @param authId a node id * @param username reference to return username * @param password reference to return password */ void liferea_auth_info_query (const gchar *authId); #endif liferea-1.13.7/src/auth_activatable.c000066400000000000000000000053621415350204600174530ustar00rootroot00000000000000/* * @file auth_activatable.c Auth Plugin Type * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "auth_activatable.h" /** * SECTION:liferea_auth_activatable * @short_description: Interface for activatable extensions providing auth infos * @see_also: #PeasExtensionSet * * #LifereaAuthActivatable is an interface which should be implemented by * extensions that want to provide a password store **/ G_DEFINE_INTERFACE (LifereaAuthActivatable, liferea_auth_activatable, G_TYPE_OBJECT) void liferea_auth_activatable_default_init (LifereaAuthActivatableInterface *iface) { /* No properties yet */ } void liferea_auth_activatable_activate (LifereaAuthActivatable * activatable) { LifereaAuthActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_AUTH_ACTIVATABLE (activatable)); iface = LIFEREA_AUTH_ACTIVATABLE_GET_IFACE (activatable); if (iface->activate) iface->activate (activatable); } void liferea_auth_activatable_deactivate (LifereaAuthActivatable * activatable) { LifereaAuthActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_AUTH_ACTIVATABLE (activatable)); iface = LIFEREA_AUTH_ACTIVATABLE_GET_IFACE (activatable); if (iface->deactivate) iface->deactivate (activatable); } void liferea_auth_activatable_query (LifereaAuthActivatable * activatable, const gchar *authId) { LifereaAuthActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_AUTH_ACTIVATABLE (activatable)); iface = LIFEREA_AUTH_ACTIVATABLE_GET_IFACE (activatable); if (iface->query) iface->query (activatable, authId); } void liferea_auth_activatable_store (LifereaAuthActivatable * activatable, const gchar *authId, const gchar *username, const gchar *password) { LifereaAuthActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_AUTH_ACTIVATABLE (activatable)); iface = LIFEREA_AUTH_ACTIVATABLE_GET_IFACE (activatable); if (iface->store) iface->store (activatable, authId, username, password); } liferea-1.13.7/src/auth_activatable.h000066400000000000000000000071141415350204600174550ustar00rootroot00000000000000/* * @file liferea_auth_activatable.h Shell Plugin Type * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_AUTH_ACTIVATABLE_H__ #define _LIFEREA_AUTH_ACTIVATABLE_H__ #include G_BEGIN_DECLS #define LIFEREA_AUTH_ACTIVATABLE_TYPE (liferea_auth_activatable_get_type ()) #define LIFEREA_AUTH_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_AUTH_ACTIVATABLE_TYPE, LifereaAuthActivatable)) #define LIFEREA_AUTH_ACTIVATABLE_IFACE(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), LIFEREA_AUTH_ACTIVATABLE_TYPE, LifereaAuthActivatableInterface)) #define LIFEREA_IS_AUTH_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_AUTH_ACTIVATABLE_TYPE)) #define LIFEREA_AUTH_ACTIVATABLE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), LIFEREA_AUTH_ACTIVATABLE_TYPE, LifereaAuthActivatableInterface)) typedef struct _LifereaAuthActivatable LifereaAuthActivatable; typedef struct _LifereaAuthActivatableInterface LifereaAuthActivatableInterface; struct _LifereaAuthActivatableInterface { GTypeInterface g_iface; void (*activate) (LifereaAuthActivatable * activatable); void (*deactivate) (LifereaAuthActivatable * activatable); void (*query) (LifereaAuthActivatable * activatable, const gchar *authId); void (*store) (LifereaAuthActivatable * activatable, const gchar *authId, const gchar *username, const gchar *password); }; GType liferea_auth_activatable_get_type (void) G_GNUC_CONST; /** * liferea_auth_activatable_activate: * @activatable: A #LifereaAuthActivatable. * * Activates the extension. */ void liferea_auth_activatable_activate (LifereaAuthActivatable *activatable); /** * liferea_auth_activatable_deactivate: * @activatable: A #LifereaAuthActivatable. * * Deactivates the extension. */ void liferea_auth_activatable_deactivate (LifereaAuthActivatable *activatable); /** * liferea_auth_activatable_query: * @activatable: a #LifereaAuthActivatable. * @authId: a unique auth info id * * Triggers a query for authentication infos for a given subscription. * Expects triggered plugins to use liferea_auth_info_add() to provide * any matches. */ void liferea_auth_activatable_query (LifereaAuthActivatable *activatable, const gchar *authId); /** * liferea_auth_activatable_store: * @activatable: a #LifereaAuthActivatable. * @authId: a unique auth info id * @username: the username to store * @password: the password to store * * Triggers a query for authentication infos for a given subscription. * Expects triggered plugins to use liferea_auth_info_add() to provide * any matches. */ void liferea_auth_activatable_store (LifereaAuthActivatable * activatable, const gchar *authId, const gchar *username, const gchar *password); G_END_DECLS #endif /* __LIFEREA_AUTH_ACTIVATABLE_H__ */ liferea-1.13.7/src/browser.c000066400000000000000000000070331415350204600156330ustar00rootroot00000000000000/** * @file browser.c Launching different external browsers * * Copyright (C) 2003-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "browser.h" #include #include "common.h" #include "conf.h" #include "debug.h" #include "ui/liferea_shell.h" /** * Returns a shell command format string which can be used to create * a browser launch command if preference for manual command is set. * * @returns a newly allocated command string (or NULL) */ static gchar * browser_get_manual_command (void) { gchar *cmd = NULL; gchar *libname; /* check for manual browser command */ conf_get_str_value (BROWSER_ID, &libname); if (g_str_equal (libname, "manual")) { /* retrieve user defined command... */ conf_get_str_value (BROWSER_COMMAND, &cmd); } g_free (libname); return cmd; } static gboolean browser_execute (const gchar *cmd, const gchar *uri) { GError *error = NULL; gchar *safeUri, *tmp, **argv, **iter; gint argc; gboolean done = FALSE; g_assert (cmd != NULL); g_assert (uri != NULL); safeUri = (gchar *)common_uri_sanitize ((xmlChar *)uri); /* If we run using a Mozilla like "-remote openURL()" mechanism we need to escape commata, but not in other cases (see SF #2901447) */ if (strstr(cmd, "openURL(")) safeUri = common_strreplace (safeUri, ",", "%2C"); /* If there is no %s in the command, then just append %s */ if (strstr (cmd, "%s")) tmp = g_strdup (cmd); else tmp = g_strdup_printf ("%s %%s", cmd); /* Parse and substitute the %s in the command */ g_shell_parse_argv (tmp, &argc, &argv, &error); g_free (tmp); if (error && (0 != error->code)) { liferea_shell_set_important_status_bar (_("Browser command failed: %s"), error->message); debug2 (DEBUG_GUI, "Browser command is invalid: %s : %s", tmp, error->message); g_error_free (error); return FALSE; } if (argv) { for (iter = argv; *iter != NULL; iter++) *iter = common_strreplace (*iter, "%s", safeUri); } tmp = g_strjoinv (" ", argv); debug1 (DEBUG_GUI, "Running the browser-remote %s command", tmp); g_spawn_async (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error); if (error && (0 != error->code)) { debug2 (DEBUG_GUI, "Browser command failed: %s : %s", tmp, error->message); liferea_shell_set_important_status_bar (_("Browser command failed: %s"), error->message); g_error_free (error); } else { liferea_shell_set_status_bar (_("Starting: \"%s\""), tmp); done = TRUE; } g_free (safeUri); g_free (tmp); g_strfreev (argv); return done; } gboolean browser_launch_URL_external (const gchar *uri) { gchar *cmd = NULL; gboolean done = FALSE; g_assert (uri != NULL); cmd = browser_get_manual_command (); if (cmd) { done = browser_execute (cmd, uri); g_free (cmd); } else { done = gtk_show_uri_on_window (GTK_WINDOW (liferea_shell_get_window ()), uri, GDK_CURRENT_TIME, NULL); } return done; } liferea-1.13.7/src/browser.h000066400000000000000000000022511415350204600156350ustar00rootroot00000000000000/** * @file browser.h Launching different external browsers * * Copyright (C) 2008-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _BROWSER_H #define _BROWSER_H #include /** * Function to execute the commands needed to open up a URL with the * browser specified in the preferences. * * @param the URI to load * * @returns TRUE if the URI was opened, or FALSE if there was an error */ gboolean browser_launch_URL_external (const gchar *uri); #endif liferea-1.13.7/src/browser_history.c000066400000000000000000000051541415350204600174160ustar00rootroot00000000000000/** * @file browser_history.c managing the internal browser history * * Copyright (C) 2012-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "browser_history.h" browserHistory * browser_history_new (void) { return g_new0 (browserHistory, 1); } static void browser_history_clear (browserHistory *history) { GList *iter; iter = history->locations; while (iter) { g_free (iter->data); iter = g_list_next (iter); } g_list_free (history->locations); history->locations = NULL; history->current = NULL; } void browser_history_free (browserHistory *history) { g_return_if_fail (NULL != history); browser_history_clear (history); g_free (history); } gchar * browser_history_forward (browserHistory *history) { GList *url = history->current; url = g_list_next (url); history->current = url; return url->data; } gchar * browser_history_back (browserHistory *history) { GList *url = history->current; url = g_list_previous (url); history->current = url; return url->data; } gboolean browser_history_can_go_forward (browserHistory *history) { return (NULL != g_list_next (history->current)); } gboolean browser_history_can_go_back (browserHistory *history) { return (NULL != g_list_previous (history->current)); } void browser_history_add_location (browserHistory *history, const gchar *url) { GList *iter; /* Do not add the same URL twice in a row... */ if (history->current && g_str_equal (history->current->data, url)) return; /* If current URL is not at the end of the list, truncate the rest of the list */ if (history->locations) { while (1) { iter = g_list_last (history->locations); if (!iter) break; if (iter == history->current) break; g_free (iter->data); history->locations = g_list_remove (history->locations, iter->data); } } history->locations = g_list_append (history->locations, g_strdup (url)); history->current = g_list_last (history->locations); } liferea-1.13.7/src/browser_history.h000066400000000000000000000045461415350204600174270ustar00rootroot00000000000000/** * @file browser_history.h managing internal browser URI history * * Copyright (C) 2012-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _BROWSER_HISTORY_H #define _BROWSER_HISTORY_H #include /** structure holding all URLs visited in a browser */ typedef struct browserHistory { GList *locations; /**< list of all visited URLs */ GList *current; /**< pointer into locations */ } browserHistory; /** * Create a new browser history. * * @returns the browser history */ browserHistory * browser_history_new (void); /** * Dispose of a browser history * * @param history the browser history */ void browser_history_free (browserHistory *history); /** * Set index of the browser history forward. * * @param history the browser history * * @returns the new selected URL (not to be free'd!) */ gchar * browser_history_forward (browserHistory *history); /** * Set index of the browser history backwards. * * @param history the browser history * * @returns the new selected URL (not to be free'd!) */ gchar * browser_history_back (browserHistory *history); /** * Check whether the history can go forward. * * @param history the browser history * * @returns TRUE if it can go forward */ gboolean browser_history_can_go_forward (browserHistory *history); /** * Check whether the history can go back. * * @param history the browser history * * @returns TRUE if it can go back */ gboolean browser_history_can_go_back (browserHistory *history); /** * Add a URL to the history. * * @param history the browser history * @param url the URL to add */ void browser_history_add_location (browserHistory *history, const gchar *url); #endif liferea-1.13.7/src/comments.c000066400000000000000000000177131415350204600160030ustar00rootroot00000000000000/** * @file comments.c comment feed handling * * Copyright (C) 2007-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "comments.h" #include "common.h" #include "db.h" #include "debug.h" #include "feed.h" #include "metadata.h" #include "net.h" #include "net_monitor.h" #include "update.h" #include "ui/itemview.h" /* Comment feeds in Liferea are simple flat lists of items attached to a single item. Each item that has a comment feed URL in its metadata list gets its comment feed updated as soon as the user triggers rendering of the item in 3 pane mode. Although rendered differently items and comment items are handled in the same way. */ static GHashTable *commentFeeds = NULL; typedef struct commentFeed { gulong itemId; /**< parent item id */ gchar *id; /**< id of the items comments feed (or NULL) */ gchar *error; /**< description of error if comments download failed (or NULL)*/ struct updateJob *updateJob; /**< update job structure used when downloading comments */ updateStatePtr updateState; /**< update states (etag, last modified, cookies, last polling times...) used when downloading comments */ } *commentFeedPtr; static void comment_feed_free (commentFeedPtr commentFeed) { if (commentFeed->updateJob) update_job_cancel_by_owner (commentFeed); if (commentFeed->updateState) update_state_free (commentFeed->updateState); g_free (commentFeed->error); g_free (commentFeed->id); g_free (commentFeed); } static void comment_feed_free_cb (gpointer key, gpointer value, gpointer user_data) { comment_feed_free (value); } void comments_deinit (void) { if (commentFeeds) { g_hash_table_foreach (commentFeeds, comment_feed_free_cb, NULL); g_hash_table_destroy (commentFeeds); commentFeeds = NULL; } } /** * Hash lookup to find comment feeds with the given id. * Returns the comment feed (or NULL). */ static commentFeedPtr comment_feed_from_id (const gchar *id) { if (!commentFeeds) return NULL; return (commentFeedPtr) g_hash_table_lookup (commentFeeds, id); } static void comments_process_update_result (const struct updateResult * const result, gpointer user_data, updateFlags flags) { feedParserCtxtPtr ctxt; commentFeedPtr commentFeed = (commentFeedPtr)user_data; itemPtr item; nodePtr node; debug_enter ("comments_process_update_result"); item = item_load (commentFeed->itemId); g_return_if_fail (item != NULL); /* note this is to update the feed URL on permanent redirects */ if (result->source && !g_strcmp0 (result->source, metadata_list_get (item->metadata, "commentFeedUri"))) { debug2 (DEBUG_UPDATE, "updating comment feed URL from \"%s\" to \"%s\"", metadata_list_get (item->metadata, "commentFeedUri"), result->source); metadata_list_set (&(item->metadata), "commentFeedUri", result->source); } if (401 == result->httpstatus) { /* unauthorized */ commentFeed->error = g_strdup (_("Authorization Error")); } else if (410 == result->httpstatus) { /* gone */ metadata_list_set (&item->metadata, "commentFeedGone", "true"); } else if (304 == result->httpstatus) { debug1(DEBUG_UPDATE, "comment feed \"%s\" did not change", result->source); } else if (result->data) { debug1(DEBUG_UPDATE, "received update result for comment feed \"%s\"", result->source); /* parse the new downloaded feed into fake node, subscription and feed */ node = node_new (feed_get_node_type ()); node_set_data (node, feed_new ()); node_set_subscription (node, subscription_new (result->source, NULL, NULL)); ctxt = feed_parser_ctxt_new (node->subscription, result->data, result->size); if (!feed_parse (ctxt)) { debug0 (DEBUG_UPDATE, "parsing comment feed failed!"); } else { itemSetPtr comments; GList *iter; /* before merging mark all downloaded items as comments */ iter = ctxt->items; while (iter) { itemPtr comment = (itemPtr) iter->data; comment->isComment = TRUE; comment->parentItemId = commentFeed->itemId; comment->parentNodeId = g_strdup (item->nodeId); iter = g_list_next (iter); } debug1 (DEBUG_UPDATE, "parsing comment feed successful (%d comments downloaded)", g_list_length(ctxt->items)); comments = db_itemset_load (commentFeed->id); itemset_merge_items (comments, ctxt->items, ctxt->feed->valid, FALSE); itemset_free (comments); /* No comment feed truncating as comment items are automatically dropped when the parent items are removed from cache. */ } node_free (ctxt->subscription->node); feed_parser_ctxt_free (ctxt); } /* update error message */ g_free (commentFeed->error); commentFeed->error = NULL; if ((result->httpstatus < 200) || (result->httpstatus >= 400)) commentFeed->error = g_strdup (network_strerror (result->httpstatus)); /* clean up... */ commentFeed->updateJob = NULL; update_state_free (commentFeed->updateState); commentFeed->updateState = update_state_copy (result->updateState); /* rerender item with new comments */ itemview_update_item (item); itemview_update (); item_unload (item); debug_exit ("comments_process_update_result"); } void comments_refresh (itemPtr item) { commentFeedPtr commentFeed = NULL; UpdateRequest *request; const gchar *url; if (!network_monitor_is_online ()) return; if (metadata_list_get (item->metadata, "commentFeedGone")) { debug0 (DEBUG_UPDATE, "Comment feed returned HTTP 410. Not updating anymore!"); return; } url = metadata_list_get (item->metadata, "commentFeedUri"); if (url) { debug2 (DEBUG_UPDATE, "Updating comments for item \"%s\" (comment URL: %s)", item->title, url); // FIXME: restore update state from DB? if (item->commentFeedId) { commentFeed = comment_feed_from_id (item->commentFeedId); } else { item->commentFeedId = node_new_id (); db_item_update (item); } if (!commentFeed) { commentFeed = g_new0 (struct commentFeed, 1); commentFeed->id = g_strdup (item->commentFeedId); commentFeed->itemId = item->id; if (!commentFeeds) commentFeeds = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (commentFeeds, commentFeed->id, commentFeed); } request = update_request_new ( url, commentFeed->updateState, NULL // FIXME: use copy of parent subscription options ); commentFeed->updateJob = update_execute_request (commentFeed, request, comments_process_update_result, commentFeed, FEED_REQ_PRIORITY_HIGH | FEED_REQ_NO_FEED); /* Item view refresh to change link from "Update" to "Updating..." */ itemview_update_item (item); itemview_update (); } } void comments_to_xml (xmlNodePtr parentNode, const gchar *id) { xmlNodePtr commentsNode; commentFeedPtr commentFeed; itemSetPtr itemSet; GList *iter; commentFeed = comment_feed_from_id (id); if (!commentFeed) return; commentsNode = xmlNewChild (parentNode, NULL, BAD_CAST "comments", NULL); itemSet = db_itemset_load (id); g_return_if_fail (itemSet != NULL); iter = itemSet->ids; while (iter) { itemPtr comment = item_load (GPOINTER_TO_UINT (iter->data)); item_to_xml (comment, commentsNode); item_unload (comment); iter = g_list_next (iter); } xmlNewTextChild (commentsNode, NULL, BAD_CAST "updateState", BAD_CAST ((commentFeed->updateJob)?"updating":"ok")); if (commentFeed->error) xmlNewTextChild (commentsNode, NULL, BAD_CAST "updateError", BAD_CAST commentFeed->error); itemset_free (itemSet); } liferea-1.13.7/src/comments.h000066400000000000000000000026151415350204600160030ustar00rootroot00000000000000/** * @file comments.h comment feed handling * * Copyright (C) 2007-2008 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _COMMENTS_H #define _COMMENTS_H #include #include #include "item.h" /** * Frees everything related with comments */ void comments_deinit (void); /** * Triggers immediate comments retrieval (or update) for the given item. * * @param item the item */ void comments_refresh (itemPtr item); /** * Adds the comments and state of the given comment feed id to the * passed XML node. * * @param parentNode XML parent node * @param id the comment feed id */ void comments_to_xml (xmlNodePtr parentNode, const gchar *id); #endif liferea-1.13.7/src/common.c000066400000000000000000000252361415350204600154450ustar00rootroot00000000000000/** * @file common.c common routines for Liferea * * Copyright (C) 2003-2013 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004 Karl Soderstrom * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "debug.h" #if defined (G_OS_WIN32) && !defined (HAVE_GMTIME_R) #define gmtime_r(t,o) gmtime_s (o,t) #endif static gboolean pathsChecked = FALSE; long common_parse_long (const gchar *str, long def) { long num; if (str == NULL) return def; if (0 == (sscanf (str,"%ld", &num))) num = def; return num; } static void common_check_dir (gchar *path) { if (!g_file_test (path, G_FILE_TEST_IS_DIR)) { if (0 != g_mkdir_with_parents (path, S_IRUSR | S_IWUSR | S_IXUSR)) { g_error (_("Cannot create cache directory \"%s\"!"), path); } } g_free (path); } static void common_init_paths (void) { gchar *lifereaCachePath = g_build_filename (g_get_user_cache_dir(), "liferea", NULL); common_check_dir (g_strdup (lifereaCachePath)); common_check_dir (g_build_filename (lifereaCachePath, "feeds", NULL)); common_check_dir (g_build_filename (lifereaCachePath, "favicons", NULL)); common_check_dir (g_build_filename (lifereaCachePath, "plugins", NULL)); common_check_dir (g_build_filename (g_get_user_config_dir(), "liferea", NULL)); common_check_dir (g_build_filename (g_get_user_data_dir(), "liferea", NULL)); /* ensure reasonable default umask */ umask (077); g_free (lifereaCachePath); pathsChecked = TRUE; } gchar * common_create_data_filename (const gchar *filename) { if (!pathsChecked) common_init_paths (); return g_build_filename (g_get_user_data_dir (), "liferea", filename, NULL); } gchar * common_create_config_filename (const gchar *filename) { if (!pathsChecked) common_init_paths (); return g_build_filename (g_get_user_config_dir (), "liferea", filename, NULL); } gchar * common_create_cache_filename (const gchar *folder, const gchar *filename, const gchar *extension) { gchar *result; if (!pathsChecked) common_init_paths (); result = g_strdup_printf ("%s%s%s%s%s%s%s", g_get_user_cache_dir (), G_DIR_SEPARATOR_S "liferea" G_DIR_SEPARATOR_S, folder ? folder : "", folder ? G_DIR_SEPARATOR_S : "", filename, extension ? "." : "", extension ? extension : ""); return result; } xmlChar * common_uri_escape (const xmlChar *url) { xmlChar *result, *tmp; g_assert (NULL != url); /* xmlURIEscape returns NULL if spaces are in the URL, so we need to replace them first (see SF #2965158) */ tmp = (xmlChar *)common_strreplace (g_strdup ((gchar *)url), " ", "%20"); result = xmlURIEscape (tmp); g_free (tmp); /* workaround if escaping somehow fails... */ if (!result) result = (xmlChar *)g_strdup ((gchar *)url); return result; } xmlChar * common_uri_unescape (const xmlChar *url) { return (xmlChar *)xmlURIUnescapeString ((char *)url, -1, NULL); } xmlChar * common_uri_sanitize (const xmlChar *uri) { xmlChar *tmp, *result; /* We must escape all dangerous characters (e.g. & and spaces) in the URL. As we do not know if the URL is already escaped we simply unescape and reescape it. */ tmp = common_uri_unescape (uri); result = common_uri_escape (tmp); g_free (tmp); return result; } /* to correctly escape and expand URLs */ xmlChar * common_build_url (const gchar *url, const gchar *baseURL) { xmlChar *escapedURL, *absURL, *escapedBaseURL; escapedURL = common_uri_escape ((xmlChar *)url); if (baseURL) { escapedBaseURL = common_uri_escape ((xmlChar *)baseURL); absURL = xmlBuildURI (escapedURL, escapedBaseURL); if (absURL) xmlFree (escapedURL); else absURL = escapedURL; xmlFree (escapedBaseURL); } else { absURL = escapedURL; } return absURL; } /* * Replacement for pango_find_base_dir * Based on code from pango_unichar_direction and pango_find_base_dir */ PangoDirection common_find_base_dir (const gchar *text, gint length) { FriBidiCharType fbd_ch_type; PangoDirection dir = PANGO_DIRECTION_NEUTRAL; const gchar *p; gunichar ch; G_STATIC_ASSERT (sizeof (FriBidiChar) == sizeof (gunichar)); g_return_val_if_fail (text != NULL || length == 0, PANGO_DIRECTION_NEUTRAL); p = text; while ((length < 0 || p < text + length) && *p) { ch = g_utf8_get_char (p); fbd_ch_type = fribidi_get_bidi_type (ch); if (!FRIBIDI_IS_STRONG (fbd_ch_type)) dir = PANGO_DIRECTION_NEUTRAL; else if (FRIBIDI_IS_RTL (fbd_ch_type)) dir = PANGO_DIRECTION_RTL; else dir = PANGO_DIRECTION_LTR; if (dir != PANGO_DIRECTION_NEUTRAL) break; p = g_utf8_next_char (p); } return dir; } /* * Returns a string that can be used for the HTML "dir" attribute. * Direction is taken from a string, regardless of any language tags. */ const gchar * common_get_text_direction (const gchar *text) { PangoDirection pango_direction = PANGO_DIRECTION_NEUTRAL; if (text) pango_direction = common_find_base_dir (text, -1); if (pango_direction == PANGO_DIRECTION_RTL) return ("rtl"); else return ("ltr"); } const gchar * common_get_app_direction (void) { GtkTextDirection gtk_direction; gtk_direction = gtk_widget_get_default_direction (); if (gtk_direction == GTK_TEXT_DIR_RTL) return ("rtl"); else return ("ltr"); } #ifndef HAVE_STRSEP /* code taken from glibc-2.2.1/sysdeps/generic/strsep.c */ char * common_strsep (char **stringp, const char *delim) { char *begin, *end; begin = *stringp; if (begin == NULL) return NULL; /* A frequent case is when the delimiter string contains only one character. Here we don't need to call the expensive `strpbrk' function and instead work using `strchr'. */ if (delim[0] == '\0' || delim[1] == '\0') { char ch = delim[0]; if (ch == '\0') end = NULL; else { if (*begin == ch) end = begin; else if (*begin == '\0') end = NULL; else end = strchr (begin + 1, ch); } } else /* Find the end of the token. */ end = strpbrk (begin, delim); if (end) { /* Terminate the token and set *STRINGP past NUL character. */ *end++ = '\0'; *stringp = end; } else /* No more delimiters; this is the last token. */ *stringp = NULL; return begin; } #endif /* HAVE_STRSEP */ /* Taken from gaim 24 June 2004, copyrighted by the gaim developers under the GPL, etc.... It was slightly modified to free the passed string */ gchar * common_strreplace (gchar *string, const gchar *delimiter, const gchar *replacement) { gchar **split; gchar *ret; g_return_val_if_fail (string != NULL, NULL); g_return_val_if_fail (delimiter != NULL, NULL); g_return_val_if_fail (replacement != NULL, NULL); split = g_strsplit (string, delimiter, 0); ret = g_strjoinv (replacement, split); g_strfreev (split); g_free (string); return ret; } typedef unsigned chartype; /* strcasestr is Copyright (C) 1994, 1996-2000, 2004 Free Software Foundation, Inc. It was taken from the GNU C Library, which is licenced under the GPL v2.1 or (at your option) newer version. */ char * common_strcasestr (const char *phaystack, const char *pneedle) { register const unsigned char *haystack, *needle; register chartype b, c; haystack = (const unsigned char *) phaystack; needle = (const unsigned char *) pneedle; b = tolower(*needle); if (b != '\0') { haystack--; /* possible ANSI violation */ do { c = *++haystack; if (c == '\0') goto ret0; } while (tolower(c) != (int) b); c = tolower(*++needle); if (c == '\0') goto foundneedle; ++needle; goto jin; for (;;) { register chartype a; register const unsigned char *rhaystack, *rneedle; do { a = *++haystack; if (a == '\0') goto ret0; if (tolower(a) == (int) b) break; a = *++haystack; if (a == '\0') goto ret0; shloop: ; } while (tolower(a) != (int) b); jin: a = *++haystack; if (a == '\0') goto ret0; if (tolower(a) != (int) c) goto shloop; rhaystack = haystack-- + 1; rneedle = needle; a = tolower(*rneedle); if (tolower(*rhaystack) == (int) a) do { if (a == '\0') goto foundneedle; ++rhaystack; a = tolower(*++needle); if (tolower(*rhaystack) != (int) a) break; if (a == '\0') goto foundneedle; ++rhaystack; a = tolower(*++needle); } while (tolower (*rhaystack) == (int) a); needle = rneedle; /* took the register-poor approach */ if (a == '\0') break; } } foundneedle: return (char*) haystack; ret0: return 0; } gboolean common_str_is_empty (const gchar *s) { if (s == NULL) return TRUE; while (*s != '\0') { if (!g_ascii_isspace (*s)) return FALSE; s++; } return TRUE; } time_t common_get_mod_time (const gchar *file) { struct stat attribute; struct tm tm; if (stat (file, &attribute) == 0) { gmtime_r (&(attribute.st_mtime), &tm); return mktime (&tm); } else { /* this is no error as this method is used to check for files */ return 0; } } gchar * common_get_localized_filename (const gchar *str) { const gchar *const *languages = g_get_language_names(); int i = 0; while (languages[i] != NULL) { gchar *filename = g_strdup_printf (str, strcmp (languages[i], "C") ? languages[i] : "en"); if (g_file_test (filename, G_FILE_TEST_IS_REGULAR)) return filename; g_free (filename); i++; } g_warning ("No file found for %s", str); return NULL; } void common_copy_file (const gchar *src, const gchar *dest) { gchar *content; gsize length; if (g_file_get_contents (src, &content, &length, NULL)) g_file_set_contents (dest, content, length, NULL); g_free (content); } liferea-1.13.7/src/common.h000066400000000000000000000150721415350204600154470ustar00rootroot00000000000000/** * @file common.h common routines * * Copyright (C) 2003-2012 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _COMMON_H #define _COMMON_H #include #include #include #include #include #include #include /* * Standard gettext macros */ #ifdef ENABLE_NLS # include # undef _ # define _(String) dgettext (PACKAGE, String) # define Q_(String) g_strip_context ((String), gettext (String)) # ifdef gettext_noop # define N_(String) gettext_noop (String) # else # define N_(String) (String) # endif #else # define textdomain(String) (String) # define gettext(String) (String) # define dgettext(Domain,Message) (Message) # define dcgettext(Domain,Message,Type) (Message) # define bindtextdomain(Domain,Directory) (Domain) # define _(String) (String) # define Q_(String) g_strip_context ((String), (String)) # define N_(String) (String) #endif /** * Parses the given string as a number. * * @param str the string to parse * @param def default value to return on parsing error * * @returns result value */ long common_parse_long (const gchar *str, long def); /** * Method to build user data file names. * * @param filename the user data filename (with extension) * * @returns a newly allocated filename string (to be free'd using g_free) */ gchar * common_create_data_filename (const gchar *filename); /** * Method to build config file names. * * @param filename the cache filename (with extension) * * @returns a newly allocated filename string (to be free'd using g_free) */ gchar * common_create_config_filename (const gchar *filename); /** * Method to build cache file names. * * @param folder a subfolder in the cache dir (optional) * @param filename the cache filename (without extension) * @param extension the cache filename extension * * @returns a newly allocated filename string (to be free'd using g_free) */ gchar * common_create_cache_filename(const gchar *folder, const gchar *filename, const gchar *extension); /** * Takes an URI and returns a new string containing the escaped URI. * * @param uri the URI to escape * * @returns new escaped URI string */ xmlChar * common_uri_escape(const xmlChar *uri); /** * Takes an URI and returns a new string containing the unescaped URI. * * @param uri the URI to unescape * * @returns new unescaped URI string */ xmlChar * common_uri_unescape(const xmlChar *uri); /** * Takes an URI of uncertain safety (e.g. partially escaped) and * returns if fully escaped safe for passing to a shell or browser. * This means the resulting URL is ensured to have no quotes, spaces * or &. Note: commata are not escaped! * * @param uri the URI to escape * * @returns new escaped URI string */ xmlChar * common_uri_sanitize(const xmlChar *uri); /** * To correctly escape and expand URLs, does not touch the * passed strings. * * @param url relative URL * @param baseURL base URL * * @returns new string with resulting absolute URL */ xmlChar * common_build_url(const gchar *url, const gchar *baseURL); /** * Replacement for deprecated pango_find_base_dir * Searches a string the first character that has a strong direction, * according to the Unicode bidirectional algorithm. * * @param text The text to process. Must be valid UTF-8 * * @param length Length of text in bytes * (may be -1 if text is nul-terminated) * * @returns The direction corresponding to the first strong character. * If no such character is found, then * PANGO_DIRECTION_NEUTRAL is returned. */ PangoDirection common_find_base_dir (const gchar *text, gint length); /** * Analyzes the string, returns a direction setting immediately * useful for insertion into HTML * * @param text string to analyze * * @returns a constant "ltr" (default) or "rtl" */ const gchar * common_get_text_direction(const gchar *text); /** * Returns the overall directionality of the application * * @returns a constant "ltr" (default) or "rtl" */ const gchar * common_get_app_direction(void); #ifndef HAVE_STRSEP char * common_strsep(char **stringp, const char *delim); #define strsep(a,b) common_strsep(a,b) #endif /** * Replaces delimiter in string with a replacement string. * * @param string original string (will be freed) * @param delimiter match string * @param replacement replacement string * * @returns a new modified string */ gchar *common_strreplace(gchar *string, const gchar *delimiter, const gchar *replacement); /** * Case insensitive strstr() like searching. * * @param pneedle a string to find * @param phaystack the string to search in * * @returns first found position or NULL */ char * common_strcasestr(const char *phaystack, const char *pneedle); /** * Checks if a string is empty, when leading and trailing whitespace is ignored * * @param string a string to check * * @returns TRUE if string only contains whitespace or is NULL, FALSE otherwise */ gboolean common_str_is_empty (const gchar *string); /** * Get file modification timestamp * * @param *file the file name * * @returns modification timestamp (or 0 if file doesn't exist) */ time_t common_get_mod_time(const gchar *file); /** * Create a localized filename. * * This function tries all applicable locale names and replaces * the %s in str with the first one that points to an existing file. * "en" is always among the searched locale names. * * @param str full path containing one %s * * @returns a string with %s replaced (to be freed by the caller), or NULL */ gchar *common_get_localized_filename (const gchar *str); /** * Copy a source file to a target filename. * * @param src absolute source file path * @param dest absolute destination file path */ void common_copy_file (const gchar *src, const gchar *dest); #endif liferea-1.13.7/src/conf.c000066400000000000000000000205641415350204600151010ustar00rootroot00000000000000/** * @file conf.c Liferea configuration (GSettings access) * * Copyright (C) 2011 Mikel Olasagasti Uranga * Copyright (C) 2003-2015 Lars Windolf * Copyright (C) 2004,2005 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include "common.h" #include "conf.h" #include "debug.h" #include "net.h" #include "update.h" #include "ui/liferea_shell.h" #define MAX_GCONF_PATHLEN 256 #define LIFEREA_SCHEMA_NAME "net.sf.liferea" #define DESKTOP_SCHEMA_NAME "org.gnome.desktop.interface" static GSettings *settings; static GSettings *desktop_settings; /* Function prototypes */ static void conf_proxy_reset_settings_cb(GSettings *settings, guint cnxn_id, gchar *key, gpointer user_data); static void conf_toolbar_style_settings_cb(GSettings *settings, guint cnxn_id, gchar *key, gpointer user_data); static void conf_ensure_migrated (const gchar *name) { gboolean needed = TRUE; GKeyFile *kf; gchar **list; gsize i, n; kf = g_key_file_new (); g_key_file_load_from_data_dirs (kf, "gsettings-data-convert", NULL, G_KEY_FILE_NONE, NULL); list = g_key_file_get_string_list (kf, "State", "converted", &n, NULL); if (list) { for (i = 0; i < n; i++) { if (strcmp (list[i], name) == 0) { needed = FALSE; break; } } g_strfreev (list); } g_key_file_free (kf); if (needed) g_spawn_command_line_sync ("gsettings-data-convert", NULL, NULL, NULL, NULL); } /* called once on startup */ void conf_init (void) { /* ensure we migrated from gconf to gsettings */ conf_ensure_migrated (LIFEREA_SCHEMA_NAME); /* initialize GSettings client */ settings = g_settings_new (LIFEREA_SCHEMA_NAME); desktop_settings = g_settings_new(DESKTOP_SCHEMA_NAME); g_signal_connect ( desktop_settings, "changed::" TOOLBAR_STYLE, G_CALLBACK (conf_toolbar_style_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_DETECT_MODE, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_HOST, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_PORT, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_USEAUTH, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_USER, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); g_signal_connect ( settings, "changed::" PROXY_PASSWD, G_CALLBACK (conf_proxy_reset_settings_cb), NULL ); /* Load settings into static buffers */ conf_proxy_reset_settings_cb (NULL, 0, NULL, NULL); } void conf_deinit (void) { g_object_unref (settings); g_object_unref (desktop_settings); } static void conf_toolbar_style_settings_cb (GSettings *settings, guint cnxn_id, gchar *key, gpointer user_data) { gchar *style = conf_get_toolbar_style (); if (style) { liferea_shell_set_toolbar_style (style); g_free (style); } } static void conf_proxy_reset_settings_cb (GSettings *settings, guint cnxn_id, gchar *key, gpointer user_data) { gchar *proxyname, *proxyusername, *proxypassword; gint proxyport; gint proxydetectmode; gboolean proxyuseauth; proxyname = NULL; proxyport = 0; proxyusername = NULL; proxypassword = NULL; conf_get_int_value (PROXY_DETECT_MODE, &proxydetectmode); #if !WEBKIT_CHECK_VERSION (2, 15, 3) if (proxydetectmode != PROXY_DETECT_MODE_AUTO) { GtkWidget *dialog = gtk_message_dialog_new (NULL, 0, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, _("Your version of WebKitGTK+ doesn't support changing the proxy settings from Liferea. The system's default proxy settings will be used.")); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); conf_set_int_value (PROXY_DETECT_MODE, PROXY_DETECT_MODE_AUTO); return; } #endif switch (proxydetectmode) { default: case 0: debug0 (DEBUG_CONF, "proxy auto detect is configured"); /* nothing to do, all done by libproxy inside libsoup */ break; case 1: debug0 (DEBUG_CONF, "proxy is disabled by user"); /* nothing to do */ break; case 2: debug0 (DEBUG_CONF, "manual proxy is configured"); conf_get_str_value (PROXY_HOST, &proxyname); conf_get_int_value (PROXY_PORT, &proxyport); conf_get_bool_value (PROXY_USEAUTH, &proxyuseauth); if (proxyuseauth) { conf_get_str_value (PROXY_USER, &proxyusername); conf_get_str_value (PROXY_PASSWD, &proxypassword); } break; } debug4 (DEBUG_CONF, "Manual proxy settings are now %s:%d %s:%s", proxyname != NULL ? proxyname : "NULL", proxyport, proxyusername != NULL ? proxyusername : "NULL", proxypassword != NULL ? proxypassword : "NULL"); network_set_proxy (proxydetectmode, proxyname, proxyport, proxyusername, proxypassword); } /*----------------------------------------------------------------------*/ /* generic configuration access methods */ /*----------------------------------------------------------------------*/ void conf_set_bool_value (const gchar *key, gboolean value) { g_assert (key != NULL); g_settings_set_boolean (settings, key, value); } void conf_set_str_value (const gchar *key, const gchar *value) { g_assert (key != NULL); g_settings_set_string (settings, key, value); } void conf_set_strv_value (const gchar *key, const gchar **value) { g_assert (key != NULL); g_settings_set_strv (settings, key, value); } void conf_set_int_value (const gchar *key, gint value) { g_assert (key != NULL); debug2 (DEBUG_CONF, "Setting %s to %d", key, value); g_settings_set_int (settings, key, value); } gchar * conf_get_toolbar_style(void) { gchar *style; conf_get_str_value (TOOLBAR_STYLE, &style); /* check if we don't override the toolbar style */ if (strcmp(style, "") == 0) { g_free (style); conf_get_str_value_from_schema (desktop_settings,"toolbar-style", &style); } return style; } gboolean conf_get_bool_value_from_schema (GSettings *gsettings, const gchar *key, gboolean *value) { g_assert (key != NULL); g_assert (value != NULL); if (gsettings == NULL) gsettings = settings; *value = g_settings_get_boolean (gsettings,key); return *value; } gboolean conf_get_str_value_from_schema (GSettings *gsettings, const gchar *key, gchar **value) { g_assert (key != NULL); g_assert (value != NULL); if (gsettings == NULL) gsettings = settings; *value = g_settings_get_string (gsettings, key); return (NULL != value); } gboolean conf_get_strv_value_from_schema (GSettings *gsettings, const gchar *key, gchar ***value) { g_assert (key != NULL); g_assert (value != NULL); if (gsettings == NULL) gsettings = settings; *value = g_settings_get_strv (gsettings, key); return (NULL != value); } gboolean conf_get_int_value_from_schema (GSettings *gsettings, const gchar *key, gint *value) { g_assert (key != NULL); g_assert (value != NULL); if (gsettings == NULL) gsettings = settings; *value = g_settings_get_int (gsettings,key); return (NULL != value); } gboolean conf_get_default_font_from_schema (const gchar *key, gchar **value) { g_assert (key != NULL); g_assert (value != NULL); if (desktop_settings) *value = g_strdup (g_settings_get_string (desktop_settings, key)); return (NULL != value); } void conf_signal_connect (const gchar *signal, GCallback cb, gpointer data) { g_signal_connect (settings, signal, cb, data); } void conf_bind (const gchar *key, gpointer object, const gchar *property, GSettingsBindFlags flags) { g_assert (settings); g_settings_bind (settings, key, object, property, flags); } liferea-1.13.7/src/conf.h000066400000000000000000000163051415350204600151040ustar00rootroot00000000000000/** * @file conf.h Liferea configuration (GSettings access) * * Copyright (C) 2011 Mikel Olasagasti Uranga * Copyright (C) 2003-2017 Lars Windolf * Copyright (C) 2004,2005 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _CONF_H #define _CONF_H #include #include /* browsing settings */ #define BROWSE_INSIDE_APPLICATION "browse-inside-application" #define BROWSE_KEY_SETTING "browse-key-setting" #define BROWSER_ID "browser-id" #define BROWSER_COMMAND "browser" #define DEFAULT_VIEW_MODE "default-view-mode" #define DEFAULT_FONT "document-font-name" #define USER_FONT "browser-font" #define DISABLE_JAVASCRIPT "disable-javascript" #define SOCIAL_BM_SITE "social-bm-site" #define ENABLE_PLUGINS "enable-plugins" #define ENABLE_ITP "enable-itp" #define ENABLE_READER_MODE "enable-reader-mode" /* enclosure handling */ #define DOWNLOAD_CUSTOM_COMMAND "download-custom-command" #define DOWNLOAD_TOOL "download-tool" #define DOWNLOAD_USE_CUSTOM_COMMAND "download-use-custom-command" /* feed handling settings */ #define DEFAULT_MAX_ITEMS "maxitemcount" #define DEFAULT_UPDATE_INTERVAL "default-update-interval" #define STARTUP_FEED_ACTION "startup-feed-action" /* folder handling settings */ #define FOLDER_DISPLAY_MODE "folder-display-mode" #define FOLDER_DISPLAY_HIDE_READ "folder-display-hide-read" #define REDUCED_FEEDLIST "reduced-feedlist" /* GUI settings and persistency values */ #define CONFIRM_MARK_ALL_READ "confirm-mark-all-read" #define DISABLE_TOOLBAR "disable-toolbar" #define TOOLBAR_STYLE "toolbar-style" #define LAST_WINDOW_STATE "last-window-state" #define LAST_WINDOW_X "last-window-x" #define LAST_WINDOW_Y "last-window-y" #define LAST_WINDOW_WIDTH "last-window-width" #define LAST_WINDOW_HEIGHT "last-window-height" #define LAST_WINDOW_MAXIMIZED "last-window-maximized" #define LAST_VPANE_POS "last-vpane-pos" #define LAST_HPANE_POS "last-hpane-pos" #define LAST_WPANE_POS "last-wpane-pos" #define LAST_ZOOMLEVEL "last-zoomlevel" #define LAST_NODE_SELECTED "last-node-selected" #define LIST_VIEW_COLUMN_ORDER "list-view-column-order" /* networking settings */ #define PROXY_DETECT_MODE "proxy-detect-mode" #define PROXY_HOST "proxy-host" #define PROXY_PORT "proxy-port" #define PROXY_USEAUTH "proxy-use-authentication" #define PROXY_USER "proxy-authentication-user" #define PROXY_PASSWD "proxy-authentication-password" #define DO_NOT_TRACK "do-not-track" /* initializing methods */ void conf_init (void); void conf_deinit (void); /* preferences access methods */ #define conf_get_bool_value(key, value) conf_get_bool_value_from_schema (NULL, key, value) #define conf_get_str_value(key, value) conf_get_str_value_from_schema (NULL, key, value) #define conf_get_strv_value(key, value) conf_get_strv_value_from_schema (NULL, key, value) #define conf_get_int_value(key, value) conf_get_int_value_from_schema (NULL, key, value) /** * Retrieves the value of the given boolean configuration key. * * @param gsettings gsettings schema to use * @param key the configuration key * @param value the value, if the function returned FALSE it's always FALSE * * @returns TRUE if the configuration key was found */ gboolean conf_get_bool_value_from_schema (GSettings *gsettings, const gchar *key, gboolean *value); /** * Retrieves the value of the given string configuration key. * The string has to be freed by the caller. * * @param gsettings gsettings schema to use * @param key the configuration key * @param value the value, if the function returned FALSE an empty string * * @returns TRUE if the configuration key was found */ gboolean conf_get_str_value_from_schema (GSettings *gsettings,const gchar *key, gchar **value); /** * Retrieves the value of the given string array configuration key. * The string array has to be freed by the caller. * * @param gsettings gsettings schema to use * @param key the configuration key * @param value the value, if the function returned FALSE an empty string * * @returns TRUE if the configuration key was found */ gboolean conf_get_strv_value_from_schema (GSettings *gsettings,const gchar *key, gchar ***value); /** * Retrieves the value of the given integer configuration key. * * @param gsettings gsettings schema to use * @param key the configuration key * @param value the value, if the function returned FALSE it's always 0 * * @returns TRUE if the configuration key was found */ gboolean conf_get_int_value_from_schema (GSettings *gsettings, const gchar *key, gint *value); /** * Sets the value of the given boolean configuration key. * * @param key the configuration key * @param value the new boolean value */ void conf_set_bool_value (const gchar *key, gboolean value); /** * Sets the value of the given string configuration key. * The given value will not be free'd after setting it! * * @param key the configuration key * @param value the new string value */ void conf_set_str_value (const gchar *key, const gchar *value); /** * Sets the value of the given string configuration key. * The given value will not be free'd after setting it! * * @param key the configuration key * @param value the new string value */ void conf_set_strv_value (const gchar *key, const gchar **value); /** * Sets the value of the given integer configuration key * * @param key the configuration key * @param value the new integer value */ void conf_set_int_value (const gchar *key, gint value); /** * Returns the current toolbar configuration. * * @returns a string (to be free'd using g_free) */ gchar * conf_get_toolbar_style (void); /** * Get the current system default font from desktop schema * * @param key the configuration key * @param value the value, if the function returned FALSE it's always 0 * * @returns TRUE if the configuration key was found */ gboolean conf_get_default_font_from_schema (const gchar *key, gchar **value); /** * Connect to a signal in the default GSettings object * * @param signal the signal to connect to * @param cb callback to invoke when the signal is emitted * @param data user data to pass to the callback */ void conf_signal_connect (const gchar *signal, GCallback cb, gpointer data); /** * conf_bind: * @key: the configuration key * @object: a GObject * @property: the object's property to bind * @flags: binding flags * * This is a convenience function that calls g_settings_bind with Liferea settings. */ void conf_bind (const gchar *key, gpointer object, const gchar *property, GSettingsBindFlags flags); #endif liferea-1.13.7/src/date.c000066400000000000000000000302101415350204600150560ustar00rootroot00000000000000/** * @file date.c date formatting routines * * Copyright (C) 2008-2011 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * The date formatting was reused from the Evolution code base * * Author: Chris Lahey * Jeffrey Stedfast * * Copyright 2000 Helix Code, Inc. (www.helixcode.com) * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "date.h" #include #include "common.h" #include "debug.h" /* date formatting methods */ /** * Originally from Evolution e-util.c * * Function to do a last minute fixup of the AM/PM stuff if the locale * and gettext haven't done it right. Most English speaking countries * except the USA use the 24 hour clock (UK, Australia etc). However * since they are English nobody bothers to write a language * translation (gettext) file. So the locale turns off the AM/PM, but * gettext does not turn on the 24 hour clock. Leaving a mess. * * This routine checks if AM/PM are defined in the locale, if not it * forces the use of the 24 hour clock. * * The function itself is a front end on strftime and takes exactly * the same arguments. * * TODO: Actually remove the '%p' from the fixed up string so that * there isn't a stray space. **/ static gchar * e_utf8_strftime_fix_am_pm (const char *fmt, GDateTime *tm) { gchar *buf; char *sp; char *ffmt; gchar *datestr; if (g_strstr_len (fmt, -1, "%p")==NULL && g_strstr_len (fmt, -1, "%P")==NULL) { /* No AM/PM involved - can use the fmt string directly */ datestr = g_date_time_format (tm, fmt); } else { /* Get the AM/PM symbol from the locale */ buf = g_date_time_format (tm, "%p"); if (buf && buf[0]) { /** * AM/PM have been defined in the locale * so we can use the fmt string directly **/ datestr = g_date_time_format (tm, fmt); } else { /** * No AM/PM defined by locale * must change to 24 hour clock **/ ffmt=g_strdup(fmt); for (sp=ffmt; (sp = g_strstr_len (sp, -1, "%l")); sp++) { /** * Maybe this should be 'k', but I have never * seen a 24 clock actually use that format **/ sp[1]='H'; } for (sp=ffmt; (sp=g_strstr_len (sp, -1, "%I")); sp++) { sp[1]='H'; } datestr = g_date_time_format (tm, ffmt); g_free(ffmt); } g_free (buf); } return datestr; } /* This function is originally from the Evolution 2.6.2 code (e-cell-date.c) */ static gchar * date_format_nice (gint64 date) { GDateTime *then, *now, *yesterday; gchar *temp, *buf; gboolean done = FALSE; then = g_date_time_new_from_unix_local (date); now = g_date_time_new_now_local (); if ((date == 0) || (then == NULL)) { return g_strdup (""); } /* if (nowdate - date < 60 * 60 * 8 && nowdate > date) { e_utf8_strftime_fix_am_pm (buf, TIMESTRLEN, _("%l:%M %p"), &then); done = TRUE; }*/ if (!done) { if (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (now) && g_date_time_get_year (then) == g_date_time_get_year (now)) { /* translation hint: date format for today, reorder format codes as necessary */ buf = e_utf8_strftime_fix_am_pm (_("Today %l:%M %p"), then); done = TRUE; } } if (!done) { yesterday = g_date_time_add_days (now, -1); if (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (yesterday) && g_date_time_get_year (then) == g_date_time_get_year (yesterday)) { /* translation hint: date format for yesterday, reorder format codes as necessary */ buf = e_utf8_strftime_fix_am_pm (_("Yesterday %l:%M %p"), then); done = TRUE; } g_date_time_unref (yesterday); } if (!done) { yesterday = g_date_time_add_days (now, -6); if ((g_date_time_compare (now, then) > 0) && (g_date_time_compare (then, yesterday) > 0 || (g_date_time_get_day_of_year (then) == g_date_time_get_day_of_year (yesterday) && g_date_time_get_year (then) == g_date_time_get_year (yesterday)))) { /* translation hint: date format for dates older than 2 days but not older than a week, reorder format codes as necessary */ buf = e_utf8_strftime_fix_am_pm (_("%a %l:%M %p"), then); done = TRUE; } g_date_time_unref (yesterday); } if (!done) { if (g_date_time_get_year (then) == g_date_time_get_year (now)) { /* translation hint: date format for dates older than a week but from this year, reorder format codes as necessary */ buf = e_utf8_strftime_fix_am_pm (_("%b %d %l:%M %p"), then); } else { /* translation hint: date format for dates from the last years, reorder format codes as necessary */ buf = e_utf8_strftime_fix_am_pm (_("%b %d %Y"), then); } } g_date_time_unref (then); g_date_time_unref (now); temp = buf; while ((temp = strstr (temp, " "))) { memmove (temp, temp + 1, strlen (temp)); } temp = g_strstrip (buf); return temp; } gchar * date_format (gint64 date, const gchar *date_format) { gchar *result; GDateTime *date_tm; if (date == 0) { return g_strdup (""); } if (date_format) { date_tm = g_date_time_new_from_unix_local (date); result = e_utf8_strftime_fix_am_pm (date_format, date_tm); g_date_time_unref (date_tm); } else { result = date_format_nice (date); } return result; } /* date parsing methods */ gint64 date_parse_ISO8601 (const gchar *date) { GDateTime *datetime = NULL; guint64 year, month, day; gint64 t = 0; gchar *pos, *next, *ascii_date = NULL; g_assert (date != NULL); /* we expect at least something like "2003-08-07T15:28:19" and don't require the second fractions and the timezone info the most specific format: YYYY-MM-DDThh:mm:ss.sTZD */ /* full specified variant */ datetime = g_date_time_new_from_iso8601 (date, NULL); if (datetime) { t = g_date_time_to_unix (datetime); g_date_time_unref (datetime); if (t) return t; } /* only date */ ascii_date = g_str_to_ascii (date, "C"); ascii_date = g_strstrip (ascii_date); /* Parsing year */ year = g_ascii_strtoull (ascii_date, &next, 10); if ((*next != '-') || (next == ascii_date)) goto parsing_failed; pos = next + 1; /* Parsing month */ month = g_ascii_strtoull (pos, &next, 10); if ((*next != '-') || (next == pos)) goto parsing_failed; pos = next + 1; /* Parsing day */ day = g_ascii_strtoull (pos, &next, 10); if ((*next != '\0') || (next == pos)) goto parsing_failed; /* there were others combinations too... */ datetime = g_date_time_new_utc (year, month, day, 0,0,0); if (datetime) { t = g_date_time_to_unix (datetime); g_date_time_unref (datetime); } parsing_failed: if (!t) debug0 (DEBUG_PARSING, "Invalid ISO8601 date format! Ignoring information!"); g_free (ascii_date); return t; } /* in theory, we'd need only the RFC822 timezones here in practice, feeds also use other timezones... */ static struct { const char *name; const char *offset; } tz_offsets [] = { { "IDLW","-1200" }, { "HAST","-1000" }, { "AKST","-0900" }, { "AKDT","-0800" }, { "WESZ","+0100" }, { "WEST","+0100" }, { "WEDT","+0100" }, { "MEST","+0200" }, { "MESZ","+0200" }, { "CEST","+0200" }, { "CEDT","+0200" }, { "EEST","+0300" }, { "EEDT","+0300" }, { "IRST","+0430" }, { "CNST","+0800" }, { "ACST","+0930" }, { "ACDT","+1030" }, { "AEST","+1000" }, { "AEDT","+1100" }, { "IDLE","+1200" }, { "NZST","+1200" }, { "NZDT","+1300" }, { "GMT", "+00" }, { "EST", "-0500" }, { "EDT", "-0400" }, { "CST", "-0600" }, { "CDT", "-0500" }, { "MST", "-0700" }, { "MDT", "-0600" }, { "PST", "-0800" }, { "PDT", "-0700" }, { "HDT", "-0900" }, { "YST", "-0900" }, { "YDT", "-0800" }, { "AST", "-0400" }, { "ADT", "-0300" }, { "VST", "-0430" }, { "NST", "-0330" }, { "NDT", "-0230" }, { "WET", "+00" }, { "WEZ", "+00" }, { "IST", "+0100" }, { "CET", "+0100" }, { "MEZ", "+0100" }, { "EET", "+0200" }, { "MSK", "+0300" }, { "MSD", "+0400" }, { "IRT", "+0330" }, { "IST", "+0530" }, { "ICT", "+0700" }, { "JST", "+0900" }, { "NFT", "+1130" }, { "UT", "+00" }, { "PT", "-0800" }, { "BT", "+0300" }, { "Z", "+00" }, { "A", "-0100" }, { "M", "-1200" }, { "N", "+0100" }, { "Y", "+1200" } }; /** date_parse_rfc822_tz: * @token: String representing the timezone. * * Returns: (transfer full): a GTimeZone to be freed by g_time_zone_unref or NULL on error */ static GTimeZone * date_parse_rfc822_tz (char *token) { const char *inptr = token; int num_timezones = sizeof (tz_offsets) / sizeof ((tz_offsets)[0]); if (*inptr == '+' || *inptr == '-') { #ifdef HAVE_G_TIME_ZONE_NEW_IDENTIFIER return g_time_zone_new_identifier (inptr); #else return g_time_zone_new (inptr); #endif } else { int t; if (*inptr == '(') inptr++; for (t = 0; t < num_timezones; t++) if (!strncmp (inptr, tz_offsets[t].name, strlen (tz_offsets[t].name))) #ifdef HAVE_G_TIME_ZONE_NEW_IDENTIFIER return g_time_zone_new_identifier (tz_offsets[t].offset); #else return g_time_zone_new (tz_offsets[t].offset); #endif } return NULL; } static const gchar * rfc822_months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; GDateMonth date_parse_month (const gchar *str) { int i; for (i = 0;i < 12;i++) { if (!g_ascii_strncasecmp (str, rfc822_months[i], 3)) return i + 1; } return 0; } gint64 date_parse_RFC822 (const gchar *date) { guint64 day, month, year, hour, minute, second = 0; GTimeZone *tz = NULL; GDateTime *datetime = NULL; gint64 t = 0; gchar *pos, *next, *ascii_date = NULL; /* we expect at least something like "03 Dec 12 01:38:34" and don't require a day of week or the timezone the most specific format we expect: "Fri, 03 Dec 12 01:38:34 CET" */ /* skip day of week */ pos = g_utf8_strchr(date, -1, ','); if (pos) date = ++pos; ascii_date = g_str_to_ascii (date, "C"); /* Parsing day */ day = g_ascii_strtoull (ascii_date, &next, 10); if ((*next == '\0') || (next == ascii_date)) goto parsing_failed; pos = next; /* Parsing month */ while (pos && *pos != '\0' && g_ascii_isspace (*pos)) /* skip whitespaces before month */ pos++; if (strlen (pos) < 3) goto parsing_failed; month = date_parse_month (pos); pos += 3; /* Parsing year */ year = g_ascii_strtoull (pos, &next, 10); if ((*next == '\0') || (next == pos)) goto parsing_failed; if (year < 100) { /* If year is 2 digits, years after 68 are in 20th century (strptime convention) */ if (year > 68) year += 1900; else year += 2000; } pos = next; /* Parsing hour */ hour = g_ascii_strtoull (pos, &next, 10); if ((next == pos) || (*next != ':')) goto parsing_failed; pos = next + 1; /* Parsing minute */ minute = g_ascii_strtoull (pos, &next, 10); if (next == pos) goto parsing_failed; /* Optional second */ if (*next == ':') { pos = next + 1; second = g_ascii_strtoull (pos, &next, 10); if (next == pos) goto parsing_failed; } pos = next; /* Optional Timezone */ while (pos && *pos != '\0' && g_ascii_isspace (*pos)) /* skip whitespaces before timezone */ pos++; if (*pos != '\0') tz = date_parse_rfc822_tz (pos); if (!tz) datetime = g_date_time_new_utc (year, month, day, hour, minute, second); else { datetime = g_date_time_new (tz, year, month, day, hour, minute, second); g_time_zone_unref (tz); } if (datetime) { t = g_date_time_to_unix (datetime); g_date_time_unref (datetime); } parsing_failed: if (!t) debug0 (DEBUG_PARSING, "Invalid RFC822 date !"); g_free (ascii_date); return t; } liferea-1.13.7/src/date.h000066400000000000000000000034401415350204600150700ustar00rootroot00000000000000/** * @file date.h date formatting and parsing routines for Liferea * * Copyright (C) 2008-2009 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _DATE_H #define _DATE_H #include /** * Generic date formatting function. Uses either the * user defined format string, or (if date_format is NULL) * a formatted date string whose format string depends * on the time difference to today. * * @param t the timestamp * @param date_format NULL or a strptime format string (encoded in UTF-8) * * @returns a newly allocated formatted date string (encoded in UTF-8) */ gchar * date_format (gint64 date, const gchar *date_format); /** * Parses a ISO8601 date. * * @param date the date string to parse * * @returns timestamp */ gint64 date_parse_ISO8601 (const gchar *date); /** * Parses a RFC822 format date. This FAILS if a timezone string is * specified such as EDT or EST and that timezone is in daylight * savings time. * * @param date the date string to parse * * @returns timestamp (GMT, no daylight savings time) */ gint64 date_parse_RFC822 (const gchar *date); #endif liferea-1.13.7/src/db.c000066400000000000000000001357651415350204600145530ustar00rootroot00000000000000/** * @file db.c sqlite backend * * Copyright (C) 2007-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "item.h" #include "itemset.h" #include "metadata.h" #include "vfolder.h" static sqlite3 *db = NULL; gboolean searchFolderRebuild = FALSE; /** hash of all prepared statements */ static GHashTable *statements = NULL; static void db_view_remove (const gchar *id); static void db_prepare_stmt (sqlite3_stmt **stmt, const gchar *sql) { gint res; const char *left; res = sqlite3_prepare_v2 (db, sql, -1, stmt, &left); if ((SQLITE_BUSY == res) || (SQLITE_LOCKED == res)) { g_warning ("The Liferea cache DB seems to be used by another instance (error code=%d)! Only one accessing instance is allowed.", res); exit(1); } if (SQLITE_OK != res) g_error ("Failure while preparing statement, (error=%d, %s) SQL: \"%s\"", res, sqlite3_errmsg(db), sql); } static void db_new_statement (const gchar *name, const gchar *sql) { if (!statements) statements = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (statements, (gpointer)name, (gpointer)sql); } static sqlite3_stmt * db_get_statement (const gchar *name) { sqlite3_stmt *statement; gchar *sql; sql = (gchar *) g_hash_table_lookup (statements, name); db_prepare_stmt (&statement, sql); if (!statement) g_error ("Fatal: unknown prepared statement \"%s\" requested!", name); sqlite3_reset (statement); return statement; } static void db_exec (const gchar *sql) { gchar *err; gint res; debug1 (DEBUG_DB, "executing SQL: %s", sql); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (1 >= res) { debug2 (DEBUG_DB, " -> result: %d (%s)", res, err?err:"success"); } else { g_warning ("Unexpected status on SQL execution: %d (%s)", res, err?err:"success"); } sqlite3_free (err); } static gboolean db_table_exists (const gchar *name) { gchar *sql; sqlite3_stmt *stmt; gint res = 0; sql = sqlite3_mprintf ("SELECT COUNT(type) FROM sqlite_master WHERE type = 'table' AND name = '%s';", name); db_prepare_stmt (&stmt, sql); sqlite3_reset (stmt); if (SQLITE_ROW == sqlite3_step (stmt)) res = sqlite3_column_int (stmt, 0); sqlite3_finalize (stmt); sqlite3_free (sql); return (1 == res); } static void db_set_schema_version (gint schemaVersion) { gchar *err, *sql; sql = sqlite3_mprintf ("REPLACE INTO info (name, value) VALUES ('schemaVersion',%d);", schemaVersion); if (SQLITE_OK != sqlite3_exec (db, sql, NULL, NULL, &err)) debug1 (DEBUG_DB, "setting schema version failed: %s", err); sqlite3_free (sql); sqlite3_free (err); } static gint db_get_schema_version (void) { guint schemaVersion = 0; sqlite3_stmt *stmt; if (!db_table_exists ("info")) { db_exec ("CREATE TABLE info ( " " name TEXT, " " value TEXT, " " PRIMARY KEY (name) " ");"); db_set_schema_version (-1); } db_prepare_stmt (&stmt, "SELECT value FROM info WHERE name = 'schemaVersion'"); if (SQLITE_ROW == sqlite3_step (stmt)) schemaVersion = sqlite3_column_int (stmt, 0); sqlite3_finalize (stmt); return schemaVersion; } static void db_begin_transaction (void) { gchar *sql, *err; gint res; sql = sqlite3_mprintf ("BEGIN"); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) g_warning ("Transaction begin failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); } static void db_end_transaction (void) { gchar *sql, *err; gint res; sql = sqlite3_mprintf ("END"); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) g_warning ("Transaction end failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); } #define VACUUM_ON_FRAGMENTATION_RATIO 10 static void db_vacuum (void) { sqlite3_stmt *stmt; gint res, page_count, freelist_count; /* Determine fragmentation ratio using PRAGMA page_count PRAGMA freelist_count as suggested by adriatic in this blog post http://jeff.ecchi.ca/blog/2011/12/24/investigating-lifereas-startup-performance/#comment-19989 and perform VACUUM only when needed. */ db_prepare_stmt (&stmt, "PRAGMA page_count"); sqlite3_reset (stmt); res = sqlite3_step (stmt); if (SQLITE_ROW != res) g_error ("Could not determine page count (error code %d)!", res); page_count = sqlite3_column_int (stmt, 0); sqlite3_finalize (stmt); db_prepare_stmt (&stmt, "PRAGMA freelist_count"); sqlite3_reset (stmt); res = sqlite3_step (stmt); if (SQLITE_ROW != res) g_error ("Could not determine free list count (error code %d)!", res); freelist_count = sqlite3_column_int (stmt, 0); sqlite3_finalize (stmt); float fragmentation = (100 * (float)freelist_count/page_count); if (fragmentation > VACUUM_ON_FRAGMENTATION_RATIO) { debug2 (DEBUG_DB, "Performing VACUUM as freelist count/page count ratio %2.2f > %d", fragmentation, VACUUM_ON_FRAGMENTATION_RATIO); debug_start_measurement (DEBUG_DB); db_exec ("VACUUM;"); debug_end_measurement (DEBUG_DB, "VACUUM"); } else { debug2 (DEBUG_DB, "No VACUUM as freelist count/page count ratio %2.2f <= %d", fragmentation, VACUUM_ON_FRAGMENTATION_RATIO); } } static void db_open (void) { gchar *filename; gint res; filename = common_create_data_filename ("liferea.db"); debug1 (DEBUG_DB, "Opening DB file %s...", filename); res = sqlite3_open_v2 (filename, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (SQLITE_OK != res) g_error ("Data base file %s could not be opened (error code %d: %s)...", filename, res, sqlite3_errmsg (db)); g_free (filename); sqlite3_extended_result_codes (db, TRUE); db_exec("PRAGMA journal_mode=WAL"); db_exec("PRAGMA page_size=32768"); db_exec("PRAGMA synchronous=NORMAL"); } #define SCHEMA_TARGET_VERSION 10 /* opening or creation of database */ void db_init (void) { gint res; debug_enter ("db_init"); db_open (); /* create info table/check versioning info */ debug1 (DEBUG_DB, "current DB schema version: %d", db_get_schema_version ()); if (-1 == db_get_schema_version ()) { /* no schema version available -> first installation without tables... */ db_set_schema_version (SCHEMA_TARGET_VERSION); /* nothing exists yet, tables will be created below */ } if (SCHEMA_TARGET_VERSION < db_get_schema_version ()) g_error ("Fatal: The cache database was created by a newer version of Liferea than this one!"); if (SCHEMA_TARGET_VERSION > db_get_schema_version ()) { /* do table migration */ if (db_get_schema_version () < 5) g_error ("This version of Liferea doesn't support migrating from such an old DB file!"); if (db_get_schema_version () == 5 || db_get_schema_version () == 6) { debug0 (DEBUG_DB, "dropping triggers in preparation of database migration"); db_exec ("BEGIN; " "DROP TRIGGER item_removal; " "DROP TRIGGER item_insert; " "END;"); } if (db_get_schema_version () == 5) { /* 1.4.9 -> 1.4.10 adding parent_item_id to itemset relation */ debug0 (DEBUG_DB, "migrating from schema version 5 to 6 (this drops all comments)"); db_exec ("BEGIN; " "DELETE FROM itemsets WHERE comment = 1; " "DELETE FROM items WHERE comment = 1; " "CREATE TEMPORARY TABLE itemsets_backup(item_id,node_id,read,comment); " "INSERT INTO itemsets_backup SELECT item_id,node_id,read,comment FROM itemsets; " "DROP TABLE itemsets; " "CREATE TABLE itemsets (" " item_id INTEGER," " parent_item_id INTEGER," " node_id TEXT," " read INTEGER," " comment INTEGER," " PRIMARY KEY (item_id, node_id)" "); " "INSERT INTO itemsets SELECT item_id,0,node_id,read,comment FROM itemsets_backup; " "DROP TABLE itemsets_backup; " "REPLACE INTO info (name, value) VALUES ('schemaVersion',6); " "END;"); } if (db_get_schema_version () == 6) { /* 1.4.15 -> 1.4.16 adding parent_node_id to itemset relation */ debug0 (DEBUG_DB, "migrating from schema version 6 to 7 (this drops all comments)"); db_exec ("BEGIN; " "DELETE FROM itemsets WHERE comment = 1; " "DELETE FROM items WHERE comment = 1; " "CREATE TEMPORARY TABLE itemsets_backup(item_id,node_id,read,comment); " "INSERT INTO itemsets_backup SELECT item_id,node_id,read,comment FROM itemsets; " "DROP TABLE itemsets; " "CREATE TABLE itemsets (" " item_id INTEGER," " parent_item_id INTEGER," " node_id TEXT," " parent_node_id TEXT," " read INTEGER," " comment INTEGER," " PRIMARY KEY (item_id, node_id)" "); " "INSERT INTO itemsets SELECT item_id,0,node_id,node_id,read,comment FROM itemsets_backup; " "DROP TABLE itemsets_backup; " "REPLACE INTO info (name, value) VALUES ('schemaVersion',7); " "END;"); } if (db_get_schema_version () == 7) { /* 1.7.1 -> 1.7.2 dropping the itemsets and attention_stats relation */ db_exec ("BEGIN; " "CREATE TEMPORARY TABLE items_backup(" " item_id, " " title, " " read, " " updated, " " popup, " " marked, " " source, " " source_id, " " valid_guid, " " description, " " date, " " comment_feed_id, " " comment); " "INSERT into items_backup SELECT ROWID, title, read, updated, popup, marked, source, source_id, valid_guid, description, date, comment_feed_id, comment FROM items; " "DROP TABLE items; " "CREATE TABLE items (" " item_id INTEGER," " parent_item_id INTEGER," " node_id TEXT," " parent_node_id TEXT," " title TEXT," " read INTEGER," " updated INTEGER," " popup INTEGER," " marked INTEGER," " source TEXT," " source_id TEXT," " valid_guid INTEGER," " description TEXT," " date INTEGER," " comment_feed_id INTEGER," " comment INTEGER," " PRIMARY KEY (item_id)" ");" "INSERT INTO items SELECT itemsets.item_id, parent_item_id, node_id, parent_node_id, title, itemsets.read, updated, popup, marked, source, source_id, valid_guid, description, date, comment_feed_id, itemsets.comment FROM items_backup JOIN itemsets ON itemsets.item_id = items_backup.item_id; " "DROP TABLE items_backup; " "DROP TABLE itemsets; " "REPLACE INTO info (name, value) VALUES ('schemaVersion',8); " "END;" ); db_exec ("DROP TABLE attention_stats"); /* this is unconditional, no checks and backups needed */ } if (db_get_schema_version () == 8) { gchar *sql; sqlite3_stmt *stmt; /* 1.7.3 -> 1.7.4 change search folder handling */ db_exec ("BEGIN; " "DROP TABLE view_state; " "DROP TABLE update_state; " "CREATE TABLE search_folder_items (" " node_id STRING," " item_id INTEGER," " PRIMARY KEY (node_id, item_id)" ");" "REPLACE INTO info (name, value) VALUES ('schemaVersion',9); " "END;" ); debug0 (DEBUG_DB, "Removing all views."); sql = sqlite3_mprintf("SELECT name FROM sqlite_master WHERE type='view';"); res = sqlite3_prepare_v2 (db, sql, -1, &stmt, NULL); sqlite3_free (sql); if (SQLITE_OK != res) { debug1 (DEBUG_DB, "Could not determine views (error=%d)", res); } else { sqlite3_reset (stmt); while (sqlite3_step (stmt) == SQLITE_ROW) { const gchar *viewName = (gchar *)sqlite3_column_text (stmt, 0) + strlen("view_"); gchar *copySql = g_strdup_printf("INSERT INTO search_folder_items (node_id, item_id) SELECT '%s',item_id FROM view_%s;", viewName, viewName); db_exec (copySql); db_view_remove (viewName); g_free (copySql); } sqlite3_finalize (stmt); } } if (db_get_schema_version () == 9) { /* A parent node id to search folder relation to allow cleanups */ db_exec ("BEGIN; " "DROP TABLE search_folder_items; " "CREATE TABLE search_folder_items (" " node_id STRING," " parent_node_id STRING," " item_id INTEGER," " PRIMARY KEY (node_id, item_id)" ");" "REPLACE INTO info (name, value) VALUES ('schemaVersion',10); " "END;" ); searchFolderRebuild = TRUE; } } if (SCHEMA_TARGET_VERSION != db_get_schema_version ()) g_error ("Fatal: DB schema version not up-to-date! Running with --debug-db could give some hints about the problem!"); /* Vacuuming... */ db_vacuum (); /* Schema creation */ debug_start_measurement (DEBUG_DB); db_begin_transaction (); /* 1. Create tables if they do not exist yet */ db_exec ("CREATE TABLE items (" " item_id INTEGER," " parent_item_id INTEGER," " node_id TEXT," /* FIXME: migrate node ids to real integers */ " parent_node_id TEXT," /* FIXME: migrate node ids to real integers */ " title TEXT," " read INTEGER," " updated INTEGER," " popup INTEGER," " marked INTEGER," " source TEXT," " source_id TEXT," " valid_guid INTEGER," " description TEXT," " date INTEGER," " comment_feed_id TEXT," " comment INTEGER," " PRIMARY KEY (item_id)" ");"); db_exec ("CREATE INDEX items_idx ON items (source_id);"); db_exec ("CREATE INDEX items_idx2 ON items (comment_feed_id);"); db_exec ("CREATE INDEX items_idx3 ON items (node_id);"); db_exec ("CREATE INDEX items_idx4 ON items (item_id);"); db_exec ("CREATE INDEX items_idx5 ON items (parent_item_id);"); db_exec ("CREATE INDEX items_idx6 ON items (parent_node_id);"); db_exec ("CREATE TABLE metadata (" " item_id INTEGER," " nr INTEGER," " key TEXT," " value TEXT," " PRIMARY KEY (item_id, nr)" ");"); db_exec ("CREATE INDEX metadata_idx ON metadata (item_id);"); db_exec ("CREATE TABLE subscription (" " node_id STRING," " source STRING," " orig_source STRING," " filter_cmd STRING," " update_interval INTEGER," " default_interval INTEGER," " discontinued INTEGER," " available INTEGER," " PRIMARY KEY (node_id)" ");"); db_exec ("CREATE TABLE subscription_metadata (" " node_id STRING," " nr INTEGER," " key TEXT," " value TEXT," " PRIMARY KEY (node_id, nr)" ");"); db_exec ("CREATE INDEX subscription_metadata_idx ON subscription_metadata (node_id);"); db_exec ("CREATE TABLE node (" " node_id STRING," " parent_id STRING," " title STRING," " type INTEGER," " expanded INTEGER," " view_mode INTEGER," " sort_column INTEGER," " sort_reversed INTEGER," " PRIMARY KEY (node_id)" ");"); db_exec ("CREATE TABLE search_folder_items (" " node_id STRING," " parent_node_id STRING," " item_id INTEGER," " PRIMARY KEY (node_id, item_id)" ");"); db_end_transaction (); debug_end_measurement (DEBUG_DB, "table setup"); /* 2. Removing old triggers */ db_exec ("DROP TRIGGER item_insert;"); db_exec ("DROP TRIGGER item_update;"); db_exec ("DROP TRIGGER item_removal;"); db_exec ("DROP TRIGGER subscription_removal;"); /* 3. Cleanup of DB */ /* Note: do not check on subscriptions here, as non-subscription node types (e.g. news bin) do contain items too. */ debug0 (DEBUG_DB, "Checking for items without a feed list node...\n"); db_exec ("DELETE FROM items WHERE comment = 0 AND node_id NOT IN " "(SELECT node_id FROM node);"); debug0 (DEBUG_DB, "Checking for comments without parent item...\n"); db_exec ("BEGIN; " " CREATE TEMP TABLE tmp_id ( id );" " INSERT INTO tmp_id SELECT item_id FROM items WHERE comment = 1 AND parent_item_id NOT IN (SELECT item_id FROM items WHERE comment = 0);" /* limit to 1000 items as it is very slow */ " DELETE FROM items WHERE item_id IN (SELECT id FROM tmp_id LIMIT 1000);" " DROP TABLE tmp_id;" "END;"); debug0 (DEBUG_DB, "Checking for search folder items without a feed list node...\n"); db_exec ("DELETE FROM search_folder_items WHERE parent_node_id NOT IN " "(SELECT node_id FROM node);"); debug0 (DEBUG_DB, "Checking for search folder items without a search folder...\n"); db_exec ("DELETE FROM search_folder_items WHERE node_id NOT IN " "(SELECT node_id FROM node);"); debug0 (DEBUG_DB, "Checking for search folder with comments...\n"); db_exec ("DELETE FROM search_folder_items WHERE comment = 1;"); debug0 (DEBUG_DB, "Checking for subscription metadata without node...\n"); db_exec ("DELETE FROM subscription_metadata WHERE node_id NOT IN " "(SELECT node_id FROM node);"); debug0 (DEBUG_DB, "Removing metadata without item...\n"); db_exec ("DELETE FROM metadata WHERE item_id NOT IN " "(SELECT item_id FROM items);"); debug0 (DEBUG_DB, "DB cleanup finished. Continuing startup."); /* 4. Creating triggers (after cleanup so it is not slowed down by triggers) */ /* This trigger does explicitely not remove comments! */ db_exec ("CREATE TRIGGER item_removal DELETE ON items " "BEGIN " " DELETE FROM metadata WHERE item_id = old.item_id; " " DELETE FROM search_folder_items WHERE item_id = old.item_id; " "END;"); db_exec ("CREATE TRIGGER subscription_removal DELETE ON subscription " "BEGIN " " DELETE FROM node WHERE node_id = old.node_id; " " DELETE FROM subscription_metadata WHERE node_id = old.node_id; " " DELETE FROM search_folder_items WHERE parent_node_id = old.node_id; " "END;"); /* Note: view counting triggers are set up in the view preparation code (see db_view_create()) */ /* prepare statements */ db_new_statement ("itemsetLoadStmt", "SELECT item_id FROM items WHERE node_id = ?"); db_new_statement ("itemsetLoadOffsetStmt", "SELECT item_id FROM items WHERE comment = 0 LIMIT ? OFFSET ?"); db_new_statement ("itemsetReadCountStmt", "SELECT COUNT(item_id) FROM items " "WHERE read = 0 AND node_id = ?"); db_new_statement ("itemsetItemCountStmt", "SELECT COUNT(item_id) FROM items " "WHERE node_id = ?"); db_new_statement ("itemsetRemoveStmt", "DELETE FROM items WHERE item_id = ? OR (comment = 1 AND parent_item_id = ?)"); db_new_statement ("itemsetRemoveAllStmt", "DELETE FROM items WHERE node_id = ? OR (comment = 1 AND parent_node_id = ?)"); db_new_statement ("itemsetMarkAllPopupStmt", "UPDATE items SET popup = 0 WHERE node_id = ?"); db_new_statement ("itemLoadStmt", "SELECT " "title," "read," "updated," "popup," "marked," "source," "source_id," "valid_guid," "description," "date," "comment_feed_id," "comment," "item_id," "parent_item_id, " "node_id, " "parent_node_id " " FROM items WHERE item_id = ?"); db_new_statement ("itemUpdateStmt", "REPLACE INTO items (" "title," "read," "updated," "popup," "marked," "source," "source_id," "valid_guid," "description," "date," "comment_feed_id," "comment," "item_id," "parent_item_id," "node_id," "parent_node_id" ") values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); db_new_statement ("itemStateUpdateStmt", "UPDATE items SET read=?, marked=?, updated=? " "WHERE item_id=?"); db_new_statement ("duplicatesFindStmt", "SELECT item_id FROM items WHERE source_id = ?"); db_new_statement ("duplicateNodesFindStmt", "SELECT node_id FROM items WHERE item_id IN " "(SELECT item_id FROM items WHERE source_id = ?)"); db_new_statement ("duplicatesMarkReadStmt", "UPDATE items SET read = 1, updated = 0 WHERE source_id = ?"); db_new_statement ("metadataLoadStmt", "SELECT key,value,nr FROM metadata WHERE item_id = ? ORDER BY nr"); db_new_statement ("metadataUpdateStmt", "REPLACE INTO metadata (item_id,nr,key,value) VALUES (?,?,?,?)"); db_new_statement ("subscriptionUpdateStmt", "REPLACE INTO subscription (" "node_id," "source," "orig_source," "filter_cmd," "update_interval," "default_interval," "discontinued," "available" ") VALUES (?,?,?,?,?,?,?,?)"); db_new_statement ("subscriptionRemoveStmt", "DELETE FROM subscription WHERE node_id = ?"); db_new_statement ("subscriptionLoadStmt", "SELECT " "node_id," "source," "orig_source," "filter_cmd," "update_interval," "default_interval," "discontinued," "available " "FROM subscription"); db_new_statement ("subscriptionMetadataLoadStmt", "SELECT key,value,nr FROM subscription_metadata WHERE node_id = ? ORDER BY nr"); db_new_statement ("subscriptionMetadataUpdateStmt", "REPLACE INTO subscription_metadata (node_id,nr,key,value) VALUES (?,?,?,?)"); db_new_statement ("nodeUpdateStmt", "REPLACE INTO node (node_id,parent_id,title,type,expanded,view_mode,sort_column,sort_reversed) VALUES (?,?,?,?,?,?,?,?)"); db_new_statement ("itemUpdateSearchFoldersStmt", "REPLACE INTO search_folder_items (node_id, parent_node_id, item_id) VALUES (?,?,?)"); db_new_statement ("itemRemoveFromSearchFolderStmt", "DELETE FROM search_folder_items WHERE node_id =? AND item_id = ?;"); db_new_statement ("searchFolderLoadStmt", "SELECT item_id FROM search_folder_items WHERE node_id = ?;"); db_new_statement ("searchFolderCountStmt", "SELECT count(item_id) FROM search_folder_items WHERE node_id = ?;"); db_new_statement ("searchFolderUnreadCountStmt", "SELECT count(items.item_id) FROM search_folder_items JOIN items ON search_folder_items.item_id = items.item_id WHERE search_folder_items.node_id = ? and items.read = 0;"); db_new_statement ("nodeIdListStmt", "SELECT node_id FROM node;"); db_new_statement ("nodeRemoveStmt", "DELETE FROM node WHERE node_id = ?;"); g_assert (sqlite3_get_autocommit (db)); debug_exit ("db_init"); } void db_deinit (void) { debug_enter ("db_deinit"); if (FALSE == sqlite3_get_autocommit (db)) g_warning ("Fatal: DB not in auto-commit mode. This is a bug. Data may be lost!"); if (statements) { g_hash_table_destroy (statements); statements = NULL; } if (SQLITE_OK != sqlite3_close (db)) g_warning ("DB close failed: %s", sqlite3_errmsg (db)); db = NULL; debug_exit ("db_deinit"); } static GSList * db_metadata_list_append (GSList *metadata, const char *key, const char *value) { if (metadata_is_type_registered (key)) metadata = metadata_list_append (metadata, key, value); else debug1 (DEBUG_DB, "Trying to load unregistered metadata type %s from DB.", key); return metadata; } static GSList * db_item_metadata_load(itemPtr item) { GSList *metadata = NULL; sqlite3_stmt *stmt; gint res; stmt = db_get_statement ("metadataLoadStmt"); res = sqlite3_bind_int (stmt, 1, item->id); if (SQLITE_OK != res) g_error ("db_item_load_metadata: sqlite bind failed (error code %d)!", res); while (sqlite3_step (stmt) == SQLITE_ROW) { const char *key, *value; key = (const char *) sqlite3_column_text(stmt, 0); value = (const char *) sqlite3_column_text(stmt, 1); if (g_str_equal (key, "enclosure")) item->hasEnclosure = TRUE; metadata = db_metadata_list_append (metadata, key, value); } sqlite3_finalize (stmt); return metadata; } static void db_item_metadata_update_cb (const gchar *key, const gchar *value, guint index, gpointer user_data) { sqlite3_stmt *stmt; itemPtr item = (itemPtr)user_data; gint res; stmt = db_get_statement ("metadataUpdateStmt"); sqlite3_bind_int (stmt, 1, item->id); sqlite3_bind_int (stmt, 2, index); sqlite3_bind_text (stmt, 3, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 4, value, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Update in \"metadata\" table failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); } static void db_item_metadata_update(itemPtr item) { metadata_list_foreach(item->metadata, db_item_metadata_update_cb, item); } /* Item structure loading methods */ static itemPtr db_load_item_from_columns (sqlite3_stmt *stmt) { const gchar *tmp; itemPtr item = item_new (); item->readStatus = sqlite3_column_int (stmt, 1)?TRUE:FALSE; item->updateStatus = sqlite3_column_int (stmt, 2)?TRUE:FALSE; item->popupStatus = sqlite3_column_int (stmt, 3)?TRUE:FALSE; item->flagStatus = sqlite3_column_int (stmt, 4)?TRUE:FALSE; item->validGuid = sqlite3_column_int (stmt, 7)?TRUE:FALSE; item->time = sqlite3_column_int64 (stmt, 9); item->commentFeedId = g_strdup ((const gchar *) sqlite3_column_text (stmt, 10)); item->isComment = sqlite3_column_int (stmt, 11); item->id = sqlite3_column_int (stmt, 12); item->parentItemId = sqlite3_column_int (stmt, 13); item->nodeId = g_strdup ((const gchar *) sqlite3_column_text (stmt, 14)); item->parentNodeId = g_strdup ((const gchar *) sqlite3_column_text (stmt, 15)); item->title = g_strdup ((const gchar *) sqlite3_column_text(stmt, 0)); item->sourceId = g_strdup ((const gchar *) sqlite3_column_text(stmt, 6)); tmp = (const gchar *) sqlite3_column_text(stmt, 5); if (tmp) item->source = g_strdup (tmp); tmp = (const gchar *) sqlite3_column_text(stmt, 8); if (tmp) item->description = g_strdup (tmp); else item->description = g_strdup (""); item->metadata = db_item_metadata_load (item); return item; } itemSetPtr db_itemset_load (const gchar *id) { sqlite3_stmt *stmt; itemSetPtr itemSet; debug1 (DEBUG_DB, "loading itemset for node \"%s\"", id); itemSet = g_new0 (struct itemSet, 1); itemSet->nodeId = (gchar *)id; stmt = db_get_statement ("itemsetLoadStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); while (sqlite3_step (stmt) == SQLITE_ROW) { itemSet->ids = g_list_append (itemSet->ids, GUINT_TO_POINTER (sqlite3_column_int (stmt, 0))); } sqlite3_finalize (stmt); debug0 (DEBUG_DB, "loading of itemset finished"); return itemSet; } itemPtr db_item_load (gulong id) { sqlite3_stmt *stmt; itemPtr item = NULL; debug1 (DEBUG_DB, "loading item %lu", id); debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("itemLoadStmt"); sqlite3_bind_int (stmt, 1, id); if (sqlite3_step (stmt) == SQLITE_ROW) { item = db_load_item_from_columns (stmt); (void) sqlite3_step (stmt); } else { debug1 (DEBUG_DB, "Could not load item with id %lu!", id); } sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "item load"); return item; } /* Item modification methods */ static void db_item_search_folders_update (itemPtr item) { sqlite3_stmt *stmt; gint res; GSList *iter, *list; /* Bail on comments which are not covered by search folders */ if (item->isComment) return; /* Add item to all search folders it now belongs to */ stmt = db_get_statement ("itemUpdateSearchFoldersStmt"); iter = list = vfolder_get_all_with_item_id (item); while (iter) { vfolderPtr vfolder = (vfolderPtr)iter->data; sqlite3_reset (stmt); sqlite3_bind_text (stmt, 1, vfolder->node->id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, item->nodeId, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 3, item->id); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("item add to search folder failed (error code=%d, %s)", res, sqlite3_errmsg (db)); iter = g_slist_next (iter); } g_slist_free (list); sqlite3_finalize (stmt); /* Remove item from all search folders it does not belong (we do not check if it is in there, just remove it) */ stmt = db_get_statement ("itemRemoveFromSearchFolderStmt"); iter = list = vfolder_get_all_without_item_id (item); while (iter) { vfolderPtr vfolder = (vfolderPtr)iter->data; sqlite3_reset (stmt); sqlite3_bind_text (stmt, 1, vfolder->node->id, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, item->id); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("item remove from search folder failed (error code=%d, %s)", res, sqlite3_errmsg (db)); iter = g_slist_next (iter); } g_slist_free (list); sqlite3_finalize (stmt); } void db_item_update (itemPtr item) { sqlite3_stmt *stmt; gint res; debug2 (DEBUG_DB, "update of item \"%s\" (id=%lu)", item->title, item->id); debug_start_measurement (DEBUG_DB); db_begin_transaction (); /* Update the item... */ stmt = db_get_statement ("itemUpdateStmt"); sqlite3_bind_text (stmt, 1, item->title, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, item->readStatus?1:0); sqlite3_bind_int (stmt, 3, item->updateStatus?1:0); sqlite3_bind_int (stmt, 4, item->popupStatus?1:0); sqlite3_bind_int (stmt, 5, item->flagStatus?1:0); sqlite3_bind_text (stmt, 6, item->source, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 7, item->sourceId, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 8, item->validGuid?1:0); sqlite3_bind_text (stmt, 9, item->description, -1, SQLITE_TRANSIENT); sqlite3_bind_int64 (stmt, 10, item->time); sqlite3_bind_text (stmt, 11, item->commentFeedId, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 12, item->isComment?1:0); if (item->id) sqlite3_bind_int (stmt, 13, item->id); else sqlite3_bind_null (stmt, 13); sqlite3_bind_int (stmt, 14, item->parentItemId); sqlite3_bind_text (stmt, 15, item->nodeId, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 16, item->parentNodeId, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("item update failed (error code=%d, %s)", res, sqlite3_errmsg (db)); if (!item->id && SQLITE_DONE == res) { item->id = sqlite3_last_insert_rowid (db); debug2(DEBUG_DB, "insert into table \"items\": \"%s\" id : %lu", item->title, item->id); } sqlite3_finalize (stmt); db_item_metadata_update (item); db_item_search_folders_update (item); db_end_transaction (); debug_end_measurement (DEBUG_DB, "item update"); } void db_item_state_update (itemPtr item) { sqlite3_stmt *stmt; if (!item->id) { db_item_update (item); return; } db_item_search_folders_update (item); debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("itemStateUpdateStmt"); sqlite3_bind_int (stmt, 1, item->readStatus?1:0); sqlite3_bind_int (stmt, 2, item->flagStatus?1:0); sqlite3_bind_int (stmt, 3, item->updateStatus?1:0); sqlite3_bind_int (stmt, 4, item->id); if (sqlite3_step (stmt) != SQLITE_DONE) g_warning ("item state update failed (%s)", sqlite3_errmsg (db)); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "item state update"); } void db_item_remove (gulong id) { sqlite3_stmt *stmt; gint res; debug1 (DEBUG_DB, "removing item with id %lu", id); stmt = db_get_statement ("itemsetRemoveStmt"); sqlite3_bind_int (stmt, 1, id); sqlite3_bind_int (stmt, 2, id); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("item remove failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); } GSList * db_item_get_duplicates (const gchar *guid) { GSList *duplicates = NULL; sqlite3_stmt *stmt; gint res; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("duplicatesFindStmt"); res = sqlite3_bind_text (stmt, 1, guid, -1, SQLITE_TRANSIENT); if (SQLITE_OK != res) g_error ("db_item_get_duplicates: sqlite bind failed (error code %d)!", res); while (sqlite3_step (stmt) == SQLITE_ROW) { gulong id = sqlite3_column_int (stmt, 0); duplicates = g_slist_append (duplicates, GUINT_TO_POINTER (id)); } sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "searching for duplicates"); return duplicates; } GSList * db_item_get_duplicate_nodes (const gchar *guid) { GSList *duplicates = NULL; sqlite3_stmt *stmt; gint res; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("duplicateNodesFindStmt"); res = sqlite3_bind_text (stmt, 1, guid, -1, SQLITE_TRANSIENT); if (SQLITE_OK != res) g_error ("db_item_get_duplicates: sqlite bind failed (error code %d)!", res); while (sqlite3_step (stmt) == SQLITE_ROW) { gchar *id = g_strdup((const gchar *) sqlite3_column_text (stmt, 0)); duplicates = g_slist_append (duplicates, id); } sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "searching for duplicates"); return duplicates; } void db_itemset_remove_all (const gchar *id) { sqlite3_stmt *stmt; gint res; debug1(DEBUG_DB, "removing all items for item set with %s", id); stmt = db_get_statement ("itemsetRemoveAllStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("removing all items failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); } void db_itemset_mark_all_popup (const gchar *id) { sqlite3_stmt *stmt; gint res; debug1 (DEBUG_DB, "marking all items popup for item set with %s", id); stmt = db_get_statement ("itemsetMarkAllPopupStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("marking all items popup failed (error code=%d, %s)", res, sqlite3_errmsg(db)); sqlite3_finalize (stmt); } gboolean db_itemset_get (itemSetPtr itemSet, gulong offset, guint limit) { sqlite3_stmt *stmt; gboolean success = FALSE; debug2 (DEBUG_DB, "loading %d items offset %lu", limit, offset); stmt = db_get_statement ("itemsetLoadOffsetStmt"); sqlite3_bind_int (stmt, 1, limit); sqlite3_bind_int (stmt, 2, offset); while (sqlite3_step (stmt) == SQLITE_ROW) { itemSet->ids = g_list_append (itemSet->ids, GUINT_TO_POINTER (sqlite3_column_int (stmt, 0))); success = TRUE; } sqlite3_finalize (stmt); return success; } /* Statistics interface */ guint db_itemset_get_unread_count (const gchar *id) { sqlite3_stmt *stmt; gint res; guint count = 0; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("itemsetReadCountStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_ROW == res) count = sqlite3_column_int (stmt, 0); else g_warning("item read counting failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "counting unread items"); return count; } guint db_itemset_get_item_count (const gchar *id) { sqlite3_stmt *stmt; gint res; guint count = 0; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("itemsetItemCountStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_ROW == res) count = sqlite3_column_int (stmt, 0); else g_warning ("item counting failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "counting items"); return count; } /* This method is only used for migration from old schema versions */ static void db_view_remove_triggers (const gchar *id) { gchar *sql, *err; gint res; err = NULL; sql = sqlite3_mprintf ("DROP TRIGGER view_%s_insert_before;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) debug2 (DEBUG_DB, "Dropping trigger failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); err = NULL; sql = sqlite3_mprintf ("DROP TRIGGER view_%s_insert_after;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) debug2 (DEBUG_DB, "Dropping trigger failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); err = NULL; sql = sqlite3_mprintf ("DROP TRIGGER view_%s_delete;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) debug2 (DEBUG_DB, "Dropping trigger failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); err = NULL; sql = sqlite3_mprintf ("DROP TRIGGER view_%s_update_before;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) debug2 (DEBUG_DB, "Dropping trigger failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); err = NULL; sql = sqlite3_mprintf ("DROP TRIGGER view_%s_update_after;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) debug2 (DEBUG_DB, "Dropping trigger failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); } /* This method is only used for migration from old schema versions */ static void db_view_remove (const gchar *id) { gchar *sql, *err; gint res; debug1 (DEBUG_DB, "Dropping view \"%s\"", id); db_view_remove_triggers (id); /* Note: no need to remove anything from view_state, as this is dropped on schema migration and this method is only used during schema migration to remove all views. */ sql = sqlite3_mprintf ("DROP VIEW view_%s;", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) g_warning ("Dropping view failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); } itemSetPtr db_search_folder_load (const gchar *id) { gint res; sqlite3_stmt *stmt; itemSetPtr itemSet; debug1 (DEBUG_DB, "loading search folder node \"%s\"", id); stmt = db_get_statement ("searchFolderLoadStmt"); res = sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); if (SQLITE_OK != res) g_error ("db_search_folder_load: sqlite bind failed (error code %d)!", res); itemSet = g_new0 (struct itemSet, 1); itemSet->nodeId = (gchar *)id; while (sqlite3_step (stmt) == SQLITE_ROW) { itemSet->ids = g_list_append (itemSet->ids, GUINT_TO_POINTER (sqlite3_column_int (stmt, 0))); } sqlite3_finalize (stmt); debug1 (DEBUG_DB, "loading search folder finished (%d items)", g_list_length (itemSet->ids)); return itemSet; } void db_search_folder_reset (const gchar *id) { gchar *sql, *err; gint res; debug1 (DEBUG_DB, "resetting search folder node \"%s\"", id); sql = sqlite3_mprintf ("DELETE FROM search_folder_items WHERE node_id = '%s';", id); res = sqlite3_exec (db, sql, NULL, NULL, &err); if (SQLITE_OK != res) g_warning ("resetting search folder failed (%s) SQL: %s", err, sql); sqlite3_free (sql); sqlite3_free (err); debug0 (DEBUG_DB, "removing search folder finished"); } void db_search_folder_add_items (const gchar *id, GSList *items) { sqlite3_stmt *stmt; GSList *iter; gint res; debug2 (DEBUG_DB, "add %d items to search folder node \"%s\"", g_slist_length (items), id); stmt = db_get_statement ("itemUpdateSearchFoldersStmt"); iter = items; while (iter) { itemPtr item = (itemPtr)iter->data; sqlite3_reset (stmt); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, item->nodeId, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 3, item->id); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_error ("db_search_folder_add_items: sqlite3_step (error code %d)!", res); iter = g_slist_next (iter); } sqlite3_finalize (stmt); debug0 (DEBUG_DB, "adding items to search folder finished"); } guint db_search_folder_get_item_count (const gchar *id) { sqlite3_stmt *stmt; gint res; guint count = 0; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("searchFolderCountStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_ROW == res) count = sqlite3_column_int (stmt, 0); else g_warning("item read counting failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "counting unread items"); return count; } guint db_search_folder_get_unread_count (const gchar *id) { sqlite3_stmt *stmt; gint res; guint count = 0; debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("searchFolderUnreadCountStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_ROW == res) count = sqlite3_column_int (stmt, 0); else g_warning("item unread counting failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "counting unread items"); return count; } static GSList * db_subscription_metadata_load (const gchar *id) { GSList *metadata = NULL; sqlite3_stmt *stmt; gint res; stmt = db_get_statement ("subscriptionMetadataLoadStmt"); res = sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); if (SQLITE_OK != res) g_error ("db_subscription_metadata_load: sqlite bind failed (error code %d)!", res); while (sqlite3_step (stmt) == SQLITE_ROW) { metadata = db_metadata_list_append (metadata, (const char *) sqlite3_column_text(stmt, 0), (const char *) sqlite3_column_text(stmt, 1)); } sqlite3_finalize (stmt); return metadata; } static void db_subscription_metadata_update_cb (const gchar *key, const gchar *value, guint index, gpointer user_data) { sqlite3_stmt *stmt; nodePtr node = (nodePtr)user_data; gint res; stmt = db_get_statement ("subscriptionMetadataUpdateStmt"); sqlite3_bind_text (stmt, 1, node->id, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 2, index); sqlite3_bind_text (stmt, 3, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 4, value, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Update in \"subscription_metadata\" table failed (error code=%d, %s)", res, sqlite3_errmsg (db)); sqlite3_finalize (stmt); } static void db_subscription_metadata_update (subscriptionPtr subscription) { metadata_list_foreach (subscription->metadata, db_subscription_metadata_update_cb, subscription->node); } void db_subscription_load (subscriptionPtr subscription) { subscription->metadata = db_subscription_metadata_load (subscription->node->id); } void db_subscription_update (subscriptionPtr subscription) { sqlite3_stmt *stmt; gint res; debug1 (DEBUG_DB, "updating subscription info %s", subscription->node->id); debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("subscriptionUpdateStmt"); sqlite3_bind_text (stmt, 1, subscription->node->id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, subscription->source, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, subscription->origSource, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 4, subscription->filtercmd, -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 5, subscription->updateInterval); sqlite3_bind_int (stmt, 6, subscription->defaultInterval); sqlite3_bind_int (stmt, 7, subscription->discontinued?1:0); sqlite3_bind_int (stmt, 8, (subscription->updateError || subscription->httpError || subscription->filterError)?1:0); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Could not update subscription info for node id %s in DB (error code %d)!", subscription->node->id, res); sqlite3_finalize (stmt); db_subscription_metadata_update (subscription); debug_end_measurement (DEBUG_DB, "subscription update"); } void db_subscription_remove (const gchar *id) { sqlite3_stmt *stmt; gint res; debug1 (DEBUG_DB, "removing subscription %s", id); debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("subscriptionRemoveStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Could not remove subscription %s from DB (error code %d)!", id, res); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "subscription remove"); } void db_node_update (nodePtr node) { sqlite3_stmt *stmt; gint res; debug1 (DEBUG_DB, "updating node info %s", node->id); debug_start_measurement (DEBUG_DB); stmt = db_get_statement ("nodeUpdateStmt"); sqlite3_bind_text (stmt, 1, node->id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 2, node->parent->id, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 3, node->title, -1, SQLITE_TRANSIENT); sqlite3_bind_text (stmt, 4, node_type_to_str (node), -1, SQLITE_TRANSIENT); sqlite3_bind_int (stmt, 5, node->expanded?1:0); sqlite3_bind_int (stmt, 6, node->viewMode); sqlite3_bind_int (stmt, 7, node->sortColumn); sqlite3_bind_int (stmt, 8, node->sortReversed?1:0); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Could not update node info %s in DB (error code %d)!", node->id, res); sqlite3_finalize (stmt); debug_end_measurement (DEBUG_DB, "node update"); } static gboolean db_node_find (nodePtr node, gpointer id) { GSList *iter; if (g_str_equal (node->id, (gchar *)id)) return TRUE; iter = node->children; while (iter) { if (db_node_find ((nodePtr)iter->data, id)) return TRUE; iter = g_slist_next (iter); } return FALSE; } static void db_node_remove (const gchar *id) { sqlite3_stmt *stmt; gint res; stmt = db_get_statement ("nodeRemoveStmt"); sqlite3_bind_text (stmt, 1, id, -1, SQLITE_TRANSIENT); res = sqlite3_step (stmt); if (SQLITE_DONE != res) g_warning ("Could not remove node %s in DB (error code %d)!", id, res); sqlite3_finalize (stmt); } void db_node_cleanup (nodePtr root) { sqlite3_stmt *stmt; debug0 (DEBUG_DB, "Cleaning node ids..."); /* Fetch all node ids */ stmt = db_get_statement ("nodeIdListStmt"); while (sqlite3_step (stmt) == SQLITE_ROW) { /* Drop node ids not in feed list anymore */ const gchar *id = (const gchar *) sqlite3_column_text (stmt, 0); if (id && !db_node_find (root, (gpointer)id)) { db_subscription_remove (id); /* in case it is a subscription */ db_node_remove (id); /* in case it is a folder */ } } sqlite3_finalize (stmt); } liferea-1.13.7/src/db.h000066400000000000000000000123431415350204600145420ustar00rootroot00000000000000/** * @file db.h sqlite backend * * Copyright (C) 2007-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _DB_H #define _DB_H #include #include "item.h" #include "itemset.h" #include "subscription.h" #include "update.h" /** * Open and initialize the DB. */ void db_init (void); /** * Clean up and close the DB. */ void db_deinit (void); /* item set access (note: item sets are identified by the node id string) */ /** * Loads all items of the given node id. * * @param id the node id * * @returns a newly allocated item set, must be freed using itemset_free() */ itemSetPtr db_itemset_load (const gchar *id); /** * Removes all items of the given item set from the DB. * * @param id the node id */ void db_itemset_remove_all (const gchar *id); /** * Mass items state changing methods. Mark all items of * a given item set as old/popup. * * @param id the node id */ void db_itemset_mark_all_popup (const gchar *id); /** * Returns the number of unread items for the given item set. * * @param id the node id * * @returns the number of unread items */ guint db_itemset_get_unread_count (const gchar *id); /** * Returns the number of items for the given item set. * * @param id the node id * * @returns the number of items */ guint db_itemset_get_item_count (const gchar *id); /** * Returns a batch of items starting with the given * offset and no more than the given limit. * * To be used for batched item loading (search folder loaders) * * @param itemSet an itemset to add the items to * @param offset the current offset * @param limit maximum number of items to fetch * * @returns FALSE if no more items to fetch */ gboolean db_itemset_get (itemSetPtr itemSet, gulong offset, guint limit); /* item access (note: items are identified by the numeric item id) */ /** * Loads the item specified by id from the DB. * * @param id the id * * @returns new item structure, must be free'd using item_unload() */ itemPtr db_item_load(gulong id); /** * Updates all attributes of the item in the DB * * @param item the item */ void db_item_update(itemPtr item); /** * Removes the given item from the DB * * @param item the item */ void db_item_remove(gulong id); /** * Update the attributes related to item state only. * * @param item the item */ void db_item_state_update (itemPtr item); /** * Returns a list of item ids with the given GUID. * * @param guid the item GUID * * @returns a list of item ids */ GSList * db_item_get_duplicates(const gchar *guid); /** * Returns a list of node ids containing an item with the given GUID. * * @param guid the item GUID * * @returns a list of node ids (to be free'd using g_free) */ GSList * db_item_get_duplicate_nodes(const gchar *guid); /** * Returns an item set of all items for the given search folder id. * * @param id the search folder id * * @returns a new item set (to be free'd using itemset_free()) */ itemSetPtr db_search_folder_load (const gchar *id); /** * Removes all items from the given search folder * * @param id the search folder id */ void db_search_folder_reset (const gchar *id); /** * Add a list of item ids to a search folder. * * @param id the search folder id * @param items the list of items */ void db_search_folder_add_items (const gchar *id, GSList *items); /** * Returns the number of items for the given search folder. * * @param id the node id * * @returns the number of items */ guint db_search_folder_get_item_count (const gchar *id); /** * Returns the number of items for the given search folder. * * @param id the node id * * @returns the number of items */ guint db_search_folder_get_unread_count (const gchar *id); /** * Load the metadata and update state of the given subscription. * * @param subscription the subscription whose info to load */ void db_subscription_load (subscriptionPtr subscription); /** * Updates (or inserts) the properties of the given subscription in the DB. * * @param subscription the subscription */ void db_subscription_update (subscriptionPtr subscription); /** * Removes the subscription with the given id from the DB * * @param id the node id */ void db_subscription_remove (const gchar *id); /** * Updates the given nodes properties in the DB. * * @param node the node */ void db_node_update (nodePtr node); /** * Clean old nodes from the DB by comparing all DB nodes * against the OPML feed list. * * @param root the root node */ void db_node_cleanup (nodePtr root); #endif liferea-1.13.7/src/dbus.c000066400000000000000000000147461415350204600151160ustar00rootroot00000000000000/** * @file dbus.c DBUS interface to control Liferea * * Copyright (C) 2007 mooonz * Copyright (C) 2010 Emilio Pozuelo Monfort * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "dbus.h" #include "debug.h" #include "feedlist.h" #include "net_monitor.h" #include "subscription.h" #include "ui/liferea_shell.h" static GDBusNodeInfo *introspection_data = NULL; static const gchar introspection_xml[] = "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""; G_DEFINE_TYPE(LifereaDBus, liferea_dbus, G_TYPE_OBJECT) static gboolean liferea_dbus_ping (LifereaDBus *self, GError **err) { return TRUE; } static gboolean liferea_dbus_set_online (LifereaDBus *self, gboolean online, GError **err) { network_monitor_set_online (online); return TRUE; } static gboolean liferea_dbus_subscribe (LifereaDBus *self, const gchar *url, GError **err) { liferea_shell_present (); feedlist_add_subscription (url, NULL, NULL, 0); return TRUE; } static guint liferea_dbus_get_unread_items (LifereaDBus *self, GError **err) { return feedlist_get_unread_item_count (); } static guint liferea_dbus_get_new_items (LifereaDBus *self, GError **err) { return feedlist_get_new_item_count (); } static gboolean liferea_dbus_refresh (LifereaDBus *self, GError **err) { node_update_subscription (feedlist_get_root (), GUINT_TO_POINTER (0)); return TRUE; } static void handle_method_call (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { LifereaDBus *self = user_data; gboolean res; if (g_str_equal (method_name, "Ping")) { res = liferea_dbus_ping (self, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", res)); } else if (g_str_equal (method_name, "SetOnline") && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(b)"))) { gboolean set_online; g_variant_get (parameters, "(b)", &set_online); res = liferea_dbus_set_online (self, set_online, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", res)); } else if (g_str_equal (method_name, "Subscribe") && g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(s)"))) { const gchar *url; g_variant_get (parameters, "(s)", &url); res = liferea_dbus_subscribe (self, url, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", res)); } else if (g_str_equal (method_name, "GetUnreadItems")) { guint num = liferea_dbus_get_unread_items (self, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(i)", num)); } else if (g_str_equal (method_name, "GetNewItems")) { guint num = liferea_dbus_get_new_items (self, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(i)", num)); } else if (g_str_equal (method_name, "Refresh")) { res = liferea_dbus_refresh (self, NULL); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", res)); } else { g_warning ("Unknown method name or unknown parameters: %s", method_name); } } static const GDBusInterfaceVTable interface_vtable = { handle_method_call, NULL, NULL }; static void on_bus_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { guint id; debug_enter ("on_bus_acquired"); /* parse introspection data */ introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); id = g_dbus_connection_register_object (connection, LF_DBUS_PATH, introspection_data->interfaces[0], &interface_vtable, NULL, /* user_data */ NULL, /* user_data_free_func */ NULL); /* GError** */ g_assert (id > 0); debug_exit ("on_bus_acquired"); } static void on_name_acquired (GDBusConnection *connection, const gchar *name, gpointer user_data) { debug1 (DEBUG_GUI, "Acquired the name %s on the session bus\n", name); } static void on_name_lost (GDBusConnection *connection, const gchar *name, gpointer user_data) { debug1 (DEBUG_GUI, "Lost the name %s on the session bus\n", name); } static void liferea_dbus_init(LifereaDBus *obj) { } static void liferea_dbus_dispose (GObject *obj) { LifereaDBus *self = LIFEREA_DBUS (obj); g_bus_unown_name (self->owner_id); G_OBJECT_CLASS (liferea_dbus_parent_class)->dispose (obj); } static void liferea_dbus_class_init (LifereaDBusClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->dispose = liferea_dbus_dispose; } LifereaDBus* liferea_dbus_new (void) { LifereaDBus *obj = NULL; debug_enter ("liferea_dbus_new"); obj = (LifereaDBus*)g_object_new(LIFEREA_DBUS_TYPE, NULL); obj->owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, LF_DBUS_SERVICE, G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, on_bus_acquired, on_name_acquired, on_name_lost, NULL, NULL); debug_exit ("liferea_dbus_new"); return obj; } liferea-1.13.7/src/dbus.h000066400000000000000000000035741415350204600151200ustar00rootroot00000000000000/** * @file dbus.c DBUS interface to control Liferea * * Copyright (C) 2007 mooonz * Copyright (C) 2010 Emilio Pozuelo Monfort * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __LIFEREA_DBUS_H__ #define __LIFEREA_DBUS_H__ #include #define LF_DBUS_PATH "/org/gnome/feed/Reader" #define LF_DBUS_SERVICE "org.gnome.feed.Reader" typedef struct _LifereaDBus { GObject parent; guint owner_id; } LifereaDBus; typedef struct _LifereaDBusClass { GObjectClass parent; } LifereaDBusClass; GType liferea_dbus_get_type (void); #define LIFEREA_DBUS_TYPE (liferea_dbus_get_type ()) #define LIFEREA_DBUS(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), LIFEREA_DBUS_TYPE, LifereaDBus)) #define LIFEREA_DBUS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), LIFEREA_DBUS_TYPE, LifereaDBusClass)) #define IS_LIFEREA_DBUS(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), LIFEREA_DBUS_TYPE)) #define IS_LIFEREA_DBUS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), LIFEREA_DBUS_TYPE)) #define LIFEREA_DBUS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), LIFEREA_DBUS_TYPE, LifereaDBusClass)) LifereaDBus* liferea_dbus_new (void); #endif liferea-1.13.7/src/debug.c000066400000000000000000000121011415350204600152260ustar00rootroot00000000000000/* * Debugging output support. This was originally written for * * Pan - A Newsreader for Gtk+ * Copyright (C) 2002 Charles Kerr * * Liferea specific adaptations * Copyright (C) 2004-2012 Lars Windolf * * 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; version 2 of the License. * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include "debug.h" #if defined (G_OS_WIN32) && !defined (HAVE_LOCALTIME_R) #define localtime_r(t,o) localtime_s (o,t) #endif unsigned long debug_level = 0; static GHashTable * t2d = NULL; /**< per thread call tree depth */ static GHashTable *startTimes = NULL; static const char * debug_get_prefix (unsigned long flag) { if (flag & DEBUG_CACHE) return "CACHE "; if (flag & DEBUG_CONF) return "CONF "; if (flag & DEBUG_UPDATE) return "UPDATE "; if (flag & DEBUG_PARSING) return "PARSING"; if (flag & DEBUG_GUI) return "GUI "; if (flag & DEBUG_HTML) return "HTML "; if (flag & DEBUG_TRACE) return "TRACE "; if (flag & DEBUG_NET) return "NET "; if (flag & DEBUG_DB) return "DB "; if (flag & DEBUG_PERF) return "PERF "; if (flag & DEBUG_VFOLDER) return "VFOLDER"; return ""; } static void debug_set_depth (gint newDepth) { const gpointer self = g_thread_self (); /* Track per-thread call tree depth */ if (t2d == NULL) t2d = g_hash_table_new (g_direct_hash, g_direct_equal); if (newDepth < 0) g_hash_table_insert(t2d, self, GINT_TO_POINTER(0)); else g_hash_table_insert(t2d, self, GINT_TO_POINTER(newDepth)); } static gint debug_get_depth (void) { const gpointer self = g_thread_self (); if (!t2d) return 0; return GPOINTER_TO_INT (g_hash_table_lookup (t2d, self)); } void debug_start_measurement_func (const char * function) { gint64 *startTime = NULL; if (!function) return; if (!startTimes) startTimes = g_hash_table_new (g_str_hash, g_str_equal); startTime = (gint64 *) g_hash_table_lookup (startTimes, function); if (!startTime) { startTime = g_new0 (gint64, 1); g_hash_table_insert (startTimes, g_strdup(function), startTime); } *startTime = g_get_monotonic_time(); } void debug_end_measurement_func (const char * function, unsigned long flags, const char *name) { gint64 *startTime = NULL; gint64 endTime; unsigned long duration = 0; int i; if (!function) return; if (!startTimes) return; startTime = g_hash_table_lookup (startTimes, function); if (!startTime) return; endTime = g_get_monotonic_time(); duration = (endTime - *startTime) / 1000; if (duration < 1) return; g_print ("%s: ", debug_get_prefix (flags)); if (debug_level & DEBUG_TRACE) g_print ("[%p] ", g_thread_self ()); for (i = 0; i < debug_get_depth (); i++) g_print (" "); g_print ("= %s took %01ld,%03lds\n", name, duration / 1000, duration % 1000); if (duration > 250) debug2 (DEBUG_PERF, "function \"%s\" is slow! Took %dms.", name, duration); } void set_debug_level (unsigned long level) { debug_level = level; } void debug_printf (const char * strloc, const gchar * function, gulong flag, const gchar * fmt, ...) { char timebuf[64]; gchar * string; const gchar * prefix; time_t now_time_t; va_list args; struct tm now_tm; gint depth, i; g_return_if_fail (fmt != NULL); depth = debug_get_depth (); if (*fmt == '-') { debug_set_depth (depth - 1); depth--; } /* Get prefix */ prefix = debug_get_prefix(flag); va_start (args, fmt); string = g_strdup_vprintf (fmt, args); va_end (args); time (&now_time_t); localtime_r (&now_time_t, &now_tm); strftime (timebuf, sizeof(timebuf), "%H:%M:%S", &now_tm); if(debug_level & DEBUG_VERBOSE) { printf ("(%15s:%20s)(thread %p)(time %s) %s: %s\n", strloc, function, g_thread_self (), timebuf, prefix, string); } else { g_print ("%s: ", prefix); if (debug_level & DEBUG_TRACE) g_print ("[%p] ", g_thread_self ()); for (i = 0; i < depth; i++) g_print (" "); g_print ("%s\n", string); } fflush (NULL); g_free (string); if (*fmt == '+') debug_set_depth (depth + 1); } void debug_enter (const char *name) { debug1 (DEBUG_TRACE, "+ %s", name); if (debug_level & DEBUG_PERF) debug_start_measurement_func (name); } void debug_exit (const char *name) { debug1 (DEBUG_TRACE, "- %s", name); if (debug_level & DEBUG_PERF) debug_end_measurement_func (name, DEBUG_PERF, name); } liferea-1.13.7/src/debug.h000066400000000000000000000077111415350204600152460ustar00rootroot00000000000000/* * Debugging output support. This was originally written for * * Pan - A Newsreader for Gtk+ * Copyright (C) 2002 Charles Kerr * * Liferea specific adaptions * Copyright (C) 2004-2007 Lars Windolf * * 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; version 2 of the License. * * 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __DEBUG_H__ #define __DEBUG_H__ typedef enum { DEBUG_CACHE = (1<<0), DEBUG_CONF = (1<<1), DEBUG_UPDATE = (1<<2), DEBUG_PARSING = (1<<3), DEBUG_GUI = (1<<4), DEBUG_TRACE = (1<<5), DEBUG_HTML = (1<<6), DEBUG_NET = (1<<7), DEBUG_DB = (1<<8), DEBUG_PERF = (1<<9), DEBUG_VFOLDER = (1<<10), DEBUG_VERBOSE = (1<<11) } DebugFlags; /** * Method to save start time for a measurement. * * @param level debugging flags that enable the measurement * * Not thread-safe! */ extern void debug_start_measurement_func (const char * function); #define debug_start_measurement(level) if ((debug_level) & level) debug_start_measurement_func (PRETTY_FUNCTION) /** * Method to calculate the duration for a measurement. * The result will be printed to the debug trace. * * @param level debugging flags that enable the measurement * @param name name of the measurement * * Not thread-safe! */ extern void debug_end_measurement_func (const char * function, unsigned long flags, const char *name); #define debug_end_measurement(level, name) if ((debug_level) & level) debug_end_measurement_func (PRETTY_FUNCTION, level, name) /** * Enable debugging for one or more of the given debugging flags. * * @param flags debugging flags (see above) */ extern void set_debug_level (unsigned long flags); /** currently configured debug flag set */ extern unsigned long debug_level; /** macros for debug output */ extern void debug_printf (const char * strloc, const char * function, unsigned long level, const char* fmt, ...); #ifdef __GNUC__ #define PRETTY_FUNCTION __PRETTY_FUNCTION__ #else #define PRETTY_FUNCTION "" #endif #define debug0(level, fmt) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt) #define debug1(level, fmt, A) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A) #define debug2(level, fmt, A, B) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A, B) #define debug3(level, fmt, A, B, C) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A, B, C) #define debug4(level, fmt, A, B, C, D) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A, B, C, D) #define debug5(level, fmt, A, B, C, D, E) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A, B, C, D, E) #define debug6(level, fmt, A, B, C, D, E, F) if ((debug_level) & level) debug_printf (G_STRLOC, PRETTY_FUNCTION, level,fmt, A, B, C, D, E, F) /** * Trace method to trace function entering when function name * tracing is enabled (--debug-trace|--debug-all). Also implements * slow function detection when performance trace (--debug-perf) * is active. * * @param name function name */ extern void debug_enter (const char *name); /** * Trace method to trace function exiting when function name * tracing is enabled (--debug-trace|--debug-all). Also implements * slow function detection when performance trace (--debug-perf) * is active. * * @param name function name */ extern void debug_exit (const char *name); #endif liferea-1.13.7/src/enclosure.c000066400000000000000000000177621415350204600161610ustar00rootroot00000000000000/* * @file enclosure.c enclosures/podcast support * * Copyright (C) 2007-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "common.h" #include "conf.h" #include "debug.h" #include "enclosure.h" #include "xml.h" #include "ui/preferences_dialog.h" // FIXME: remove this! #include "ui/ui_common.h" #if !defined (G_OS_WIN32) || defined (HAVE_SYS_WAIT_H) #include #endif /* Liferea manages a MIME type configuration to allow comfortable enclosure/podcast handling that launches external applications to play or download content. The MIME type configuration is saved into a XML file in the cache directory. Enclosure download is currently done using external tools (gwget,kget,steadyflow...) and is performed by simply passing the URL. All downloads are asynchronous and download concurrency is considered to be handled by the invoked tools. */ static GSList *types = NULL; static gboolean typesLoaded = FALSE; /* The internal enclosure encoding format is either or enc:::: Examples: "http://somewhere.com/cool.mp3" "enc::::http://somewhere.com/cool.mp3" "enc:0:audio/ogg:237423414:http://somewhere.com/cool.ogg" "enc:1:x-application/pdf::https://secret.site.us/defense-plan.pdf" */ enclosurePtr enclosure_from_string (const gchar *str) { gchar **fields; enclosurePtr enclosure; enclosure = g_new0 (struct enclosure, 1); /* legacy URL, migration case... */ if (strstr (str, "enc:") != str) { enclosure->url = g_strdup (str); return enclosure; } fields = g_regex_split_simple ("^enc:([01]?):([^:]*):(\\d+):(.*)", str, 0, 0); if (6 > g_strv_length (fields)) { debug2 (DEBUG_PARSING, "Dropping incorrectly encoded enclosure: >>>%s<<< (nr of fields=%d)\n", str, g_strv_length (fields)); enclosure_free (enclosure); return NULL; } enclosure->downloaded = ('1' == *fields[1]); if (strlen (fields[2])) enclosure->mime = g_strdup (fields[2]); if (strlen (fields[3])) enclosure->size = atol (fields[3]); enclosure->url = g_strdup (fields[4]); g_strfreev (fields); return enclosure; } gchar * enclosure_values_to_string (const gchar *url, const gchar *mime, gssize size, gboolean downloaded) { gchar *result, *safeUrl; /* There are websites out there encoding -1 as size */ if (size < 0) size = 0; safeUrl = (gchar *) common_uri_escape (BAD_CAST url); result = g_strdup_printf ("enc:%s:%s:%" G_GSSIZE_FORMAT ":%s", downloaded?"1":"0", mime?mime:"", size, safeUrl); g_free (safeUrl); return result; } gchar * enclosure_to_string (enclosurePtr enclosure) { return enclosure_values_to_string (enclosure->url, enclosure->mime, enclosure->size, enclosure->downloaded); } gchar * enclosure_get_url (const gchar *str) { enclosurePtr enclosure = enclosure_from_string(str); gchar *url = NULL; if (enclosure) { url = g_strdup (enclosure->url); enclosure_free (enclosure); } return url; } gchar * enclosure_get_mime (const gchar *str) { enclosurePtr enclosure = enclosure_from_string (str); gchar *mime = NULL; if (enclosure) { mime = g_strdup (enclosure->mime); enclosure_free (enclosure); } return mime; } void enclosure_free (enclosurePtr enclosure) { g_free (enclosure->url); g_free (enclosure->mime); g_free (enclosure); } static void enclosure_mime_types_load (void) { xmlDocPtr doc; xmlNodePtr cur; encTypePtr etp; gchar *filename; typesLoaded = TRUE; filename = common_create_config_filename ("mime.xml"); if (g_file_test (filename, G_FILE_TEST_EXISTS)) { doc = xmlParseFile (filename); if (!doc) { debug0 (DEBUG_CONF, "could not load enclosure type config file!"); } else { cur = xmlDocGetRootElement (doc); if (!cur) { g_warning ("could not read root element from enclosure type config file!"); } else { while (cur) { if (!xmlIsBlankNode (cur)) { if (!xmlStrcmp (cur->name, BAD_CAST"types")) { cur = cur->xmlChildrenNode; while (cur) { if ((!xmlStrcmp (cur->name, BAD_CAST"type"))) { etp = g_new0 (struct encType, 1); etp->mime = (gchar *) xmlGetProp (cur, BAD_CAST"mime"); etp->extension = (gchar *) xmlGetProp (cur, BAD_CAST"extension"); etp->cmd = (gchar *) xmlGetProp (cur, BAD_CAST"cmd"); etp->permanent = TRUE; types = g_slist_append (types, etp); } cur = cur->next; } break; } else { g_warning (_("\"%s\" is not a valid enclosure type config file!"), filename); } } cur = cur->next; } } xmlFreeDoc (doc); } } g_free (filename); } void enclosure_mime_types_save (void) { xmlDocPtr doc; xmlNodePtr root, cur; encTypePtr etp; GSList *iter; gchar *filename; doc = xmlNewDoc (BAD_CAST "1.0"); root = xmlNewDocNode (doc, NULL, BAD_CAST"types", NULL); iter = types; while (iter) { etp = (encTypePtr)iter->data; cur = xmlNewChild (root, NULL, BAD_CAST"type", NULL); xmlNewProp (cur, BAD_CAST"cmd", BAD_CAST etp->cmd); if (etp->mime) xmlNewProp (cur, BAD_CAST"mime", BAD_CAST etp->mime); if (etp->extension) xmlNewProp (cur, BAD_CAST"extension", BAD_CAST etp->extension); iter = g_slist_next (iter); } xmlDocSetRootElement (doc, root); filename = common_create_config_filename ("mime.xml"); if (-1 == xmlSaveFormatFileEnc (filename, doc, NULL, 1)) g_warning ("Could not save to enclosure type config file!"); g_free (filename); xmlFreeDoc (doc); } const GSList * enclosure_mime_types_get (void) { if (!typesLoaded) enclosure_mime_types_load (); return types; } void enclosure_mime_type_add (encTypePtr type) { types = g_slist_append (types, type); enclosure_mime_types_save (); } void enclosure_mime_type_remove (encTypePtr type) { types = g_slist_remove (types, type); g_free (type->cmd); g_free (type->mime); g_free (type->extension); g_free (type); enclosure_mime_types_save (); } /* etp is optional, if it is missing we are in save mode */ void enclosure_download (encTypePtr type, const gchar *url, gboolean interactive) { GError *error = NULL; gchar *cmd, *urlQ; urlQ = g_shell_quote (url); if (type) { debug2 (DEBUG_UPDATE, "passing URL %s to command %s...", urlQ, type->cmd); cmd = g_strdup_printf ("%s %s", type->cmd, urlQ); } else { gchar *toolCmd = prefs_get_download_command (); if(!toolCmd) { if (interactive) ui_show_error_box (_("You have not configured a download tool yet! Please do so in the 'Enclosures' tab in Tools/Preferences.")); return; } debug2 (DEBUG_UPDATE, "downloading URL %s with %s...", urlQ, toolCmd); cmd = g_strdup_printf (toolCmd, urlQ); g_free (toolCmd); } g_free (urlQ); /* free now unnecessary stuff */ if (type && !type->permanent) enclosure_mime_type_remove (type); /* execute command */ g_spawn_command_line_async (cmd, &error); if (error && (0 != error->code)) { if (interactive) ui_show_error_box (_("Command failed: \n\n%s\n\n Please check whether the configured download tool is installed and working correctly! You can change it in the 'Download' tab in Tools/Preferences."), cmd); else g_warning ("Command \"%s\" failed!", cmd); } if (error) g_error_free (error); g_free (cmd); } liferea-1.13.7/src/enclosure.h000066400000000000000000000102351415350204600161520ustar00rootroot00000000000000/* * @file enclosure.h enclosure/podcast support * * Copyright (C) 2007-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ENCLOSURE_H #define _ENCLOSURE_H #include /* structure describing the preferences for a MIME type or file extension */ typedef struct encType { gchar *mime; /*<< either mime or extension is set */ gchar *extension; gchar *cmd; /*<< the command to launch the enclosure type */ gboolean permanent; /*<< if TRUE definition is deleted after opening and not added to the permanent list of type configs */ } *encTypePtr; /* structure describing an enclosure and its states */ typedef struct enclosure { gchar *url; /*<< enclosure download URI (absolute path) */ gchar *mime; /*<< enclosure MIME type (optional, can be NULL) */ gssize size; /*<< enclosure size (optional, can be 0, but also -1) */ gboolean downloaded; /*<< flag indicating we have downloaded the enclosure */ } *enclosurePtr; /** * enclosure_from_string: (skip) * @str: the enclosure description * * Parses enclosure description. * * Returns: (transfer full): new enclosure structure (to be free'd using enclosure_free) */ enclosurePtr enclosure_from_string (const gchar *str); /** * enclosure_values_to_string: * @url: the enclosure URL * @mime: the MIME type (optional, can be NULL) * @size: the enclosure size (optional, can be 0, and also -1) * @downloaded: downloading state (TRUE=downloaded) * * Serialize enclosure infos to string. * * Returns: (transfer full): new string (to be free'd using g_free) */ gchar * enclosure_values_to_string (const gchar *url, const gchar *mime, gssize size, gboolean downloaded); /** * enclosure_to_string: (skip) * @enclosure: the enclosure * * Serialize enclosure to string. * * Returns: (transfer full): new string (to be free'd using g_free) */ gchar * enclosure_to_string (const enclosurePtr enclosure); /** * enclosure_get_url: * @str: enclosure string to parse * * Get URL from enclosure string * * Returns: (transfer full): URL string, free after use */ gchar * enclosure_get_url (const gchar *str); /** * enclosure_get_mime: * @str: enclosure string to parse * * Get MIME type from enclosure string * * Returns: (transfer full): MIME type string, free after use */ gchar * enclosure_get_mime (const gchar *str); /** * enclosure_free: (skip) * @enclosure: the enclosure * * Free all memory associated with the enclosure. */ void enclosure_free (enclosurePtr enclosure); /** * enclosure_mime_types_get: (skip) * * Returns all configured enclosure types. * * Returns: (transfer none): list of encType structures */ const GSList * enclosure_mime_types_get (void); /** * enclosure_mime_type_add: (skip) * @type: the new definition * * Adds a new MIME type handling definition. */ void enclosure_mime_type_add (encTypePtr type); /** * enclosure_mime_type_remove: (skip) * @type: the definition to remove * * Removes an existing MIME type handling definition. * The definition will be free'd by this function. */ void enclosure_mime_type_remove (encTypePtr type); /** * enclosure_mime_types_save: (skip) * * Save all MIME type definitions. */ void enclosure_mime_types_save (void); /** * enclosure_download: (skip) * @type: ULL or pointer to type structure * @url: valid HTTP URL * @interactive: TRUE if triggered by user interaction * * Downloads a given enclosure URL into a file */ void enclosure_download (encTypePtr type, const gchar *url, gboolean interactive); #endif liferea-1.13.7/src/export.c000066400000000000000000000330421415350204600154700ustar00rootroot00000000000000/** * @file export.c OPML feed list import & export * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "export.h" #include #include #include #include #include "auth.h" #include "common.h" #include "db.h" #include "debug.h" #include "favicon.h" #include "feedlist.h" #include "folder.h" #include "node.h" #include "xml.h" #include "ui/ui_common.h" #include "ui/feed_list_view.h" struct exportData { gboolean trusted; /**< Include all the extra Liferea-specific tags */ xmlNodePtr cur; }; static void export_node_children (nodePtr node, xmlNodePtr cur, gboolean trusted); /* Used for exporting, this adds a folder or feed's node to the XML tree */ static void export_append_node_tag (nodePtr node, gpointer userdata) { xmlNodePtr cur = ((struct exportData*)userdata)->cur; gboolean internal = ((struct exportData*)userdata)->trusted; xmlNodePtr childNode; gchar *tmp; /* When exporting external OPML do not export every node type... */ if (!(internal || (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_EXPORT))) return; childNode = xmlNewChild (cur, NULL, BAD_CAST"outline", NULL); /* 1. write generic node attributes */ xmlNewProp (childNode, BAD_CAST"title", BAD_CAST node_get_title(node)); xmlNewProp (childNode, BAD_CAST"text", BAD_CAST node_get_title(node)); /* The OPML spec requires "text" */ xmlNewProp (childNode, BAD_CAST"description", BAD_CAST node_get_title(node)); if (node_type_to_str (node)) xmlNewProp (childNode, BAD_CAST"type", BAD_CAST node_type_to_str (node)); /* Don't add the following tags if we are exporting to other applications */ if (internal) { xmlNewProp (childNode, BAD_CAST"id", BAD_CAST node_get_id (node)); switch (node->sortColumn) { case NODE_VIEW_SORT_BY_TITLE: xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"title"); break; case NODE_VIEW_SORT_BY_TIME: xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"time"); break; case NODE_VIEW_SORT_BY_PARENT: xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"parent"); break; case NODE_VIEW_SORT_BY_STATE: xmlNewProp (childNode, BAD_CAST"sortColumn", BAD_CAST"state"); break; default: g_assert_not_reached(); break; } if (FALSE == node->sortReversed) xmlNewProp (childNode, BAD_CAST"sortReversed", BAD_CAST"false"); if (node->loadItemLink) xmlNewProp (childNode, BAD_CAST"loadItemLink", BAD_CAST"true"); /* Do not export the default view mode setting to avoid making it permanent. Do not use node_get_view_mode () here to ensure that the comparison works as node_get_view_mode () returns the effective mode! */ if (NODE_VIEW_MODE_DEFAULT != node->viewMode) { tmp = g_strdup_printf ("%u", node_get_view_mode(node)); xmlNewProp (childNode, BAD_CAST"viewMode", BAD_CAST tmp); g_free (tmp); } } /* 2. add node type specific stuff */ NODE_TYPE (node)->export (node, childNode, internal); /* 3. add children */ if (internal) { if (feed_list_view_is_expanded (node->id)) xmlNewProp (childNode, BAD_CAST"expanded", BAD_CAST"true"); else xmlNewProp (childNode, BAD_CAST"collapsed", BAD_CAST"true"); } if (IS_FOLDER (node)) export_node_children (node, childNode, internal); } static void export_node_children (nodePtr node, xmlNodePtr cur, gboolean trusted) { struct exportData params; params.cur = cur; params.trusted = trusted; node_foreach_child_data (node, export_append_node_tag, ¶ms); } gboolean export_OPML_feedlist (const gchar *filename, nodePtr node, gboolean trusted) { xmlDocPtr doc; xmlNodePtr cur, opmlNode; gboolean error = FALSE; gchar *backupFilename; int old_umask = 0; debug_enter ("export_OPML_feedlist"); backupFilename = g_strdup_printf ("%s~", filename); doc = xmlNewDoc (BAD_CAST"1.0"); if (doc) { opmlNode = xmlNewDocNode (doc, NULL, BAD_CAST"opml", NULL); if (opmlNode) { xmlNewProp (opmlNode, BAD_CAST"version", BAD_CAST"1.0"); /* create head */ cur = xmlNewChild (opmlNode, NULL, BAD_CAST"head", NULL); if (cur) xmlNewTextChild (cur, NULL, BAD_CAST"title", BAD_CAST"Liferea Feed List Export"); /* create body with feed list */ cur = xmlNewChild (opmlNode, NULL, BAD_CAST"body", NULL); if (cur) export_node_children (node, cur, trusted); xmlDocSetRootElement (doc, opmlNode); } else { g_warning ("could not create XML feed node for feed cache document!"); error = TRUE; } if (!trusted) old_umask = umask (022); /* give read permissions for other, per-default we wouldn't give it... */ xmlSetDocCompressMode (doc, 0); if (-1 == xmlSaveFormatFile (backupFilename, doc, TRUE)) { g_warning ("Could not export to OPML file!"); error = TRUE; } if (!trusted) umask (old_umask); xmlFreeDoc (doc); if (!error) { if (g_rename (backupFilename, filename) < 0) { g_warning (_("Error renaming %s to %s: %s\n"), backupFilename, filename, g_strerror (errno)); error = TRUE; } } } else { g_warning ("Could not create XML document!"); error = TRUE; } g_free (backupFilename); debug_exit ("export_OPML_feedlist"); return !error; } static void import_parse_outline (xmlNodePtr cur, nodePtr parentNode, gboolean trusted) { gchar *title, *typeStr, *tmp, *sortStr; xmlNodePtr child; nodePtr node; nodeTypePtr type = NULL; gboolean needsUpdate = FALSE; debug_enter("import_parse_outline"); /* 1. determine node type */ typeStr = (gchar *)xmlGetProp (cur, BAD_CAST"type"); if (typeStr) { type = node_str_to_type (typeStr); xmlFree (typeStr); } /* if we didn't find a type attribute we use heuristics */ if (!type) { /* check for a source URL */ tmp = (gchar *)xmlGetProp (cur, BAD_CAST"xmlUrl"); if (!tmp) tmp = (gchar *)xmlGetProp (cur, BAD_CAST"xmlurl"); /* AmphetaDesk */ if (!tmp) tmp = (gchar *)xmlGetProp (cur, BAD_CAST"xmlURL"); /* LiveJournal */ if (tmp) { debug0 (DEBUG_CACHE, "-> URL found assuming type feed"); type = feed_get_node_type(); xmlFree (tmp); } else { /* if the outline has no type and URL it just has to be a folder */ type = folder_get_node_type(); debug0 (DEBUG_CACHE, "-> must be a folder"); } } g_assert (NULL != type); /* Check if adding this type is allowed */ // FIXME: Prevent news bins outside root source // FIXME: Prevent search folders outside root source /* 2. do general node parsing */ node = node_new (type); node_set_parent (node, parentNode, -1); /* The id should only be used from feedlist.opml. Otherwise, it could cause corruption if the same id was imported multiple times. */ if (trusted) { gchar *id = NULL; id = (gchar *)xmlGetProp (cur, BAD_CAST"id"); /* If, for some reason, the OPML has been corrupted and there are two copies asking for a certain ID then give the second one a new ID. */ if (node_is_used_id (id)) { xmlFree (id); id = NULL; } if (id) { node_set_id (node, id); xmlFree (id); } else { needsUpdate = TRUE; } } else { needsUpdate = TRUE; } /* title */ title = (gchar *)xmlGetProp (cur, BAD_CAST"title"); if (!title || !xmlStrcmp ((xmlChar *)title, BAD_CAST"")) { if (title) xmlFree (title); title = (gchar *)xmlGetProp (cur, BAD_CAST"text"); } if (title) { node_set_title (node, title); xmlFree (title); } /* sorting order */ sortStr = (gchar *)xmlGetProp (cur, BAD_CAST"sortColumn"); if (sortStr) { if (!xmlStrcmp ((xmlChar *)sortStr, BAD_CAST"title")) node->sortColumn = NODE_VIEW_SORT_BY_TITLE; else if (!xmlStrcmp ((xmlChar *)sortStr, BAD_CAST"parent")) node->sortColumn = NODE_VIEW_SORT_BY_PARENT; else if (!xmlStrcmp ((xmlChar *)sortStr, BAD_CAST"state")) node->sortColumn = NODE_VIEW_SORT_BY_STATE; else node->sortColumn = NODE_VIEW_SORT_BY_TIME; xmlFree (sortStr); } sortStr = (gchar *)xmlGetProp (cur, BAD_CAST"sortReversed"); if (sortStr) { if(!xmlStrcmp ((xmlChar *)sortStr, BAD_CAST"false")) node->sortReversed = FALSE; xmlFree (sortStr); } /* auto item link loading flag */ tmp = (gchar *)xmlGetProp (cur, BAD_CAST"loadItemLink"); if (tmp) { if (!xmlStrcmp ((xmlChar *)tmp, BAD_CAST"true")) node->loadItemLink = TRUE; xmlFree (tmp); } /* viewing mode */ tmp = (gchar *)xmlGetProp (cur, BAD_CAST"viewMode"); if (tmp) { node_set_view_mode (node, atoi (tmp)); xmlFree (tmp); } /* expansion state */ if (xmlHasProp (cur, BAD_CAST"expanded")) node->expanded = TRUE; else if (xmlHasProp (cur, BAD_CAST"collapsed")) node->expanded = FALSE; else node->expanded = TRUE; /* 3. Try to load the favicon (needs to be done before adding to the feed list) */ node_load_icon (node); /* 4. add to GUI parent */ feedlist_node_imported (node); /* 5. import child nodes */ if (IS_FOLDER (node)) { child = cur->xmlChildrenNode; while (child) { if (!xmlStrcmp (child->name, BAD_CAST"outline")) import_parse_outline (child, node, trusted); child = child->next; } } /* 6. do node type specific parsing */ NODE_TYPE (node)->import (node, parentNode, cur, trusted); if (node->subscription) liferea_auth_info_query (node->id); /* 7. update immediately if necessary */ // FIXME: this should not be done here!!! if (node->subscription && needsUpdate) { debug1 (DEBUG_CACHE, "seems to be an import, setting new id: %s and doing first download...", node_get_id(node)); subscription_update (node->subscription, 0); } /* 8. Always update the node info in the DB to ensure a proper node entry and parent node information. Search folders would silentely fail to work without node entry. */ db_node_update (node); debug_exit ("import_parse_outline"); } static void import_parse_body (xmlNodePtr n, nodePtr parentNode, gboolean trusted) { xmlNodePtr cur; cur = n->xmlChildrenNode; while (cur) { if (!xmlStrcmp (cur->name, BAD_CAST"outline")) import_parse_outline (cur, parentNode, trusted); cur = cur->next; } } static void import_parse_OPML (xmlNodePtr n, nodePtr parentNode, gboolean trusted) { xmlNodePtr cur; cur = n->xmlChildrenNode; while (cur) { /* we ignore the head */ if (!xmlStrcmp (cur->name, BAD_CAST"body")) { import_parse_body (cur, parentNode, trusted); } cur = cur->next; } } gboolean import_OPML_feedlist (const gchar *filename, nodePtr parentNode, gboolean showErrors, gboolean trusted) { xmlDocPtr doc; xmlNodePtr cur; gboolean error = FALSE; debug1 (DEBUG_CACHE, "Importing OPML file: %s", filename); /* read the feed list */ doc = xmlParseFile (filename); if (!doc) { if (showErrors) ui_show_error_box (_("XML error while reading OPML file! Could not import \"%s\"!"), filename); else g_warning (_("XML error while reading OPML file! Could not import \"%s\"!"), filename); error = TRUE; } else { cur = xmlDocGetRootElement (doc); if (!cur) { if (showErrors) ui_show_error_box (_("Empty document! OPML document \"%s\" should not be empty when importing."), filename); else g_warning (_("Empty document! OPML document \"%s\" should not be empty when importing."), filename); error = TRUE; } else { if (!trusted) { /* set title only when importing as folder and not as OPML source */ xmlNodePtr title = xpath_find (cur, "/opml/head/title"); if (title) { xmlChar *titleStr = xmlNodeListGetString (title->doc, title->xmlChildrenNode, 1); if (titleStr) { node_set_title (parentNode, (gchar *)titleStr); xmlFree (titleStr); } } } while (cur) { if (!xmlIsBlankNode (cur)) { if (!xmlStrcmp (cur->name, BAD_CAST"opml")) { import_parse_OPML (cur, parentNode, trusted); } else { if (showErrors) ui_show_error_box (_("\"%s\" is not a valid OPML document! Liferea cannot import this file!"), filename); else g_warning (_("\"%s\" is not a valid OPML document! Liferea cannot import this file!"), filename); } } cur = cur->next; } } xmlFreeDoc (doc); } return !error; } /* UI stuff */ static void on_import_activate_cb (const gchar *filename, gpointer user_data) { if (filename) { nodePtr node = node_new (folder_get_node_type ()); node_set_title (node, _("Imported feed list")); feedlist_node_added (node); if (!import_OPML_feedlist (filename, node, TRUE /* show errors */, FALSE /* not trusted */)) { feedlist_remove_node (node); } } } void import_OPML_file (void) { ui_choose_file(_("Import Feed List"), _("Import"), FALSE, on_import_activate_cb, NULL, NULL, "*.opml|*.xml", _("OPML Files"), NULL); } static void on_export_activate_cb (const gchar *filename, gpointer user_data) { if (filename) { if (!export_OPML_feedlist (filename, feedlist_get_root (), FALSE)) ui_show_error_box (_("Error while exporting feed list!")); else ui_show_info_box (_("Feed List exported!")); } } void export_OPML_file (void) { ui_choose_file (_("Export Feed List"), _("Export"), TRUE, on_export_activate_cb, NULL, "feedlist.opml", "*.opml", _("OPML Files"), NULL); } liferea-1.13.7/src/export.h000066400000000000000000000040351415350204600154750ustar00rootroot00000000000000/** * @file export.h OPML feedlist import&export * * Copyright (C) 2003-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _EXPORT_H #define _EXPORT_H #include #include "node.h" /** * Exports a given feed list tree. Can be used to export * the static feed list but also to export subtrees. * * @param filename filename of export file * @param node root node of the tree to export * @param internal FALSE if export to other programs * is requested, will suppress export * of passwords and Liferea specifics * * @returns TRUE on success */ gboolean export_OPML_feedlist(const gchar *filename, nodePtr node, gboolean internal); /** * Reads an OPML file and inserts it into the feedlist. * * @param filename path to file that will be read for importing * @param parentNode node of the parent folder * @param showErrors set to TRUE if errors should generate a error dialog * @param trusted set to TRUE if the feedlist is being imported from a trusted source * * @returns TRUE on success */ gboolean import_OPML_feedlist(const gchar *filename, nodePtr parentNode, gboolean showErrors, gboolean trusted); /** * Called when user requested dialog to import an OPML file. */ void import_OPML_file (void); /** * Called when user requested dialog to save an OPML file. */ void export_OPML_file (void); #endif liferea-1.13.7/src/favicon.c000066400000000000000000000161721415350204600156010ustar00rootroot00000000000000/** * @file favicon.c Saving, loading and discovering favicons * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2015-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include "common.h" #include "debug.h" #include "favicon.h" #include "html.h" #include "metadata.h" GdkPixbuf * favicon_load_from_cache (const gchar *id, guint size) { struct stat statinfo; gchar *filename; GdkPixbuf *pixbuf, *result = NULL; GError *error = NULL; filename = common_create_cache_filename ("favicons", id, "png"); if (0 == stat ((const char*)filename, &statinfo)) { pixbuf = gdk_pixbuf_new_from_file (filename, &error); if (pixbuf && !error) { result = gdk_pixbuf_scale_simple (pixbuf, size, size, GDK_INTERP_BILINEAR); g_object_unref (pixbuf); } else { /* Error */ fprintf (stderr, "Failed to load pixbuf file: %s: %s\n", filename, error->message); g_error_free (error); } } g_free (filename); return result; } void favicon_remove_from_cache(const gchar *id) { gchar *filename; debug_enter("favicon_remove"); /* try to load a saved favicon */ filename = common_create_cache_filename ("favicons", id, "png"); if(g_file_test(filename, G_FILE_TEST_EXISTS)) { if(0 != unlink(filename)) g_warning ("Removal of %s failed", filename); } g_free(filename); debug_exit("favicon_remove"); } /* prevent saving overly huge favicons loaded from net */ static void favicon_pixbuf_size_prepared_cb (GdkPixbufLoader *loader, gint width, gint height, gpointer user_data) { gint max_size = 256; debug2 (DEBUG_UPDATE, " - favicon size is %d:%d", width, height); if (width > max_size || height > max_size) { width = width < max_size ? width : max_size; height = height < max_size ? height : max_size; gdk_pixbuf_loader_set_size(loader, width, height); } } gboolean favicon_save_from_data (const struct updateResult * const result, const gchar *id) { GdkPixbufLoader *loader = gdk_pixbuf_loader_new (); GdkPixbuf *pixbuf; GError *err = NULL; gboolean success = FALSE; g_signal_connect (loader, "size-prepared", G_CALLBACK (favicon_pixbuf_size_prepared_cb), NULL); if (gdk_pixbuf_loader_write (loader, (guchar *)result->data, (gsize)result->size, &err)) { if (gdk_pixbuf_loader_close (loader, &err)) { pixbuf = gdk_pixbuf_loader_get_pixbuf (loader); if (pixbuf) { gchar *tmp = common_create_cache_filename ("favicons", id, "png"); debug2 (DEBUG_UPDATE, "saving favicon %s to file %s", id, tmp); if (!gdk_pixbuf_save (pixbuf, tmp, "png", &err, NULL)) { g_warning ("Could not save favicon (id=%s) to file %s!", id, tmp); } else { success = TRUE; } g_free (tmp); } else { debug0 (DEBUG_UPDATE, "gdk_pixbuf_loader_get_pixbuf() failed!"); } } else { debug0 (DEBUG_UPDATE, "gdk_pixbuf_loader_close() failed!"); } } else { debug0 (DEBUG_UPDATE, "gdk_pixbuf_loader_write() failed!"); gdk_pixbuf_loader_close (loader, NULL); } if (err) { debug1 (DEBUG_UPDATE, "%s", err->message); g_error_free (err); } g_object_unref (loader); return success; } static gint count_slashes(const gchar *str) { const gchar *tmp = str; gint slashes = 0; slashes = 0; while(*tmp) { if(*tmp == '/') slashes++;tmp++; } return slashes; } /* * This code tries to download from a series of URLs. If there are no * favicons, this will make five downloads, three of which will be 404 * errors. Hopefully this will not cause any webservers pain because * this code should be run only once a month per feed. * * 1. --> downloading favicon from the feed (e.g. tag in atom feeds) * 2. --> downloading HTML of the feed url and looking for a favicon reference * 3. --> downloading HTML of root of webserver and looking for a favicon reference * 4. --> downloading favicon from the root HTML * 5. --> downloading favicon from directory of RSS feed * 6. --> downloading favicon from root of webserver of the RSS feed */ GSList * favicon_get_urls (subscriptionPtr subscription, const gchar *html_url) { GSList *urls = NULL; gchar *tmp, *tmp2; const gchar *source_url = subscription->source; /* case 1: the feed parser passed us an icon URL in the subscription metadata */ if (metadata_list_get (subscription->metadata, "icon")) { tmp = g_strstrip (g_strdup (metadata_list_get (subscription->metadata, "icon"))); urls = g_slist_append (urls, tmp); debug1 (DEBUG_UPDATE, "(1) adding favicon search URL: %s", tmp); } /* case 2: */ if (html_url && g_strstr_len (html_url, -1, "://")) { tmp = g_strstrip (g_strdup (html_url)); urls = g_slist_append (urls, tmp); debug1 (DEBUG_UPDATE, "(2) adding favicon search URL: %s", tmp); } /* case 3: */ g_assert (source_url); if (*source_url != '|') { tmp = tmp2 = g_strstrip (g_strdup (source_url)); if (strlen(tmp) && tmp[strlen (tmp) - 1] == '/') tmp[strlen (tmp) - 1] = 0; /* Strip trailing slash */ tmp = strrchr (tmp, '/'); if (tmp) { *tmp = 0; urls = g_slist_append (urls, g_strdup (tmp2)); debug1 (DEBUG_UPDATE, "(3) adding favicon search URL: %s", tmp2); } g_free (tmp2); } /* case 4: */ if (html_url) { if (2 < count_slashes(html_url)) { tmp = tmp2 = g_strstrip (g_strdup (html_url)); tmp = strstr (tmp, "://"); if (tmp) { tmp = strchr (tmp + 3, '/'); if (tmp) { *tmp = 0; tmp = tmp2; tmp2 = g_strdup_printf ("%s/favicon.ico", tmp); urls = g_slist_append (urls, tmp2); debug1 (DEBUG_UPDATE, "(4) adding favicon source URL: %s", tmp2); } } g_free (tmp); } } if (*source_url != '|' && 2 < count_slashes(source_url)) { /* case 5: */ tmp = tmp2 = g_strstrip (g_strdup (source_url)); tmp = strrchr(tmp, '/'); if (tmp) { *tmp = 0; tmp = tmp2; tmp2 = g_strdup_printf ("%s/favicon.ico", tmp); urls = g_slist_append (urls, tmp2); debug1(DEBUG_UPDATE, "(5) adding favicon source URL: %s", tmp2); } g_free (tmp); /* case 6: */ tmp = tmp2 = g_strstrip (g_strdup (source_url)); tmp = strstr(tmp, "://"); if (tmp) { tmp = strchr (tmp + 3, '/'); /* to skip to first subpath */ if (tmp) { *tmp = 0; tmp = tmp2; tmp2 = g_strdup_printf ("%s/favicon.ico", tmp); urls = g_slist_append (urls, tmp2); debug1 (DEBUG_UPDATE, "(6) adding favicon source URL: %s", tmp2); } } g_free (tmp); } return urls; } liferea-1.13.7/src/favicon.h000066400000000000000000000037371415350204600156110ustar00rootroot00000000000000/** * @file favicon.h Liferea favicon handling * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2015-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FAVICON_H #define _FAVICON_H #include #include #include "subscription.h" /** * favicon_load_from_cache: (skip) * Tries to load a given favicon from cache. * * @id: the node id * @size: width / height in pixel * * Returns: (transfer null): a pixmap (or NULL) */ GdkPixbuf * favicon_load_from_cache (const gchar *id, guint size); /** * favicon_remove_from_cache: * Removes a given favicon from the favicon cache. * * @id: the node id */ void favicon_remove_from_cache (const gchar *id); /** * favicon_save_from_data: * * @result: update result * @id: the node id * * Returns: TRUE on success */ gboolean favicon_save_from_data (const struct updateResult * const result, const gchar *id); /** * favicon_get_urls: (skip) * Returns a list of URLs that are download/discovery targets for favicons * and favicon links. * * @subscription: the subscription * @html_url: a base URL for all HTML links * * Returns: (transfer full): list of URL strings */ GSList * favicon_get_urls (subscriptionPtr subscription, const gchar *html_url); #endif liferea-1.13.7/src/feed.c000066400000000000000000000303371415350204600150560ustar00rootroot00000000000000/** * @file feed.c feed node and subscription type * * Copyright (C) 2003-2021 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "feed.h" #include #include "conf.h" #include "common.h" #include "db.h" #include "debug.h" #include "favicon.h" #include "feedlist.h" #include "html.h" #include "itemlist.h" #include "metadata.h" #include "node.h" #include "render.h" #include "update.h" #include "xml.h" #include "ui/icons.h" #include "ui/liferea_shell.h" #include "ui/subscription_dialog.h" #include "ui/feed_list_view.h" feedPtr feed_new (void) { feedPtr feed; feed = g_new0 (struct feed, 1); feed->cacheLimit = CACHE_DEFAULT; feed->valid = TRUE; return feed; } static void feed_import (nodePtr node, nodePtr parent, xmlNodePtr xml, gboolean trusted) { xmlChar *cacheLimitStr, *title, *tmp; feedPtr feed = NULL; xmlChar *typeStr = xmlGetProp (xml, BAD_CAST"type"); feed = feed_new (); feed->fhp = feed_type_str_to_fhp ((gchar *)typeStr); node_set_data (node, feed); node_set_subscription (node, subscription_import (xml, trusted)); /* Set the feed cache limit */ cacheLimitStr = xmlGetProp (xml, BAD_CAST "cacheLimit"); if (cacheLimitStr && !xmlStrcmp (cacheLimitStr, BAD_CAST"unlimited")) feed->cacheLimit = CACHE_UNLIMITED; else feed->cacheLimit = common_parse_long ((gchar *)cacheLimitStr, CACHE_DEFAULT); xmlFree (cacheLimitStr); /* enclosure auto download flag */ tmp = xmlGetProp (xml, BAD_CAST"encAutoDownload"); if (tmp && !xmlStrcmp (tmp, BAD_CAST"true")) feed->encAutoDownload = TRUE; xmlFree (tmp); /* comment feed handling flag */ tmp = xmlGetProp (xml, BAD_CAST"ignoreComments"); if (tmp && !xmlStrcmp (tmp, BAD_CAST"true")) feed->ignoreComments = TRUE; xmlFree (tmp); tmp = xmlGetProp (xml, BAD_CAST"markAsRead"); if (tmp && !xmlStrcmp (tmp, BAD_CAST"true")) feed->markAsRead = TRUE; xmlFree (tmp); tmp = xmlGetProp (xml, BAD_CAST"html5Extract"); if (tmp && !xmlStrcmp (tmp, BAD_CAST"true")) feed->html5Extract = TRUE; xmlFree (tmp); title = xmlGetProp (xml, BAD_CAST"title"); if (!title || !xmlStrcmp (title, BAD_CAST"")) { if (title) xmlFree (title); title = xmlGetProp (xml, BAD_CAST"text"); } node_set_title (node, (gchar *)title); xmlFree (title); if (node->subscription) debug4 (DEBUG_CACHE, "import feed: title=%s source=%s typeStr=%s interval=%d", node_get_title (node), subscription_get_source (node->subscription), typeStr, subscription_get_update_interval (node->subscription)); xmlFree (typeStr); } static void feed_export (nodePtr node, xmlNodePtr xml, gboolean trusted) { feedPtr feed = (feedPtr) node->data; gchar *cacheLimit = NULL; if (node->subscription) subscription_export (node->subscription, xml, trusted); if (trusted) { if (feed->cacheLimit >= 0) cacheLimit = g_strdup_printf ("%d", feed->cacheLimit); if (feed->cacheLimit == CACHE_UNLIMITED) cacheLimit = g_strdup ("unlimited"); if (cacheLimit) xmlNewProp (xml, BAD_CAST"cacheLimit", BAD_CAST cacheLimit); if (feed->encAutoDownload) xmlNewProp (xml, BAD_CAST"encAutoDownload", BAD_CAST"true"); if (feed->ignoreComments) xmlNewProp (xml, BAD_CAST"ignoreComments", BAD_CAST"true"); if (feed->markAsRead) xmlNewProp (xml, BAD_CAST"markAsRead", BAD_CAST"true"); if (feed->html5Extract) xmlNewProp (xml, BAD_CAST"html5Extract", BAD_CAST"true"); } if (node->subscription) debug3 (DEBUG_CACHE, "adding feed: source=%s interval=%d cacheLimit=%s", subscription_get_source (node->subscription), subscription_get_update_interval (node->subscription), (cacheLimit != NULL ? cacheLimit : "")); g_free (cacheLimit); } static void feed_add_xml_attributes (nodePtr node, xmlNodePtr feedNode) { feedPtr feed = (feedPtr)node->data; gchar *tmp; xmlNewTextChild (feedNode, NULL, BAD_CAST"feedId", BAD_CAST node_get_id (node)); xmlNewTextChild (feedNode, NULL, BAD_CAST"feedTitle", BAD_CAST node_get_title (node)); if (node->subscription) { subscription_to_xml (node->subscription, feedNode); tmp = g_strdup_printf("%d", node->subscription->error); xmlNewTextChild(feedNode, NULL, BAD_CAST"error", BAD_CAST tmp); g_free(tmp); } tmp = g_strdup_printf("%d", node->available?1:0); xmlNewTextChild(feedNode, NULL, BAD_CAST"feedStatus", BAD_CAST tmp); g_free(tmp); tmp = g_strdup_printf("file://%s", node_get_favicon_file (node)); xmlNewTextChild(feedNode, NULL, BAD_CAST"favicon", BAD_CAST tmp); g_free(tmp); if(feed->parseErrors && (strlen(feed->parseErrors->str) > 0)) xmlNewTextChild(feedNode, NULL, BAD_CAST"parseError", BAD_CAST feed->parseErrors->str); } xmlDocPtr feed_to_xml (nodePtr node, xmlNodePtr feedNode) { xmlDocPtr doc = NULL; if (!feedNode) { doc = xmlNewDoc (BAD_CAST"1.0"); feedNode = xmlNewDocNode (doc, NULL, BAD_CAST"feed", NULL); xmlDocSetRootElement (doc, feedNode); } feed_add_xml_attributes (node, feedNode); return doc; } guint feed_get_max_item_count (nodePtr node) { gint default_max_items; feedPtr feed = (feedPtr)node->data; switch (feed->cacheLimit) { case CACHE_DEFAULT: conf_get_int_value (DEFAULT_MAX_ITEMS, &default_max_items); return default_max_items; break; case CACHE_DISABLE: case CACHE_UNLIMITED: return G_MAXUINT; break; default: return feed->cacheLimit; break; } } // HTML5 Headline enrichment static void feed_enrich_item_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { itemPtr item; gchar *article; if (!result->data || result->httpstatus >= 400) return; item = item_load (GPOINTER_TO_UINT (userdata)); if (!item) return; article = html_get_article (result->data, result->source); if (article) article = xhtml_strip_dhtml (article); if (article) { // Enable AMP images by replacing by gchar **tmp_split = g_strsplit(article, "metadata), "richContent", article); db_item_update (item); itemlist_update_item (item); g_free (article); } else { // If there is no HTML5 article try to fetch AMP source if there is one gchar *ampurl = html_get_amp_url (result->data); if (ampurl) { UpdateRequest *request; debug3 (DEBUG_PARSING, "Fetching AMP HTML %ld %s : %s", item->id, item->title, ampurl); request = update_request_new ( ampurl, NULL, // No update state needed? How do we prevent an endless redirection loop? NULL // Explicitely do not the feed's proxy/auth options to 3rd parties like Google (AMP)! ); update_execute_request (NULL, request, feed_enrich_item_cb, item, FEED_REQ_NO_FEED); g_free (ampurl); } } item_unload (item); } /** * Checks content of an items source and tries to crawl content */ void feed_enrich_item (subscriptionPtr subscription, itemPtr item) { UpdateRequest *request; if (!item->source) { debug1 (DEBUG_PARSING, "Cannot HTML5-enrich item %s because it has no source!\n", item->title); return; } // Don't enrich twice if (NULL != metadata_list_get (item->metadata, "richContent")) { debug1 (DEBUG_PARSING, "Skipping already HTML5 enriched item %s\n", item->title); return; } // Fetch item->link document and try to parse it as XHTML debug3 (DEBUG_PARSING, "Fetching HTML5 %ld %s : %s", item->id, item->title, item->source); request = update_request_new ( item->source, NULL, // updateState subscription->updateOptions // Pass options of parent feed (e.g. password, proxy...) ); update_execute_request (subscription, request, feed_enrich_item_cb, GUINT_TO_POINTER (item->id), FEED_REQ_NO_FEED); } /* implementation of subscription type interface */ static void feed_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { feedParserCtxtPtr ctxt; nodePtr node = subscription->node; debug_enter ("feed_process_update_result"); ctxt = feed_parser_ctxt_new (subscription, result->data, result->size); /* try to parse the feed */ if (!feed_parse (ctxt)) { /* No feed found, display an error */ node->available = FALSE; } else if (!ctxt->feed->fhp) { /* There's a feed but no handler. This means autodiscovery * found a feed, but we still need to download it. * An update should be in progress that will process it */ } else { /* Feed found, process it */ itemSetPtr itemSet; node->available = TRUE; /* merge the resulting items into the node's item set */ itemSet = node_get_itemset (node); node->newCount = itemset_merge_items (itemSet, ctxt->items, ctxt->feed->valid, ctxt->feed->markAsRead); if (node->newCount) itemlist_merge_itemset (itemSet); itemset_free (itemSet); /* restore user defined properties if necessary */ if ((flags & FEED_REQ_RESET_TITLE) && ctxt->title) node_set_title (node, ctxt->title); // FIXME: this duplicates the db_subscription_update() in subscription.c if (flags > 0) db_subscription_update (subscription); } feed_parser_ctxt_free (ctxt); // FIXME: this should not be here, but in subscription.c if (FETCH_ERROR_NONE != subscription->error) node->available = FALSE; debug_exit ("feed_process_update_result"); } static gboolean feed_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { /* Nothing to do. Feeds require no subscription extra handling. */ return TRUE; } /* implementation of the node type interface */ static itemSetPtr feed_load (nodePtr node) { return db_itemset_load(node->id); } static void feed_save (nodePtr node) { /* Nothing to do. Feeds do not have any UI states */ } static void feed_update_counters (nodePtr node) { node->itemCount = db_itemset_get_item_count (node->id); node->unreadCount = db_itemset_get_unread_count (node->id); } static void feed_remove (nodePtr node) { feed_list_view_remove_node (node); favicon_remove_from_cache (node->id); db_subscription_remove (node->id); } static const gchar * feed_get_direction(nodePtr feed) { if (node_get_title (feed)) return (common_get_text_direction (node_get_title (feed))); else return ("ltr"); } static gchar * feed_render (nodePtr node) { gchar *output = NULL; xmlDocPtr doc; renderParamPtr params; const gchar *text_direction = NULL; text_direction = feed_get_direction (node); params = render_parameter_new (); render_parameter_add (params, "appDirection='%s'", common_get_app_direction ()); render_parameter_add (params, "txtDirection='%s'", text_direction); doc = feed_to_xml (node, NULL); output = render_xml (doc, "feed", params); xmlFreeDoc (doc); return output; } static gboolean feed_add (void) { subscription_dialog_new (); return TRUE; } static void feed_properties (nodePtr node) { subscription_prop_dialog_new (node->subscription); } static void feed_free (nodePtr node) { feedPtr feed = (feedPtr)node->data; if (feed->parseErrors) g_string_free (feed->parseErrors, TRUE); g_free (feed); } subscriptionTypePtr feed_get_subscription_type (void) { static struct subscriptionType sti = { feed_prepare_update_request, feed_process_update_result }; return &sti; } nodeTypePtr feed_get_node_type (void) { static struct nodeType nti = { NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_UPDATE | NODE_CAPABILITY_UPDATE_FAVICON | NODE_CAPABILITY_EXPORT, "feed", /* not used, feed format ids are used instead */ NULL, feed_import, feed_export, feed_load, feed_save, feed_update_counters, feed_remove, feed_render, feed_add, feed_properties, feed_free }; nti.icon = icon_get (ICON_DEFAULT); return &nti; } liferea-1.13.7/src/feed.h000066400000000000000000000077621415350204600150710ustar00rootroot00000000000000/** * @file feed.h common feed handling interface * * Copyright (C) 2003-2021 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FEED_H #define _FEED_H #include #include #include "node_type.h" #include "subscription_type.h" /* * The feed concept in Liferea comprises several standalone concepts: * * 1.) A "feed" is an XML/XML-like document to be parsed * (-> see feed_parser.h) * * 2.) A "feed" is a node type that is visible in the feed list. * * 3.) A "feed" is a subscription type: a way of updating. * * The feed.h interface provides default methods for 2.) and 3.) that * are used per-default but might be overwritten by node source, node * type or subscription type specific implementations. */ /** Common structure to hold all information about a single feed. */ typedef struct feed { struct feedHandler *fhp; /**< Feed format parsing handler. */ /* feed cache state properties */ gint cacheLimit; /**< Amount of cache to save: See the cache_limit enum */ /* feed parsing state */ gboolean valid; /**< FALSE if there was an error in xml_parse_feed() */ GString *parseErrors; /**< Detailed textual description of parsing errors (e.g. library error handler output) */ gint64 time; /**< Feeds modified date */ /* feed specific behaviour settings */ gboolean encAutoDownload; /**< if TRUE do automatically download enclosures */ gboolean ignoreComments; /**< if TRUE ignore comment feeds for this feed */ gboolean markAsRead; /**< if TRUE downloaded items are automatically marked as read */ gboolean html5Extract; /**< if TRUE try to fetch extra content via HTML5 / Google AMP */ gboolean alwaysShowInReduced; /**< for newsbins only, if TRUE always show when using reduced feed list */ } *feedPtr; /** * Create a new feed structure. * * @returns a new feed structure */ feedPtr feed_new(void); /** * Serialization helper function for rendering purposes. * * @param node the feed node to serialize * @param feedNode XML node to add feed attributes to, * or NULL if a new XML document is to * be created * * @returns a new XML document (if feedNode was NULL) */ xmlDocPtr feed_to_xml(nodePtr node, xmlNodePtr xml); // FIXME: doesn't seem to belong here (looks like a subscription type method) /** * Returns the feed-specific maximum cache size. * If none is set it returns the global default * setting. * * @param node the feed node * * @returns max item count */ guint feed_get_max_item_count(nodePtr node); // FIXME: doesn't seem to belong here (looks like a subscription method) /** * feed_enrich_item: * Tries to fetch extra content for the item description * * @subscription: the subscription * @item: the item */ void feed_enrich_item (subscriptionPtr subscription, itemPtr item); /** * Returns the subscription type implementation for simple feed nodes. * This subscription type is used as the default subscription type. */ subscriptionTypePtr feed_get_subscription_type (void); #define IS_FEED(node) (node->type == feed_get_node_type ()) /** * Returns the node type implementation for simple feed nodes. * This node type is the default node type for non-folder nodes. */ nodeTypePtr feed_get_node_type (void); #endif liferea-1.13.7/src/feed_parser.c000066400000000000000000000201571415350204600164310ustar00rootroot00000000000000/** * @file feed_parser.c parsing of different feed formats * * Copyright (C) 2008-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "common.h" #include "debug.h" #include "html.h" #include "metadata.h" #include "xml.h" #include "parsers/atom10.h" #include "parsers/html5_feed.h" #include "parsers/ldjson_feed.h" #include "parsers/rss_channel.h" #define AUTO_DISCOVERY_MAX_REDIRECTS 5 static GSList *feedHandlers = NULL; /**< list of available parser implementations */ struct feed_type { gint id_num; gchar *id_str; }; static GSList * feed_parsers_get_list (void) { if (feedHandlers) return feedHandlers; feedHandlers = g_slist_append (feedHandlers, rss_init_feed_handler ()); feedHandlers = g_slist_append (feedHandlers, atom10_init_feed_handler ()); /* Order is important ! */ feedHandlers = g_slist_append (feedHandlers, ldjson_init_feed_handler ()); feedHandlers = g_slist_append (feedHandlers, html5_init_feed_handler ()); return feedHandlers; } const gchar * feed_type_fhp_to_str (feedHandlerPtr fhp) { if (!fhp) return NULL; return fhp->typeStr; } feedHandlerPtr feed_type_str_to_fhp (const gchar *str) { GSList *iter; feedHandlerPtr fhp = NULL; if (!str) return NULL; for (iter = feed_parsers_get_list (); iter != NULL; iter = iter->next) { fhp = (feedHandlerPtr)iter->data; if (!strcmp(str, fhp->typeStr)) return fhp; } return NULL; } feedParserCtxtPtr feed_parser_ctxt_new (subscriptionPtr subscription, const gchar *data, gsize size) { feedParserCtxtPtr ctxt; ctxt = g_new0 (struct feedParserCtxt, 1); ctxt->subscription = subscription; ctxt->feed = (feedPtr)subscription->node->data; ctxt->data = data; ctxt->dataLength = size; ctxt->tmpdata = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); return ctxt; } void feed_parser_ctxt_free (feedParserCtxtPtr ctxt) { if (ctxt) { /* Don't free the itemset! */ g_hash_table_destroy (ctxt->tmpdata); g_free (ctxt->title); g_free (ctxt); } } /** * This function tries to find a feed link for a given HTTP URI. It * tries to download it. If it finds a valid feed source it parses * this source instead into the given feed parsing context. It also * replaces the HTTP URI with the found feed source. */ static gboolean feed_parser_auto_discover (feedParserCtxtPtr ctxt) { gchar *source = NULL; GSList *links; debug2 (DEBUG_UPDATE, "Starting feed auto discovery (%s) redirects=%d", subscription_get_source (ctxt->subscription), ctxt->subscription->autoDiscoveryTries); links = html_auto_discover_feed (ctxt->data, subscription_get_source (ctxt->subscription)); if (links) source = links->data; // FIXME: let user choose feed! /* FIXME: we only need the !g_str_equal as a workaround after a 404 */ if (source && !g_str_equal (source, subscription_get_source (ctxt->subscription))) { debug1 (DEBUG_UPDATE, "Discovered link: %s", source); subscription_set_source (ctxt->subscription, source); /* The feed that was processed wasn't the correct one, we need to redownload it. * Cancel the update in case there's one in progress */ subscription_cancel_update (ctxt->subscription); subscription_update (ctxt->subscription, FEED_REQ_RESET_TITLE); g_free (source); return TRUE; } debug0 (DEBUG_UPDATE, "No feed link found!"); return FALSE; } static void feed_parser_ctxt_cleanup (feedParserCtxtPtr ctxt) { /* free old temp. parsing data, don't free right after parsing because it can be used until the last feed request is finished */ g_hash_table_destroy (ctxt->tmpdata); ctxt->tmpdata = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); /* we always drop old metadata */ // FIXME: this is bad, doesn't belong here at all metadata_list_free (ctxt->subscription->metadata); ctxt->subscription->metadata = NULL; } /** * General feed source parsing function. Parses the passed feed source * and tries to determine the source type. If all feed handlers fail * tries to do HTML5 feed extraction. If this also fails starts feed * link auto-discovery. * * @param ctxt feed parsing context * * @returns FALSE if auto discovery is indicated, * TRUE if feed type was recognized and parsing was successful */ gboolean feed_parse (feedParserCtxtPtr ctxt) { xmlNodePtr xmlNode = NULL, htmlNode = NULL; xmlDocPtr xmlDoc, htmlDoc; gboolean autoDiscovery = FALSE, success = FALSE; debug_enter ("feed_parse"); g_assert (NULL == ctxt->items); if (ctxt->feed->parseErrors) g_string_truncate (ctxt->feed->parseErrors, 0); else ctxt->feed->parseErrors = g_string_new (NULL); /* Prepare two documents, one parse as XML and one as XHTML. Depending on the feed parser being an HTML parser the one or the other is used. */ /* 1.) try to parse downloaded data as XML */ do { if (NULL == (xmlDoc = xml_parse_feed (ctxt))) { ctxt->subscription->error = FETCH_ERROR_XML; break; } if (NULL == (xmlNode = xmlDocGetRootElement (xmlDoc))) { ctxt->subscription->error = FETCH_ERROR_XML; g_string_append (ctxt->feed->parseErrors, _("Empty document!")); break; } while (xmlNode && xmlIsBlankNode (xmlNode)) { xmlNode = xmlNode->next; } if (!xmlNode->name) { g_string_append (ctxt->feed->parseErrors, _("Invalid XML!")); break; } } while (0); /* 2.) Prepare data as XHTML */ do { if (NULL == (htmlDoc = xhtml_parse (ctxt->data, ctxt->dataLength))) break; if (NULL == (htmlNode = xmlDocGetRootElement (htmlDoc))) { //g_string_append (ctxt->feed->parseErrors, _("Empty document!")); break; } } while (0); /* determine the syndication format and start parser with either XML or XHTML doc */ GSList *handlerIter = feed_parsers_get_list (); while (handlerIter) { feedHandlerPtr handler = (feedHandlerPtr)(handlerIter->data); xmlNodePtr node = handler->html?htmlNode:xmlNode; if (node && handler && handler->checkFormat && (*(handler->checkFormat))(node->doc, node)) { ctxt->feed->fhp = handler; feed_parser_ctxt_cleanup (ctxt); (*(handler->feedParser)) (ctxt, handler->html?htmlNode:xmlNode); success = TRUE; break; } handlerIter = handlerIter->next; } /* 3.) None of the feed formats did work, chance is high that we are working on a HTML documents. Let's look for feed links inside it! */ if (!success) { ctxt->subscription->autoDiscoveryTries++; if (ctxt->subscription->autoDiscoveryTries > AUTO_DISCOVERY_MAX_REDIRECTS) { debug2 (DEBUG_UPDATE, "Stopping feed auto discovery (%s) after too many redirects (limit is %d)", subscription_get_source (ctxt->subscription), AUTO_DISCOVERY_MAX_REDIRECTS); } else { autoDiscovery = feed_parser_auto_discover (ctxt); } } if (htmlDoc) xmlFreeDoc (htmlDoc); if (xmlDoc) xmlFreeDoc (xmlDoc); /* 4.) Update subscription error status */ if (!success && !autoDiscovery) { /* Fuzzy test for HTML document */ if ((strstr (ctxt->data, "") || strstr (ctxt->data, "") || strstr (ctxt->data, "data, "subscription->error = FETCH_ERROR_DISCOVER; } else { if (ctxt->feed->fhp) { debug1 (DEBUG_UPDATE, "discovered feed format: %s", feed_type_fhp_to_str (ctxt->feed->fhp)); ctxt->subscription->autoDiscoveryTries = 0; } else { /* Auto discovery found a link that is being processed asynchronously, for now we do not know wether it will succeed. Still our auto-discovery was successful. */ } success = TRUE; ctxt->subscription->error = FETCH_ERROR_NONE; } debug_exit ("feed_parse"); return success; } liferea-1.13.7/src/feed_parser.h000066400000000000000000000070041415350204600164320ustar00rootroot00000000000000/** * @file feed_parser.h parsing of different feed formats * * Copyright (C) 2008-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FEED_PARSER_H #define _FEED_PARSER_H #include #include "feed.h" /** Holds all information used on feed parsing time */ typedef struct feedParserCtxt { subscriptionPtr subscription; /**< the subscription the feed belongs to (optional) */ feedPtr feed; /**< the feed structure to fill */ GList *items; /**< the list of new items */ struct item *item; /**< the item currently parsed (or NULL) */ GHashTable *tmpdata; /**< tmp data hash used during stateful parsing */ gchar *title; /**< resulting feed/channel title */ const gchar *data; /**< data buffer to parse */ gsize dataLength; /**< length of the data buffer */ } *feedParserCtxtPtr; /** * Function type which parses the given feed data. * * @param ctxt feed parsing context * @param cur the XML node to parse */ typedef void (*feedParserFunc) (feedParserCtxtPtr ctxt, xmlNodePtr cur); /** * Function type which checks a given XML document if it has the expected format. * * @param doc the XML document * @param cur the XML node to parse * * @return TRUE if the XML document has the correct format */ typedef gboolean (*checkFormatFunc) (xmlDocPtr doc, xmlNodePtr cur); /** feed handler interface */ typedef struct feedHandler { const gchar *typeStr; /**< string representation of the feed type */ feedParserFunc feedParser; /**< feed type parse function */ checkFormatFunc checkFormat; /**< Parser for the feed type*/ gboolean html; /**< TRUE if this is a HTML parser (as opposed tp XML feeds) */ } *feedHandlerPtr; /** * Creates a new feed parsing context. * * @subscription the feed's subscription * @data the feed data to parse * @size size of feed data * * @returns a new feed parsing context */ feedParserCtxtPtr feed_parser_ctxt_new (subscriptionPtr subscription, const gchar *data, gsize size); /** * Frees the given parser context. Note: it does * not free the list of new items! * * @param ctxt the feed parsing context */ void feed_parser_ctxt_free (feedParserCtxtPtr ctxt); /** * Lookup a feed type string from the feed type id. * * @param id feed type id * * @returns feed parser implementation */ feedHandlerPtr feed_type_str_to_fhp (const gchar *id); /** * Get feed type id for a given parser implementation. * * @param fhp feed type handler * * @returns feed type id */ const gchar *feed_type_fhp_to_str (feedHandlerPtr fhp); /** * General feed source parsing function. Parses the passed feed source * and tries to determine the source type. * * @param ctxt feed parsing context * * @returns FALSE if auto discovery is indicated, * TRUE if feed type was recognized */ gboolean feed_parse (feedParserCtxtPtr ctxt); #endif liferea-1.13.7/src/feedlist.c000066400000000000000000000366471415350204600157640ustar00rootroot00000000000000/* * @file feedlist.c subscriptions as an hierarchic tree * * Copyright (C) 2005-2019 Lars Windolf * Copyright (C) 2005-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "comments.h" #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "itemlist.h" #include "net_monitor.h" #include "node.h" #include "update.h" #include "vfolder.h" #include "ui/feed_list_view.h" #include "ui/itemview.h" #include "ui/liferea_shell.h" #include "ui/feed_list_view.h" #include "fl_sources/node_source.h" static void feedlist_save (void); struct _FeedList { GObject parentInstance; guint newCount; /*<< overall new item count */ nodePtr rootNode; /*<< the feed list root node */ nodePtr selectedNode; /*<< matches the node selected in the feed list tree view, which is not necessarily the displayed one (e.g. folders without recursive display enabled) */ guint saveTimer; /*<< timer id for delayed feed list saving */ guint autoUpdateTimer; /*<< timer id for auto update */ gboolean loading; /*<< prevents the feed list being saved before it is completely loaded */ }; enum { NEW_ITEMS, /*<< node has new items after update */ NODE_UPDATED, /*<< node display info (title, unread count) has changed */ LAST_SIGNAL }; #define ROOTNODE feedlist->rootNode #define SELECTED feedlist->selectedNode static guint feedlist_signals[LAST_SIGNAL] = { 0 }; FeedList *feedlist = NULL; // singleton G_DEFINE_TYPE (FeedList, feedlist, G_TYPE_OBJECT); static void feedlist_free_node (nodePtr node) { if (node->children) node_foreach_child (node, feedlist_free_node); node->parent->children = g_slist_remove (node->parent->children, node); node_free (node); } static void feedlist_finalize (GObject *object) { /* Stop all timer based activity */ if (feedlist->autoUpdateTimer) { g_source_remove (feedlist->autoUpdateTimer); feedlist->autoUpdateTimer = 0; } if (feedlist->saveTimer) { g_source_remove (feedlist->saveTimer); feedlist->saveTimer = 0; } /* Enforce synchronous save upon exit */ feedlist_save (); /* Save last selection for next start */ if (feedlist->selectedNode) conf_set_str_value (LAST_NODE_SELECTED, feedlist->selectedNode->id); /* And destroy everything */ feedlist_foreach (feedlist_free_node); node_free (ROOTNODE); ROOTNODE = NULL; /* This might also be a good place to get for some other cleanup */ comments_deinit (); } static void feedlist_class_init (FeedListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = feedlist_finalize; feedlist_signals[NEW_ITEMS] = g_signal_new ("new-items", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); feedlist_signals[NODE_UPDATED] = g_signal_new ("node-updated", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); } static gboolean feedlist_auto_update (void *data) { debug_enter ("feedlist_auto_update"); if (network_monitor_is_online ()) node_auto_update_subscription (ROOTNODE); else debug0 (DEBUG_UPDATE, "no update processing because we are offline!"); debug_exit ("feedlist_auto_update"); return TRUE; } static void on_network_status_changed (gpointer instance, gboolean online, gpointer data) { if (online) feedlist_auto_update (NULL); } /* This method is used to initialize the node states in the feed list */ static void feedlist_init_node (nodePtr node) { if (node->expanded) feed_list_view_set_expansion (node, TRUE); if (node->subscription) db_subscription_load (node->subscription); node_update_counters (node); feed_list_view_update_node (node->id); /* Necessary to initially set folder unread counters */ node_foreach_child (node, feedlist_init_node); } static void feedlist_init (FeedList *fl) { gint startup_feed_action; debug_enter ("feedlist_init"); /* 1. Prepare globally accessible singleton */ g_assert (NULL == feedlist); feedlist = fl; feedlist->loading = TRUE; /* 2. Set up a root node and import the feed list source structure. */ debug0 (DEBUG_CACHE, "Setting up root node"); ROOTNODE = node_source_setup_root (); /* 3. Ensure folder expansion and unread count*/ debug0 (DEBUG_CACHE, "Initializing node state"); feedlist_foreach (feedlist_init_node); /* 4. Check if feeds do need updating. */ debug0 (DEBUG_UPDATE, "Performing initial feed update"); conf_get_int_value (STARTUP_FEED_ACTION, &startup_feed_action); if (0 == startup_feed_action) { /* Update all feeds */ if (network_monitor_is_online ()) { debug0 (DEBUG_UPDATE, "initial update: updating all feeds"); node_auto_update_subscription (feedlist_get_root ()); } else { debug0 (DEBUG_UPDATE, "initial update: prevented because we are offline"); } } else { debug0 (DEBUG_UPDATE, "initial update: resetting feed counter"); feedlist_reset_update_counters (NULL); } /* 5. Purge old nodes from the database */ db_node_cleanup (feedlist_get_root ()); /* 6. Start automatic updating */ feedlist->autoUpdateTimer = g_timeout_add_seconds (10, feedlist_auto_update, NULL); g_signal_connect (network_monitor_get (), "online-status-changed", G_CALLBACK (on_network_status_changed), NULL); /* 7. Finally save the new feed list state */ feedlist->loading = FALSE; feedlist_schedule_save (); debug_exit ("feedlist_init"); } static void feedlist_unselect(void); nodePtr feedlist_get_root (void) { return ROOTNODE; } nodePtr feedlist_get_selected (void) { return SELECTED; } static nodePtr feedlist_get_parent_node (void) { g_assert (NULL != ROOTNODE); if (!SELECTED) return ROOTNODE; if (IS_FOLDER (SELECTED)) return SELECTED; if (SELECTED->parent) return SELECTED->parent; return ROOTNODE; } nodePtr feedlist_find_node (nodePtr parent, feedListFindType type, const gchar *str) { GSList *iter; g_assert (str); iter = parent->children; while (iter) { gboolean found = FALSE; nodePtr result, node = (nodePtr)iter->data; /* Check child node */ switch (type) { case NODE_BY_URL: if (node->subscription) found = g_str_equal (str, subscription_get_source (node->subscription)); break; case NODE_BY_ID: found = g_str_equal (str, node->id); break; case FOLDER_BY_TITLE: if (IS_FOLDER (node)) found = g_str_equal (str, node->title); break; default: break; } if (found) return node; /* And recurse */ result = feedlist_find_node (node, type, str); if (result) return result; iter = g_slist_next (iter); } return NULL; } gboolean feedlist_is_writable (void) { nodePtr node; node = feedlist_get_parent_node (); return (0 != (NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)); } static void feedlist_update_node_counters (nodePtr node) { node_update_counters (node); /* update with parent propagation */ if (node->needsUpdate) feed_list_view_update_node (node->id); if (node->children) node_foreach_child (node, feedlist_update_node_counters); } void feedlist_mark_all_read (nodePtr node) { if (!node) return; feedlist_reset_new_item_count (); if (node != ROOTNODE) node_mark_all_read (node); else node_foreach_child (ROOTNODE, node_mark_all_read); feedlist_foreach (feedlist_update_node_counters); itemview_select_item (NULL); itemview_update_all_items (); itemview_update (); } /* statistic handling methods */ guint feedlist_get_unread_item_count (void) { if (!feedlist) return 0; return (ROOTNODE->unreadCount > 0)?ROOTNODE->unreadCount:0; } guint feedlist_get_new_item_count (void) { if (!feedlist) return 0; return (feedlist->newCount > 0)?feedlist->newCount:0; } void feedlist_reset_new_item_count (void) { if (feedlist->newCount) feedlist->newCount = 0; feedlist_new_items (0); } void feedlist_add_folder (const gchar *title) { nodePtr parent; g_assert (NULL != title); parent = feedlist_get_parent_node (); if(0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) return; node_source_add_folder (parent->source->root, title); } void feedlist_add_subscription (const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags) { nodePtr parent; g_assert (NULL != source); parent = feedlist_get_parent_node (); if (0 == (NODE_TYPE (parent->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) { g_warning ("feedlist_add_subscription: this should never happen!"); return; } node_source_add_subscription (parent->source->root, subscription_new (source, filter, options)); } void feedlist_add_subscription_check_duplicate(const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags) { nodePtr duplicateNode = NULL; duplicateNode = feedlist_find_node (feedlist_get_root (), NODE_BY_URL, source); if (!duplicateNode) { feedlist_add_subscription (source, filter, options, FEED_REQ_PRIORITY_HIGH); } else { feed_list_view_add_duplicate_url_subscription (subscription_new (source, filter, options), duplicateNode); } } void feedlist_node_imported (nodePtr node) { feed_list_view_add_node (node); feedlist_schedule_save (); } void feedlist_node_added (nodePtr node) { gint position = -1; g_assert (NULL == node->parent); if (SELECTED && !IS_FOLDER (SELECTED)) { position = g_slist_index (SELECTED->parent->children, SELECTED); if (position > -1) position++; /* insert after selected child index */ } node_set_parent (node, feedlist_get_parent_node (), position); if (node->subscription) db_subscription_update (node->subscription); db_node_update (node); feedlist_node_imported (node); feed_list_view_select (node); } void feedlist_remove_node (nodePtr node) { if (node->source->root != node) node_source_remove_node (node->source->root, node); else feedlist_node_removed (node); } void feedlist_node_removed (nodePtr node) { if (node == SELECTED) feedlist_unselect (); /* First remove all children */ node_foreach_child (node, feedlist_node_removed); node_remove (node); feed_list_view_remove_node (node); node->parent->children = g_slist_remove (node->parent->children, node); node_free (node); feedlist_schedule_save (); } /* Checks if the given node is a subscription node and has at least one unread item or is selected, if yes it is added to the list ref passed as user_data */ static void feedlist_collect_unread (nodePtr node, gpointer user_data) { GSList **list = (GSList **)user_data; if (node->children) { node_foreach_child_data (node, feedlist_collect_unread, user_data); return; } if (!node->subscription) return; if (!node->unreadCount && !g_str_equal (node->id, SELECTED->id)) return; *list = g_slist_append (*list, g_strdup (node->id)); } nodePtr feedlist_find_unread_feed (nodePtr folder) { GSList *list = NULL; nodePtr result = NULL; feedlist_foreach_data (feedlist_collect_unread, &list); if (list) { // Pass 1: try after selected node in list if (SELECTED) { GSList *s = g_slist_find_custom (list, SELECTED->id, (GCompareFunc)g_strcmp0); if (s) s = g_slist_next (s); if (s) result = node_from_id (s->data); } // Pass 2: just return first node in list if (!result) result = node_from_id (list->data); g_slist_free_full (list, g_free); } return result; } /* selection handling */ static void feedlist_unselect (void) { SELECTED = NULL; itemview_set_displayed_node (NULL); itemview_update (); itemlist_unload (FALSE /* mark all read */); feed_list_view_select (NULL); liferea_shell_update_feed_menu (TRUE, FALSE, FALSE); liferea_shell_update_allitems_actions (FALSE, FALSE); } static void feedlist_selection_changed (gpointer obj, gchar * nodeId, gpointer data) { debug_enter ("feedlist_selection_changed"); nodePtr node = node_from_id (nodeId); if (node) { if (node != SELECTED) { debug1 (DEBUG_GUI, "new selected node: %s", node?node_get_title (node):"none"); /* When the user selects a feed in the feed list we assume that he got notified of the new items or isn't interested in the event anymore... */ if (0 != feedlist->newCount) feedlist_reset_new_item_count (); /* Unload visible items. */ itemlist_unload (TRUE); /* Load items of new selected node. */ SELECTED = node; if (SELECTED) { liferea_shell_set_view_mode (node_get_view_mode (SELECTED)); itemlist_load (SELECTED); } else { itemview_clear (); } } else { debug1 (DEBUG_GUI, "selected node stayed: %s", node?node_get_title (node):"none"); } } else { debug1 (DEBUG_GUI, "failed to resolve node id: %s", nodeId); } debug_exit ("feedlist_selection_changed"); } static gboolean feedlist_schedule_save_cb (gpointer user_data) { /* step 1: request each node to save its state, that is mostly needed for nodes that are node sources */ feedlist_foreach (node_save); /* step 2: request saving for the root node and thereby forcing the default source to write an OPML file */ NODE_SOURCE_TYPE (ROOTNODE)->source_export (ROOTNODE); feedlist->saveTimer = 0; return FALSE; } void feedlist_schedule_save (void) { if (feedlist->loading || feedlist->saveTimer) return; debug0 (DEBUG_CONF, "Scheduling feedlist save"); /* By waiting here 5s and checking feedlist_save_time we hope to catch bulks of feed list changes and save less often */ feedlist->saveTimer = g_timeout_add_seconds (5, feedlist_schedule_save_cb, NULL); } /* Handling updates */ void feedlist_new_items (guint newCount) { feedlist->newCount += newCount; /* On subsequent feed updates with cache drops more new items can be reported than effectively were merged. The simplest way to catch this case is by checking for new count > unread count here. */ if (feedlist->newCount > ROOTNODE->unreadCount) feedlist->newCount = ROOTNODE->unreadCount; g_signal_emit_by_name (feedlist, "new-items", feedlist->newCount); } void feedlist_node_was_updated (nodePtr node) { node_update_counters (node); feedlist_schedule_save (); g_signal_emit_by_name (feedlist, "node-updated", node->title); } /* This method is only to be used when exiting the program! */ static void feedlist_save (void) { debug0 (DEBUG_CONF, "Forced feed list save"); feedlist_schedule_save_cb (NULL); } void feedlist_reset_update_counters (nodePtr node) { guint64 now; if (!node) node = feedlist_get_root (); now = g_get_real_time(); node_foreach_child_data (node, node_reset_update_counter, &now); } FeedList * feedlist_create (gpointer flv) { FeedList *fl = FEED_LIST (g_object_new (FEED_LIST_TYPE, NULL)); g_signal_connect (flv, "selection-changed", G_CALLBACK (feedlist_selection_changed), fl); return fl; } liferea-1.13.7/src/feedlist.h000066400000000000000000000170111415350204600157510ustar00rootroot00000000000000/* * @file feedlist.h subscriptions as an hierarchic tree * * Copyright (C) 2005-2019 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FEEDLIST_H #define _FEEDLIST_H #include #include #include "node.h" #include "update.h" G_BEGIN_DECLS #define FEED_LIST_TYPE (feedlist_get_type ()) G_DECLARE_FINAL_TYPE (FeedList, feedlist, FEED, LIST, GObject) /** * feedlist_create: (skip) * * Set up the feed list. * * Returns: (transfer full): the feed list instance */ FeedList * feedlist_create (gpointer feedListView); /** * feedlist_get_selected: (skip) * * Get currently selected feed list node * * Returns: (transfer none) (nullable): selected node (or NULL) */ nodePtr feedlist_get_selected (void); /** * feedlist_get_unread_item_count: * * Query overall number of unread items. * * Returns: overall number of unread items. */ guint feedlist_get_unread_item_count (void); /** * feedlist_get_new_item_count: * * Query overall number of new items. * * Note: result might be slightly off, but error * won't aggregate over time. * * Returns: overall number of new items. */ guint feedlist_get_new_item_count (void); /** * feedlist_reset_new_item_count: * * Reset the global feed list new item counter. * * TODO: use signal instead */ void feedlist_reset_new_item_count (void); /** * feedlist_node_was_updated: (skip) * @node: the updated node * * To be called when a feed is updated and has * new or dropped items forcing a node unread count * update for all affected nodes in the feed list. * */ void feedlist_node_was_updated (nodePtr node); /** * feedlist_get_root: (skip) * * Helper function to query the feed list root node. * * Returns: (transfer none): the feed list root node */ nodePtr feedlist_get_root (void); typedef enum { NODE_BY_URL, NODE_BY_ID, FOLDER_BY_TITLE } feedListFindType; /** * feedlist_find_node: (skip) * @parent: (nullable): parent node to traverse from (or NULL) * @type: NODE_BY_(URL|FOLDER_TITLE|ID) * @str: string to compare to * * Search trough list of subscriptions for a node matching exactly * to an criteria defined by the find type and comparison string. * Searches recursively from a given parent node or the root node. * Always returns just the first occurence in traversal order. * * Returns: (nullable) (transfer none): a nodePtr or NULL */ nodePtr feedlist_find_node (nodePtr parent, feedListFindType type, const gchar *str); /** * feedlist_add_subscription: (skip) * @source: the subscriptions source URL * @filter: (nullable): NULL or the filter for the subscription * @options: (nullable): NULL or the update options * @flags: download request flags * * Adds a new subscription to the feed list. */ void feedlist_add_subscription (const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags); /** * feedlist_add_subscription_check_duplicate: (skip) * @source: the subscriptions source URL * @filter: (nullable): NULL or the filter for the subscription * @options: (nullable): NULL or the update options * @flags: download request flags * * Adds a new subscription to the feed list, if there are no subscriptions with * the same URL, or opens a confirmation dialog else. */ void feedlist_add_subscription_check_duplicate (const gchar *source, const gchar *filter, updateOptionsPtr options, gint flags); /** * feedlist_add_folder: * @title: the title of the new folder. * * Adds a folder to the feed list without any user interaction. */ void feedlist_add_folder (const gchar *title); /** * feedlist_node_added: (skip) * @node: the new node * * Notifies the feed list controller that a new node * was added to the feed list. This method will insert * the new node into the feed list view and select * the new node. * * This method is used for all node types (feeds, folders...). * * Before calling this method the node must be given * a parent node using node_set_parent(). * */ void feedlist_node_added (nodePtr node); /** * feedlist_node_imported: (skip) * @node: the new node * * Notifies the feed list controller that a new node * was added to the feed list. Similar to feedlist_node_added() * the new node will be added to the feed list but the * selection won't be changed. * * This method is used for all node types (feeds, folders...). * * Before calling this method the node must be given * a parent node using node_set_parent(). * */ void feedlist_node_imported (nodePtr node); /** * feedlist_remove_node: (skip) * @node: the node to remove * * Removes the given node from the feed list. */ void feedlist_remove_node (nodePtr node); /** * feedlist_node_removed: (skip) * @node: the removed node * * Notifies the feed list controller that an existing * node was removed from it's source (feed list subtree) * and is to be destroyed and to be removed from the * feed list view. * */ void feedlist_node_removed (nodePtr node); /** * feedlist_schedule_save: (skip) * * Schedules a save requests for the feed list within the next 5s. * Triggers state saving for all feed list sources. */ void feedlist_schedule_save (void); /** * feedlist_reset_update_counters: (skip) * @node: (nullable): the node (or NULL for whole feed list) * * Resets the update counter of all childs of the given node * */ void feedlist_reset_update_counters (nodePtr node); gboolean feedlist_is_writable (void); /** * feedlist_mark_all_read: (skip) * @node: the node to start with * * Triggers a recursive mark-all-read on the given node * and updates the feed list afterwards. * */ void feedlist_mark_all_read (nodePtr node); /* feed list iterating interface */ /** * feedlist_foreach: (skip) * @func: the function to process all found elements * * Helper function to recursivly call node methods for all * nodes in the feed list. This method is just a wrapper for * node_foreach_child(). */ #define feedlist_foreach(func) node_foreach_child(feedlist_get_root(), func) /** * feedlist_foreach_data: (skip) * @func: the function to process all found elements * @user_data: specifies the second argument that func should be passed * * Helper function to recursivly call node methods for all * nodes in the feed list. This method is just a wrapper for * node_foreach_child_data(). * */ #define feedlist_foreach_data(func, user_data) node_foreach_child_data(feedlist_get_root(), func, user_data) /** * feedlist_find_unread_feed: (skip) * @folder: the folder to search * * Tries to find the first node with an unread item in the given folder. * * Returns: (nullable) (transfer none): a found node or NULL */ nodePtr feedlist_find_unread_feed (nodePtr folder); /** * feedlist_new_items: (skip) * @newCount: number of new and unread items * * To be called when node subscription update gained new items. */ void feedlist_new_items (guint newCount); G_END_DECLS #endif liferea-1.13.7/src/fl_sources/000077500000000000000000000000001415350204600161455ustar00rootroot00000000000000liferea-1.13.7/src/fl_sources/Makefile.am000066400000000000000000000025251415350204600202050ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in AM_CPPFLAGS = \ -DPACKAGE_DATA_DIR=\""$(datadir)"\" \ -DPACKAGE_LIB_DIR=\""$(pkglibdir)"\" \ -DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \ -I$(top_srcdir)/src \ $(PACKAGE_CFLAGS) noinst_LIBRARIES = libliflsources.a libliflsources_a_SOURCES = node_source.c node_source.h \ node_source_activatable.c node_source_activatable.h \ default_source.c default_source.h \ dummy_source.c dummy_source.h \ google_reader_api_edit.c google_reader_api_edit.h \ google_reader_api.h \ google_source.c google_source.h \ json_api_mapper.c json_api_mapper.h \ opml_source.c opml_source.h \ reedah_source.c reedah_source.h \ reedah_source_feed.c \ reedah_source_feed_list.c reedah_source_feed_list.h \ theoldreader_source.c theoldreader_source.h \ theoldreader_source_feed.c \ theoldreader_source_feed_list.c theoldreader_source_feed_list.h \ ttrss_source.c ttrss_source.h \ ttrss_source_feed.c \ ttrss_source_feed_list.c ttrss_source_feed_list.h libliflsources_a_CFLAGS = $(PACKAGE_FLAGS) liferea-1.13.7/src/fl_sources/default_source.c000066400000000000000000000143171415350204600213230ustar00rootroot00000000000000/** * @file default_source.c default static feed list source * * Copyright (C) 2005-2014 Lars Windolf * Copyright (C) 2005-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include "common.h" #include "debug.h" #include "export.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "migrate.h" #include "update.h" #include "fl_sources/default_source.h" #include "fl_sources/node_source.h" /** lock to prevent feed list saving while loading */ static gboolean feedlistImport = TRUE; static gchar * default_source_source_get_feedlist (nodePtr node) { return common_create_config_filename ("feedlist.opml"); } static void default_source_import (nodePtr node) { gchar *filename, *backupFilename; gchar *content; gssize length; debug_enter ("default_source_source_import"); g_assert (TRUE == feedlistImport); filename = default_source_source_get_feedlist (node); backupFilename = g_strdup_printf("%s.backup", filename); if (g_file_test (filename, G_FILE_TEST_EXISTS)) { if (!import_OPML_feedlist (filename, node, FALSE, TRUE)) g_error ("Fatal: Feed list import failed! You might want to try to restore\n" "the feed list file %s from the backup in %s", filename, backupFilename); /* upon successful import create a backup copy of the feed list */ if (g_file_get_contents (filename, &content, (gsize *)&length, NULL)) { g_file_set_contents (backupFilename, content, length, NULL); g_free (content); } } else { /* If subscriptions could not be loaded try cache migration or provide a default feed list */ gchar *filename14 = g_strdup_printf ("%s/.liferea_1.4/feedlist.opml", g_get_home_dir ()); gchar *filename16 = g_strdup_printf ("%s/.liferea_1.6/feedlist.opml", g_get_home_dir ()); gchar *filename18 = g_strdup_printf ("%s/.liferea_1.8/feedlist.opml", g_get_home_dir ()); if (g_file_test (filename18, G_FILE_TEST_EXISTS)) { migration_execute (MIGRATION_FROM_18, node); } else if (g_file_test (filename16, G_FILE_TEST_EXISTS)) { migration_execute (MIGRATION_FROM_16, node); } else if (g_file_test (filename14, G_FILE_TEST_EXISTS)) { migration_execute (MIGRATION_FROM_14, node); } else { gchar *filename = common_get_localized_filename (PACKAGE_DATA_DIR "/" PACKAGE "/opml/feedlist_%s.opml"); if (!filename) g_error ("Fatal: No migration possible and no default feedlist found!"); if (!import_OPML_feedlist (filename, node, FALSE, TRUE)) g_error ("Fatal: Feed list import failed!"); g_free (filename); } g_free (filename18); g_free (filename16); g_free (filename14); } g_free (filename); g_free (backupFilename); feedlistImport = FALSE; debug_exit ("default_source_source_import"); } static void default_source_export (nodePtr node) { gchar *filename; if (feedlistImport) return; debug_enter ("default_source_source_export"); g_assert (node->source->root == feedlist_get_root ()); filename = default_source_source_get_feedlist (node); export_OPML_feedlist (filename, node->source->root, TRUE); g_free (filename); debug_exit ("default_source_source_export"); } static void default_source_auto_update (nodePtr node) { node_foreach_child (node, node_auto_update_subscription); } static nodePtr default_source_add_subscription (nodePtr node, subscriptionPtr subscription) { /* For the local feed list source subscriptions are always feed subscriptions implemented by the feed node and subscription type... */ nodePtr child = node_new (feed_get_node_type ()); node_set_title (child, _("New Subscription")); node_set_data (child, feed_new ()); node_set_subscription (child, subscription); /* feed subscription type is implicit */ feedlist_node_added (child); subscription_update (subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); return child; } static nodePtr default_source_add_folder (nodePtr node, const gchar *title) { /* For the local feed list source folders are always real folders implemented by the folder node type... */ nodePtr child = node_new (folder_get_node_type()); node_set_title (child, title); feedlist_node_added (child); return child; } static void default_source_remove_node (nodePtr node, nodePtr child) { /* The default source can always immediately serve remove requests. */ feedlist_node_removed (child); } static void default_source_init (void) { } static void default_source_deinit (void) { } /* node source type definition */ static struct nodeSourceType nst = { .id = "fl_default", .name = "Static Feed List", .capabilities = NODE_SOURCE_CAPABILITY_IS_ROOT | NODE_SOURCE_CAPABILITY_HIERARCHIC_FEEDLIST | NODE_SOURCE_CAPABILITY_ADD_FEED | NODE_SOURCE_CAPABILITY_ADD_FOLDER | NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST, .feedSubscriptionType = NULL, .sourceSubscriptionType = NULL, .source_type_init = default_source_init, .source_type_deinit = default_source_deinit, .source_new = NULL, .source_delete = NULL, .source_import = default_source_import, .source_export = default_source_export, .source_get_feedlist = default_source_source_get_feedlist, .source_auto_update = default_source_auto_update, .free = NULL, .add_subscription = default_source_add_subscription, .add_folder = default_source_add_folder, .remove_node = default_source_remove_node, .convert_to_local = NULL }; nodeSourceTypePtr default_source_get_type (void) { nst.feedSubscriptionType = feed_get_subscription_type (); return &nst; } liferea-1.13.7/src/fl_sources/default_source.h000066400000000000000000000021501415350204600213200ustar00rootroot00000000000000/** * @file default_source.h default static feedlist provider * * Copyright (C) 2005 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _DEFAULT_SOURCE_H #define _DEFAULT_SOURCE_H #ifdef HAVE_CONFIG_H # include #endif #include "fl_sources/node_source.h" /** * Returns default source type implementation info. */ nodeSourceTypePtr default_source_get_type(void); #endif /* _DEFAULT_SOURCE_H */ liferea-1.13.7/src/fl_sources/dummy_source.c000066400000000000000000000032311415350204600210230ustar00rootroot00000000000000/** * @file dummy_source.c dummy feed list source * * Copyright (C) 2006-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/dummy_source.h" #include "ui/icons.h" static gchar * dummy_source_get_feedlist(nodePtr node) { return NULL; } static void dummy_source_noop (nodePtr node) { } static void dummy_source_import(nodePtr node) { node->icon = (gpointer)icon_get (ICON_UNAVAILABLE); } static void dummy_source_init(void) { } static void dummy_source_deinit(void) { } /* feed list provider plugin definition */ static struct nodeSourceType nst = { .id = NODE_SOURCE_TYPE_DUMMY_ID, .name = "Dummy Feed List Source", .source_type_init = dummy_source_init, .source_type_deinit = dummy_source_deinit, .source_import = dummy_source_import, .source_export = dummy_source_noop, .source_get_feedlist = dummy_source_get_feedlist, .source_auto_update = dummy_source_noop }; nodeSourceTypePtr dummy_source_get_type(void) { return &nst; } liferea-1.13.7/src/fl_sources/dummy_source.h000066400000000000000000000020441415350204600210310ustar00rootroot00000000000000/** * @file dummy_source.h dummy feed list source * * Copyright (C) 2005-2007 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _DUMMY_SOURCE_H #define _DUMMY_SOURCE_H #include "fl_sources/node_source.h" /** * Returns dummy source type implementation info. */ nodeSourceTypePtr dummy_source_get_type(void); #endif /* _DUMMY_SOURCE_H */ liferea-1.13.7/src/fl_sources/google_reader_api.h000066400000000000000000000036271415350204600217550ustar00rootroot00000000000000/** * @file google_reader_api.h Interface for implementing the Google Reader API * * Copyright (C) 2014-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _GOOGLE_READER_API_H #define _GOOGLE_READER_API_H /** A set of tags (states) defined by Google Reader */ #define GOOGLE_READER_TAG_KEPT_UNREAD "user/-/state/com.google/kept-unread" #define GOOGLE_READER_TAG_READ "user/-/state/com.google/read" #define GOOGLE_READER_TAG_TRACKING_KEPT_UNREAD "user/-/state/com.google/tracking-kept-unread" #define GOOGLE_READER_TAG_STARRED "user/-/state/com.google/starred" typedef struct googleReaderApi { gboolean json; /**< Returns mostly JSON */ /** Endpoint definitions */ const char *unread_count; const char *subscription_list; const char *add_subscription; const char *add_subscription_post; const char *remove_subscription; const char *remove_subscription_post; const char *edit_tag; const char *edit_tag_add_post; const char *edit_tag_ar_tag_post; const char *edit_tag_remove_post; const char *edit_add_label; const char *edit_add_label_post; const char *token; /* when extending this list add assertions in node_source_type_register! */ } googleReaderApi; #endif liferea-1.13.7/src/fl_sources/google_reader_api_edit.c000066400000000000000000000445731415350204600227620ustar00rootroot00000000000000/** * @file google_reader_api_edit.c Google Reader API syncing support * * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2014-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "google_reader_api_edit.h" #include "common.h" #include "debug.h" #include "feedlist.h" #include "json.h" #include "update.h" #include "subscription.h" /** * A structure to indicate an edit to the node source remote feed "database". * These edits are put in a queue and processed in sequential order * so that the API endpoint does not end up processing the requests in an * unintended order. */ typedef struct GoogleReaderAction { /** * The guid of the item to edit. This will be ignored if the * edit is acting on an subscription rather than an item. */ gchar* guid; /** * A MANDATORY feed url to containing the item, or the url of the * subscription to edit. */ gchar* feedUrl; /** * The source type. Currently known types are "feed" and "user". * "user" sources are used, for example, for items that are links (as * opposed to posts) in broadcast-friends. The unique id of the source * is of the form /. */ gchar* feedUrlType; /** * An optional label id to use for an action (e.g. to add a feed label) * Must be of syntax "user/-/label/MyLabel" */ gchar* label; /** * A callback function on completion of the edit. */ void (*callback) (nodeSourcePtr source, struct GoogleReaderAction* edit, gboolean success); /** * The type of this GoogleReaderAction. */ int actionType; /** * The node source this action runs against (mandatory). */ nodeSourcePtr source; /** * The action result data (available on callback) */ gchar *response; } *GoogleReaderActionPtr; enum { EDIT_ACTION_MARK_READ, EDIT_ACTION_MARK_UNREAD, EDIT_ACTION_TRACKING_MARK_UNREAD, /**< every UNREAD request, should be followed by tracking-kept-unread */ EDIT_ACTION_MARK_STARRED, EDIT_ACTION_MARK_UNSTARRED, EDIT_ACTION_ADD_SUBSCRIPTION, EDIT_ACTION_REMOVE_SUBSCRIPTION, EDIT_ACTION_ADD_LABEL }; typedef struct GoogleReaderAction* editPtr; typedef struct GoogleReaderActionCtxt { gchar *nodeId; GoogleReaderActionPtr action; } *GoogleReaderActionCtxtPtr; static void google_reader_api_edit_push (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean head); static GoogleReaderActionPtr google_reader_api_action_new (int actionType) { GoogleReaderActionPtr action = g_slice_new0 (struct GoogleReaderAction); action->actionType = actionType; return action; } static void google_reader_api_action_free (GoogleReaderActionPtr action) { g_free (action->guid); g_free (action->feedUrl); g_free (action->label); g_slice_free (struct GoogleReaderAction, action); } static GoogleReaderActionCtxtPtr google_reader_api_action_context_new(nodeSourcePtr source, GoogleReaderActionPtr action) { GoogleReaderActionCtxtPtr ctxt = g_slice_new0(struct GoogleReaderActionCtxt); ctxt->nodeId = g_strdup(source->root->id); ctxt->action = action; return ctxt; } static void google_reader_api_action_context_free(GoogleReaderActionCtxtPtr ctxt) { g_free(ctxt->nodeId); g_slice_free(struct GoogleReaderActionCtxt, ctxt); } static void google_reader_api_edit_action_complete (const struct updateResult* const result, gpointer userdata, updateFlags flags) { GoogleReaderActionCtxtPtr editCtxt = (GoogleReaderActionCtxtPtr) userdata; GoogleReaderActionPtr action = editCtxt->action; nodePtr node = node_from_id (editCtxt->nodeId); gboolean failed = FALSE; google_reader_api_action_context_free (editCtxt); if (!node) { google_reader_api_action_free (action); return; /* probably got deleted before this callback */ } // FIXME: suboptimal check as some results are text, some XML, some JSON... if (!g_str_equal (result->data, "OK")) { if (result->data == NULL) { failed = TRUE; } else { if (node->source->type->api.json) { JsonParser *parser = json_parser_new (); if (!json_parser_load_from_data (parser, result->data, -1, NULL)) { debug0 (DEBUG_UPDATE, "Failed to parse JSON update"); failed = TRUE; } else { const gchar *error = json_get_string (json_parser_get_root (parser), "error"); if (error) { debug1 (DEBUG_UPDATE, "Request failed with error '%s'", error); failed = TRUE; } } // FIXME: also check for "errors" array g_object_unref (parser); } else { failed = TRUE; } } } if (action->callback) { action->response = result->data; action->callback (node->source, action, !failed); } google_reader_api_action_free (action); if (failed) { debug1 (DEBUG_UPDATE, "The edit action failed with result: %s\n", result->data); return; /** @todo start a timer for next processing */ } /* process anything else waiting on the edit queue */ google_reader_api_edit_process (node->source); } /* the following google_reader_api_* functions are simply functions that convert a GoogleReaderActionPtr to a UpdateRequest */ static void google_reader_api_add_subscription (GoogleReaderActionPtr action, UpdateRequest *request, const gchar* token) { update_request_set_source (request, action->source->type->api.add_subscription); gchar *s_escaped = g_uri_escape_string (action->feedUrl, NULL, TRUE); request->postdata = g_strdup_printf (action->source->type->api.add_subscription_post, s_escaped, token); g_free (s_escaped); } static void google_reader_api_remove_subscription (GoogleReaderActionPtr action, UpdateRequest *request, const gchar* token) { update_request_set_source (request, action->source->type->api.remove_subscription); gchar* s_escaped = g_uri_escape_string (action->feedUrl, NULL, TRUE); g_assert (!request->postdata); request->postdata = g_strdup_printf (action->source->type->api.remove_subscription_post, s_escaped, token); g_free (s_escaped); } static void google_reader_api_add_label (GoogleReaderActionPtr action, UpdateRequest *request, const gchar* token) { update_request_set_source (request, action->source->type->api.edit_add_label); gchar* s_escaped = g_uri_escape_string (action->feedUrl, NULL, TRUE); gchar *a_escaped = g_uri_escape_string (action->label, NULL, TRUE); request->postdata = g_strdup_printf (action->source->type->api.edit_add_label_post, s_escaped, a_escaped, token); g_free (a_escaped); g_free (s_escaped); } static void google_reader_api_edit_tag (GoogleReaderActionPtr action, UpdateRequest *request, const gchar *token) { update_request_set_source (request, action->source->type->api.edit_tag); const gchar* prefix = "feed"; gchar* s_escaped = g_uri_escape_string (action->feedUrl, NULL, TRUE); gchar* a_escaped = NULL; gchar* i_escaped = g_uri_escape_string (action->guid, NULL, TRUE);; gchar* postdata = NULL; /* * If the source of the item is a feed then the source *id* will be of * the form tag:google.com,2005:reader/feed/http://foo.com/bar * If the item is a shared link it is of the form * tag:google.com,2005:reader/user//source/com.google/link * It is possible that there are items other thank link that has * the ../user/.. id. The GR API requires the strings after ..:reader/ * while GoogleReaderAction only gives me after :reader/feed/ (or * :reader/user/ as the case might be). I therefore need to guess * the prefix ('feed/' or 'user/') from just this information. */ if (strstr(action->feedUrl, "://") == NULL) prefix = "user" ; if (action->actionType == EDIT_ACTION_MARK_UNREAD) { a_escaped = g_uri_escape_string (GOOGLE_READER_TAG_KEPT_UNREAD, NULL, TRUE); gchar *r_escaped = g_uri_escape_string (GOOGLE_READER_TAG_READ, NULL, TRUE); postdata = g_strdup_printf (action->source->type->api.edit_tag_ar_tag_post, i_escaped, prefix, s_escaped, a_escaped, r_escaped, token); g_free (r_escaped); } else if (action->actionType == EDIT_ACTION_MARK_READ) { a_escaped = g_uri_escape_string (GOOGLE_READER_TAG_READ, NULL, TRUE); postdata = g_strdup_printf (action->source->type->api.edit_tag_add_post, i_escaped, prefix, s_escaped, a_escaped, token); } else if (action->actionType == EDIT_ACTION_TRACKING_MARK_UNREAD) { a_escaped = g_uri_escape_string (GOOGLE_READER_TAG_TRACKING_KEPT_UNREAD, NULL, TRUE); postdata = g_strdup_printf (action->source->type->api.edit_tag_add_post, i_escaped, prefix, s_escaped, a_escaped, token); } else if (action->actionType == EDIT_ACTION_MARK_STARRED) { a_escaped = g_uri_escape_string (GOOGLE_READER_TAG_STARRED, NULL, TRUE) ; postdata = g_strdup_printf (action->source->type->api.edit_tag_add_post, i_escaped, prefix, s_escaped, a_escaped, token); } else if (action->actionType == EDIT_ACTION_MARK_UNSTARRED) { gchar* r_escaped = g_uri_escape_string(GOOGLE_READER_TAG_STARRED, NULL, TRUE); postdata = g_strdup_printf (action->source->type->api.edit_tag_remove_post, i_escaped, prefix, s_escaped, r_escaped, token); } else g_assert (FALSE); g_free (s_escaped); g_free (a_escaped); g_free (i_escaped); request->postdata = postdata; } static void google_reader_api_edit_token_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { nodePtr node; const gchar* token; GoogleReaderActionPtr action; UpdateRequest *request; if (result->httpstatus != 200 || result->data == NULL) { /* FIXME: What is the behaviour that should go here? */ return; } node = node_from_id ((gchar*) userdata); g_free (userdata); if (!node) return; token = result->data; if (!node->source || g_queue_is_empty (node->source->actionQueue)) return; action = g_queue_peek_head (node->source->actionQueue); request = update_request_new ( "NOT THE REAL URL", // real URL will be set later based on action node->subscription->updateState, node->subscription->updateOptions ); update_request_set_auth_value (request, node->source->authToken); if (action->actionType == EDIT_ACTION_MARK_READ || action->actionType == EDIT_ACTION_MARK_UNREAD || action->actionType == EDIT_ACTION_TRACKING_MARK_UNREAD || action->actionType == EDIT_ACTION_MARK_STARRED || action->actionType == EDIT_ACTION_MARK_UNSTARRED) google_reader_api_edit_tag (action, request, token); else if (action->actionType == EDIT_ACTION_ADD_SUBSCRIPTION) google_reader_api_add_subscription (action, request, token); else if (action->actionType == EDIT_ACTION_REMOVE_SUBSCRIPTION) google_reader_api_remove_subscription (action, request, token); else if (action->actionType == EDIT_ACTION_ADD_LABEL) google_reader_api_add_label (action, request, token); debug1 (DEBUG_UPDATE, "google_reader_api: postdata [%s]", request->postdata); update_execute_request (node->source, request, google_reader_api_edit_action_complete, google_reader_api_action_context_new(node->source, action), FEED_REQ_NO_FEED); action = g_queue_pop_head (node->source->actionQueue); } void google_reader_api_edit_process (nodeSourcePtr source) { UpdateRequest *request; g_assert (source); if (g_queue_is_empty (source->actionQueue)) return; /* * Google reader has a system of tokens. So first, I need to request a * token from google, before I can make the actual edit request. The * code here is the token code, the actual edit commands are in * google_reader_api_edit_token_cb */ request = update_request_new ( source->type->api.token, source->root->subscription->updateState, source->root->subscription->updateOptions ); update_request_set_auth_value(request, source->authToken); update_execute_request (source, request, google_reader_api_edit_token_cb, g_strdup(source->root->id), FEED_REQ_NO_FEED); } static void google_reader_api_edit_push_ (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean head) { g_assert (source->actionQueue); action->source = source; if (head) g_queue_push_head (source->actionQueue, action); else g_queue_push_tail (source->actionQueue, action); } static void google_reader_api_edit_push (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean head) { g_assert (source); g_assert (source->actionQueue); google_reader_api_edit_push_ (source, action, head); /** @todo any flags I should specify? */ if (source->loginState == NODE_SOURCE_STATE_NONE) subscription_update (source->root->subscription, NODE_SOURCE_UPDATE_ONLY_LOGIN); else if (source->loginState == NODE_SOURCE_STATE_ACTIVE) google_reader_api_edit_process (source); } static void update_read_state_callback (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean success) { if (!success) debug0 (DEBUG_UPDATE, "Failed to change item state!"); } void google_reader_api_edit_mark_read (nodeSourcePtr source, const gchar *guid, const gchar *feedUrl, gboolean newStatus) { GoogleReaderActionPtr action; action = google_reader_api_action_new (newStatus?EDIT_ACTION_MARK_READ:EDIT_ACTION_MARK_UNREAD); action->guid = g_strdup (guid); action->feedUrl = g_strdup (feedUrl); action->callback = update_read_state_callback; google_reader_api_edit_push (source, action, FALSE); if (newStatus == FALSE) { /* * According to the Google Reader API, to mark an item unread, * I also need to mark it as tracking-kept-unread in a separate * network call. */ action = google_reader_api_action_new (EDIT_ACTION_TRACKING_MARK_UNREAD); action->guid = g_strdup (guid); action->feedUrl = g_strdup (feedUrl); google_reader_api_edit_push (source, action, FALSE); } } static void update_starred_state_callback(nodeSourcePtr source, GoogleReaderActionPtr action, gboolean success) { if (!success) debug0 (DEBUG_UPDATE, "Failed to change item state!"); } void google_reader_api_edit_mark_starred (nodeSourcePtr source, const gchar *guid, const gchar *feedUrl, gboolean newStatus) { GoogleReaderActionPtr action = google_reader_api_action_new (newStatus?EDIT_ACTION_MARK_STARRED:EDIT_ACTION_MARK_UNSTARRED); action->guid = g_strdup (guid); action->feedUrl = g_strdup (feedUrl); action->callback = update_starred_state_callback; google_reader_api_edit_push (source, action, FALSE); } static void google_reader_api_edit_add_subscription_complete (nodeSourcePtr source, GoogleReaderActionPtr action) { /* * It is possible that remote changed the name of the URL that * was sent to it. In that case, we need to recover the URL * from the list. But a node with the old URL has already * been created. Allow the subscription update call to fix that. */ GSList* cur = source->root->children ; for(; cur; cur = g_slist_next (cur)) { nodePtr node = (nodePtr) cur->data; if (node->subscription) { if (g_str_equal (node->subscription->source, action->feedUrl)) { subscription_set_source (node->subscription, ""); feedlist_node_added (node); } } } subscription_update (source->root->subscription, NODE_SOURCE_UPDATE_ONLY_LIST); } static void google_reader_api_edit_add_subscription2_cb (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean success) { google_reader_api_edit_add_subscription_complete (source, action); } /* Subscription callback #1 (to set label (folder) before updating the feed list) */ static void google_reader_api_edit_add_subscription_cb (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean success) { if (success) { if (action->label) { /* Extract returned new feed id (FIXME: is this only TheOldReader specific?) */ const gchar *id = NULL; JsonParser *parser = json_parser_new (); if (!json_parser_load_from_data (parser, action->response, -1, NULL)) { debug0 (DEBUG_UPDATE, "Failed to parse JSON response"); } else { id = json_get_string (json_parser_get_root (parser), "streamId"); if (!id) debug0 (DEBUG_UPDATE, "ERROR: Found no 'streamId' in response. Cannot set folder label!"); } if (id) { GoogleReaderActionPtr a = google_reader_api_action_new (EDIT_ACTION_ADD_LABEL); a->feedUrl = g_strdup (id); a->label = g_strdup (action->label); a->callback = google_reader_api_edit_add_subscription2_cb; google_reader_api_edit_push (source, a, TRUE); } g_object_unref (parser); } else { google_reader_api_edit_add_subscription_complete (source, action); } } else { debug0 (DEBUG_UPDATE, "Failed to subscribe"); // FIXME: real error handling (dialog...) } } void google_reader_api_edit_add_subscription (nodeSourcePtr source, const gchar* feedUrl, const gchar *label) { GoogleReaderActionPtr action = google_reader_api_action_new (EDIT_ACTION_ADD_SUBSCRIPTION); action->feedUrl = g_strdup (feedUrl); action->label = g_strdup (label); action->callback = google_reader_api_edit_add_subscription_cb; google_reader_api_edit_push (source, action, TRUE); } static void google_reader_api_edit_remove_callback (nodeSourcePtr source, GoogleReaderActionPtr action, gboolean success) { if (success) { /* * The node was removed from the feedlist, but could have * returned because of an update before this edit request * completed. No cleaner way to handle this. */ GSList* cur = source->root->children ; for(; cur; cur = g_slist_next (cur)) { nodePtr node = (nodePtr) cur->data ; if (g_str_equal (node->subscription->source, action->feedUrl)) { feedlist_node_removed (node); return; } } } else debug0 (DEBUG_UPDATE, "Failed to remove subscription"); } void google_reader_api_edit_remove_subscription (nodeSourcePtr source, const gchar* feedUrl) { GoogleReaderActionPtr action = google_reader_api_action_new (EDIT_ACTION_REMOVE_SUBSCRIPTION); action->feedUrl = g_strdup (feedUrl); action->callback = google_reader_api_edit_remove_callback; google_reader_api_edit_push (source, action, TRUE); } gboolean google_reader_api_edit_is_in_queue (nodeSourcePtr source, const gchar* guid) { /* this is inefficient, but works for the time being */ GList *cur = source->actionQueue->head; for(; cur; cur = g_list_next (cur)) { GoogleReaderActionPtr action = cur->data; if (action->guid && g_str_equal (action->guid, guid)) return TRUE; } return FALSE; } liferea-1.13.7/src/fl_sources/google_reader_api_edit.h000066400000000000000000000061401415350204600227530ustar00rootroot00000000000000/** * @file google_reader_api_edit.h Google Reader API syncing support * * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _REEDAH_SOURCE_EDIT_H #define _REEDAH_SOURCE_EDIT_H #include "fl_sources/node_source.h" #include /** * Process the waiting edits on the edit queue. Call this if the state of * the nodeSource has changed. * * @param gsource The nodeSource whose editQueue should be processed. */ void google_reader_api_edit_process (nodeSourcePtr gsource); /** Edit wrappers */ /** * Mark the given item as read. * * @param gsource The nodeSource structure * @param guid The guid of the item whose status is to be edited * @param feedUrl The feedUrl of the feed containing the item. * @param newStatus The new read status of the item (TRUE for read) */ void google_reader_api_edit_mark_read (nodeSourcePtr gsource, const gchar* guid, const gchar* feedUrl, gboolean newStatus); /** * Mark the given item as starred. * * @param gsource The nodeSource structure * @param guid The guid of the item whose status is to be edited * @param feedUrl The feedUrl of the feed containing the item. * @param newStatus The new read status of the item (TRUE for read) */ void google_reader_api_edit_mark_starred (nodeSourcePtr gsource, const gchar *guid, const gchar *feedUrl, gboolean newStatus); /** * Add a subscription to the google source. * * @param gsource The nodeSource structure * @param feedUrl the feed to add * @param label label for new feed (or NULL) */ void google_reader_api_edit_add_subscription (nodeSourcePtr gsource, const gchar* feedUrl, const gchar* label); /** * Remove a subscription from the google source. * * @param gsource The nodeSource structure * @param feedUrl the feed to remove */ void google_reader_api_edit_remove_subscription (nodeSourcePtr gsource, const gchar* feedUrl); /** * Add a category for a subscription * * @param gsource The nodeSource structure * @param feedUrl the feed to set the category */ void google_reader_api_edit_add_label (nodeSourcePtr gsource, const gchar* feedUrl, const gchar *label); /** * See if an item with give guid is being modified * in the queue. * * @param nodeSource the nodeSource structure * @param guid the guid of the item */ gboolean google_reader_api_edit_is_in_queue (nodeSourcePtr gsource, const gchar* guid); #endif liferea-1.13.7/src/fl_sources/google_source.c000066400000000000000000000071451415350204600211540ustar00rootroot00000000000000/** * @file google_source.c Google reader feed list source support * * Copyright (C) 2007-2013 Lars Windolf * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2011 Peter Oliver * Copyright (C) 2011 Sergey Snitsaruk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/google_source.h" #include #include #include #include #include "common.h" #include "debug.h" #include "feedlist.h" #include "item_state.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "update.h" #include "xml.h" #include "ui/liferea_dialog.h" #include "fl_sources/node_source.h" #include "fl_sources/opml_source.h" /** default Google reader subscription list update interval = once a day */ #define GOOGLE_SOURCE_UPDATE_INTERVAL 60*60*24 /** create a google source with given node as root */ static GoogleSourcePtr google_source_new (nodePtr node) { GoogleSourcePtr source = g_new0 (struct GoogleSource, 1) ; return source; } static void google_source_free (GoogleSourcePtr gsource) { update_job_cancel_by_owner (gsource); g_free (gsource); } /* node source type implementation */ static void google_source_auto_update (nodePtr node) { } static void google_source_init (void) { metadata_type_register ("GoogleBroadcastOrigFeed", METADATA_TYPE_URL); metadata_type_register ("sharedby", METADATA_TYPE_TEXT); } static void google_source_deinit (void) { } static void google_source_import (nodePtr node) { opml_source_import (node); if (!node->data) node->data = (gpointer) google_source_new (node); } static void google_source_cleanup (nodePtr node) { GoogleSourcePtr reader = (GoogleSourcePtr) node->data; google_source_free(reader); node->data = NULL ; } /** * Convert all subscriptions of a google source to local feeds * * @param node The node to migrate (not the nodeSource!) */ static void google_source_convert_to_local (nodePtr node) { /* Nothing to do when migrating */ } /* node source type definition */ static struct nodeSourceType nst = { .id = "fl_google", .name = N_("Google Reader"), .capabilities = NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL, .source_type_init = google_source_init, .source_type_deinit = google_source_deinit, .source_new = NULL, .source_delete = opml_source_remove, .source_import = google_source_import, .source_export = opml_source_export, .source_get_feedlist = opml_source_get_feedlist, .source_auto_update = google_source_auto_update, .free = google_source_cleanup, .item_set_flag = NULL, .item_mark_read = NULL, .add_folder = NULL, .add_subscription = NULL, .remove_node = NULL, .convert_to_local = google_source_convert_to_local }; nodeSourceTypePtr google_source_get_type (void) { nst.feedSubscriptionType = feed_get_subscription_type (); return &nst; } liferea-1.13.7/src/fl_sources/google_source.h000066400000000000000000000023331415350204600211530ustar00rootroot00000000000000/** * @file google_source.h Google Reader feed list source support * * Copyright (C) 2007-2013 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _GOOGLE_SOURCE_H #define _GOOGLE_SOURCE_H #include "fl_sources/node_source.h" /** * A nodeSource specific for keeping dead Google Reader sources */ typedef struct GoogleSource { /* No data anymore, since this is a dummy only */ } *GoogleSourcePtr; /** * @returns Google Reader source type implementation info. */ nodeSourceTypePtr google_source_get_type (void); #endif liferea-1.13.7/src/fl_sources/json_api_mapper.c000066400000000000000000000103471415350204600214640ustar00rootroot00000000000000/** * @file json_api_mapper.c data mapper for JSON APIs * * Copyright (C) 2013 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "json_api_mapper.h" #include #include "debug.h" #include "item.h" #include "metadata.h" #include "xml.h" JsonNode * json_api_get_node (JsonNode *parent, const gchar *mapping) { JsonNode *node = parent; gchar **step; gchar **steps = g_strsplit (g_strdup (mapping), "/", 0); step = steps; if (!*step) return node; while (*(step + 1) && node) { node = json_get_node (node, *step); step++; } g_strfreev (steps); return node; } const gchar * json_api_get_string (JsonNode *parent, const gchar *mapping) { JsonNode *node; const gchar *field; if (!parent || !mapping) return NULL; node = json_api_get_node (parent, mapping); if (!node) return NULL; field = strrchr (mapping, '/'); if (!field) field = mapping; else field++; return json_get_string (node, field); } gint json_api_get_int (JsonNode *parent, const gchar *mapping) { JsonNode *node; const gchar *field; if (!parent || !mapping) return 0; node = json_api_get_node (parent, mapping); if (!node) return 0; field = strrchr (mapping, '/'); if (!field) field = mapping; else field++; return json_get_int (node, field); } gboolean json_api_get_bool (JsonNode *parent, const gchar *mapping) { JsonNode *node; const gchar *field; if (!parent || !mapping) return FALSE; node = json_api_get_node (parent, mapping); if (!node) return FALSE; field = strrchr (mapping, '/'); if (!field) field = mapping; else field++; return json_get_bool (node, field); } GList * json_api_get_items (const gchar *json, const gchar *root, jsonApiMapping *mapping, jsonApiItemCallbackFunc callback) { GList *items = NULL; JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, json, -1, NULL)) { JsonArray *array = json_node_get_array (json_get_node (json_parser_get_root (parser), root)); GList *elements = json_array_get_elements (array); GList *iter = elements; debug1 (DEBUG_PARSING, "JSON API: found items root node \"%s\"", root); while (iter) { JsonNode *node = (JsonNode *)iter->data; itemPtr item = item_new (); /* Parse default feeds */ item_set_id (item, json_api_get_string (node, mapping->id)); item_set_title (item, json_api_get_string (node, mapping->title)); item_set_source (item, json_api_get_string (node, mapping->link)); item->time = json_api_get_int (node, mapping->updated); item->readStatus = json_api_get_bool (node, mapping->read); item->flagStatus = json_api_get_bool (node, mapping->flag); if (mapping->negateRead) item->readStatus = !item->readStatus; /* Handling encoded content */ const gchar *content; gchar *xhtml; content = json_api_get_string (node, mapping->description); if (mapping->xhtml) { xhtml = xhtml_extract_from_string (content, NULL); item_set_description (item, xhtml); xmlFree (xhtml); } else { item_set_description (item, content); } /* Optional meta data */ const gchar *tmp = json_api_get_string (node, mapping->author); if (tmp) item->metadata = metadata_list_append (item->metadata, "author", tmp); items = g_list_append (items, (gpointer)item); /* Allow optional item callback to process stuff */ if (callback) (*callback)(node, item); iter = g_list_next (iter); } g_list_free (elements); g_object_unref (parser); } else { debug1 (DEBUG_PARSING, "Could not parse JSON \"%s\"", json); } return items; } liferea-1.13.7/src/fl_sources/json_api_mapper.h000066400000000000000000000044141415350204600214670ustar00rootroot00000000000000/** * @file json_api_mapper.h data mapper for JSON APIs * * Copyright (C) 2013 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _JSON_API_MAPPER_H #define _JSON_API_MAPPER_H #include "item.h" #include "json.h" typedef struct jsonApiMapping { const gchar *id; /**< list of location steps to id field */ const gchar *title; /**< list of location steps to title field */ const gchar *link; /**< list of location steps to link field */ const gchar *description; /**< list of location steps to description field */ const gchar *updated; /**< list of location steps to updated field */ const gchar *author; /**< list of location steps to author field */ const gchar *read; /**< list of location steps to read field */ const gchar *flag; /**< list of location steps to flagged/marked/starred field */ gboolean negateRead; /**< TRUE if read status is to be negated (i.e. is "unread flag") */ gboolean xhtml; /**< TRUE if description field is XHTML */ } jsonApiMapping; typedef void (*jsonApiItemCallbackFunc)(JsonNode *node, itemPtr item); /** * Extracts all items from a JSON document. * * @param json the JSON document to parse * @param root the name of the root node (e.g. "items") * @param mapping hash table defining location steps and logic * @param callback optional callback function to process item node * for everything that cannot be easily mapped * * @returns a list of items (all to be freed with item_free()) or NULL */ GList * json_api_get_items (const gchar *json, const gchar *root, jsonApiMapping *mapping, jsonApiItemCallbackFunc callback); #endif liferea-1.13.7/src/fl_sources/node_source.c000066400000000000000000000454141415350204600206260ustar00rootroot00000000000000/* * @file node_source.c generic node source provider implementation * * Copyright (C) 2005-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/node_source.h" #include #include #include #include "common.h" #include "db.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "item_state.h" #include "node.h" #include "node_type.h" #include "plugins_engine.h" #include "ui/icons.h" #include "ui/liferea_dialog.h" #include "ui/ui_common.h" #include "ui/feed_list_view.h" #include "fl_sources/default_source.h" #include "fl_sources/dummy_source.h" #include "fl_sources/google_source.h" #include "fl_sources/opml_source.h" #include "fl_sources/reedah_source.h" #include "fl_sources/theoldreader_source.h" #include "fl_sources/ttrss_source.h" #include "fl_sources/node_source_activatable.h" static GSList *nodeSourceTypes = NULL; static PeasExtensionSet *extensions = NULL; nodePtr node_source_root_from_node (nodePtr node) { while (node->parent->source == node->source) node = node->parent; return node; } static nodeSourceTypePtr node_source_type_find (const gchar *typeStr, guint capabilities) { GSList *iter = nodeSourceTypes; while (iter) { nodeSourceTypePtr type = (nodeSourceTypePtr)iter->data; if (((NULL == typeStr) || !strcmp(type->id, typeStr)) && ((0 == capabilities) || (type->capabilities & capabilities))) return type; iter = g_slist_next (iter); } g_print ("Could not find source type \"%s\"\n!", typeStr); return NULL; } gboolean node_source_type_register (nodeSourceTypePtr type) { debug1 (DEBUG_PARSING, "Registering node source type %s", type->name); /* allow the plugin to initialize */ type->source_type_init (); /* Check if Google reader clones provide all API methods */ if(type->capabilities & NODE_SOURCE_CAPABILITY_GOOGLE_READER_API) { g_assert (type->api.unread_count); g_assert (type->api.subscription_list); g_assert (type->api.add_subscription); g_assert (type->api.add_subscription_post); g_assert (type->api.remove_subscription); g_assert (type->api.remove_subscription_post); g_assert (type->api.edit_add_label); g_assert (type->api.edit_add_label_post); g_assert (type->api.edit_tag); g_assert (type->api.edit_tag_add_post); g_assert (type->api.edit_tag_remove_post); g_assert (type->api.edit_tag_ar_tag_post); g_assert (type->api.token); } nodeSourceTypes = g_slist_append (nodeSourceTypes, type); return TRUE; } nodePtr node_source_setup_root (void) { nodePtr rootNode; nodeSourceTypePtr type; debug_enter ("node_source_setup_root"); /* we need to register all source types once before doing anything... */ node_source_type_register (default_source_get_type ()); node_source_type_register (dummy_source_get_type ()); node_source_type_register (opml_source_get_type ()); node_source_type_register (google_source_get_type ()); node_source_type_register (reedah_source_get_type ()); node_source_type_register (ttrss_source_get_type ()); node_source_type_register (theoldreader_source_get_type ()); extensions = peas_extension_set_new (PEAS_ENGINE (liferea_plugins_engine_get_default ()), LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE, NULL); liferea_plugins_engine_set_default_signals (extensions, NULL); type = node_source_type_find (NULL, NODE_SOURCE_CAPABILITY_IS_ROOT); if (!type) g_error ("No root capable node source found!"); rootNode = node_new (root_get_node_type()); rootNode->title = g_strdup ("root"); rootNode->source = g_new0 (struct nodeSource, 1); rootNode->source->root = rootNode; rootNode->source->type = type; type->source_import (rootNode); debug_exit ("node_source_setup_root"); return rootNode; } static void node_source_set_feed_subscription_type (nodePtr folder, subscriptionTypePtr type) { GSList *iter; for (iter = folder->children; iter; iter = g_slist_next(iter)) { nodePtr node = (nodePtr) iter->data; if (node->subscription) node->subscription->type = type; /* Recurse for hierarchic nodes... */ node_source_set_feed_subscription_type (node, type); } } static void node_source_import (nodePtr node, nodePtr parent, xmlNodePtr xml, gboolean trusted) { nodeSourceTypePtr type; xmlChar *typeStr = NULL; debug_enter ("node_source_import"); typeStr = xmlGetProp (xml, BAD_CAST"sourceType"); if (!typeStr) typeStr = xmlGetProp (xml, BAD_CAST"pluginType"); /* for migration only */ if (typeStr) { debug2 (DEBUG_CACHE, "creating node source instance (type=%s,id=%s)", typeStr, node->id); node->available = FALSE; /* scan for matching node source and create new instance */ type = node_source_type_find ((const gchar *)typeStr, 0); if (!type) { /* Source type is not available for some reason, but we need a representation to keep the node source in the feed list. So we load a dummy source type instead and save the real source id in the unused node's data field */ type = node_source_type_find (NODE_SOURCE_TYPE_DUMMY_ID, 0); g_assert (NULL != type); node->data = g_strdup ((gchar *)typeStr); } node->available = TRUE; node->source = NULL; node_source_new (node, type, NULL); node_set_subscription (node, subscription_import (xml, trusted)); type->source_import (node); /* Set subscription type for all child nodes imported */ node_source_set_feed_subscription_type (node, type->feedSubscriptionType); if (!strcmp ((gchar *)typeStr, "fl_bloglines")) { g_print ("Removing obsolete Bloglines subscription."); feedlist_node_removed (node); } } else { g_print ("No source type given for node \"%s\". Ignoring it.", node_get_title (node)); } debug_exit ("node_source_import"); } static void node_source_export (nodePtr node, xmlNodePtr xml, gboolean trusted) { debug_enter ("node_source_export"); debug2 (DEBUG_CACHE, "node source export for node %s, id=%s", node->title, NODE_SOURCE_TYPE (node)->id); /* If the node source type was loaded using the dummy node source type we need to restore the original node source type id from temporarily saved into node->data */ if (!strcmp (NODE_SOURCE_TYPE (node)->id, NODE_SOURCE_TYPE_DUMMY_ID)) xmlNewProp (xml, BAD_CAST"sourceType", BAD_CAST (node->data)); else xmlNewProp (xml, BAD_CAST"sourceType", BAD_CAST (NODE_SOURCE_TYPE(node)->id)); subscription_export (node->subscription, xml, trusted); NODE_SOURCE_TYPE (node)->source_export (node); debug_exit("node_source_export"); } void node_source_new (nodePtr node, nodeSourceTypePtr type, const gchar *url) { subscriptionPtr subscription; g_assert (NULL == node->source); node->source = g_new0 (struct nodeSource, 1); node->source->root = node; node->source->type = type; node->source->loginState = NODE_SOURCE_STATE_NONE; node->source->actionQueue = g_queue_new (); node_set_title (node, type->name); if (url) { subscription = subscription_new (url, NULL, NULL); node_set_subscription (node, subscription); subscription->type = node->source->type->sourceSubscriptionType; } } void node_source_set_state (nodePtr node, gint newState) { debug3 (DEBUG_UPDATE, "node source '%s' now in state %d (was %d)", node->id, newState, node->source->loginState); /* State transition actions below... */ if (newState == NODE_SOURCE_STATE_ACTIVE) node->source->authFailures = 0; if (newState == NODE_SOURCE_STATE_NONE) { node->source->authFailures++; node->available = FALSE; } if (node->source->authFailures >= NODE_SOURCE_MAX_AUTH_FAILURES) newState = NODE_SOURCE_STATE_NO_AUTH; node->source->loginState = newState; } void node_source_set_auth_token (nodePtr node, gchar *token) { g_assert (!node->source->authToken); debug2 (DEBUG_UPDATE, "node source \"%s\" Auth token found: %s", node->id, token); node->source->authToken = token; node_source_set_state (node, NODE_SOURCE_STATE_ACTIVE); } /* source instance creation dialog */ static void on_node_source_type_selected (GtkTreeSelection *selection, gpointer user_data) { GtkTreeIter iter; GtkTreeModel *model; if (gtk_tree_selection_get_selected (selection, &model, &iter)) gtk_widget_set_sensitive (GTK_WIDGET (user_data), TRUE); else gtk_widget_set_sensitive (GTK_WIDGET (user_data), FALSE); } static void on_node_source_type_response (GtkDialog *dialog, gint response_id, gpointer user_data) { GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; nodeSourceTypePtr type; if (response_id == GTK_RESPONSE_OK) { selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (liferea_dialog_lookup (GTK_WIDGET (dialog), "type_list"))); g_assert (NULL != selection); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, 1, &type, -1); if (type) type->source_new (); } } gtk_widget_destroy (GTK_WIDGET (dialog)); } static gboolean feed_list_node_source_type_dialog (void) { GSList *iter = nodeSourceTypes; GtkWidget *dialog, *treeview; GtkTreeStore *treestore; GtkCellRenderer *renderer; GtkTreeIter treeiter; nodeSourceTypePtr type; if (!nodeSourceTypes) { ui_show_error_box (_("No feed list source types found!")); return FALSE; } /* set up the dialog */ dialog = liferea_dialog_new ("node_source"); treestore = gtk_tree_store_new (2, G_TYPE_STRING, G_TYPE_POINTER); /* add available feed list source to treestore */ while (iter) { type = (nodeSourceTypePtr) iter->data; if (type->capabilities & NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION) { gtk_tree_store_append (treestore, &treeiter, NULL); gtk_tree_store_set (treestore, &treeiter, 0, type->name, 1, type, -1); } iter = g_slist_next (iter); } treeview = liferea_dialog_lookup (dialog, "type_list"); g_assert (NULL != treeview); renderer = gtk_cell_renderer_text_new (); g_object_set (renderer, "wrap-width", 400, NULL); g_object_set (renderer, "wrap-mode", PANGO_WRAP_WORD, NULL); gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (treeview), -1, _("Source Type"), renderer, "markup", 0, NULL); gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (treestore)); g_object_unref (treestore); gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)), GTK_SELECTION_SINGLE); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_node_source_type_response), NULL); g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview))), "changed", G_CALLBACK (on_node_source_type_selected), liferea_dialog_lookup (dialog, "ok_button")); return TRUE; } void node_source_update (nodePtr node) { if (node->subscription) { /* Reset NODE_SOURCE_STATE_NO_AUTH as this is a manual user interaction and no auto-update so we can query for credentials again. */ if (node->source->loginState == NODE_SOURCE_STATE_NO_AUTH) node_source_set_state (node, NODE_SOURCE_STATE_NONE); subscription_update (node->subscription, 0); /* Note that node sources are required to auto-update child nodes themselves once login and feed list update is fine. */ } else { /* for default source */ node_foreach_child_data (node, node_update_subscription, GUINT_TO_POINTER (0)); } } void node_source_auto_update (nodePtr node) { NODE_SOURCE_TYPE (node)->source_auto_update (node); } static gboolean node_source_is_logged_in (nodePtr node) { if (FALSE == (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_CAN_LOGIN)) return TRUE; if (node->source->loginState != NODE_SOURCE_STATE_ACTIVE) ui_show_error_box (_("Login for '%s' has not yet completed! Please wait until login is done."), node->title); return node->source->loginState == NODE_SOURCE_STATE_ACTIVE; } nodePtr node_source_add_subscription (nodePtr node, subscriptionPtr subscription) { if (!node_source_is_logged_in (node)) return NULL; if (NODE_SOURCE_TYPE (node)->add_subscription) return NODE_SOURCE_TYPE (node)->add_subscription (node, subscription); else g_print ("node_source_add_subscription(): called on node source type that doesn't implement me!"); return NULL; } nodePtr node_source_add_folder (nodePtr node, const gchar *title) { if (!node_source_is_logged_in (node)) return NULL; if (NODE_SOURCE_TYPE (node)->add_folder) return NODE_SOURCE_TYPE (node)->add_folder (node, title); else g_print ("node_source_add_folder(): called on node source type that doesn't implement me!"); return NULL; } void node_source_update_folder (nodePtr node, nodePtr folder) { if (!node_source_is_logged_in (node)) return; if (!folder) folder = node->source->root; if (node->parent != folder) { debug2 (DEBUG_UPDATE, "Moving node \"%s\" to folder \"%s\"", node->title, folder->title); node_reparent (node, folder); } } nodePtr node_source_find_or_create_folder (nodePtr parent, const gchar *id, const gchar *name) { nodePtr folder = NULL; gchar *folderNodeId; if (!id) return parent->source->root; /* No id means folder is root node */ folderNodeId = g_strdup_printf ("%s-folder-%s", NODE_SOURCE_TYPE (parent->source->root)->id, id); folder = node_from_id (folderNodeId); if (!folder) { folder = node_new (folder_get_node_type ()); node_set_id (folder, folderNodeId); node_set_title (folder, name); node_set_parent (folder, parent, -1); feedlist_node_imported (folder); subscription_update (folder->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); } return folder; } void node_source_remove_node (nodePtr node, nodePtr child) { if (!node_source_is_logged_in (node)) return; g_assert (child != node); g_assert (child != child->source->root); if (NODE_SOURCE_TYPE (node)->remove_node) NODE_SOURCE_TYPE (node)->remove_node (node, child); else g_print ("node_source_remove_node(): called on node source type that doesn't implement me!"); } void node_source_item_mark_read (nodePtr node, itemPtr item, gboolean newState) { /* Item read state changes are optional for node source implementations. If they are supported the implementation has to call item_read_state_changed(), otherwise we do call it here. */ if (NODE_SOURCE_TYPE (node)->item_mark_read) NODE_SOURCE_TYPE (node)->item_mark_read (node, item, newState); else item_read_state_changed (item, newState); } void node_source_item_set_flag (nodePtr node, itemPtr item, gboolean newState) { /* Item flag state changes are optional for node source implementations. If they are supported the implementation has to call item_flag_state_changed(), otherwise we do call it here. */ if (NODE_SOURCE_TYPE (node)->item_set_flag) NODE_SOURCE_TYPE (node)->item_set_flag (node, item, newState); else item_flag_state_changed (item, newState); } static void node_source_convert_to_local_child_node (nodePtr node) { /* Ensure to remove special subscription types and cancel updates Note: we expect that all feeds already have the subscription URL set. This might need to be done by the node type specific convert_to_local() method! */ if (node->subscription) { update_job_cancel_by_owner ((gpointer)node); update_job_cancel_by_owner ((gpointer)node->subscription); debug2 (DEBUG_UPDATE, "Converting feed: %s = %s\n", node->title, node->subscription->source); node->subscription->type = feed_get_subscription_type (); } if (IS_FOLDER (node)) node_foreach_child (node, node_source_convert_to_local_child_node); node->source = ((nodePtr)feedlist_get_root ())->source; } void node_source_convert_to_local (nodePtr node) { g_assert (node == node->source->root); /* Preparation */ update_job_cancel_by_owner ((gpointer)node); update_job_cancel_by_owner ((gpointer)node->subscription); update_job_cancel_by_owner ((gpointer)node->source); /* Give the node source type the chance to do things ... */ if (NULL != NODE_SOURCE_TYPE (node)->convert_to_local) NODE_SOURCE_TYPE (node)->convert_to_local (node); /* Perform conversion */ debug0 (DEBUG_UPDATE, "Converting root node to folder..."); node->source = ((nodePtr)feedlist_get_root ())->source; node->type = folder_get_node_type (); node->subscription = NULL; /* leaking subscription is ok */ node->data = NULL; /* leaking data is ok */ node_foreach_child (node, node_source_convert_to_local_child_node); feedlist_schedule_save (); /* FIXME: something is not perfect, because if you immediately remove the subscription tree afterwards there is a double free */ ui_show_info_box (_("The '%s' subscription was successfully converted to local feeds!"), node->title); } /* implementation of the node type interface */ static void node_source_remove (nodePtr node) { if (!node_source_is_logged_in (node)) return; g_assert (node == node->source->root); if (NULL != NODE_SOURCE_TYPE (node)->source_delete) NODE_SOURCE_TYPE (node)->source_delete (node); feed_list_view_remove_node (node); } static void node_source_save (nodePtr node) { node_foreach_child (node, node_save); } static void node_source_free (nodePtr node) { if (NULL != NODE_SOURCE_TYPE (node)->free) NODE_SOURCE_TYPE (node)->free (node); g_free (node->source->authToken); g_free (node->source); node->source = NULL; } nodeTypePtr node_source_get_node_type (void) { static nodeTypePtr nodeType; if (!nodeType) { /* derive the node source node type from the folder node type */ nodeType = (nodeTypePtr) g_new0 (struct nodeType, 1); nodeType->id = "source"; nodeType->icon = icon_get (ICON_DEFAULT); nodeType->capabilities = NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_SHOW_ITEM_FAVICONS | NODE_CAPABILITY_UPDATE_CHILDS | NODE_CAPABILITY_UPDATE | NODE_CAPABILITY_UPDATE_FAVICON | NODE_CAPABILITY_ADD_CHILDS | NODE_CAPABILITY_REMOVE_CHILDS; nodeType->import = node_source_import; nodeType->export = node_source_export; nodeType->load = folder_get_node_type()->load; nodeType->save = node_source_save; nodeType->update_counters = folder_get_node_type()->update_counters; nodeType->remove = node_source_remove; nodeType->render = node_default_render; nodeType->request_add = feed_list_node_source_type_dialog; nodeType->request_properties = feed_list_view_rename_node; nodeType->free = node_source_free; } return nodeType; } liferea-1.13.7/src/fl_sources/node_source.h000066400000000000000000000333271415350204600206330ustar00rootroot00000000000000/* * @file node_source.h generic node source interface * * Copyright (C) 2005-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NODE_SOURCE_H #define _NODE_SOURCE_H #include #include #include "node.h" #include "node_type.h" #include "subscription_type.h" #include "fl_sources/google_reader_api.h" /* Liferea allows to have different sources in the feed list. These sources are called "node sources" henceforth. Node sources can (but do not need to) be single instance only. Node sources do provide a subtree of the feed list that can be read-only or not. A node source might allow or not allow to add sub folders and reorder (DnD) folder contents. A node source might allow hierarchic grouping of its subtree or not. These properties are determined by the node source type capability flags. The node source concept itself is a node type. The implementation of this node type can be found in node_source.c. The default node source type must be capable of serving as the root node for all other source types. This mean it has to ensure to load all other node source instances at their insertion nodes in the feed list. Each source type has to be able to serve user requests and is responsible for keeping its feed list node's states up-to-date. A source type implementation can omit all callbacks marked as optional. */ typedef enum { NODE_SOURCE_CAPABILITY_IS_ROOT = (1<<0), /*<< flag only for default feed list source */ NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION = (1<<1), /*<< feed list source is user created */ NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST = (1<<2), /*<< the feed list tree of the source can be changed */ NODE_SOURCE_CAPABILITY_ADD_FEED = (1<<3), /*<< feeds can be added to the source */ NODE_SOURCE_CAPABILITY_ADD_FOLDER = (1<<4), /*<< folders can be added to the source */ NODE_SOURCE_CAPABILITY_HIERARCHIC_FEEDLIST = (1<<5), /*<< the feed list tree of the source can have hierarchic folders */ NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC = (1<<6), /*<< the item state can and should be sync'ed with remote */ NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL = (1<<7), /*<< node sources of this type can be converted to internal subscription lists */ NODE_SOURCE_CAPABILITY_GOOGLE_READER_API = (1<<8), /*<< node sources of this type are Google Reader clones */ NODE_SOURCE_CAPABILITY_CAN_LOGIN = (1<<9) /*<< node source needs login (means loginState member is to be used) */ } nodeSourceCapability; /* Node source state model */ typedef enum { NODE_SOURCE_STATE_NONE = 0, /*<< no authentication tried so far */ NODE_SOURCE_STATE_IN_PROGRESS, /*<< authentication in progress */ NODE_SOURCE_STATE_ACTIVE, /*<< authentication succeeded */ NODE_SOURCE_STATE_NO_AUTH, /*<< authentication has failed */ NODE_SOURCE_STATE_MIGRATE, /*<< source will be migrated, do not do anything anymore! */ } nodeSourceState; /* Node source subscription update flags */ typedef enum { /* * Update only the subscription list, and not each node underneath it. * Note: Uses higher 16 bits to avoid conflict. */ NODE_SOURCE_UPDATE_ONLY_LIST = (1<<16), /* * Only login, do not do any updates. */ NODE_SOURCE_UPDATE_ONLY_LOGIN = (1<<17) } nodeSourceUpdate; /* * Number of auth failures after which we stop bothering the user while * auto-updating until he manually updates again. */ #define NODE_SOURCE_MAX_AUTH_FAILURES 3 /* feed list node source type */ typedef struct nodeSourceType { const gchar *id; /*<< a unique feed list source type identifier */ const gchar *name; /*<< a descriptive source name (for preferences and menus) */ gulong capabilities; /*<< bitmask of feed list source capabilities */ googleReaderApi api; /*<< OPTIONAL endpoint definitions for Google Reader like JSON API */ /* The subscription type for all child nodes that are subscriptions */ subscriptionTypePtr feedSubscriptionType; /* The subscription type for the source itself (can be NULL) */ subscriptionTypePtr sourceSubscriptionType; /* source type loading and unloading methods */ void (*source_type_init)(void); void (*source_type_deinit)(void); /* * This OPTIONAL callback is used to create an instance * of the implemented source type. It is to be called by * the parent source node_request_add_*() implementation. * Mandatory for all sources except the root source. */ void (*source_new)(void); /* * This OPTIONAL callback is used to delete an instance * of the implemented source type. It is to be called * by the parent source node_remove() implementation. * Mandatory for all sources except the root provider source. */ void (*source_delete)(nodePtr node); /* * This MANDATORY method is called when the source is to * create the feed list subtree attached to the source root * node. */ void (*source_import)(nodePtr node); /* * This MANDATORY method is called when the source is to * save it's feed list subtree (if necessary at all). This * is not a request to save the data of the attached nodes! */ void (*source_export)(nodePtr node); /* * This MANDATORY method is called to get an OPML representation * of the feedlist of the given node source. Returns a newly * allocated filename string that is to be freed by the * caller. */ gchar * (*source_get_feedlist)(nodePtr node); /* * This MANDATARY method is called to request the source to update * its subscriptions list and the child subscriptions according * the its update interval. */ void (*source_auto_update)(nodePtr node); /* * Frees all data of the given node source instance. To be called * during node_free() for a source node. */ void (*free) (nodePtr node); /* * Changes the flag state of an item. This is to allow node source type * implementations to synchronize remote item states. * * This is an OPTIONAL method. */ void (*item_set_flag) (nodePtr node, itemPtr item, gboolean newState); /* * Mark an item as read. This is to allow node source type * implementations to synchronize remote item states. * * This is an OPTIONAL method. */ void (*item_mark_read) (nodePtr node, itemPtr item, gboolean newState); /* * Add a new folder to the feed list provided by node * source. OPTIONAL, but must be implemented when * NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST and * NODE_SOURCE_CAPABILITY_HIERARCHIC_FEEDLIST are set. */ nodePtr (*add_folder) (nodePtr node, const gchar *title); /* * Add a new subscription to the feed list provided * by the node source. OPTIONAL method, that must be implemented * when NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST is set. * * The implementation could propagate the added subscription * to a remote feed list service. * * The implementation MUST create and return a new child node * setup with the given subscription which might be changed as necessary. * * The returned node will be automatically added to the feed list UI. * Initial update and state saving will be triggered automatically. */ nodePtr (*add_subscription) (nodePtr node, struct subscription *subscription); /* * Removes an existing node (subscription or folder) from the feed list * provided by the node source. OPTIONAL method that must be * implemented when NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST is set. */ void (*remove_node) (nodePtr node, nodePtr child); /* * Converts all subscriptions to default source subscriptions. * * This is an OPTIONAL method. */ void (*convert_to_local) (nodePtr node); } *nodeSourceTypePtr; /* feed list source instance */ typedef struct nodeSource { nodeSourceTypePtr type; /*<< node source type of this source instance */ nodePtr root; /*<< insertion node of this node source instance */ GQueue *actionQueue; /*<< queue for async actions */ gint loginState; /*<< The current login state */ gchar *authToken; /*<< The authorization token */ gint authFailures; /*<< Number of authentication failures */ } *nodeSourcePtr; /* Use this to cast the node source type from a node structure. */ #define NODE_SOURCE_TYPE(node) ((nodeSourcePtr)(node->source))->type #define NODE_SOURCE_TYPE_DUMMY_ID "fl_dummy" /** * node_source_root_from_node: (skip) * @node: any child node * * Get the root node of a feed list source for any given child node. * * Returns: node source root node */ nodePtr node_source_root_from_node (nodePtr node); /** * node_source_setup_root: (skip) * * Scans the source type list for the root source provider. * If found creates a new root source and starts it's import. * * Returns: a newly created root node */ nodePtr node_source_setup_root (void); /** * node_source_new: (skip) * @node: a newly created node * @nodeSourceType: the node source type * @url: subscription URL * * Creates a new source and assigns it to the given new node. * To be used to prepare a source node before adding it to the * feed list. This method takes care of setting the proper source * subscription type and setting up the subscription if url != NULL. * The caller needs set additional auth info for the subscription. */ void node_source_new (nodePtr node, nodeSourceTypePtr nodeSourceType, const gchar *url); /** * node_source_set_state: (skip) * @node: the node source node * @newState: the new state * * Change state of the node source by node */ void node_source_set_state (nodePtr node, gint newState); /** * node_source_set_auth_token: (skip) * @node: a node * @token: a string * * Store any type of authentication token (e.g. a cookie or session id) * * FIXME: maybe drop this in favour of node metadata */ void node_source_set_auth_token (nodePtr node, gchar *token); /** * node_source_update: (skip) * @node: the source node * * Force the source to update its subscription list and * the child subscriptions themselves. */ void node_source_update (nodePtr node); /** * node_source_auto_update: (skip) * @node: the source node * * Request the source to update its subscription list and * the child subscriptions if necessary according to the * update interval of the source. */ void node_source_auto_update (nodePtr node); /** * node_source_add_subscription: (skip) * @node: the source node * @subscription: the new subscription * * Called when a new subscription has been added to the node source. * * Returns: a new node intialized with the new subscription */ nodePtr node_source_add_subscription (nodePtr node, struct subscription *subscription); /** * node_source_remove_node: (skip) * @node: the source node * @child: the child node to remove * * Called when an existing subscription is to be removed from a node source. */ void node_source_remove_node (nodePtr node, nodePtr child); /** * node_source_add_folder: (skip) * @node: the source node * @title: the folder title * * Called when a new folder is to be added to a node source feed list. * * Returns: a new node representing the new folder */ nodePtr node_source_add_folder (nodePtr node, const gchar *title); /** * node_source_update_folder: (skip) * @node: any node * @folder: the target folder * * Called to update a nodes folder. If current folder != given folder * the node will be reparented. */ void node_source_update_folder (nodePtr node, nodePtr folder); /** * node_source_find_or_create_folder: (skip) * @parent: Parent folder (or source root node) * @id: Folder/category id (or NULL) * @label: Folder display name * * Find a folder by the name under parent or create it. * * If a node source doesn't provide ids the category display name should be * used as id. The worst thing happening then is to evenly named categories * being merged into one (which the user can easily workaround by renaming * on the remote side). * * Returns: a valid nodePtr */ nodePtr node_source_find_or_create_folder (nodePtr parent, const gchar *id, const gchar *label); /** * node_source_item_mark_read: (skip) * @node: the source node * @item: the affected item * @newState: the new item read state * * Called when the read state of an item changes. */ void node_source_item_mark_read (nodePtr node, itemPtr item, gboolean newState); /** * node_source_set_item_flag: (skip) * @node: the source node * @item: the affected item * @newState: the new item flag state * * Called when the flag state of an item changes. */ void node_source_item_set_flag (nodePtr node, itemPtr item, gboolean newState); /** * node_source_convert_to_local: (skip) * @node: the source node * * Converts all subscriptions to default source subscriptions. */ void node_source_convert_to_local (nodePtr node); /** * node_source_type_register: (skip) * @type: the type to register * * Registers a new node source type. Needs to be called before feed list import! * To be used only via NodeSourceTypeActivatable */ gboolean node_source_type_register (nodeSourceTypePtr type); /* implementation of the node type interface */ #define IS_NODE_SOURCE(node) (node->type == node_source_get_node_type ()) /** * node_source_get_node_type: (skip) * * Returns: the node source node type implementation. */ nodeTypePtr node_source_get_node_type (void); #endif liferea-1.13.7/src/fl_sources/node_source_activatable.c000066400000000000000000000041101415350204600231510ustar00rootroot00000000000000/* * @file node_type_activatable.c Node Source Plugin Type * * Copyright (C) 2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "node_source_activatable.h" /** * SECTION:node_source_activatable * @short_description: Interface for activatable extensions providing a new node source type * @see_also: #PeasExtensionSet * * #LifereaNodeSourceActivatable is an interface which should be implemented by * extensions that want to a new node source type (usually online news aggregators) **/ G_DEFINE_INTERFACE (LifereaNodeSourceActivatable, liferea_node_source_activatable, G_TYPE_OBJECT) void liferea_node_source_activatable_default_init (LifereaNodeSourceActivatableInterface *iface) { /* No properties yet */ } void liferea_node_source_activatable_activate (LifereaNodeSourceActivatable * activatable) { LifereaNodeSourceActivatableInterface *iface; g_return_if_fail (IS_LIFEREA_NODE_SOURCE_ACTIVATABLE (activatable)); iface = LIFEREA_NODE_SOURCE_ACTIVATABLE_GET_IFACE (activatable); if (iface->activate) iface->activate (activatable); } void liferea_node_source_activatable_deactivate (LifereaNodeSourceActivatable * activatable) { LifereaNodeSourceActivatableInterface *iface; g_return_if_fail (IS_LIFEREA_NODE_SOURCE_ACTIVATABLE (activatable)); iface = LIFEREA_NODE_SOURCE_ACTIVATABLE_GET_IFACE (activatable); if (iface->deactivate) iface->deactivate (activatable); } liferea-1.13.7/src/fl_sources/node_source_activatable.h000066400000000000000000000045331415350204600231670ustar00rootroot00000000000000/* * @file node_source_activatable.h Node Source Plugin Type * * Copyright (C) 2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_NODE_SOURCE_ACTIVATABLE_H__ #define _LIFEREA_NODE_SOURCE_ACTIVATABLE_H__ #include #include G_BEGIN_DECLS #define LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE (liferea_node_source_activatable_get_type ()) #define LIFEREA_NODE_SOURCE_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE, LifereaNodeSourceActivatable)) #define LIFEREA_NODE_SOURCE_ACTIVATABLE_IFACE(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE, LifereaNodeSourceActivatableInterface)) #define IS_LIFEREA_NODE_SOURCE_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE)) #define LIFEREA_NODE_SOURCE_ACTIVATABLE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), LIFEREA_NODE_SOURCE_ACTIVATABLE_TYPE, LifereaNodeSourceActivatableInterface)) typedef struct _LifereaNodeSourceActivatable LifereaNodeSourceActivatable; typedef struct _LifereaNodeSourceActivatableInterface LifereaNodeSourceActivatableInterface; struct _LifereaNodeSourceActivatableInterface { GTypeInterface g_iface; void (*activate) (LifereaNodeSourceActivatable * activatable); void (*deactivate) (LifereaNodeSourceActivatable * activatable); }; GType liferea_node_source_activatable_get_type (void) G_GNUC_CONST; void liferea_node_source_activatable_activate (LifereaNodeSourceActivatable *activatable); void liferea_node_source_activatable_deactivate (LifereaNodeSourceActivatable *activatable); G_END_DECLS #endif /* __LIFEREA_NODE_SOURCE_ACTIVATABLE_H__ */ liferea-1.13.7/src/fl_sources/opml_source.c000066400000000000000000000263631415350204600206520ustar00rootroot00000000000000/** * @file opml_source.c OPML Planet/Blogroll feed list source * * Copyright (C) 2006-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/opml_source.h" #include #include "common.h" #include "debug.h" #include "export.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "node.h" #include "xml.h" #include "ui/icons.h" #include "ui/liferea_dialog.h" #include "ui/ui_common.h" /** default OPML update interval = once a day */ #define OPML_SOURCE_UPDATE_INTERVAL 60*60*24 /* OPML subscription list helper functions */ typedef struct mergeCtxt { nodePtr rootNode; /**< root node of the OPML feed list source */ nodePtr parent; /**< currently processed feed list node */ xmlNodePtr xmlNode; /**< currently processed XML node of old OPML doc */ } *mergeCtxtPtr; static void opml_source_merge_feed (xmlNodePtr match, gpointer user_data) { mergeCtxtPtr mergeCtxt = (mergeCtxtPtr)user_data; xmlChar *url, *title; gchar *expr; nodePtr node = NULL; url = xmlGetProp (match, BAD_CAST"xmlUrl"); title = xmlGetProp (match, BAD_CAST"title"); if (!title) title = xmlGetProp (match, BAD_CAST"description"); if (!title) title = xmlGetProp (match, BAD_CAST"text"); if (!title && !url) return; if (url) expr = g_strdup_printf ("//outline[@xmlUrl = '%s']", url); else expr = g_strdup_printf ("//outline[@title = '%s']", title); if (!xpath_find (mergeCtxt->xmlNode, expr)) { debug2(DEBUG_UPDATE, "adding %s (%s)", title, url); if (url) { node = node_new (feed_get_node_type ()); node_set_data (node, feed_new ()); node_set_subscription (node, subscription_new ((gchar *)url, NULL, NULL)); } else { node = node_new (folder_get_node_type ()); } node_set_title (node, (gchar *)title); node_set_parent (node, mergeCtxt->parent, -1); feedlist_node_imported (node); subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); } /* Recursion if this is a folder */ if (!url) { if (!node) { /* if the folder node wasn't created above it must already exist and we search it in the parents children list */ GSList *iter = mergeCtxt->parent->children; while (iter) { if (g_str_equal (title, node_get_title (iter->data))) node = iter->data; iter = g_slist_next (iter); } } if (node) { mergeCtxtPtr mc = g_new0 (struct mergeCtxt, 1); mc->rootNode = mergeCtxt->rootNode; mc->parent = node; mc->xmlNode = mergeCtxt->xmlNode; // FIXME: must be correct child! xpath_foreach_match (match, "./outline", opml_source_merge_feed, (gpointer)mc); g_free (mc); } else { g_print ("opml_source_merge_feed(): bad! bad! very bad!"); } } g_free (expr); xmlFree (title); xmlFree (url); } static void opml_source_check_for_removal (nodePtr node, gpointer user_data) { gchar *expr = NULL; if (IS_FEED (node)) { expr = g_strdup_printf ("//outline[ @xmlUrl='%s' ]", subscription_get_source (node->subscription)); } else if (IS_FOLDER (node)) { node_foreach_child_data (node, opml_source_check_for_removal, user_data); expr = g_strdup_printf ("//outline[ (@title='%s') or (@text='%s') or (@description='%s')]", node->title, node->title, node->title); } else { g_print ("opml_source_check_for_removal(): This should never happen..."); return; } if (!xpath_find ((xmlNodePtr)user_data, expr)) { debug1 (DEBUG_UPDATE, "removing %s...", node_get_title (node)); feedlist_node_removed (node); } else { debug1 (DEBUG_UPDATE, "keeping %s...", node_get_title (node)); } g_free (expr); } /* OPML subscription type implementation */ static gboolean opml_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { /* Nothing to do here for simple OPML subscriptions */ return TRUE; } static void opml_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { nodePtr node = subscription->node; mergeCtxtPtr mergeCtxt; xmlDocPtr doc, oldDoc; xmlNodePtr root, title; debug1 (DEBUG_UPDATE, "OPML download finished data=%d", result->data); node->available = FALSE; if (result->data) { doc = xml_parse (result->data, result->size, NULL); if (doc) { gchar *filename; root = xmlDocGetRootElement (doc); /* Go through all existing nodes and remove those whose URLs are not in new feed list. Also removes those URLs from the list that have corresponding existing nodes. */ node_foreach_child_data (node, opml_source_check_for_removal, (gpointer)root); opml_source_export (node); /* save new feed list tree to disk to ensure correct document in next step */ /* Merge up-to-date OPML feed list. */ filename = opml_source_get_feedlist (node); oldDoc = xmlParseFile (filename); g_free (filename); mergeCtxt = g_new0 (struct mergeCtxt, 1); mergeCtxt->rootNode = node; mergeCtxt->parent = node; mergeCtxt->xmlNode = xmlDocGetRootElement (oldDoc); if (g_str_equal (node_get_title (node), OPML_SOURCE_DEFAULT_TITLE)) { title = xpath_find (root, "/opml/head/title"); if (title) { xmlChar *titleStr = xmlNodeListGetString(title->doc, title->xmlChildrenNode, 1); if (titleStr) { node_set_title (node, (gchar *)titleStr); xmlFree (titleStr); } } } xpath_foreach_match (root, "/opml/body/outline", opml_source_merge_feed, (gpointer)mergeCtxt); g_free (mergeCtxt); xmlFreeDoc (oldDoc); xmlFreeDoc (doc); opml_source_export (node); /* save new feed list tree to disk */ node->available = TRUE; } else { g_print ("Cannot parse downloaded OPML document!"); } } node_foreach_child_data (node, node_update_subscription, GUINT_TO_POINTER (0)); } /* subscription type definition */ static struct subscriptionType opmlSubscriptionType = { opml_subscription_prepare_update_request, opml_subscription_process_update_result }; /* OPML source type implementation */ static void ui_opml_source_get_source_url (void); gchar * opml_source_get_feedlist (nodePtr node) { return common_create_cache_filename ("plugins", node->id, "opml"); } void opml_source_import (nodePtr node) { gchar *filename; debug_enter ("opml_source_import"); /* We only ship an icon for opml, not for other sources */ if (g_str_equal (NODE_SOURCE_TYPE (node)->id, "fl_opml")) node->icon = icon_create_from_file ("fl_opml.png"); debug1 (DEBUG_CACHE, "starting import of opml source instance (id=%s)", node->id); filename = opml_source_get_feedlist (node); if (g_file_test (filename, G_FILE_TEST_EXISTS)) { import_OPML_feedlist (filename, node, FALSE, TRUE); } else { g_print ("cannot open \"%s\"", filename); node->available = FALSE; } g_free (filename); subscription_set_update_interval (node->subscription, OPML_SOURCE_UPDATE_INTERVAL); node->subscription->type = &opmlSubscriptionType; debug_exit ("opml_source_import"); } void opml_source_export (nodePtr node) { gchar *filename; debug_enter ("opml_source_export"); /* Although the OPML structure won't change, it needs to be saved so that the feed ids are saved to disk after the first import or updates of the source OPML. */ g_assert (node == node->source->root); filename = opml_source_get_feedlist (node); export_OPML_feedlist (filename, node, TRUE); g_free (filename); debug1 (DEBUG_CACHE, "adding OPML source: title=%s", node_get_title(node)); debug_exit ("opml_source_export"); } void opml_source_remove (nodePtr node) { gchar *filename; /* step 1: delete all child nodes */ node_foreach_child (node, feedlist_node_removed); g_assert (!node->children); /* step 2: delete source instance OPML cache file */ filename = opml_source_get_feedlist (node); unlink (filename); g_free (filename); } static void opml_source_auto_update (nodePtr node) { guint64 now; now = g_get_real_time(); /* do daily updates for the feed list and feed updates according to the default interval */ if (node->subscription->updateState->lastPoll + (guint64)OPML_SOURCE_UPDATE_INTERVAL * (guint64)G_USEC_PER_SEC <= now) node_source_update (node); } static void opml_source_init(void) { } static void opml_source_deinit(void) { } /* node source type definition */ static struct nodeSourceType nst = { .id = "fl_opml", .name = N_("Planet, BlogRoll, OPML"), .sourceSubscriptionType = &opmlSubscriptionType, .capabilities = NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION, .source_type_init = opml_source_init, .source_type_deinit = opml_source_deinit, .source_new = ui_opml_source_get_source_url, .source_delete = opml_source_remove, .source_import = opml_source_import, .source_export = opml_source_export, .source_get_feedlist = opml_source_get_feedlist, .source_auto_update = opml_source_auto_update, .free = NULL, .item_set_flag = NULL, .item_mark_read = NULL, .add_folder = NULL, .add_subscription = NULL, .remove_node = NULL, .convert_to_local = NULL }; nodeSourceTypePtr opml_source_get_type (void) { nst.feedSubscriptionType = feed_get_subscription_type (); return &nst; } /* GUI callbacks */ static void on_opml_source_selected (GtkDialog *dialog, gint response_id, gpointer user_data) { nodePtr node; if (response_id == GTK_RESPONSE_OK) { node = node_new (node_source_get_node_type ()); node_set_title (node, OPML_SOURCE_DEFAULT_TITLE); node_source_new (node, opml_source_get_type (), gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "location_entry")))); feedlist_node_added (node); node_source_update (node); } gtk_widget_destroy (GTK_WIDGET (dialog)); } static void on_opml_file_selected (const gchar *filename, gpointer user_data) { GtkWidget *dialog = GTK_WIDGET (user_data); if (filename && dialog) gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (dialog, "location_entry")), g_strdup(filename)); } static void on_opml_file_choose_clicked (GtkButton *button, gpointer user_data) { ui_choose_file (_("Choose OPML File"), _("_Open"), FALSE, on_opml_file_selected, NULL, NULL, "*.opml|*.xml", _("OPML Files"), user_data); } static void ui_opml_source_get_source_url (void) { GtkWidget *dialog, *button; dialog = liferea_dialog_new ("opml_source"); button = liferea_dialog_lookup (dialog, "select_button"); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_opml_source_selected), NULL); g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (on_opml_file_choose_clicked), dialog); } liferea-1.13.7/src/fl_sources/opml_source.h000066400000000000000000000033321415350204600206460ustar00rootroot00000000000000/** * @file opml_source.h OPML Planet/Blogroll feed list provider * * Copyright (C) 2005-2009 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _OPML_SOURCE_H #define _OPML_SOURCE_H #include "node.h" #include "subscription_type.h" #include "fl_sources/node_source.h" #define OPML_SOURCE_DEFAULT_TITLE _("New OPML Subscription") /* General OPML source handling functions */ /** * Determine OPML cache file name. * * @param node the node of the OPML source * * @returns newly allocated filename */ gchar * opml_source_get_feedlist(nodePtr node); /** * Imports an OPML source. * * @param node the node of the OPML source */ void opml_source_import(nodePtr node); /** * Exports an OPML source. * * @param node the node of the OPML source */ void opml_source_export(nodePtr node); /** * Removes a OPML source. * * @param node the node of the OPML source */ void opml_source_remove(nodePtr node); /** * Returns OPML source type implementation info. */ nodeSourceTypePtr opml_source_get_type(void); #endif liferea-1.13.7/src/fl_sources/reedah_source.c000066400000000000000000000262371415350204600211330ustar00rootroot00000000000000/** * @file reedah_source.c Reedah source support * * Copyright (C) 2007-2016 Lars Windolf * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2011 Peter Oliver * Copyright (C) 2011 Sergey Snitsaruk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/reedah_source.h" #include #include #include #include #include "common.h" #include "debug.h" #include "feedlist.h" #include "item_state.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "update.h" #include "xml.h" #include "ui/auth_dialog.h" #include "ui/liferea_dialog.h" #include "fl_sources/google_reader_api_edit.h" #include "fl_sources/node_source.h" #include "fl_sources/opml_source.h" #include "fl_sources/reedah_source_feed_list.h" /** default Reedah subscription list update interval = once a day */ #define NODE_SOURCE_UPDATE_INTERVAL (guint64)(60*60*24) * (guint64)G_USEC_PER_SEC /** create a Reedah source with given node as root */ static ReedahSourcePtr reedah_source_new (nodePtr node) { ReedahSourcePtr source = g_new0 (struct ReedahSource, 1) ; source->root = node; source->lastTimestampMap = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); return source; } static void reedah_source_free (ReedahSourcePtr source) { if (!source) return; update_job_cancel_by_owner (source); g_hash_table_unref (source->lastTimestampMap); g_free (source); } static void reedah_source_login_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { nodePtr node = (nodePtr) userdata; gchar *tmp = NULL; subscriptionPtr subscription = node->subscription; debug1 (DEBUG_UPDATE, "Reedah login processing... %s", result->data); if (result->data && result->httpstatus == 200) tmp = strstr (result->data, "Auth="); if (tmp) { gchar *ttmp = tmp; tmp = strchr (tmp, '\n'); if (tmp) *tmp = '\0'; node_source_set_auth_token (node, g_strdup_printf ("GoogleLogin auth=%s", ttmp + 5)); /* now that we are authenticated trigger updating to start data retrieval */ if (!(flags & NODE_SOURCE_UPDATE_ONLY_LOGIN)) subscription_update (subscription, flags); /* process any edits waiting in queue */ google_reader_api_edit_process (node->source); } else { debug0 (DEBUG_UPDATE, "Reedah login failed! no Auth token found in result!"); g_free (subscription->updateError); subscription->updateError = g_strdup (_("Login failed!")); node_source_set_state (node, NODE_SOURCE_STATE_NO_AUTH); auth_dialog_new (subscription, flags); } } /** * Perform a login to Reedah, if the login completes the * ReedahSource will have a valid Auth token and will have loginStatus to * NODE_SOURCE_LOGIN_ACTIVE. */ void reedah_source_login (ReedahSourcePtr source, guint32 flags) { gchar *username, *password; UpdateRequest *request; subscriptionPtr subscription = source->root->subscription; if (source->root->source->loginState != NODE_SOURCE_STATE_NONE) { /* this should not happen, as of now, we assume the session * doesn't expire. */ debug1 (DEBUG_UPDATE, "Logging in while login state is %d\n", source->root->source->loginState); } request = update_request_new ( REEDAH_READER_LOGIN_URL, subscription->updateState, NULL // auth is done via POST below! ); /* escape user and password as both are passed using an URI */ username = g_uri_escape_string (subscription->updateOptions->username, NULL, TRUE); password = g_uri_escape_string (subscription->updateOptions->password, NULL, TRUE); request->postdata = g_strdup_printf (REEDAH_READER_LOGIN_POST, username, password); g_free (username); g_free (password); node_source_set_state (source->root, NODE_SOURCE_STATE_IN_PROGRESS); update_execute_request (source, request, reedah_source_login_cb, source->root, flags | FEED_REQ_NO_FEED); } /* node source type implementation */ static void reedah_source_auto_update (nodePtr node) { guint64 now; ReedahSourcePtr source = (ReedahSourcePtr) node->data; if (node->source->loginState == NODE_SOURCE_STATE_NONE) { node_source_update (node); return; } if (node->source->loginState == NODE_SOURCE_STATE_IN_PROGRESS) return; /* the update will start automatically anyway */ debug0 (DEBUG_UPDATE, "reedah_source_auto_update()"); now = g_get_real_time(); /* do daily updates for the feed list and feed updates according to the default interval */ if (node->subscription->updateState->lastPoll + NODE_SOURCE_UPDATE_INTERVAL <= now) { subscription_update (node->subscription, 0); source->lastQuickUpdate = g_get_real_time(); } else if (source->lastQuickUpdate + REEDAH_SOURCE_QUICK_UPDATE_INTERVAL <= now) { reedah_source_opml_quick_update (source); google_reader_api_edit_process (node->source); source->lastQuickUpdate = g_get_real_time(); } } static void reedah_source_init (void) { metadata_type_register ("reedah-feed-id", METADATA_TYPE_TEXT); } static void reedah_source_deinit (void) { } static void reedah_source_import (nodePtr node) { opml_source_import (node); node->subscription->updateInterval = -1; node->subscription->type = node->source->type->sourceSubscriptionType; if (!node->data) node->data = (gpointer) reedah_source_new (node); } static nodePtr reedah_source_add_subscription (nodePtr node, subscriptionPtr subscription) { // FIXME: determine correct category from parent folder name google_reader_api_edit_add_subscription (node_source_root_from_node (node)->data, subscription->source, NULL); // FIXME: leaking subscription? // FIXME: somehow the async subscribing doesn't cause the feed list to update return NULL; } static void reedah_source_remove_node (nodePtr node, nodePtr child) { gchar *url; ReedahSourcePtr source = node->data; if (child == node) { feedlist_node_removed (child); return; } url = g_strdup (child->subscription->source); feedlist_node_removed (child); /* propagate the removal only if there aren't other copies */ if (!feedlist_find_node (source->root, NODE_BY_URL, url)) google_reader_api_edit_remove_subscription (node->source, url); g_free (source); } /* GUI callbacks */ static void on_reedah_source_selected (GtkDialog *dialog, gint response_id, gpointer user_data) { nodePtr node; if (response_id == GTK_RESPONSE_OK) { node = node_new (node_source_get_node_type ()); node_source_new (node, reedah_source_get_type (), "http://www.reedah.com/reader"); subscription_set_auth_info (node->subscription, gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "userEntry"))), gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "passwordEntry")))); node->data = reedah_source_new (node); feedlist_node_added (node); node_source_update (node); } gtk_widget_destroy (GTK_WIDGET (dialog)); } static void ui_reedah_source_get_account_info (void) { GtkWidget *dialog; dialog = liferea_dialog_new ("reedah_source"); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_reedah_source_selected), NULL); } static void reedah_source_cleanup (nodePtr node) { ReedahSourcePtr reader = (ReedahSourcePtr) node->data; reedah_source_free(reader); node->data = NULL ; } static void reedah_source_item_set_flag (nodePtr node, itemPtr item, gboolean newStatus) { google_reader_api_edit_mark_starred (node->source, item->sourceId, node->subscription->source, newStatus); item_flag_state_changed (item, newStatus); } static void reedah_source_item_mark_read (nodePtr node, itemPtr item, gboolean newStatus) { google_reader_api_edit_mark_read (node->source, item->sourceId, node->subscription->source, newStatus); item_read_state_changed (item, newStatus); } /** * Convert all subscriptions of a Reedah source to local feeds * * @param node The node to migrate (not the nodeSource!) */ static void reedah_source_convert_to_local (nodePtr node) { node_source_set_state (node, NODE_SOURCE_STATE_MIGRATE); } /* node source type definition */ extern struct subscriptionType reedahSourceFeedSubscriptionType; extern struct subscriptionType reedahSourceOpmlSubscriptionType; #define BASE_URL "http://www.reedah.com/reader/api/0/" static struct nodeSourceType nst = { .id = "fl_reedah", .name = N_("Reedah"), .capabilities = NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION | NODE_SOURCE_CAPABILITY_CAN_LOGIN | NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST | NODE_SOURCE_CAPABILITY_ADD_FEED | NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC | NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL | NODE_SOURCE_CAPABILITY_GOOGLE_READER_API, .api.subscription_list = BASE_URL "subscription/list", .api.unread_count = BASE_URL "unread-count?all=true&client=liferea", .api.token = BASE_URL "token", .api.add_subscription = BASE_URL "subscription/edit?client=liferea", .api.add_subscription_post = "s=feed%%2F%s&i=null&ac=subscribe&T=%s", .api.remove_subscription = BASE_URL "subscription/edit?client=liferea", .api.remove_subscription_post = "s=feed%%2F%s&i=null&ac=unsubscribe&T=%s", .api.edit_tag = BASE_URL "edit-tag?client=liferea", .api.edit_tag_add_post = "i=%s&s=%s%%2F%s&a=%s&ac=edit-tags&T=%s&async=true", .api.edit_tag_remove_post = "i=%s&s=%s%%2F%s&r=%s&ac=edit-tags&T=%s&async=true", .api.edit_tag_ar_tag_post = "i=%s&s=%s%%2F%s&a=%s&r=%s&ac=edit-tags&T=%s&async=true", .api.edit_add_label = BASE_URL "edit?client=liferea", .api.edit_add_label_post = "s=%s%%2F%s&a=%s&ac=edit&T=%s&async=true", .feedSubscriptionType = &reedahSourceFeedSubscriptionType, .sourceSubscriptionType = &reedahSourceOpmlSubscriptionType, .source_type_init = reedah_source_init, .source_type_deinit = reedah_source_deinit, .source_new = ui_reedah_source_get_account_info, .source_delete = opml_source_remove, .source_import = reedah_source_import, .source_export = opml_source_export, .source_get_feedlist = opml_source_get_feedlist, .source_auto_update = reedah_source_auto_update, .free = reedah_source_cleanup, .item_set_flag = reedah_source_item_set_flag, .item_mark_read = reedah_source_item_mark_read, .add_folder = NULL, .add_subscription = reedah_source_add_subscription, .remove_node = reedah_source_remove_node, .convert_to_local = reedah_source_convert_to_local }; nodeSourceTypePtr reedah_source_get_type (void) { return &nst; } liferea-1.13.7/src/fl_sources/reedah_source.h000066400000000000000000000065201415350204600211310ustar00rootroot00000000000000/** * @file reedah_source.h Reedah feed list source support * * Copyright (C) 2007-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _REEDAH_SOURCE_H #define _REEDAH_SOURCE_H #include "fl_sources/node_source.h" /** * A nodeSource specific for Reedah */ typedef struct ReedahSource { nodePtr root; /**< the root node in the feed list */ /** * A map from a subscription source to a timestamp when it was last * updated according to API */ GHashTable *lastTimestampMap; /** * A timestamp when the last Quick update took place. */ guint64 lastQuickUpdate; } *ReedahSourcePtr; /** * API documentation, Reedah is closely modelled after Google Reader, the * differences are outlined under the first link, the second is a documentation * of the original Google Reader API. * * @see https://www.reedah.com/developers.php * @see http://code.google.com/p/pyrfeed/wiki/GoogleReaderAPI */ /** * Reedah Login API. * @param Email The account email id. * @param Passwd The account password. * @return The return data has a line "Auth=xxxx" which will be used as an * Authorization header in future requests. */ #define REEDAH_READER_LOGIN_URL "https://www.reedah.com/accounts/ClientLogin" #define REEDAH_READER_LOGIN_POST "service=reader&Email=%s&Passwd=%s&source=liferea&continue=http://www.reedah.com" /** Interval (in micro seconds) for doing a Quick Update: 10min */ #define REEDAH_SOURCE_QUICK_UPDATE_INTERVAL 600 * G_USEC_PER_SEC /** * @returns Reedah source type implementation info. */ nodeSourceTypePtr reedah_source_get_type (void); /** * Find a child node with the given feed source URL. * * @param gsource ReedahSource * @param source a feed source URL to search * * @returns a node (or NULL) */ nodePtr reedah_source_get_node_from_source (ReedahSourcePtr gsource, const gchar* source); /** * Tries to update the entire source quickly, by updating only those feeds * which are known to be updated. Suitable for g_timeout_add. This is an * internal function. * * @param data A pointer to a node id of the source. This pointer will * be g_free'd if the update fails. * * @returns FALSE on update failure */ gboolean reedah_source_quick_update_timeout (gpointer gsource); /** * Migrate a google source child-node from a Liferea 1.4 style read-only * google source nodes. * * @param node The node to migrate (not the nodeSource!) */ void reedah_source_migrate_node (nodePtr node); /** * Perform login for the given Google source. * * @param gsource a ReedahSource * @param flags network request flags */ void reedah_source_login (ReedahSourcePtr gsource, guint32 flags); #endif liferea-1.13.7/src/fl_sources/reedah_source_feed.c000066400000000000000000000152441415350204600221120ustar00rootroot00000000000000/** * @file theoldreader_source_feed.c TheOldReader feed subscription routines * * Copyright (C) 2013-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include "common.h" #include "debug.h" #include "db.h" #include "feedlist.h" #include "item_state.h" #include "itemlist.h" #include "json.h" #include "json_api_mapper.h" #include "metadata.h" #include "node.h" #include "reedah_source.h" #include "subscription.h" #include "xml.h" void reedah_source_migrate_node (nodePtr node) { /* scan the node for bad ID's, if so, brutally remove the node */ itemSetPtr itemset = node_get_itemset (node); GList *iter = itemset->ids; for (; iter; iter = g_list_next (iter)) { itemPtr item = item_load (GPOINTER_TO_UINT (iter->data)); if (item && item->sourceId) { if (!g_str_has_prefix(item->sourceId, "tag:google.com")) { debug1(DEBUG_UPDATE, "Item with sourceId [%s] will be deleted.", item->sourceId); db_item_remove(GPOINTER_TO_UINT(iter->data)); } } if (item) item_unload (item); } /* cleanup */ itemset_free (itemset); } static void reedah_item_callback (JsonNode *node, itemPtr item) { JsonNode *canonical, *categories; GList *elements, *iter; /* Determine link: path is "canonical[0]/@href" */ canonical = json_get_node (node, "canonical"); if (canonical && JSON_NODE_TYPE (canonical) == JSON_NODE_ARRAY) { iter = elements = json_array_get_elements (json_node_get_array (canonical)); while (iter) { const gchar *href = json_get_string ((JsonNode *)iter->data, "href"); if (href) { item_set_source (item, href); break; } iter = g_list_next (iter); } g_list_free (elements); } /* Determine read state: check for category with ".*state/com.google/read" */ categories = json_get_node (node, "categories"); if (categories && JSON_NODE_TYPE (categories) == JSON_NODE_ARRAY) { iter = elements = json_array_get_elements (json_node_get_array (canonical)); while (iter) { const gchar *category = json_node_get_string ((JsonNode *)iter->data); if (category) { item->readStatus = (strstr (category, "state\\/com.google\\/read") != NULL); break; } iter = g_list_next (iter); } g_list_free (elements); } } static void reedah_feed_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult* const result, updateFlags flags) { if (result->data && result->httpstatus == 200) { GList *items = NULL; jsonApiMapping mapping; /* We expect to get something like this [{"crawlTimeMsec":"1375821312282", "id"::"tag:google.com,reader:2005\/item\/4ee371db36f84de2", "categories":["user\/15724899091976567759\/state\/com.google\/reading-list", "user\/15724899091976567759\/state\/com.google\/fresh"], "title":"Firefox 23 Arrives With New Logo, Mixed Content Blocker, and Network Monitor", "published":1375813680, "updated":1375821312, "alternate":[{"href":"http://rss.slashdot.org/~r/Slashdot/slashdot/~3/Q4450FchLQo/story01.htm","type":"text/html"}], "canonical":[{"href":"http://slashdot.feedsportal.com/c/35028/f/647410/s/2fa2b59c/sc[...]", "type":"text/html"}], "summary":{"direction":"ltr","content":"An anonymous reader writes [...]"}, "author":"Soulskill", "origin":{"streamId":"feed/http://rss.slashdot.org/Slashdot/slashdot","title":"Slashdot", "htmlurl":"http://slashdot.org/" }, [...] */ /* Note: The link and read status cannot be mapped as there might be multiple ones so the callback helper function extracts the first from the array */ mapping.id = "id"; mapping.title = "title"; mapping.link = NULL; mapping.description = "summary/content"; mapping.read = NULL; mapping.updated = "updated"; mapping.author = "author"; mapping.flag = "marked"; mapping.xhtml = TRUE; mapping.negateRead = TRUE; items = json_api_get_items (result->data, "items", &mapping, &reedah_item_callback); /* merge against feed cache */ if (items) { itemSetPtr itemSet = node_get_itemset (subscription->node); subscription->node->newCount = itemset_merge_items (itemSet, items, TRUE /* feed valid */, FALSE /* markAsRead */); itemlist_merge_itemset (itemSet); itemset_free (itemSet); subscription->node->available = TRUE; } else { subscription->node->available = FALSE; g_string_append (((feedPtr)subscription->node->data)->parseErrors, _("Could not parse JSON returned by Reedah API!")); } } else { subscription->node->available = FALSE; } } static gboolean reedah_feed_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { debug0 (DEBUG_UPDATE, "preparing Reedah feed subscription for update\n"); ReedahSourcePtr source = (ReedahSourcePtr) node_source_root_from_node (subscription->node)->data; g_assert(source); if (source->root->source->loginState == NODE_SOURCE_STATE_NONE) { subscription_update (node_source_root_from_node (subscription->node)->subscription, 0) ; return FALSE; } if (!metadata_list_get (subscription->metadata, "reedah-feed-id")) { g_print ("Skipping Reedah feed '%s' (%s) without id!", subscription->source, subscription->node->id); return FALSE; } debug0 (DEBUG_UPDATE, "Setting cookies for a Reedah subscription"); gchar* source_escaped = g_uri_escape_string(metadata_list_get (subscription->metadata, "reedah-feed-id"), NULL, TRUE); // FIXME: move to .h // FIXME: do not use 30 gchar* newUrl = g_strdup_printf ("http://www.reedah.com/reader/api/0/stream/contents/%s?client=liferea&n=30", source_escaped); update_request_set_source (request, newUrl); g_free (newUrl); g_free (source_escaped); update_request_set_auth_value (request, source->root->source->authToken); return TRUE; } struct subscriptionType reedahSourceFeedSubscriptionType = { reedah_feed_subscription_prepare_update_request, reedah_feed_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/reedah_source_feed_list.c000066400000000000000000000236121415350204600231430ustar00rootroot00000000000000/** * @file reedah_source_feed_list.c Reedah feed list handling routines * * Copyright (C) 2013-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "reedah_source_feed_list.h" #include #include #include "common.h" #include "db.h" #include "debug.h" #include "feedlist.h" #include "folder.h" #include "json.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "subscription_icon.h" #include "xml.h" // FIXME #include "fl_sources/opml_source.h" #include "fl_sources/reedah_source.h" static void reedah_source_check_node_for_removal (nodePtr node, gpointer user_data) { JsonArray *array = (JsonArray *)user_data; GList *iter, *elements; gboolean found = FALSE; if (IS_FOLDER (node)) { /* Auto-remove folders if they do not have children */ if (!node->children) feedlist_node_removed (node); node_foreach_child_data (node, reedah_source_check_node_for_removal, user_data); } else { elements = iter = json_array_get_elements (array); while (iter) { JsonNode *json_node = (JsonNode *)iter->data; // FIXME: Compare with unescaped string if (g_str_equal (node->subscription->source, json_get_string (json_node, "id") + 5)) { debug1 (DEBUG_UPDATE, "node: %s", node->subscription->source); found = TRUE; break; } iter = g_list_next (iter); } g_list_free (elements); if (!found) feedlist_node_removed (node); } } /* subscription list merging functions */ static void reedah_source_merge_feed (ReedahSourcePtr source, const gchar *url, const gchar *title, const gchar *id, nodePtr folder) { nodePtr node; node = feedlist_find_node (source->root, NODE_BY_URL, url); if (!node) { debug2 (DEBUG_UPDATE, "adding %s (%s)", title, url); node = node_new (feed_get_node_type ()); node_set_title (node, title); node_set_data (node, feed_new ()); node_set_subscription (node, subscription_new (url, NULL, NULL)); node->subscription->type = source->root->source->type->feedSubscriptionType; /* Save Reedah feed id which we need to fetch items... */ node->subscription->metadata = metadata_list_append (node->subscription->metadata, "reedah-feed-id", id); db_subscription_update (node->subscription); node_set_parent (node, source->root, -1); feedlist_node_imported (node); /** * @todo mark the ones as read immediately after this is done * the feed as retrieved by this has the read and unread * status inherently. */ subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); subscription_icon_update (node->subscription); } else { node_source_update_folder (node, folder); } } /* OPML subscription type implementation */ static void reedah_subscription_opml_cb (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { ReedahSourcePtr source = (ReedahSourcePtr) subscription->node->data; subscription->updateJob = NULL; // FIXME: the following code is very similar to ttrss! if (result->data && result->httpstatus == 200) { JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, result->data, -1, NULL)) { JsonArray *array = json_node_get_array (json_get_node (json_parser_get_root (parser), "subscriptions")); GList *iter, *elements, *citer, *celements; /* We expect something like this: [{"id":"feed\/http:\/\/rss.slashdot.org\/Slashdot\/slashdot", "title":"Slashdot", "categories":[], "firstitemmsec":"1368112925514", "htmlUrl":"null"}, ... Note that the data doesn't contain an URL. We recover it from the id field. */ elements = iter = json_array_get_elements (array); /* Add all new nodes we find */ while (iter) { JsonNode *categories, *node = (JsonNode *)iter->data; nodePtr folder = NULL; /* Check for categories, if there use first one as folder */ categories = json_get_node (node, "categories"); if (categories && JSON_NODE_TYPE (categories) == JSON_NODE_ARRAY) { citer = celements = json_array_get_elements (json_node_get_array (categories)); while (citer) { const gchar *label = json_get_string ((JsonNode *)citer->data, "label"); if (label) { folder = node_source_find_or_create_folder (source->root, label, label); break; } citer = g_list_next (citer); } g_list_free (celements); } /* ignore everything without a feed url */ if (json_get_string (node, "id")) { reedah_source_merge_feed (source, json_get_string (node, "id") + 5, // FIXME: Unescape string! json_get_string (node, "title"), json_get_string (node, "id"), folder); } iter = g_list_next (iter); } g_list_free (elements); /* Remove old nodes we cannot find anymore */ node_foreach_child_data (source->root, reedah_source_check_node_for_removal, array); /* Save new subscription tree to OPML cache file */ opml_source_export (subscription->node); subscription->node->available = TRUE; } else { g_print ("Invalid JSON returned on Reedah feed list request! >>>%s<<<", result->data); } g_object_unref (parser); } else { subscription->node->available = FALSE; debug0 (DEBUG_UPDATE, "reedah_subscription_cb(): ERROR: failed to get subscription list!"); } if (!(flags & NODE_SOURCE_UPDATE_ONLY_LIST)) node_foreach_child_data (subscription->node, node_update_subscription, GUINT_TO_POINTER (0)); } /** functions for an efficient updating mechanism */ static void reedah_source_opml_quick_update_helper (xmlNodePtr match, gpointer userdata) { ReedahSourcePtr gsource = (ReedahSourcePtr) userdata; xmlNodePtr xmlNode; xmlChar *id, *newestItemTimestamp; nodePtr node = NULL; const gchar *oldNewestItemTimestamp; xmlNode = xpath_find (match, "./string[@name='id']"); id = xmlNodeGetContent (xmlNode); if (g_str_has_prefix ((gchar *)id, "feed/")) node = feedlist_find_node (gsource->root, NODE_BY_URL, (gchar *)(id + strlen ("feed/"))); else { xmlFree (id); return; } if (node == NULL) { xmlFree (id); return; } xmlNode = xpath_find (match, "./number[@name='newestItemTimestampUsec']"); newestItemTimestamp = xmlNodeGetContent (xmlNode); oldNewestItemTimestamp = g_hash_table_lookup (gsource->lastTimestampMap, node->subscription->source); if (!oldNewestItemTimestamp || (newestItemTimestamp && !g_str_equal (newestItemTimestamp, oldNewestItemTimestamp))) { debug3(DEBUG_UPDATE, "ReedahSource: auto-updating %s " "[oldtimestamp%s, timestamp %s]", id, oldNewestItemTimestamp, newestItemTimestamp); g_hash_table_insert (gsource->lastTimestampMap, g_strdup (node->subscription->source), g_strdup ((gchar *)newestItemTimestamp)); subscription_update (node->subscription, 0); } xmlFree (newestItemTimestamp); xmlFree (id); } static void reedah_source_opml_quick_update_cb (const struct updateResult* const result, gpointer userdata, updateFlags flags) { ReedahSourcePtr gsource = (ReedahSourcePtr) userdata; xmlDocPtr doc; if (!result->data) { /* what do I do? */ debug0 (DEBUG_UPDATE, "ReedahSource: Unable to get unread counts, this update is aborted."); return; } doc = xml_parse (result->data, result->size, NULL); if (!doc) { debug0 (DEBUG_UPDATE, "ReedahSource: The XML failed to parse, maybe the session has expired. (FIXME)"); return; } xpath_foreach_match (xmlDocGetRootElement (doc), "/object/list[@name='unreadcounts']/object", reedah_source_opml_quick_update_helper, gsource); xmlFreeDoc (doc); } gboolean reedah_source_opml_quick_update(ReedahSourcePtr source) { subscriptionPtr subscription = source->root->subscription; UpdateRequest *request = update_request_new ( source->root->source->type->api.unread_count, subscription->updateState, subscription->updateOptions ); update_request_set_auth_value(request, source->root->source->authToken); update_execute_request (source, request, reedah_source_opml_quick_update_cb, source, 0); return TRUE; } static void reedah_source_opml_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { reedah_subscription_opml_cb (subscription, result, flags); } static gboolean reedah_source_opml_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { nodePtr node = subscription->node; ReedahSourcePtr source = (ReedahSourcePtr)node->data; g_assert(node->source); if (node->source->loginState == NODE_SOURCE_STATE_NONE) { debug0(DEBUG_UPDATE, "ReedahSource: login"); reedah_source_login (source, 0) ; return FALSE; } debug1 (DEBUG_UPDATE, "updating Reedah subscription (node id %s)", node->id); update_request_set_source (request, node->source->type->api.subscription_list); update_request_set_auth_value (request, node->source->authToken); return TRUE; } /* OPML subscription type definition */ struct subscriptionType reedahSourceOpmlSubscriptionType = { reedah_source_opml_subscription_prepare_update_request, reedah_source_opml_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/reedah_source_feed_list.h000066400000000000000000000024301415350204600231430ustar00rootroot00000000000000/** * @file reedah_source_feed_list.h Reedah feed list handling * * Copyright (C) 2013-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/reedah_source.h" /** * Find a node by the source id. * * @param gsource the Reedah source * @param source the feed id to find * * @returns a node (or NULL) */ nodePtr reedah_source_opml_get_node_by_source(ReedahSourcePtr gsource, const gchar *source); /** * Perform a quick update of the Reedah source. * * @param gsource the Reedah source */ gboolean reedah_source_opml_quick_update (ReedahSourcePtr gsource); liferea-1.13.7/src/fl_sources/theoldreader_source.c000066400000000000000000000266761415350204600223540ustar00rootroot00000000000000/** * @file theoldreader_source.c TheOldReader feed list source support * * Copyright (C) 2007-2016 Lars Windolf * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2011 Peter Oliver * Copyright (C) 2011 Sergey Snitsaruk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/theoldreader_source.h" #include #include #include "common.h" #include "debug.h" #include "feedlist.h" #include "folder.h" #include "item_state.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "update.h" #include "ui/auth_dialog.h" #include "ui/liferea_dialog.h" #include "fl_sources/node_source.h" #include "fl_sources/opml_source.h" #include "fl_sources/google_reader_api_edit.h" #include "fl_sources/theoldreader_source_feed_list.h" /** default TheOldReader subscription list update interval = once a day */ #define THEOLDREADER_SOURCE_UPDATE_INTERVAL 60*60*24 /** create a source with given node as root */ static TheOldReaderSourcePtr theoldreader_source_new (nodePtr node) { TheOldReaderSourcePtr source = g_new0 (struct TheOldReaderSource, 1) ; source->root = node; source->lastTimestampMap = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); source->folderToCategory = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); return source; } static void theoldreader_source_free (TheOldReaderSourcePtr source) { if (!source) return; update_job_cancel_by_owner (source); g_hash_table_unref (source->lastTimestampMap); g_hash_table_destroy (source->folderToCategory); g_free (source); } static void theoldreader_source_login_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { nodePtr node = (nodePtr) userdata; gchar *tmp = NULL; subscriptionPtr subscription = node->subscription; debug1 (DEBUG_UPDATE, "TheOldReader login processing... %s", result->data); if (result->data && result->httpstatus == 200) tmp = strstr (result->data, "Auth="); if (tmp) { gchar *ttmp = tmp; tmp = strchr (tmp, '\n'); if (tmp) *tmp = '\0'; node_source_set_auth_token (node, g_strdup_printf ("GoogleLogin auth=%s", ttmp + 5)); /* now that we are authenticated trigger updating to start data retrieval */ if (!(flags & NODE_SOURCE_UPDATE_ONLY_LOGIN)) subscription_update (subscription, flags); /* process any edits waiting in queue */ google_reader_api_edit_process (node->source); } else { debug0 (DEBUG_UPDATE, "TheOldReader login failed! no Auth token found in result!"); subscription->node->available = FALSE; g_free (subscription->updateError); subscription->updateError = g_strdup (_("Login failed!")); node_source_set_state (node, NODE_SOURCE_STATE_NO_AUTH); auth_dialog_new (subscription, flags); } } /** * Perform a login to TheOldReader, if the login completes the * TheOldReaderSource will have a valid Auth token and will have loginStatus to * NODE_SOURCE_LOGIN_ACTIVE. */ void theoldreader_source_login (TheOldReaderSourcePtr source, guint32 flags) { gchar *username, *password; UpdateRequest *request; subscriptionPtr subscription = source->root->subscription; if (source->root->source->loginState != NODE_SOURCE_STATE_NONE) { /* this should not happen, as of now, we assume the session * doesn't expire. */ debug1(DEBUG_UPDATE, "Logging in while login state is %d\n", source->root->source->loginState); } request = update_request_new ( THEOLDREADER_READER_LOGIN_URL, NULL, subscription->updateOptions ); /* escape user and password as both are passed using an URI */ username = g_uri_escape_string (subscription->updateOptions->username, NULL, TRUE); password = g_uri_escape_string (subscription->updateOptions->password, NULL, TRUE); request->postdata = g_strdup_printf (THEOLDREADER_READER_LOGIN_POST, username, password); g_free (username); g_free (password); node_source_set_state (source->root, NODE_SOURCE_STATE_IN_PROGRESS); update_execute_request (source, request, theoldreader_source_login_cb, source->root, flags | FEED_REQ_NO_FEED); } /* node source type implementation */ static void theoldreader_source_auto_update (nodePtr node) { if (node->source->loginState == NODE_SOURCE_STATE_NONE) { node_source_update (node); return; } if (node->source->loginState == NODE_SOURCE_STATE_IN_PROGRESS) return; /* the update will start automatically anyway */ debug0 (DEBUG_UPDATE, "theoldreader_source_auto_update()"); subscription_auto_update (node->subscription); } static void theoldreader_source_init (void) { metadata_type_register ("theoldreader-feed-id", METADATA_TYPE_TEXT); } static void theoldreader_source_deinit (void) { } static void theoldreader_source_import (nodePtr node) { opml_source_import (node); node->subscription->updateInterval = -1; node->subscription->type = node->source->type->sourceSubscriptionType; if (!node->data) node->data = (gpointer) theoldreader_source_new (node); } static nodePtr theoldreader_source_add_subscription (nodePtr root, subscriptionPtr subscription) { nodePtr parent; gchar *categoryId = NULL; TheOldReaderSourcePtr source = (TheOldReaderSourcePtr)root->data; /* Determine correct category from selected folder name */ parent = feedlist_get_selected (); if (parent) { if (parent->subscription) parent = parent->parent; categoryId = g_hash_table_lookup (source->folderToCategory, parent->id); } google_reader_api_edit_add_subscription (root->source, subscription->source, categoryId); // FIXME: leaking subscription? // FIXME: somehow the async subscribing doesn't cause the feed list to update return NULL; } static void theoldreader_source_remove_node (nodePtr node, nodePtr child) { gchar *url; TheOldReaderSourcePtr source = (TheOldReaderSourcePtr) node->data; if (child == node) { feedlist_node_removed (child); return; } url = g_strdup (child->subscription->source); feedlist_node_removed (child); /* propagate the removal only if there aren't other copies */ if (!feedlist_find_node (source->root, NODE_BY_URL, url)) google_reader_api_edit_remove_subscription (node->source, url); g_free (url); } /* GUI callbacks */ static void on_theoldreader_source_selected (GtkDialog *dialog, gint response_id, gpointer user_data) { nodePtr node; if (response_id == GTK_RESPONSE_OK) { node = node_new (node_source_get_node_type ()); node_source_new (node, theoldreader_source_get_type (), "http://theoldreader.com/reader"); subscription_set_auth_info (node->subscription, gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "userEntry"))), gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "passwordEntry")))); node->data = theoldreader_source_new (node); feedlist_node_added (node); node_source_update (node); } gtk_widget_destroy (GTK_WIDGET (dialog)); } static void ui_theoldreader_source_get_account_info (void) { GtkWidget *dialog; dialog = liferea_dialog_new ("theoldreader_source"); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_theoldreader_source_selected), NULL); } static void theoldreader_source_cleanup (nodePtr node) { TheOldReaderSourcePtr reader = (TheOldReaderSourcePtr) node->data; theoldreader_source_free(reader); node->data = NULL; } static void theoldreader_source_item_set_flag (nodePtr node, itemPtr item, gboolean newStatus) { google_reader_api_edit_mark_starred (node->source, item->sourceId, node->subscription->source, newStatus); item_flag_state_changed (item, newStatus); } static void theoldreader_source_item_mark_read (nodePtr node, itemPtr item, gboolean newStatus) { google_reader_api_edit_mark_read (node->source, item->sourceId, node->subscription->source, newStatus); item_read_state_changed (item, newStatus); } /** * Convert all subscriptions of a google source to local feeds * * @param node The node to migrate (not the nodeSource!) */ static void theoldreader_source_convert_to_local (nodePtr node) { node_source_set_state (node, NODE_SOURCE_STATE_MIGRATE); } /* node source type definition */ extern struct subscriptionType theOldReaderSourceFeedSubscriptionType; extern struct subscriptionType theOldReaderSourceOpmlSubscriptionType; #define BASE_URL "https://theoldreader.com/reader/api/0/" static struct nodeSourceType nst = { .id = "fl_theoldreader", .name = N_("TheOldReader"), .capabilities = NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION | NODE_SOURCE_CAPABILITY_CAN_LOGIN | NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST | NODE_SOURCE_CAPABILITY_ADD_FEED | NODE_SOURCE_CAPABILITY_ADD_FOLDER | NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC | NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL | NODE_SOURCE_CAPABILITY_GOOGLE_READER_API, .api.json = TRUE, .api.subscription_list = BASE_URL "subscription/list?output=json", .api.unread_count = BASE_URL "unread-count?all=true&client=liferea", .api.token = BASE_URL "token", .api.add_subscription = BASE_URL "subscription/edit?client=liferea", .api.add_subscription_post = "s=feed%%2F%s&ac=subscribe&T=%s", .api.remove_subscription = BASE_URL "subscription/edit?client=liferea", .api.remove_subscription_post = "s=feed%%2F%s&ac=unsubscribe&T=%s", .api.edit_tag = BASE_URL "edit-tag?client=liferea", .api.edit_tag_add_post = "i=%s&s=%s%%2F%s&a=%s&ac=edit-tags&T=%s&async=true", .api.edit_tag_remove_post = "i=%s&s=%s%%2F%s&r=%s&ac=edit-tags&T=%s&async=true", .api.edit_tag_ar_tag_post = "i=%s&s=%s%%2F%s&a=%s&r=%s&ac=edit-tags&T=%s&async=true", .api.edit_add_label = BASE_URL "subscription/edit?client=liferea", .api.edit_add_label_post = "s=%s&a=%s&ac=edit&T=%s", .feedSubscriptionType = &theOldReaderSourceFeedSubscriptionType, .sourceSubscriptionType = &theOldReaderSourceOpmlSubscriptionType, .source_type_init = theoldreader_source_init, .source_type_deinit = theoldreader_source_deinit, .source_new = ui_theoldreader_source_get_account_info, .source_delete = opml_source_remove, .source_import = theoldreader_source_import, .source_export = opml_source_export, .source_get_feedlist = opml_source_get_feedlist, .source_auto_update = theoldreader_source_auto_update, .free = theoldreader_source_cleanup, .item_set_flag = theoldreader_source_item_set_flag, .item_mark_read = theoldreader_source_item_mark_read, .add_folder = NULL, .add_subscription = theoldreader_source_add_subscription, .remove_node = theoldreader_source_remove_node, .convert_to_local = theoldreader_source_convert_to_local }; nodeSourceTypePtr theoldreader_source_get_type (void) { return &nst; } liferea-1.13.7/src/fl_sources/theoldreader_source.h000066400000000000000000000055251415350204600223470ustar00rootroot00000000000000/** * @file theoldreader_source.h TheOldReader feed list source support * * Copyright (C) 2007-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _THEOLDREADER_SOURCE_H #define _THEOLDREADER_SOURCE_H #include "fl_sources/node_source.h" /** * A nodeSource specific for TheOldReader */ typedef struct TheOldReaderSource { nodePtr root; /**< the root node in the feed list */ /** * A map from a subscription source to a timestamp when it was last * updated (provided by Google). */ GHashTable *lastTimestampMap; GHashTable *folderToCategory; /**< Lookup hash for folder node id to TTRSS category id */ } *TheOldReaderSourcePtr; /** * TheOldReader API URL's * In each of the following, the _URL indicates the URL to use, and _POST * indicates the corresponging postdata to send. * @see https://github.com/krasnoukhov/theoldreader-api */ /** * TheOldReader Login api. * @param Email The google account email id. * @param Passwd The google account password. * @return The return data has a line "Auth=xxxx" which will be used as an * Authorization header in future requests. */ #define THEOLDREADER_READER_LOGIN_URL "https://theoldreader.com/accounts/ClientLogin" #define THEOLDREADER_READER_LOGIN_POST "service=reader&Email=%s&Passwd=%s&source=liferea&continue=http://theoldreader.com" /** * @returns TheOldReader source type implementation info. */ nodeSourceTypePtr theoldreader_source_get_type (void); /** * Find a child node with the given feed source URL. * * @param gsource TheOldReaderSource * @param source a feed source URL to search * * @returns a node (or NULL) */ nodePtr theoldreader_source_get_node_from_source (TheOldReaderSourcePtr gsource, const gchar* source); /** * Migrate a google source child-node from a Liferea 1.4 style read-only * google source nodes. * * @param node The node to migrate (not the nodeSource!) */ void theoldreader_source_migrate_node (nodePtr node); /** * Perform login for the given Google source. * * @param gsource a TheOldReaderSource * @param flags network request flags */ void theoldreader_source_login (TheOldReaderSourcePtr gsource, guint32 flags); #endif liferea-1.13.7/src/fl_sources/theoldreader_source_feed.c000066400000000000000000000161551415350204600233260ustar00rootroot00000000000000/** * @file theoldreader_source_feed.c TheOldReader feed subscription routines * * Copyright (C) 2008 Arnold Noronha * Copyright (C) 2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include "common.h" #include "debug.h" #include "xml.h" #include "feedlist.h" #include "google_reader_api_edit.h" #include "theoldreader_source.h" #include "subscription.h" #include "node.h" #include "metadata.h" #include "db.h" #include "item_state.h" void theoldreader_source_migrate_node (nodePtr node) { /* scan the node for bad ID's, if so, brutally remove the node */ itemSetPtr itemset = node_get_itemset (node); GList *iter = itemset->ids; for (; iter; iter = g_list_next (iter)) { itemPtr item = item_load (GPOINTER_TO_UINT (iter->data)); if (item && item->sourceId) { // FIXME: does this work? if (!g_str_has_prefix(item->sourceId, "tag:google.com")) { debug1(DEBUG_UPDATE, "Item with sourceId [%s] will be deleted.", item->sourceId); db_item_remove(GPOINTER_TO_UINT(iter->data)); } } if (item) item_unload (item); } /* cleanup */ itemset_free (itemset); } static itemPtr theoldreader_source_load_item_from_sourceid (nodePtr node, gchar *sourceId, GHashTable *cache) { gpointer ret = g_hash_table_lookup (cache, sourceId); itemSetPtr itemset; int num = g_hash_table_size (cache); GList *iter; itemPtr item = NULL; if (ret) return item_load (GPOINTER_TO_UINT (ret)); /* skip the top 'num' entries */ itemset = node_get_itemset (node); iter = itemset->ids; while (num--) iter = g_list_next (iter); for (; iter; iter = g_list_next (iter)) { item = item_load (GPOINTER_TO_UINT (iter->data)); if (item) { if (item->sourceId) { /* save to cache */ g_hash_table_insert (cache, g_strdup(item->sourceId), (gpointer) item->id); if (g_str_equal (item->sourceId, sourceId)) { itemset_free (itemset); return item; } } item_unload (item); } } g_print ("Could not find item for %s!", sourceId); itemset_free (itemset); return NULL; } static void theoldreader_source_item_retrieve_status (const xmlNodePtr entry, subscriptionPtr subscription, GHashTable *cache) { xmlNodePtr xml; nodePtr node = subscription->node; xmlChar *id = NULL; gboolean read = FALSE; xml = entry->children; g_assert (xml); /* Note: at the moment TheOldReader doesn't exposed a "starred" label like Google Reader did. It also doesn't expose the like feature it implements. Therefore we cannot sync the flagged state with TheOldReader. */ for (xml = entry->children; xml; xml = xml->next) { if (g_str_equal (xml->name, "id")) id = xmlNodeGetContent (xml); if (g_str_equal (xml->name, "category")) { xmlChar* label = xmlGetProp (xml, BAD_CAST"label"); if (!label) continue; if (g_str_equal (label, "read")) read = TRUE; xmlFree (label); } } if (!id) { g_print ("Skipping item without id in theoldreader_source_item_retrieve_status()!"); return; } itemPtr item = theoldreader_source_load_item_from_sourceid (node, (gchar *)id, cache); if (item && item->sourceId) { if (g_str_equal (item->sourceId, id) && !google_reader_api_edit_is_in_queue(node->source, (gchar *)id)) { if (item->readStatus != read) item_read_state_changed (item, read); } } if (item) item_unload (item); xmlFree (id); } static void theoldreader_feed_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult* const result, updateFlags flags) { gchar *id; debug_start_measurement (DEBUG_UPDATE); /* Save old subscription metadata which contains "theoldreader-feed-id" which is mission critical and the feed parser currently drops all previous metadata :-( */ id = g_strdup (metadata_list_get (subscription->metadata, "theoldreader-feed-id")); /* Always do standard feed parsing to get the items... */ feed_get_subscription_type ()->process_update_result (subscription, result, flags); /* Set remote id again */ metadata_list_set (&subscription->metadata, "theoldreader-feed-id", id); g_free (id); if (!result->data) return; xmlDocPtr doc = xml_parse (result->data, result->size, NULL); if (doc) { xmlNodePtr root = xmlDocGetRootElement (doc); xmlNodePtr entry = root->children ; GHashTable *cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); while (entry) { if (!g_str_equal (entry->name, "entry")) { entry = entry->next; continue; /* not an entry */ } theoldreader_source_item_retrieve_status (entry, subscription, cache); entry = entry->next; } g_hash_table_unref (cache); xmlFreeDoc (doc); } else { debug0 (DEBUG_UPDATE, "theoldreader_feed_subscription_process_update_result(): Couldn't parse XML!"); g_print ("theoldreader_feed_subscription_process_update_result(): Couldn't parse XML!"); } debug_end_measurement (DEBUG_UPDATE, "theoldreader_feed_subscription_process_update_result"); } static gboolean theoldreader_feed_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { debug0 (DEBUG_UPDATE, "preparing TheOldReader feed subscription for update"); TheOldReaderSourcePtr source = (TheOldReaderSourcePtr) node_source_root_from_node (subscription->node)->data; g_assert (source); if (source->root->source->loginState == NODE_SOURCE_STATE_NONE) { subscription_update (node_source_root_from_node (subscription->node)->subscription, 0) ; return FALSE; } if (!metadata_list_get (subscription->metadata, "theoldreader-feed-id")) { g_print ("Skipping TheOldReader feed '%s' (%s) without id!", subscription->source, subscription->node->id); return FALSE; } debug1 (DEBUG_UPDATE, "Setting cookies for a TheOldReader subscription '%s'", subscription->source); gchar* source_escaped = g_uri_escape_string(request->source, NULL, TRUE); gchar* newUrl = g_strdup_printf ("http://theoldreader.com/reader/atom/%s", metadata_list_get (subscription->metadata, "theoldreader-feed-id")); update_request_set_source (request, newUrl); g_free (newUrl); g_free (source_escaped); update_request_set_auth_value (request, subscription->node->source->authToken); return TRUE; } struct subscriptionType theOldReaderSourceFeedSubscriptionType = { theoldreader_feed_subscription_prepare_update_request, theoldreader_feed_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/theoldreader_source_feed_list.c000066400000000000000000000175021415350204600243560ustar00rootroot00000000000000/** * @file theoldreader_source_feed_list.c TheOldReader feed list handling * * Copyright (C) 2013-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "theoldreader_source_feed_list.h" #include #include #include "common.h" #include "db.h" #include "debug.h" #include "feedlist.h" #include "folder.h" #include "json.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "subscription_icon.h" #include "xml.h" #include "fl_sources/opml_source.h" #include "fl_sources/theoldreader_source.h" static void theoldreader_source_check_node_for_removal (nodePtr node, gpointer user_data) { JsonArray *array = (JsonArray *)user_data; GList *iter, *elements; gboolean found = FALSE; if (IS_FOLDER (node)) { /* Auto-remove folders if they do not have children */ if (!node->children) feedlist_node_removed (node); node_foreach_child_data (node, theoldreader_source_check_node_for_removal, user_data); } else { elements = iter = json_array_get_elements (array); while (iter) { JsonNode *json_node = (JsonNode *)iter->data; if (g_str_equal (node->subscription->source, json_get_string (json_node, "url"))) { debug1 (DEBUG_UPDATE, "node: %s", node->subscription->source); found = TRUE; break; } iter = g_list_next (iter); } g_list_free (elements); if (!found) feedlist_node_removed (node); } } static void theoldreader_source_merge_feed (TheOldReaderSourcePtr source, const gchar *url, const gchar *title, const gchar *id, nodePtr folder) { nodePtr node; node = feedlist_find_node (source->root, NODE_BY_URL, url); if (!node) { debug2 (DEBUG_UPDATE, "adding %s (%s)", title, url); node = node_new (feed_get_node_type ()); node_set_title (node, title); node_set_data (node, feed_new ()); node_set_subscription (node, subscription_new (url, NULL, NULL)); node->subscription->type = source->root->source->type->feedSubscriptionType; /* Save TheOldReader feed id which we need to fetch items... */ node->subscription->metadata = metadata_list_append (node->subscription->metadata, "theoldreader-feed-id", id); db_subscription_update (node->subscription); node_set_parent (node, folder?folder:source->root, -1); feedlist_node_imported (node); /** * @todo mark the ones as read immediately after this is done * the feed as retrieved by this has the read and unread * status inherently. */ subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); subscription_icon_update (node->subscription); } else { node_source_update_folder (node, folder); } } /* JSON subscription list processing implementation */ static void theoldreader_subscription_cb (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { TheOldReaderSourcePtr source = (TheOldReaderSourcePtr) subscription->node->data; debug1 (DEBUG_UPDATE,"theoldreader_subscription_cb(): %s", result->data); subscription->updateJob = NULL; // FIXME: the following code is very similar to ttrss! if (result->data && result->httpstatus == 200) { JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, result->data, -1, NULL)) { JsonArray *array = json_node_get_array (json_get_node (json_parser_get_root (parser), "subscriptions")); GList *iter, *elements, *citer, *celements; /* We expect something like this: [{"id":"feed/51d49b79d1716c7b18000025", "title":"LZone", "categories":[{"id":"user/-/label/myfolder","label":"myfolder"}], "sortid":"51d49b79d1716c7b18000025", "firstitemmsec":"1371403150181", "url":"https://lzone.de/rss.xml", "htmlUrl":"https://lzone.de", "iconUrl":"http://s.yeoldereader.com/system/uploads/feed/picture/5152/884a/4dce/57aa/7e00/icon_0a6a.ico"}, ... */ elements = iter = json_array_get_elements (array); /* Add all new nodes we find */ while (iter) { JsonNode *categories, *node = (JsonNode *)iter->data; nodePtr folder = NULL; /* Check for categories, if there use first one as folder */ categories = json_get_node (node, "categories"); if (categories && JSON_NODE_TYPE (categories) == JSON_NODE_ARRAY) { citer = celements = json_array_get_elements (json_node_get_array (categories)); while (citer) { const gchar *label = json_get_string ((JsonNode *)citer->data, "label"); const gchar *id = json_get_string ((JsonNode *)citer->data, "id"); if (label) { folder = node_source_find_or_create_folder (source->root, label, label); /* Store category id also for folder (needed when subscribing new feeds) */ g_hash_table_insert (source->folderToCategory, g_strdup (folder->id), g_strdup (id)); break; } citer = g_list_next (citer); } g_list_free (celements); } /* ignore everything without a feed url */ if (json_get_string (node, "url")) { theoldreader_source_merge_feed (source, json_get_string (node, "url"), json_get_string (node, "title"), json_get_string (node, "id"), folder); } iter = g_list_next (iter); } g_list_free (elements); /* Remove old nodes we cannot find anymore */ node_foreach_child_data (source->root, theoldreader_source_check_node_for_removal, array); /* Save new subscription tree to OPML cache file */ opml_source_export (subscription->node); subscription->node->available = TRUE; } else { g_print ("Invalid JSON returned on TheOldReader request! >>>%s<<<", result->data); } g_object_unref (parser); } else { subscription->node->available = FALSE; debug0 (DEBUG_UPDATE, "theoldreader_subscription_cb(): ERROR: failed to get subscription list!"); } if (!(flags & NODE_SOURCE_UPDATE_ONLY_LIST)) node_foreach_child_data (subscription->node, node_update_subscription, GUINT_TO_POINTER (0)); } static void theoldreader_source_opml_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { theoldreader_subscription_cb (subscription, result, flags); } static gboolean theoldreader_source_opml_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { nodePtr node = subscription->node; g_assert(node->source); if (node->source->loginState == NODE_SOURCE_STATE_NONE) { debug0 (DEBUG_UPDATE, "TheOldReaderSource: login"); theoldreader_source_login (node->data, 0); return FALSE; } debug1 (DEBUG_UPDATE, "updating TheOldReader subscription (node id %s)", node->id); update_request_set_source (request, node->source->type->api.subscription_list); update_request_set_auth_value (request, node->source->authToken); return TRUE; } /* OPML subscription type definition */ struct subscriptionType theOldReaderSourceOpmlSubscriptionType = { theoldreader_source_opml_subscription_prepare_update_request, theoldreader_source_opml_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/theoldreader_source_feed_list.h000066400000000000000000000021141415350204600243540ustar00rootroot00000000000000/** * Copyright (C) 2009 Adrian Bunk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/theoldreader_source.h" /** * Find a node by the source id. * * @param gsource the TheOldReader source * @param source the feed id to find * * @returns a node (or NULL) */ nodePtr theoldreader_source_opml_get_node_by_source(TheOldReaderSourcePtr gsource, const gchar *source); liferea-1.13.7/src/fl_sources/ttrss_source.c000066400000000000000000000360471415350204600210620ustar00rootroot00000000000000/** * @file ttrss_source.c Tiny Tiny RSS feed list source support * * Copyright (C) 2010-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "fl_sources/ttrss_source.h" #include #include #include #include #include "common.h" #include "debug.h" #include "db.h" #include "feedlist.h" #include "item_state.h" #include "json.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "update.h" #include "ui/auth_dialog.h" #include "ui/ui_common.h" #include "ui/liferea_dialog.h" #include "fl_sources/node_source.h" #include "fl_sources/opml_source.h" /** Initialize a TinyTinyRSS source with given node as root */ static ttrssSourcePtr ttrss_source_new (nodePtr node) { ttrssSourcePtr source = g_new0 (struct ttrssSource, 1) ; source->root = node; source->apiLevel = 0; source->categories = g_hash_table_new (g_direct_hash, g_direct_equal); source->folderToCategory = g_hash_table_new (g_str_hash, g_str_equal); return source; } static void ttrss_source_free (ttrssSourcePtr source) { if (!source) return; update_job_cancel_by_owner (source); g_hash_table_destroy (source->categories); g_hash_table_destroy (source->folderToCategory); g_free (source->session_id); g_free (source); } static void ttrss_source_set_login_error (ttrssSourcePtr source, gchar *msg) { g_free (source->root->subscription->updateError); source->root->subscription->updateError = msg; source->root->available = FALSE; g_print ("TinyTinyRSS login failed: error '%s'!\n", msg); node_source_set_state (source->root, NODE_SOURCE_STATE_NONE); } static void ttrss_source_login_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { ttrssSourcePtr source = (ttrssSourcePtr) userdata; subscriptionPtr subscription = source->root->subscription; JsonParser *parser; debug1 (DEBUG_UPDATE, "TinyTinyRSS login processing... >>>%s<<<", result->data); g_assert (!source->session_id); if (!(result->data && result->httpstatus == 200)) { ttrss_source_set_login_error (source, g_strdup_printf ("Login request failed with HTTP error %d", result->httpstatus)); return; } parser = json_parser_new (); if (!json_parser_load_from_data (parser, result->data, -1, NULL)) { ttrss_source_set_login_error (source, g_strdup ("Invalid JSON returned on login!")); return; } JsonNode *node = json_parser_get_root (parser); /* Check for API specified error... */ if (json_get_string (json_get_node (node, "content"), "error")) { const gchar *error = json_get_string (json_get_node (node, "content"), "error"); ttrss_source_set_login_error (source, g_strdup (error)); if (g_str_equal (error, "LOGIN_ERROR")) auth_dialog_new (source->root->subscription, flags); return; } /* Success! Get SID and API Level. */ source->apiLevel = json_get_int (json_get_node (node, "content"), "api_level"); source->session_id = g_strdup (json_get_string (json_get_node (node, "content"), "session_id")); if (source->session_id) { debug2 (DEBUG_UPDATE, "TinyTinyRSS Found session_id: >>>%s<<< (API level %d)!", source->session_id, source->apiLevel); node_source_set_state (subscription->node, NODE_SOURCE_STATE_ACTIVE); if (!(flags & NODE_SOURCE_UPDATE_ONLY_LOGIN)) subscription_update (subscription, flags); } else { ttrss_source_set_login_error (source, g_strdup_printf ("No session_id found in response!\n%s", result->data)); } g_object_unref (parser); } /** * Perform a login to tt-rss, if the login completes the ttrssSource will * have a valid sid and will have loginStatus NODE_SOURCE_LOGIN_ACTIVE. */ void ttrss_source_login (ttrssSourcePtr source, guint32 flags) { gchar *username, *password, *source_uri; UpdateRequest *request; subscriptionPtr subscription = source->root->subscription; if (source->root->source->loginState != NODE_SOURCE_STATE_NONE) { /* this should not happen, as of now, we assume the session doesn't expire. */ debug1 (DEBUG_UPDATE, "Logging in while login state is %d", source->root->source->loginState); } source->url = metadata_list_get (subscription->metadata, "ttrss-url"); if (!source->url) { ttrss_source_set_login_error (source, g_strdup ("Fatal: We've lost the TinyTinyRSS server URL! Please re-subscribe!")); return; } source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, NULL, // updateState subscription->updateOptions ); g_free (source_uri); /* escape user and password for JSON call */ username = g_strescape (subscription->updateOptions->username, NULL); password = g_strescape (subscription->updateOptions->password, NULL); request->postdata = g_strdup_printf (TTRSS_JSON_LOGIN, username, password); g_free (username); g_free (password); node_source_set_state (source->root, NODE_SOURCE_STATE_IN_PROGRESS); update_execute_request (source, request, ttrss_source_login_cb, source, flags | FEED_REQ_NO_FEED); } /* node source type implementation */ static void ttrss_source_auto_update (nodePtr node) { if (node->source->loginState == NODE_SOURCE_STATE_NONE) { node_source_update (node); return; } if (node->source->loginState == NODE_SOURCE_STATE_IN_PROGRESS) return; /* the update will start automatically anyway */ debug0 (DEBUG_UPDATE, "ttrss_source_auto_update()"); subscription_auto_update (node->subscription); } static void ttrss_source_init (void) { metadata_type_register ("ttrss-url", METADATA_TYPE_URL); metadata_type_register ("ttrss-feed-id", METADATA_TYPE_TEXT); } static void ttrss_source_deinit (void) { } static void ttrss_source_import (nodePtr node) { opml_source_import (node); node->subscription->updateInterval = -1; node->subscription->type = node->source->type->sourceSubscriptionType; if (!node->data) node->data = (gpointer) ttrss_source_new (node); } static void ttrss_source_subscribe_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { subscriptionPtr subscription = (subscriptionPtr) userdata; debug2 (DEBUG_UPDATE, "TinyTinyRSS subscribe result processing... status:%d >>>%s<<<", result->httpstatus, result->data); if (200 != result->httpstatus) { ui_show_error_box (_("TinyTinyRSS HTTP API not reachable!")); return; } /* Result should be {"seq":0,"status":0,"content":{"status":{"code":1}}} */ // FIXME: poor mans matching if (!strstr (result->data, "\"code\":1")) { ui_show_error_box (_("TinyTinyRSS subscribing to feed failed! Check if you really passed a feed URL!")); return; } /* As TinyTinyRSS does not return the id of the newly subscribed feed we need to reload the entire feed list. */ node_source_update (subscription->node->source->root); } static nodePtr ttrss_source_add_subscription (nodePtr root, subscriptionPtr subscription) { nodePtr parent; gchar *username, *password; ttrssSourcePtr source = (ttrssSourcePtr)root->data; UpdateRequest *request; gint categoryId = 0; gchar *source_uri; /* Determine correct category from selected folder name */ parent = feedlist_get_selected (); if (parent) { if (parent->subscription) parent = parent->parent; categoryId = GPOINTER_TO_INT (g_hash_table_lookup (source->folderToCategory, parent->id)); } source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, NULL, root->subscription->updateOptions ); g_free (source_uri); /* escape user and password for JSON call */ username = g_strescape (root->subscription->updateOptions->username, NULL); password = g_strescape (root->subscription->updateOptions->password, NULL); request->postdata = g_strdup_printf (TTRSS_JSON_SUBSCRIBE, source->session_id, subscription->source, categoryId, username, password); g_free (username); g_free (password); update_execute_request (source, request, ttrss_source_subscribe_cb, source, 0 /* flags */); return NULL; } static void ttrss_source_remove_node_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { nodePtr node = (nodePtr) userdata; debug2 (DEBUG_UPDATE, "TinyTinyRSS remove node result processing... status:%d >>>%s<<<", result->httpstatus, result->data); if (200 != result->httpstatus) { ui_show_error_box (_("TinyTinyRSS HTTP API not reachable!")); return; } /* We expect the following {"seq":0,"status":0,"content":{"status":"OK"}} */ // FIXME: poor mans matching if (!strstr (result->data, "\"status\":0")) { ui_show_error_box (_("TinyTinyRSS unsubscribing feed failed!")); return; } feedlist_node_removed (node); } static void ttrss_source_remove_node (nodePtr root, nodePtr node) { ttrssSourcePtr source = (ttrssSourcePtr)root->data; UpdateRequest *request; const gchar *id; gchar *source_uri; // FIXME: Check for login? if (source->apiLevel < 5) { ui_show_info_box (_("This TinyTinyRSS version does not support removing feeds. Upgrade to version %s or later!"), "1.7.6"); return; } id = metadata_list_get (node->subscription->metadata, "ttrss-feed-id"); if (!id) { g_print ("Cannot remove node on remote side as ttrss-feed-id is unknown!"); return; } source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, NULL, root->subscription->updateOptions ); g_free (source_uri); request->postdata = g_strdup_printf (TTRSS_JSON_UNSUBSCRIBE, source->session_id, id); update_execute_request (source, request, ttrss_source_remove_node_cb, node, 0 /* flags */); } /* GUI callbacks */ static void on_ttrss_source_selected (GtkDialog *dialog, gint response_id, gpointer user_data) { if (response_id == GTK_RESPONSE_OK) { nodePtr node; node = node_new (node_source_get_node_type ()); node_source_new (node, ttrss_source_get_type (), ""); /* This is a bit ugly: we need to prevent the tt-rss base URL from being lost by unwanted permanent redirects on the getFeeds call, so we save it as the homepage meta data value... */ metadata_list_set (&node->subscription->metadata, "ttrss-url", gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "serverUrlEntry")))); subscription_set_auth_info (node->subscription, gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "userEntry"))), gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET(dialog), "passwordEntry")))); node->data = (gpointer)ttrss_source_new (node); feedlist_node_added (node); node_source_update (node); db_node_update (node); /* because of metadate_list_set() above */ } gtk_widget_destroy (GTK_WIDGET (dialog)); } static void ui_ttrss_source_get_account_info (void) { GtkWidget *dialog; dialog = liferea_dialog_new ("ttrss_source"); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_ttrss_source_selected), NULL); } static void ttrss_source_cleanup (nodePtr node) { ttrssSourcePtr source = (ttrssSourcePtr) node->data; ttrss_source_free (source); node->data = NULL; } static void ttrss_source_remote_update_cb (const struct updateResult * const result, gpointer userdata, updateFlags flags) { debug2 (DEBUG_UPDATE, "TinyTinyRSS update result processing... status:%d >>>%s<<<", result->httpstatus, result->data); } /* FIXME: Only simple synchronous item change requests... Get async! */ static void ttrss_source_item_set_flag (nodePtr node, itemPtr item, gboolean newStatus) { nodePtr root = node_source_root_from_node (node); ttrssSourcePtr source = (ttrssSourcePtr)root->data; UpdateRequest *request; gchar *source_uri; source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, NULL, root->subscription->updateOptions ); g_free (source_uri); request->postdata = g_strdup_printf (TTRSS_JSON_UPDATE_ITEM_FLAG, source->session_id, item_get_id(item), newStatus?1:0 ); update_execute_request (source, request, ttrss_source_remote_update_cb, source, 0 /* flags */); item_flag_state_changed (item, newStatus); } static void ttrss_source_item_mark_read (nodePtr node, itemPtr item, gboolean newStatus) { nodePtr root = node_source_root_from_node (node); ttrssSourcePtr source = (ttrssSourcePtr)root->data; UpdateRequest *request; gchar *source_uri; source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, NULL, root->subscription->updateOptions ); g_free (source_uri); request->postdata = g_strdup_printf (TTRSS_JSON_UPDATE_ITEM_UNREAD, source->session_id, item_get_id(item), newStatus?0:1 ); update_execute_request (source, request, ttrss_source_remote_update_cb, source, 0 /* flags */); item_read_state_changed (item, newStatus); } /** * Convert all subscriptions of a google source to local feeds * * @param node The node to migrate (not the nodeSource!) */ static void ttrss_source_convert_to_local (nodePtr node) { node_source_set_state (node, NODE_SOURCE_STATE_MIGRATE); } /* node source type definition */ extern struct subscriptionType ttrssSourceFeedSubscriptionType; extern struct subscriptionType ttrssSourceSubscriptionType; static struct nodeSourceType nst = { .id = "fl_ttrss", .name = N_("Tiny Tiny RSS"), .capabilities = NODE_SOURCE_CAPABILITY_DYNAMIC_CREATION | NODE_SOURCE_CAPABILITY_CAN_LOGIN | NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC | NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST | NODE_SOURCE_CAPABILITY_ADD_FEED | NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL, .feedSubscriptionType = &ttrssSourceFeedSubscriptionType, .sourceSubscriptionType = &ttrssSourceSubscriptionType, .source_type_init = ttrss_source_init, .source_type_deinit = ttrss_source_deinit, .source_new = ui_ttrss_source_get_account_info, .source_delete = opml_source_remove, .source_import = ttrss_source_import, .source_export = opml_source_export, .source_get_feedlist = opml_source_get_feedlist, .source_auto_update = ttrss_source_auto_update, .free = ttrss_source_cleanup, .item_set_flag = ttrss_source_item_set_flag, .item_mark_read = ttrss_source_item_mark_read, .add_folder = NULL, /* not supported by current tt-rss JSON API (v1.8) */ .add_subscription = ttrss_source_add_subscription, .remove_node = ttrss_source_remove_node, .convert_to_local = ttrss_source_convert_to_local }; nodeSourceTypePtr ttrss_source_get_type (void) { return &nst; } liferea-1.13.7/src/fl_sources/ttrss_source.h000066400000000000000000000076421415350204600210660ustar00rootroot00000000000000/** * @file ttrss_source.h Tiny Tiny RSS feed list source support * * Copyright (C) 2010-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _TTRSS_SOURCE_H #define _TTRSS_SOURCE_H #include #include "fl_sources/node_source.h" /** * A nodeSource specific for tt-rss */ typedef struct ttrssSource { nodePtr root; /**< the root node in the feed list */ gchar *session_id; /**< the current session id */ const gchar *url; /**< the API base URL */ gint apiLevel; /**< The API level reported by the instance (or 0) */ GHashTable *categories; /**< Lookup hash for TTRSS feed id to TTRSS category id */ GHashTable *folderToCategory; /**< Lookup hash for folder node id to TTRSS category id */ } *ttrssSourcePtr; /** * TinyTinyRSS JSON API is documented here: * * http://tt-rss.org/redmine/projects/tt-rss/wiki/JsonApiReference */ #define TTRSS_URL "%s/api/" /** * TinyTinyRSS Login API * * @param user The tt-rss account id * @param passwd The tt-rss account password * * @returns {"session_id":"xxx"} or {"error":"xxx"} */ #define TTRSS_JSON_LOGIN "{\"op\":\"login\", \"user\":\"%s\", \"password\":\"%s\"}" /** * Fetch TinyTinyRSS feed list. * * @param sid session id * * @returns JSON feed list */ #define TTRSS_JSON_SUBSCRIPTION_LIST "{\"op\":\"getFeeds\", \"sid\":\"%s\", \"cat_id\":\"-3\", \"include_nested\":\"true\"}" /** * Add a subscription to TinyTinyRSS * * @param sid session id * @param feed_url URL * @param category_id category id (or 0) * @param login user name * @param password password */ #define TTRSS_JSON_SUBSCRIBE "{\"op\":\"subscribeToFeed\", \"sid\":\"%s\", \"feed_url\":\"%s\", \"category_id\":%d, \"login\":\"%s\", \"password\":\"%s\"}" /** * Removes a subscription from TinyTinyRSS * * @param sid session id * @param feed_id TinyTinyRSS feed id */ #define TTRSS_JSON_UNSUBSCRIBE "{\"op\":\"unsubscribeFeed\", \"sid\":\"%s\", \"feed_id\":\"%s\"}" /** * Fetch tt-rss categories list (default is fetching it tree like) * * @returns JSON categories list */ #define TTRSS_JSON_CATEGORIES_LIST "{\"op\":\"getFeedTree\", \"sid\":\"%s\", \"include_empty\":\"true\"}" /** * Fetch TinyTinyRSS headlines for a given feed. * * @param sid session id * @param feed_id tt-rss feed id * @param limit feed cache size * * @returns JSON feed list */ #define TTRSS_JSON_HEADLINES "{\"op\":\"getHeadlines\", \"sid\":\"%s\", \"feed_id\":\"%s\", \"limit\":\"%d\", \"show_content\":\"true\", \"view_mode\":\"all_articles\", \"include_attachments\":\"true\"}" /** * Toggle item flag state. * * @param sid session id * @param item_id tt-rss item id * @param mode 0 = unflagged, 1 = flagged */ #define TTRSS_JSON_UPDATE_ITEM_FLAG "{\"op\":\"updateArticle\", \"sid\":\"%s\", \"article_ids\":\"%s\", \"mode\":\"%d\", \"field\":\"0\"}" /** * Toggle item read state. * * @param sid session id * @param item_id tt-rss item id * @param mode 0 = read, 1 = unread */ #define TTRSS_JSON_UPDATE_ITEM_UNREAD "{\"op\":\"updateArticle\", \"sid\":\"%s\", \"article_ids\":\"%s\", \"mode\":\"%d\", \"field\":\"2\"}" /** * Returns ttss source type implementation info. */ nodeSourceTypePtr ttrss_source_get_type (void); void ttrss_source_login (ttrssSourcePtr source, guint32 flags); #endif liferea-1.13.7/src/fl_sources/ttrss_source_feed.c000066400000000000000000000143711415350204600220410ustar00rootroot00000000000000/** * @file ttrss_source_feed.c Tiny Tiny RSS feed subscription routines * * Copyright (C) 2010-2013 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include "common.h" #include "db.h" #include "debug.h" #include "enclosure.h" #include "feedlist.h" #include "itemlist.h" #include "itemset.h" #include "json.h" #include "metadata.h" #include "subscription.h" #include "xml.h" #include "fl_sources/ttrss_source.h" static void ttrss_feed_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult* const result, updateFlags flags) { if (result->data && result->httpstatus == 200) { JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, result->data, -1, NULL)) { JsonArray *array = json_node_get_array (json_get_node (json_parser_get_root (parser), "content")); JsonNode *attachments; GList *elements = json_array_get_elements (array); GList *iter = elements; GList *items = NULL; /* We expect to get something like this [{"id":118, "unread":true, "marked":false, "updated":1287927675, "is_updated":false, "title":"IBM Says New ...", "link":"http:\/\/rss.slashdot.org\/~r\/Slashdot\/slashdot\/~3\/ALuhNKO3NV4\/story01.htm", "feed_id":"5", "content":"coondoggie writes ..." }, {"id":117, "unread":true, "marked":false, "updated":1287923814, [...] */ while (iter) { JsonNode *node = (JsonNode *)iter->data; itemPtr item = item_new (); gchar *id; const gchar *content; gchar *xhtml; id = g_strdup_printf ("%" G_GINT64_FORMAT, json_get_int (node, "id")); item_set_id (item, id); g_free (id); item_set_title (item, json_get_string (node, "title")); item_set_source (item, json_get_string (node, "link")); content = json_get_string (node, "content"); xhtml = xhtml_extract_from_string (content, NULL); item_set_description (item, xhtml); xmlFree (xhtml); item->time = json_get_int (node, "updated"); if (json_get_bool (node, "unread")) { item->readStatus = FALSE; } else { item->readStatus = TRUE; } if (json_get_bool (node, "marked")) item->flagStatus = TRUE; /* Extract enclosures */ attachments = json_get_node (node, "attachments"); if (attachments && JSON_NODE_TYPE (attachments) == JSON_NODE_ARRAY) { GList *aiter, *alist; alist = aiter = json_array_get_elements (json_node_get_array (attachments)); while (aiter) { JsonNode *enc_node = (JsonNode *)aiter->data; /* attachment nodes should look like this: {"id":"1562", "content_url":"http:\/\/...", "content_type":"audio\/mpeg", "post_id":"44572", "title":"...", "duration":"29446311"}] */ if (json_get_string (enc_node, "content_url") && json_get_string (enc_node, "content_type")) { gchar *encStr = enclosure_values_to_string ( json_get_string (enc_node, "content_url"), json_get_string (enc_node, "content_type"), 0 /* length unknown to TinyTiny RSS*/, FALSE /* not yet downloaded */); item->metadata = metadata_list_append (item->metadata, "enclosure", encStr); item->hasEnclosure = TRUE; g_free (encStr); } aiter = g_list_next (aiter); } g_list_free (alist); } items = g_list_append (items, (gpointer)item); iter = g_list_next (iter); } g_list_free (elements); /* merge against feed cache */ if (items) { itemSetPtr itemSet = node_get_itemset (subscription->node); subscription->node->newCount = itemset_merge_items (itemSet, items, TRUE /* feed valid */, FALSE /* markAsRead */); itemlist_merge_itemset (itemSet); itemset_free (itemSet); } subscription->node->available = TRUE; } else { subscription->node->available = FALSE; g_string_append (((feedPtr)subscription->node->data)->parseErrors, _("Could not parse JSON returned by TinyTinyRSS API!")); } g_object_unref (parser); } else { subscription->node->available = FALSE; } } static gboolean ttrss_feed_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { nodePtr root = node_source_root_from_node (subscription->node); ttrssSourcePtr source = (ttrssSourcePtr) root->data; const gchar *feed_id; gchar *source_name; gint fetchCount; debug0 (DEBUG_UPDATE, "TinyTinyRSS preparing feed subscription for update"); g_assert(root->source); if (root->source->loginState == NODE_SOURCE_STATE_NONE) { subscription_update (root->subscription, 0); return FALSE; } feed_id = metadata_list_get (subscription->metadata, "ttrss-feed-id"); if (!feed_id) { g_print ("Dropping TinyTinyRSS feed without id! (%s)", subscription->node->title); feedlist_node_removed (subscription->node); return FALSE; } /* We can always max out as TinyTinyRSS does limit results itself */ fetchCount = feed_get_max_item_count (subscription->node); request->postdata = g_strdup_printf (TTRSS_JSON_HEADLINES, source->session_id, feed_id, fetchCount); source_name = g_strdup_printf (TTRSS_URL, source->url); update_request_set_source (request, source_name); g_free (source_name); return TRUE; } struct subscriptionType ttrssSourceFeedSubscriptionType = { ttrss_feed_subscription_prepare_update_request, ttrss_feed_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/ttrss_source_feed_list.c000066400000000000000000000342701415350204600230740ustar00rootroot00000000000000/** * @file ttrss_source_feed_list.c tt-rss feed list handling routines. * * Copyright (C) 2010-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ttrss_source_feed_list.h" #include #include #include "common.h" #include "db.h" #include "debug.h" #include "feedlist.h" #include "folder.h" #include "json.h" #include "metadata.h" #include "node.h" #include "subscription.h" #include "subscription_icon.h" #include "fl_sources/opml_source.h" #include "fl_sources/ttrss_source.h" /* subscription list merging functions */ static void ttrss_source_check_node_for_removal (nodePtr node, gpointer user_data) { JsonArray *array = (JsonArray *)user_data; GList *iter, *elements; gboolean found = FALSE; if (IS_FOLDER (node)) { /* Auto-remove folders if they do not have children */ if (!node->children) feedlist_node_removed (node); node_foreach_child_data (node, ttrss_source_check_node_for_removal, user_data); } else { elements = iter = json_array_get_elements (array); while (iter) { JsonNode *json_node = (JsonNode *)iter->data; if (g_str_equal (node->subscription->source, json_get_string (json_node, "feed_url"))) { debug1 (DEBUG_UPDATE, "node: %s", node->subscription->source); found = TRUE; break; } iter = g_list_next (iter); } g_list_free (elements); if (!found) feedlist_node_removed (node); } } static void ttrss_source_merge_feed (ttrssSourcePtr source, const gchar *url, const gchar *title, gint64 id, nodePtr folder) { nodePtr node; gchar *tmp; /* check if node to be merged already exists */ node = feedlist_find_node (source->root, NODE_BY_URL, url); if (!node) { debug2 (DEBUG_UPDATE, "adding %s (%s)", title, url); node = node_new (feed_get_node_type ()); node_set_title (node, title); node_set_data (node, feed_new ()); node_set_subscription (node, subscription_new (url, NULL, NULL)); node->subscription->type = source->root->source->type->feedSubscriptionType; /* Save tt-rss feed id which we need to fetch items... */ tmp = g_strdup_printf ("%" G_GINT64_FORMAT, id); metadata_list_set (&node->subscription->metadata, "ttrss-feed-id", tmp); g_free (tmp); node_set_parent (node, folder?folder:source->root, -1); feedlist_node_imported (node); /** * @todo mark the ones as read immediately after this is done * the feed as retrieved by this has the read and unread * status inherently. */ subscription_update (node->subscription, FEED_REQ_RESET_TITLE | FEED_REQ_PRIORITY_HIGH); subscription_icon_update (node->subscription); /* Important: we must not loose the feed id! */ db_subscription_update (node->subscription); } else { node_source_update_folder (node, folder); } } /* source subscription type implementation */ static void ttrss_source_subscription_list_cb (const struct updateResult * const result, gpointer user_data, guint32 flags) { subscriptionPtr subscription = (subscriptionPtr) user_data; ttrssSourcePtr source = (ttrssSourcePtr) subscription->node->data; debug1 (DEBUG_UPDATE,"ttrss_subscription_cb(): %s", result->data); subscription->updateJob = NULL; if (result->data && result->httpstatus == 200) { JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, result->data, -1, NULL)) { JsonNode *content = json_get_node (json_parser_get_root (parser), "content"); JsonArray *array; GList *iter, *elements; /* We expect something like this: [ {"feed_url":"http://feeds.arstechnica.com/arstechnica/everything", "title":"Ars Technica", "id":6, "unread":20, "has_icon":true, "cat_id":0, "last_updated":1287853210}, {"feed_url":"http://rss.slashdot.org/Slashdot/slashdot", "title":"Slashdot", "id":5, "unread":33, "has_icon":true, "cat_id":0, "last_updated":1287853206}, [...] Or an error message that could look like this: {"seq":null,"status":1,"content":{"error":"NOT_LOGGED_IN"}} */ if (!content || (JSON_NODE_TYPE (content) != JSON_NODE_ARRAY)) { debug0 (DEBUG_UPDATE, "ttrss_subscription_cb(): Failed to get subscription list!"); subscription->node->available = FALSE; return; } array = json_node_get_array (content); elements = iter = json_array_get_elements (array); /* Add all new nodes we find */ while (iter) { JsonNode *node = (JsonNode *)iter->data; /* Get category id */ gchar *category = NULL; gint cat_id = json_get_int (node, "cat_id"); if (cat_id > 0) category = g_strdup_printf ("%d", cat_id); /* ignore everything without a feed url */ if (json_get_string (node, "feed_url")) { ttrss_source_merge_feed (source, json_get_string (node, "feed_url"), json_get_string (node, "title"), json_get_int (node, "id"), node_source_find_or_create_folder (source->root, category, NULL)); } iter = g_list_next (iter); } g_list_free (elements); /* Remove old nodes we cannot find anymore */ node_foreach_child_data (source->root, ttrss_source_check_node_for_removal, array); /* Save new subscription tree to OPML cache file */ opml_source_export (subscription->node); subscription->node->available = TRUE; } else { g_print ("Invalid JSON returned on TinyTinyRSSS request! >>>%s<<<", result->data); } g_object_unref (parser); } else { subscription->node->available = FALSE; debug0 (DEBUG_UPDATE, "ttrss_subscription_cb(): ERROR: failed to get TinyTinyRSS subscription list!"); } if (!(flags & NODE_SOURCE_UPDATE_ONLY_LIST)) node_foreach_child_data (subscription->node, node_update_subscription, GUINT_TO_POINTER (0)); } static void ttrss_source_update_subscription_list (ttrssSourcePtr source, subscriptionPtr subscription) { UpdateRequest *request; gchar *source_uri; source_uri = g_strdup_printf (TTRSS_URL, source->url); request = update_request_new ( source_uri, subscription->updateState, subscription->updateOptions ); g_free (source_uri); request->postdata = g_strdup_printf (TTRSS_JSON_SUBSCRIPTION_LIST, source->session_id); subscription->updateJob = update_execute_request (subscription, request, ttrss_source_subscription_list_cb, subscription, FEED_REQ_NO_FEED); } static void ttrss_source_merge_categories (ttrssSourcePtr source, nodePtr parent, gint parentId, JsonNode *items) { JsonArray *array = json_node_get_array (items); GList *iter, *elements; elements = iter = json_array_get_elements (array); while (iter) { JsonNode *node = (JsonNode *)iter->data; gint id = json_get_int (node, "bare_id"); if (id > 0) { const gchar *type = json_get_string (node, "type"); const gchar *name = json_get_string (node, "name"); /* ignore everything without a name or bare_id */ if (name) { /* Process child categories */ if (type && g_str_equal (type, "category")) { nodePtr folder; gchar *folderId = g_strdup_printf ("%d", id); debug2 (DEBUG_UPDATE, "TinyTinyRSS category id=%ld name=%s", id, name); folder = node_source_find_or_create_folder (parent, folderId, name); g_free (folderId); /* Process child categories ... */ if (json_get_node (node, "items")) { /* Store category id also for folder (needed when subscribing new feeds) */ g_hash_table_insert (source->folderToCategory, g_strdup (folder->id), GINT_TO_POINTER (id)); /* Recurse... */ ttrss_source_merge_categories (source, folder, id, json_get_node (node, "items")); } /* Process child feeds */ } else { debug3 (DEBUG_UPDATE, "TinyTinyRSS feed=%s folder=%d (%ld)", name, parentId, id); g_hash_table_insert (source->categories, GINT_TO_POINTER (id), GINT_TO_POINTER (parentId)); } } } iter = g_list_next (iter); } g_list_free (elements); } static void ttrss_subscription_process_update_result (subscriptionPtr subscription, const struct updateResult * const result, updateFlags flags) { ttrssSourcePtr source = (ttrssSourcePtr) subscription->node->data; debug1 (DEBUG_UPDATE, "ttrss_subscription_process_update_result: %s", result->data); if (result->data && result->httpstatus == 200) { JsonParser *parser = json_parser_new (); if (json_parser_load_from_data (parser, result->data, -1, NULL)) { JsonNode *content = json_get_node (json_parser_get_root (parser), "content"); JsonNode *categories, *items; /* We expect something like this: {"categories":{"identifier":"id","label":"name","items":[ {"id":"CAT:-1","items":[ {"id":"FEED:-4","name":"All articles","unread":1547,"type":"feed","error":"","updated":"","icon":"images\/tag.png","bare_id":-4,"auxcounter":0}, {"id":"FEED:-3","name":"Fresh articles","unread":0,"type":"feed","error":"","updated":"","icon":"images\/fresh.png","bare_id":-3,"auxcounter":0}, {"id":"FEED:-1","name":"Starred articles","unread":0,"type":"feed","error":"","updated":"","icon":"images\/mark_set.svg","bare_id":-1,"auxcounter":0}, {"id":"FEED:-2","name":"Published articles","unread":0,"type":"feed","error":"","updated":"","icon":"images\/pub_set.svg","bare_id":-2,"auxcounter":0}, {"id":"FEED:0","name":"Archived articles","unread":0,"type":"feed","error":"","updated":"","icon":"images\/archive.png","bare_id":0,"auxcounter":0}, {"id":"FEED:-6","name":"Recently read","unread":0,"type":"feed","error":"","updated":"","icon":"images\/recently_read.png","bare_id":-6,"auxcounter":0}], "name":"Special","type":"category","unread":0,"bare_id":-1}, {"id":"CAT:1","bare_id":1,"auxcounter":0,"name":"OSS","items":[ {"id":"CAT:2","bare_id":2,"name":"News","items":[ {"id":"FEED:4","bare_id":4,"auxcounter":0,"name":"Tiny Tiny RSS: Forum","checkbox":false,"unread":0,"error":"","icon":false,"param":"Jan 03, 13:15"}, {"id":"FEED:3","bare_id":3,"auxcounter":0,"name":"Tiny Tiny RSS: New Releases","checkbox":false,"unread":0,"error":"","icon":false,"param":"Jan 03, 13:15"}], "checkbox":false,"type":"category","unread":0,"child_unread":0,"auxcounter":0,"param":"(2 feeds)"}, {"id":"FEED:6","bare_id":6,"auxcounter":0,"name":"Ars Technica","checkbox":false,"unread":0,"error":"","icon":"feed-icons\/6.ico","param":"Sep 18, 20:43"}, {"id":"FEED:7","bare_id":7,"auxcounter":0,"name":"LZone","checkbox":false,"unread":0,"error":"","icon":"feed-icons\/7.ico","param":"Jul 06, 23:09"}], "checkbox":false,"type":"category","unread":0,"child_unread":0,"param":"(4 feeds)"}, {"id":"CAT:0","bare_id":0,"auxcounter":0,"name":"Uncategorized","items":[ {"id":"FEED:5","bare_id":5,"auxcounter":0,"name":"Slashdot","checkbox":false,"error":"","icon":false,"param":"Jan 03, 13:15","unread":0,"type":"feed"}], "type":"category","checkbox":false,"unread":0,"child_unread":0,"param":"(1 feed)"}]}}} So we need to: - ignore all negative categories - treat feeds in category #0 as root level feeds - traverse all categories > #1 - remember category ids in source->categories hash As we need to perform a subscription list update anyway we can ignore all feed infos */ if (!content) { debug0 (DEBUG_UPDATE, "ttrss_subscription_process_update_result(): Failed to get subscription list!"); subscription->node->available = FALSE; return; } categories = json_get_node (content, "categories"); if (!categories) { debug0 (DEBUG_UPDATE, "ttrss_subscription_process_update_result(): Failed to get categories list: no 'categories' element found!"); subscription->node->available = FALSE; return; } items = json_get_node (categories, "items"); if (!items || (JSON_NODE_TYPE (items) != JSON_NODE_ARRAY)) { debug0 (DEBUG_UPDATE, "ttrss_subscription_process_update_result(): Failed to get categories list: no 'categories' element found!"); subscription->node->available = FALSE; return; } /* Process categories tree recursively */ g_hash_table_remove_all (source->categories); ttrss_source_merge_categories (source, source->root, 0, items); /* And trigger the actual feed fetching */ ttrss_source_update_subscription_list (source, subscription); } else { g_print ("Invalid JSON returned on TinyTinyRSS request! >>>%s<<<", result->data); } g_object_unref (parser); } else { subscription->node->available = FALSE; debug0 (DEBUG_UPDATE, "ttrss_subscription_process_update_result(): Failed to get categories list!"); } } static gboolean ttrss_subscription_prepare_update_request (subscriptionPtr subscription, UpdateRequest *request) { nodePtr node = subscription->node; ttrssSourcePtr source = (ttrssSourcePtr) subscription->node->data; gchar *source_uri; debug0 (DEBUG_UPDATE, "ttrss_subscription_prepare_update_request"); g_assert (node->source); if (node->source->loginState == NODE_SOURCE_STATE_NONE) { debug0 (DEBUG_UPDATE, "TinyTinyRSS login"); ttrss_source_login (source, 0); return FALSE; } debug1 (DEBUG_UPDATE, "TinyTinyRSS updating subscription (node id %s)", node->id); /* Updating the TinyTinyRSS subscription means updating the list of categories and the list of feeds in 2 requests and if the installation is not self-updating to run a remote update for each feed before fetching it's items */ source_uri = g_strdup_printf (TTRSS_URL, source->url); update_request_set_source (request, source_uri); g_free (source_uri); request->postdata = g_strdup_printf (TTRSS_JSON_CATEGORIES_LIST, source->session_id); return TRUE; } /* OPML subscription type definition */ struct subscriptionType ttrssSourceSubscriptionType = { ttrss_subscription_prepare_update_request, ttrss_subscription_process_update_result }; liferea-1.13.7/src/fl_sources/ttrss_source_feed_list.h000066400000000000000000000017041415350204600230750ustar00rootroot00000000000000/** * @file ttrss_source_feed_list.h tt-rss source feed list handling * * Copyright (C) 2010 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _TTRSS_SOURCE_FEED_LIST #define _TTRSS_SOURCE_FEED_LIST #include "fl_sources/ttrss_source.h" #endif liferea-1.13.7/src/folder.c000066400000000000000000000102731415350204600154230ustar00rootroot00000000000000/** * @file folder.c sub folders for hierarchic node sources * * Copyright (C) 2006-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "folder.h" #include "common.h" #include "debug.h" #include "feedlist.h" #include "itemset.h" #include "node.h" #include "vfolder.h" #include "ui/feed_list_view.h" #include "ui/icons.h" #include "ui/ui_folder.h" /* Note: The folder node type implements the behaviour of a folder like node in the feed list. The two most important features are viewing the unread items of all child feeds and displaying the aggregated unread count of all child feeds. The folder node type does not implement the hierarchy of the feed list! */ static void folder_merge_child_items (nodePtr node, gpointer user_data) { itemSetPtr folderItemSet = (itemSetPtr)user_data; itemSetPtr nodeItemSet; if (IS_VFOLDER (node)) return; nodeItemSet = node_get_itemset (node); folderItemSet->ids = g_list_concat (folderItemSet->ids, nodeItemSet->ids); nodeItemSet->ids = NULL; itemset_free (nodeItemSet); } static itemSetPtr folder_load (nodePtr node) { itemSetPtr itemSet; itemSet = g_new0 (struct itemSet, 1); itemSet->nodeId = node->id; node_foreach_child_data (node, folder_merge_child_items, itemSet); return itemSet; } static void folder_import (nodePtr node, nodePtr parent, xmlNodePtr cur, gboolean trusted) { /* Folders have no special properties to be imported. */ } static void folder_export (nodePtr node, xmlNodePtr cur, gboolean trusted) { /* Folders have no special properties to be exported. */ } static void folder_save (nodePtr node) { /* A folder has no own state but must give all childs the chance to save theirs */ node_foreach_child (node, node_save); } static void folder_add_child_update_counters (nodePtr node, gpointer user_data) { guint *unreadCount = (guint *)user_data; if (!IS_VFOLDER (node)) *unreadCount += node->unreadCount; } static void folder_update_counters (nodePtr node) { /* We never need a total item count for folders. Only the total unread count is interesting */ node->itemCount = 0; node->unreadCount = 0; node_foreach_child_data (node, folder_add_child_update_counters, &node->unreadCount); } static void folder_remove (nodePtr node) { /* Ensure that there are no children anymore */ g_assert (!node->children); } nodeTypePtr folder_get_node_type (void) { static struct nodeType fnti = { NODE_CAPABILITY_SHOW_ITEM_FAVICONS | NODE_CAPABILITY_ADD_CHILDS | NODE_CAPABILITY_REMOVE_CHILDS | NODE_CAPABILITY_SUBFOLDERS | NODE_CAPABILITY_REORDER | NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_UPDATE_CHILDS | NODE_CAPABILITY_EXPORT, "folder", NULL, folder_import, folder_export, folder_load, folder_save, folder_update_counters, folder_remove, node_default_render, ui_folder_add, feed_list_view_rename_node, NULL }; fnti.icon = icon_get (ICON_FOLDER); return &fnti; } nodeTypePtr root_get_node_type (void) { /* the root node is identical to the folder type, just a different node type... */ static struct nodeType rnti = { NODE_CAPABILITY_ADD_CHILDS | NODE_CAPABILITY_REMOVE_CHILDS | NODE_CAPABILITY_SUBFOLDERS | NODE_CAPABILITY_REORDER | NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_UPDATE_CHILDS | NODE_CAPABILITY_EXPORT, "root", NULL, /* and no need for an icon */ folder_import, folder_export, folder_load, folder_save, folder_update_counters, folder_remove, node_default_render, ui_folder_add, feed_list_view_rename_node, NULL }; return &rnti; } liferea-1.13.7/src/folder.h000066400000000000000000000030651415350204600154310ustar00rootroot00000000000000/** * @file folder.h sub folders for hierarchic node soures * * Copyright (C) 2006-2008 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FOLDER_H #define _FOLDER_H #include "node_type.h" #define IS_FOLDER(node) (node->type == folder_get_node_type ()) /* Liferea supports different node sources in the feed list tree. Some of those can be organized by sub folders implemented by this node type. The root node of the Liferea feed list tree is the implemented as a special folder and therefore shares almost all of the folder functionality. Being a read/write enabled folder it allows to add other node sources as childs. */ /** * Returns the implementation of the folder node type. */ nodeTypePtr folder_get_node_type(void); /** * Returns the implementation of the root node type. */ nodeTypePtr root_get_node_type(void); #endif liferea-1.13.7/src/html.c000066400000000000000000000241121415350204600151110ustar00rootroot00000000000000/** * @file html.c HTML parsing * * Copyright (C) 2004 ahmed el-helw * Copyright (C) 2004-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include "common.h" #include "debug.h" #include "html.h" #include "render.h" #include "xml.h" enum { LINK_RSS_ALTERNATE, LINK_AMPHTML }; #define XPATH_LINK_RSS_ALTERNATE "/html/head/link[@rel='alternate'][@type='application/atom+xml' or @type='application/rss+xml' or @type='application/rdf+xml' or @type='text/xml']" #define XPATH_LINK_MS_TILE_IMAGE "/html/head/meta[@name='msapplication-TileImage']" #define XPATH_LINK_SAFARI_MASK_ICON "/html/head/link[@rel='mask-icon']" #define XPATH_LINK_LARGE_ICON "/html/head/link[@rel='icon' or @rel='shortcut icon'][@sizes='192x192' or @sizes='144x144' or @sizes='128x128']" #define XPATH_LINK_LARGE_ICON_OTHER_SIZES "/html/head/link[@rel='icon' or @rel='shortcut icon'][@sizes]" #define XPATH_LINK_FAVICON "/html/head/link[@rel='icon' or @rel='shortcut icon' or @rel='SHORTCUT ICON'][not(@sizes)]" #define XPATH_LINK_APPLE_TOUCH_ICON "/html/head/link[@rel='apple-touch-icon' or @rel='apple-touch-icon-precomposed'][@sizes='180x180' or @sizes='152x152' or @sizes='144x144' or @sizes='120x120']" #define XPATH_LINK_APPLE_TOUCH_ICON_NO_SIZE "/html/head/link[@rel='apple-touch-icon' or @rel='apple-touch-icon-precomposed'][not(@sizes)]" #define XPATH_LINK_APPLE_TOUCH_ICON_OTHER_SIZES "/html/head/link[@rel='apple-touch-icon' or @rel='apple-touch-icon-precomposed'][@sizes]" /** * Fetch attribute of a html tag string */ static gchar * html_get_attrib (const gchar* str, gchar *attrib_name) { gchar *res; const gchar *tmp, *tmp2; size_t len = 0; gchar quote; /*debug1(DEBUG_PARSING, "fetching href %s", str); */ tmp = common_strcasestr (str, attrib_name); if (!tmp) return NULL; tmp += 5; /* skip spaces up to the first quote. This is really slightly wrong. SGML allows unquoted atributes, but not if they contain slashes, so 99% of all URIs will require quotes. */ while (*tmp != '\"' && *tmp != '\'') { if (*tmp == '>' || *tmp == '\0' || !isspace(*tmp)) return NULL; tmp++; } quote = *tmp; /* The type of quote mark used to delimit the arg */ tmp++; tmp2 = tmp; while ((*tmp2 != quote && *(tmp2-1) != '\\') && /* Escaped quote*/ *tmp2 != '\0') tmp2++, len++; res = g_strndup (tmp, len); return res; } static gchar * html_check_link_ref (const gchar* str, gint linkType) { gchar *res; res = html_get_attrib (str, "href"); if (linkType == LINK_RSS_ALTERNATE) { if ((common_strcasestr (str, "alternate") != NULL) && ((common_strcasestr (str, "text/xml") != NULL) || (common_strcasestr (str, "rss+xml") != NULL) || (common_strcasestr (str, "rdf+xml") != NULL) || (common_strcasestr (str, "atom+xml") != NULL))) return res; } else if (linkType == LINK_AMPHTML) { if (common_strcasestr (str, "amphtml") != NULL) return res; } g_free (res); return NULL; } /** * Search tag in a html content, return link of the tag pointed by href */ static gchar * search_tag_link_dirty (const gchar* data, const gchar *tagName, gchar** tagEnd) { gchar *ptr; const gchar *tmp = data; gchar *result = NULL; gchar *tstr; gchar *endptr; gchar *tagname_start; if (tagEnd) *tagEnd = NULL; tagname_start = g_strconcat("<", tagName, NULL); ptr = common_strcasestr (tmp, tagname_start); g_free(tagname_start); if (!ptr) return NULL; endptr = strchr (ptr, '>'); if (!endptr) return NULL; *endptr = '\0'; tstr = g_strdup (ptr); *endptr = '>'; result = html_get_attrib (tstr, "href"); g_free (tstr); if (tagEnd) { endptr++; *tagEnd = endptr; } if (result) { /* URIs can contain escaped things.... * All ampersands must be escaped, for example */ result = unhtmlize (result); } return result; } // FIXME: implement multiple links static gchar * search_links_dirty (const gchar* data, gint linkType) { gchar *ptr; const gchar *tmp = data; gchar *result = NULL; gchar *res; gchar *tstr; gchar *endptr; while (1) { ptr = common_strcasestr (tmp, "'); if (!endptr) return NULL; *endptr = '\0'; tstr = g_strdup (ptr); *endptr = '>'; res = html_check_link_ref (tstr, linkType); g_free (tstr); if (res) { result = res; break; /* deactivated as long as we support only subscribing to the first found link (BTW this code crashes on sites like Groklaw!) gchar* t; if(result == NULL) result = res; else { t = g_strdup_printf("%s\n%s", result, res); g_free(res); g_free(result); result = t; }*/ } tmp = endptr; } result = unhtmlize (result); /* URIs can contain escaped things.... All ampersands must be escaped, for example */ return result; } static void html_auto_discover_collect_links (xmlNodePtr match, gpointer user_data) { GSList **links = (GSList **)user_data; gchar *link = xml_get_attribute (match, "href"); if (link) *links = g_slist_append (*links, link); } static void html_auto_discover_collect_meta (xmlNodePtr match, gpointer user_data) { GSList **values = (GSList **)user_data; gchar *value = xml_get_attribute (match, "content"); if (value) *values = g_slist_append (*values, value); } GSList * html_auto_discover_feed (const gchar* data, const gchar *defaultBaseUri) { GSList *iter, *links = NULL; gchar *baseUri = NULL; xmlDocPtr doc; xmlNodePtr node, root; // If possible we want to use XML instead of tag soup doc = xhtml_parse ((gchar *)data, (size_t)strlen(data)); if (!doc) return NULL; root = xmlDocGetRootElement (doc); // Base URL resolving node = xpath_find (root, "/html/head/base"); if (node) baseUri = xml_get_attribute (node, "href"); if (!baseUri) baseUri = g_strdup (search_tag_link_dirty (data, "base", NULL)); if (!baseUri) baseUri = g_strdup (defaultBaseUri); debug0 (DEBUG_UPDATE, "searching through link tags"); xpath_foreach_match (root, XPATH_LINK_RSS_ALTERNATE, html_auto_discover_collect_links, (gpointer)&links); if (!links) { gchar *tmp = search_links_dirty (data, LINK_RSS_ALTERNATE); if (tmp) links = g_slist_append (links, tmp); } /* Turn relative URIs into absolute URIs */ iter = links; while (iter) { gchar *tmp = iter->data; iter->data = common_build_url (tmp, baseUri); g_free (tmp); debug1 (DEBUG_UPDATE, "search result: %s", (gchar *)iter->data); iter = g_slist_next (iter); } g_free (baseUri); xmlFreeDoc (doc); return links; } GSList * html_discover_favicon (const gchar * data, const gchar * defaultBaseUri) { xmlDocPtr doc; xmlNodePtr node, root; GSList *results = NULL, *iter; gchar *baseUri = NULL; doc = xhtml_parse ((gchar *)data, (size_t)strlen(data)); if (!doc) return NULL; root = xmlDocGetRootElement (doc); // Base URL resolving node = xpath_find (root, "/html/head/base"); if (node) baseUri = xml_get_attribute (node, "href"); if (!baseUri) baseUri = g_strdup (search_tag_link_dirty (data, "base", NULL)); if (!baseUri) baseUri = g_strdup (defaultBaseUri); debug0 (DEBUG_UPDATE, "searching through link tags"); /* First try icons with guaranteed sizes */ xpath_foreach_match (root, XPATH_LINK_LARGE_ICON, html_auto_discover_collect_links, (gpointer)&results); xpath_foreach_match (root, XPATH_LINK_APPLE_TOUCH_ICON, html_auto_discover_collect_links, (gpointer)&results); xpath_foreach_match (root, XPATH_LINK_MS_TILE_IMAGE, html_auto_discover_collect_meta, (gpointer)&results); xpath_foreach_match (root, XPATH_LINK_SAFARI_MASK_ICON, html_auto_discover_collect_links, (gpointer)&results); /* Next try probably larger ones */ xpath_foreach_match (root, XPATH_LINK_APPLE_TOUCH_ICON_NO_SIZE, html_auto_discover_collect_links, (gpointer)&results); /* no size with Apple touch usually means 180x180px */ xpath_foreach_match (root, XPATH_LINK_LARGE_ICON_OTHER_SIZES, html_auto_discover_collect_links, (gpointer)&results); /* usually 96x96px and below */ xpath_foreach_match (root, XPATH_LINK_APPLE_TOUCH_ICON_OTHER_SIZES, html_auto_discover_collect_links, (gpointer)&results); /* usually 96x96px and below */ /* Finally try to small favicon */ xpath_foreach_match (root, XPATH_LINK_FAVICON, html_auto_discover_collect_links, (gpointer)&results); /* has to be last! */ /* Turn relative URIs into absolute URIs */ iter = results; while (iter) { gchar *tmp = iter->data; iter->data = common_build_url (tmp, baseUri); g_free (tmp); debug1 (DEBUG_UPDATE, "search result: %s", (gchar *)iter->data); iter = g_slist_next (iter); } g_free (baseUri); xmlFreeDoc (doc); return results; } gchar * html_get_article (const gchar *data, const gchar *baseUri) { xmlDocPtr doc; xmlNodePtr root; gchar *result = NULL; doc = xhtml_parse ((gchar *)data, (size_t)strlen (data)); if (!doc) { debug1 (DEBUG_PARSING, "XHTML parsing error during HTML5 fetch of '%s'\n", baseUri); return NULL; } root = xmlDocGetRootElement (doc); if (root) { xmlDocPtr article = xhtml_extract_doc (root, 1, baseUri); if (article) { /* For debug output xmlSaveCtxt *s; s = xmlSaveToFd(0, NULL, 0); xmlSaveDoc(s, article); xmlSaveClose(s); */ result = render_xml (article, "html5-extract", NULL); xmlFreeDoc (article); } xmlFreeDoc (doc); } return result; } gchar * html_get_amp_url (const gchar *data) { return search_links_dirty (data, LINK_AMPHTML); } liferea-1.13.7/src/html.h000066400000000000000000000043271415350204600151240ustar00rootroot00000000000000/** * @file html.h HTML parsing * * Copyright (C) 2004 ahmed el-helw * Copyright (C) 2017-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _HTML_H #define _HTML_H #include /** * html_auto_discover_feed: * * HTML feed auto discovery function. Searches the * passed HTML document for feed links and returns * one if at least one link could be found. * * @data: HTML source * @baseUri: URI that relative links will be based off of * Returns: a list of feed URLs or NULL. Must be freed by caller. */ GSList * html_auto_discover_feed(const gchar* data, const gchar *baseUri); /** * html_discover_favicon: * * Search for favicon links in a HTML file's head section * * @data: HTML source * @baseUri: URI of the downloaded HTML used to resolve relative URIs * * Returns: List of absolute URL for different favicon resolutions, or NULL. Must be fully freed by caller. */ GSList * html_discover_favicon(const gchar* data, const gchar *baseUri); /** * html_get_article: * * Parse HTML as XML to check wether it contains an HTML5 article. * * @data: the HTML to check * @baseUri: URI of the downloaded HTML used to resolve relative URIs * * Returns: XHTML fragment representing the article or NULL */ gchar * html_get_article(const gchar *data, const gchar *baseUri); /** * html_get_amp_url: * * Parse HTML and returns AMP URL if found * * @data: the HTML to check * * Returns: AMP URL or NULL. Must be free'd by caller */ gchar * html_get_amp_url(const gchar *data); #endif liferea-1.13.7/src/item.c000066400000000000000000000230511415350204600151040ustar00rootroot00000000000000/** * @file item.c item handling * * Copyright (C) 2003-2021 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "item.h" #include #include #include "comments.h" #include "common.h" #include "date.h" #include "db.h" #include "debug.h" #include "feedlist.h" #include "metadata.h" #include "render.h" #include "xml.h" itemPtr item_new (void) { itemPtr item; item = g_new0 (struct item, 1); item->popupStatus = TRUE; return item; } itemPtr item_load (gulong id) { return db_item_load (id); } itemPtr item_copy (itemPtr item) { itemPtr copy = item_new (); item_set_title (copy, item->title); item_set_source (copy, item->source); item_set_description (copy, item->description); item_set_id (copy, item->sourceId); copy->updateStatus = item->updateStatus; copy->readStatus = item->readStatus; copy->popupStatus = FALSE; copy->flagStatus = item->flagStatus; copy->time = item->time; copy->validGuid = item->validGuid; copy->hasEnclosure = item->hasEnclosure; /* the following line allows state propagation in item.c */ copy->nodeId = NULL; copy->sourceNr = item->id; /* this copies metadata */ copy->metadata = metadata_list_copy (item->metadata); /* no deep copy of comments necessary as they are automatically retrieved when reading the article */ return copy; } void item_set_title (itemPtr item, const gchar * title) { g_free (item->title); if (title) item->title = g_strstrip (g_strdelimit (g_strdup (title), "\r\n", ' ')); else item->title = g_strdup (""); } void item_set_description (itemPtr item, const gchar *description) { if (!description) return; if (item->description) if (!(strlen (description) > strlen (item->description))) return; g_free (item->description); item->description = g_strdup (description); } void item_set_source (itemPtr item, const gchar * source) { g_free (item->source); if (source) item->source = g_strstrip (g_strdup (source)); else item->source = NULL; } void item_set_id (itemPtr item, const gchar * id) { g_free (item->sourceId); item->sourceId = g_strdup (id); } const gchar * item_get_id(itemPtr item) { return item->sourceId; } const gchar * item_get_title(itemPtr item) {return item->title; } const gchar * item_get_description(itemPtr item) { return item->description; } const gchar * item_get_source(itemPtr item) { return item->source; } static GRegex *whitespace_strip_re = NULL; gchar * item_get_teaser (itemPtr item) { gchar *input, *tmpDesc; gchar *teaser = NULL; if (!whitespace_strip_re) { whitespace_strip_re = g_regex_new ("(\n+|\\s\\s+)", G_REGEX_MULTILINE, 0, NULL); g_assert (NULL != whitespace_strip_re); } input = unxmlize (g_strdup (item->description)); tmpDesc = g_regex_replace_literal (whitespace_strip_re, input, -1, 0, " ", 0, NULL); if (strlen (tmpDesc) > 200) { // Truncate hard at pos 200 and search backward for a space tmpDesc[200] = 0; gchar *last_space = g_strrstr (tmpDesc, " "); if (last_space) { *last_space = 0; teaser = tmpDesc; } } if (!teaser) return NULL; teaser = g_strstrip (g_markup_escape_text (teaser, -1)); g_free (input); g_free (tmpDesc); return teaser; } gchar * item_make_link (itemPtr item) { const gchar *src; gchar *link; src = item_get_source (item); if (!src) return NULL; /* check for absolute URL */ if (strstr (src, "://")) { link = g_strdup (src); } else { const gchar * base = item_get_base_url (item); link = (gchar *) common_build_url (src, base); if (!link) { debug0 (DEBUG_PARSING, "Feed contains relative link and invalid base URL"); return NULL; } } return link; } const gchar * item_get_author(itemPtr item) { gchar *author; author = (gchar *)metadata_list_get (item->metadata, "author"); return author; } void item_unload (itemPtr item) { g_free (item->title); g_free (item->source); g_free (item->sourceId); g_free (item->description); g_free (item->commentFeedId); g_free (item->nodeId); g_free (item->parentNodeId); g_assert (NULL == item->tmpdata); /* should be free after rendering */ metadata_list_free (item->metadata); g_free (item); } const gchar * item_get_base_url (itemPtr item) { /* item->node is always the source node for the item never a search folder or folder */ return node_get_base_url (node_from_id (item->nodeId)); } void item_to_xml (itemPtr item, gpointer xmlNode) { xmlNodePtr parentNode = (xmlNodePtr)xmlNode; xmlNodePtr duplicatesNode; xmlNodePtr itemNode; gchar *tmp; itemNode = xmlNewChild (parentNode, NULL, BAD_CAST "item", NULL); g_return_if_fail (itemNode); xmlNewTextChild (itemNode, NULL, BAD_CAST "title", BAD_CAST (item_get_title (item)?item_get_title (item):"")); if (item_get_description (item)) { /* Prefer full article over feed inline content */ const gchar *content = metadata_list_get (item->metadata, "richContent"); if (NULL == content) content = item_get_description (item); tmp = xhtml_strip_dhtml (content); xmlNewTextChild (itemNode, NULL, BAD_CAST "description", BAD_CAST tmp); g_free (tmp); } if (item_get_source (item)) xmlNewTextChild (itemNode, NULL, BAD_CAST "source", BAD_CAST item_get_source (item)); tmp = g_strdup_printf ("%ld", item->id); xmlNewTextChild (itemNode, NULL, BAD_CAST "nr", BAD_CAST tmp); g_free (tmp); tmp = g_strdup_printf ("%d", item->readStatus?1:0); xmlNewTextChild (itemNode, NULL, BAD_CAST "readStatus", BAD_CAST tmp); g_free (tmp); tmp = g_strdup_printf ("%d", item->updateStatus?1:0); xmlNewTextChild (itemNode, NULL, BAD_CAST "updateStatus", BAD_CAST tmp); g_free (tmp); tmp = g_strdup_printf ("%d", item->flagStatus?1:0); xmlNewTextChild (itemNode, NULL, BAD_CAST "mark", BAD_CAST tmp); g_free (tmp); tmp = g_strdup_printf ("%ld", item->time); xmlNewTextChild (itemNode, NULL, BAD_CAST "time", BAD_CAST tmp); g_free (tmp); tmp = date_format (item->time, NULL); xmlNewTextChild (itemNode, NULL, BAD_CAST "timestr", BAD_CAST tmp); g_free (tmp); if (item->validGuid) { GSList *iter, *duplicates; duplicatesNode = xmlNewChild(itemNode, NULL, BAD_CAST "duplicates", NULL); duplicates = iter = db_item_get_duplicates(item->sourceId); while (iter) { gulong id = GPOINTER_TO_UINT (iter->data); itemPtr duplicate = item_load (id); if (duplicate) { nodePtr duplicateNode = node_from_id (duplicate->nodeId); if (duplicateNode && (item->id != duplicate->id)) xmlNewTextChild (duplicatesNode, NULL, BAD_CAST "duplicateNode", BAD_CAST node_get_title (duplicateNode)); item_unload (duplicate); } iter = g_slist_next (iter); } g_slist_free (duplicates); } xmlNewTextChild (itemNode, NULL, BAD_CAST "sourceId", BAD_CAST item->nodeId); tmp = g_strdup_printf ("%ld", item->id); xmlNewTextChild (itemNode, NULL, BAD_CAST "sourceNr", BAD_CAST tmp); g_free (tmp); metadata_add_xml_nodes (item->metadata, itemNode); nodePtr feedNode = node_from_id (item->parentNodeId); if (feedNode) { feedPtr feed = (feedPtr)feedNode->data; if (feed) { if (!feed->ignoreComments) { if (item->commentFeedId) comments_to_xml (itemNode, item->commentFeedId); } else { xmlNewTextChild (itemNode, NULL, BAD_CAST "commentsSuppressed", BAD_CAST "true"); } } } } static const gchar * item_get_text_direction (itemPtr item) { if (item_get_title (item)) return (common_get_text_direction (item_get_title (item))); if (item_get_description (item)) return (common_get_text_direction (item_get_description (item))); /* what can we do? */ return ("ltr"); } gchar * item_render (itemPtr item, guint viewMode) { renderParamPtr params; gchar *output = NULL, *baseUrl = NULL; nodePtr node; xmlDocPtr doc; xmlNodePtr xmlNode; debug_enter ("item_render"); /* don't use node from htmlView_priv as this would be wrong for folders and other merged item sets */ node = node_from_id (item->nodeId); /* do the XML serialization */ doc = xmlNewDoc (BAD_CAST "1.0"); xmlNode = xmlNewDocNode (doc, NULL, BAD_CAST "itemset", NULL); xmlDocSetRootElement (doc, xmlNode); item_to_xml(item, xmlDocGetRootElement (doc)); if (IS_FEED (node)) { xmlNodePtr feed; feed = xmlNewChild (xmlDocGetRootElement (doc), NULL, BAD_CAST "feed", NULL); feed_to_xml (node, feed); } /* do the XSLT rendering */ params = render_parameter_new (); if (NULL != node_get_base_url (node)) { baseUrl = (gchar *) common_uri_escape ( BAD_CAST node_get_base_url (node)); render_parameter_add (params, "baseUrl='%s'", baseUrl); } render_parameter_add (params, "showFeedName='%d'", (node != feedlist_get_selected ())?1:0); render_parameter_add (params, "txtDirection='%s'", item_get_text_direction (item)); render_parameter_add (params, "appDirection='%s'", common_get_app_direction ()); output = render_xml (doc, "item", params); /* For debugging use: xmlSaveFormatFile("/tmp/test.xml", doc, 1); */ xmlFreeDoc (doc); g_free (baseUrl); debug_exit ("item_render"); return output; } liferea-1.13.7/src/item.h000066400000000000000000000152451415350204600151170ustar00rootroot00000000000000/* * @file item.h item handling * * Copyright (C) 2003-2017 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEM_H #define _ITEM_H #include /* Currently Liferea knows only a single type of items used for the itemset types feed, folder and search folder. So each feed list type provider must provide it's data using the item interface. */ /* ------------------------------------------------------------ */ /* item interface */ /* ------------------------------------------------------------ */ /* * An item stores a particular entry in a feed or a search. * Each item belongs to an item set. An itemset is a collection * of items. There are different item set types (e.g. feed, * folder,vfolder or plugin). Each item has a source node. * The item set node and the item source node is different * for folders and vfolders. */ typedef struct item { gulong id; /*<< internally unique item id */ /* those fields should not be accessed directly. Accessors are provided. */ gboolean readStatus; /*<< TRUE if the item has been read */ gboolean popupStatus; /*<< TRUE if the item was downloaded and is yet to be displayed by the popup notification feature */ gboolean updateStatus; /*<< TRUE if the item content was updated */ gboolean flagStatus; /*<< TRUE if the item has been flagged */ gboolean hasEnclosure; /*<< TRUE if this item has at least one enclosure */ gchar *title; /*<< Title */ gchar *source; /*<< URL to the post online */ gchar *sourceId; /*<< "Unique" syndication item identifier, for example in RSS */ gboolean validGuid; /*<< TRUE if id of this item is a GUID and can be used for duplicate detection */ gchar *description; /*<< XHTML string containing the item's description */ GSList *metadata; /*<< Metadata of this item */ GHashTable *tmpdata; /*<< Temporary data hash used during stateful parsing */ gint64 time; /*<< Last modified date of the headline */ gchar *commentFeedId; /*<< Id of the comment feed of this item (or NULL if there is no comment feed) */ /* comment item properties */ gulong parentItemId; /*<< Id of the parent item the item belongs to(or 0 if no comment item) */ gboolean isComment; /*<< TRUE if item is from a comment feed */ /* item source properties */ gchar *nodeId; /*<< Node id the containing node. Might be a comment feed id. */ gchar *parentNodeId; /*<< Real parent node id. Always a feed list node id. */ gulong sourceNr; /*<< Either equal to nr or the number of the item this one is a copy of */ /* remote states used during sync of remote accounts */ gboolean remoteReadStatus; /*<< TRUE if the remote copy of the item has been read */ gboolean remoteFlagStatus; /*<< TRUE if the remote copy of the item has been flagged */ } *itemPtr; /** * item_new: (skip) * Allocates a new item structure. * * Returns: (transfer full): the new structure */ itemPtr item_new(void); /** * item_load: (skip) * @id: item id to load * * Returns the item structure for the given item id or * NULL if no such item does exist. The caller has to free * the item with item_unload() once it is not used anymore. * * Returns: (transfer full) (nullable): item structure */ itemPtr item_load(gulong id); /** * item_copy: (skip) * @item: the item to copy * * Method to create a copy of an item. The copy will be * linked to the original item to allow state update * propagation (to be used with vfolders). * * Returns: (transfer full): copy of the item. */ itemPtr item_copy(itemPtr item); /** * item_get_base_url: (skip) * @item: the item * * Returns the base URL for the given item. * * Returns: base URL */ const gchar * item_get_base_url(itemPtr item); /** * item_unload: (skip) * @item: the item to unload * * Free the memory used by an itempointer. The item needs to be * removed from the itemlist before calling this function. * */ void item_unload(itemPtr item); /* methods to access properties */ /* Returns the id of item. */ const gchar * item_get_id(itemPtr item); /* Returns the title of item. */ const gchar * item_get_title(itemPtr item); /* Returns the description of item. */ const gchar * item_get_description(itemPtr item); /* Returns the source of item. */ const gchar * item_get_source(itemPtr item); /** * item_get_teaser: (skip) * @item: the item * * Create a plain text teaser from the item description * * Returns: (transfer full): newly allocated string to be free'd using g_free() (or NULL) */ gchar * item_get_teaser(itemPtr item); /** * item_make_link: (skip) * @item: the item * * Returns the resolved link for the item. * * Returns: (transfer full): newly allocated URI to be free'd using g_free() */ gchar * item_make_link(itemPtr item); /** * item_get_author: (skip) * @item: the item * * Returns the resolved author for the item * * Returns: pointer to string in GSList meta data */ const gchar * item_get_author (itemPtr item); /* Sets the item title */ void item_set_title(itemPtr item, const gchar * title); /** * item_set_description: (skip) * @item: the item * @description: the content * * Sets the item description. If called more than once it * will merge the new description against the old one deciding * on the best to keep. * */ void item_set_description (itemPtr item, const gchar *description); /* Sets the item source */ void item_set_source(itemPtr item, const gchar * source); /* Sets the item id */ void item_set_id(itemPtr item, const gchar * id); /** * item_to_xml: (skip) * @item: the item to serialize * @parentNode: the xmlNodePtr to add to * * Adds an XML node to the given item. * */ void item_to_xml (itemPtr item, gpointer parentNode); /** * item_render: (skip) * @item: the item to serialize * @viewMode: the item view mode * * Uses item_xml() and adds additional feed info to the item info for rendering * * Returns XML string (to be free'd using g_free()) */ gchar * item_render (itemPtr item, guint viewMode); #endif liferea-1.13.7/src/item_history.c000066400000000000000000000056361415350204600166760ustar00rootroot00000000000000/** * @file item_history.c tracking recently viewed items * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "item_history.h" #include #include "ui/liferea_shell.h" #define MAX_HISTORY_SIZE 250 static struct itemHistory { GList *items; /**< FIFO list of all items viewed */ GList *current; /**< the currently list element */ guint lastId; /**< Avoid duplicate add */ } *itemHistory = NULL; void item_history_add (guint id) { if (!itemHistory) itemHistory = g_new0 (struct itemHistory, 1); /* Duplicate add by some selection effect */ if (itemHistory->lastId == id) return; /* Duplicate add by history navigation */ if (itemHistory->current && GPOINTER_TO_UINT (itemHistory->current->data) == id) return; itemHistory->items = g_list_append (itemHistory->items, GUINT_TO_POINTER (id)); itemHistory->current = g_list_last (itemHistory->items); itemHistory->lastId = id; /* if list has reached max size remove first element */ if (g_list_length (itemHistory->items) > MAX_HISTORY_SIZE) itemHistory->items = g_list_remove (itemHistory->items, itemHistory->items); liferea_shell_update_history_actions (); } itemPtr item_history_get_next (void) { itemPtr item = NULL; if (!itemHistory->current) return NULL; while (!item && item_history_has_next ()) { itemHistory->current = g_list_next (itemHistory->current); item = item_load (GPOINTER_TO_UINT (itemHistory->current->data)); } liferea_shell_update_history_actions (); return item; } itemPtr item_history_get_previous (void) { itemPtr item = NULL; if (!itemHistory->current) return NULL; while (!item && item_history_has_previous ()) { itemHistory->current = g_list_previous (itemHistory->current); item = item_load (GPOINTER_TO_UINT (itemHistory->current->data)); } liferea_shell_update_history_actions (); return item; } gboolean item_history_has_next (void) { if (!itemHistory || !itemHistory->items) return FALSE; if (g_list_last (itemHistory->items) == itemHistory->current) return FALSE; return TRUE; } gboolean item_history_has_previous (void) { if (!itemHistory || !itemHistory->items) return FALSE; if (itemHistory->items == itemHistory->current) return FALSE; return TRUE; } liferea-1.13.7/src/item_history.h000066400000000000000000000032021415350204600166660ustar00rootroot00000000000000/** * @file item_history.h tracking recently viewed items * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEM_HISTORY #define _ITEM_HISTORY #include "item.h" /** * Add a new item to the item history stack. * * @param id the id of the item to add */ void item_history_add (guint id); /** * Returns the previous item in the item history. * * @returns an item (or NULL) to be free'd with item_unload() */ itemPtr item_history_get_previous (void); /** * Returns the next item in history. * * @returns an item (or NULL) to be free'd with item_unload() */ itemPtr item_history_get_next (void); /** * Check whether a previous item exists in the item history. * * @returns TRUE if there is an item */ gboolean item_history_has_previous (void); /** * Check whether a following item exists in the item history. * * @returns TRUE if there is an item */ gboolean item_history_has_next (void); #endif liferea-1.13.7/src/item_loader.c000066400000000000000000000065511415350204600164400ustar00rootroot00000000000000/** * @file item_loader.c Asynchronously loading items * * Copyright (C) 2011-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "item_loader.h" #define ITEM_LOADER_GET_PRIVATE item_loader_get_instance_private struct ItemLoaderPrivate { fetchCallbackPtr fetchCallback; /**< the function to call after each item fetch */ gpointer fetchCallbackData; /**< user data for the fetch callback */ nodePtr node; /**< the node we are loading items for */ guint idleId; /**< fetch callback source id */ }; enum { ITEM_BATCH_FETCHED, FINISHED, LAST_SIGNAL }; static guint item_loader_signals[LAST_SIGNAL] = { 0 }; static GObjectClass *parent_class = NULL; G_DEFINE_TYPE_WITH_CODE (ItemLoader, item_loader, G_TYPE_OBJECT, G_ADD_PRIVATE (ItemLoader)); static void item_loader_finalize (GObject *object) { ItemLoader *il = ITEM_LOADER (object); if (il->priv->idleId) { g_source_remove (il->priv->idleId); il->priv->idleId = 0; } G_OBJECT_CLASS (parent_class)->finalize (object); } static void item_loader_class_init (ItemLoaderClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); parent_class = g_type_class_peek_parent (klass); object_class->finalize = item_loader_finalize; item_loader_signals[ITEM_BATCH_FETCHED] = g_signal_new ("item-batch-fetched", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); item_loader_signals[FINISHED] = g_signal_new ("finished", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void item_loader_init (ItemLoader *il) { il->priv = ITEM_LOADER_GET_PRIVATE (il); } nodePtr item_loader_get_node (ItemLoader *il) { return il->priv->node; } static gboolean item_loader_fetch (gpointer user_data) { ItemLoader *il = ITEM_LOADER (user_data); GSList *resultItems = NULL; gboolean result; result = (*il->priv->fetchCallback)(il->priv->fetchCallbackData, &resultItems); if (result) g_signal_emit_by_name (il, "item-batch-fetched", resultItems); else { il->priv->idleId = 0; g_signal_emit_by_name (il, "finished"); } return result; } void item_loader_start (ItemLoader *il) { il->priv->idleId = g_idle_add (item_loader_fetch, il); } ItemLoader * item_loader_new (fetchCallbackPtr fetchCallback, nodePtr node, gpointer fetchCallbackData) { ItemLoader *il; il = ITEM_LOADER (g_object_new (ITEM_LOADER_TYPE, NULL)); il->priv->node = node; il->priv->fetchCallback = fetchCallback; il->priv->fetchCallbackData = fetchCallbackData; return il; } liferea-1.13.7/src/item_loader.h000066400000000000000000000061711415350204600164430ustar00rootroot00000000000000/** * @file item_loader.h Interface for asynchronous item loading * * Copyright (C) 2011 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEM_LOADER_H #define _ITEM_LOADER_H #include #include "node.h" /* ItemLoader concept: an ItemLoader instance runs a fetch callback repeatedly collecting the items the fetch callback provides. One each fetch when there were items the loader emits a callback with the itemset as parameter for an item view to present. */ typedef struct ItemLoaderPrivate ItemLoaderPrivate; typedef struct ItemLoader { GObject parent; /*< private >*/ ItemLoaderPrivate *priv; } ItemLoader; typedef struct ItemLoaderClass { GObjectClass parent; } ItemLoaderClass; GType item_loader_get_type (void); #define ITEM_LOADER_TYPE (item_loader_get_type ()) #define ITEM_LOADER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ITEM_LOADER_TYPE, ItemLoader)) #define ITEM_LOADER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ITEM_LOADER_TYPE, ItemLoaderClass)) #define IS_ITEM_LOADER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ITEM_LOADER_TYPE)) #define IS_ITEM_LOADER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ITEM_LOADER_TYPE)) #define ITEM_LOADER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ITEM_LOADER_TYPE, ItemLoaderClass)) /** * Definition of item loader fetch callback to be * implemented by specific item loaders. This callback * is called multiple times to fetch item batches. The * batch size is determined by the specific implementation. * * @param user_data ItemLoader type specific data * @param items Result items (to be free'd by caller) * * @returns FALSE if loading has finished */ typedef gboolean (*fetchCallbackPtr)(gpointer user_data, GSList **items); /** * Set up a new item loader with a specific fetch function. * * @param fetchCallback the item fetching function * @param node the node we are loading items for * @param user_data ItemLoader type specific data * * @returns the new ItemLoader instance */ ItemLoader * item_loader_new (fetchCallbackPtr fetchCallback, nodePtr node, gpointer user_data); /** * Returns the node an item loader is loading items for. * * @param il the item loader * * @returns node */ nodePtr item_loader_get_node (ItemLoader *il); /** * Starts the item loader to load items with idle priority. * * @param il the item loader */ void item_loader_start (ItemLoader *il); #endif liferea-1.13.7/src/item_state.c000066400000000000000000000117541415350204600163130ustar00rootroot00000000000000/** * @file item_state.c item state controller * * Copyright (C) 2007-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "db.h" #include "debug.h" #include "feedlist.h" #include "item.h" #include "item_state.h" #include "itemset.h" #include "itemlist.h" #include "node.h" #include "vfolder.h" #include "fl_sources/node_source.h" static void item_state_set_recount_flag (nodePtr node) { node->needsRecount = TRUE; } void item_set_flag_state (itemPtr item, gboolean newState) { if (newState == item->flagStatus) return; node_source_item_set_flag (node_from_id (item->nodeId), item, newState); } void item_flag_state_changed (itemPtr item, gboolean newState) { /* 1. set value in memory */ item->flagStatus = newState; /* 2. save state to DB */ db_item_state_update (item); /* 3. update vfolder counters */ vfolder_foreach (node_update_counters); /* 4. update item list GUI state */ itemlist_update_item (item); /* no duplicate state propagation to avoid copies in the "Important" search folder */ } void item_set_read_state (itemPtr item, gboolean newState) { /* Read and update state are coupled insofar as they are changed by the same user actions. So we do something here if either the read state has changed or the updated flag is set (which is always just reset). */ if (newState == item->readStatus && !item->updateStatus) return; node_source_item_mark_read (node_from_id (item->nodeId), item, newState); } void item_read_state_changed (itemPtr item, gboolean newState) { nodePtr node; debug_start_measurement (DEBUG_GUI); /* 1. set values in memory */ item->readStatus = newState; item->updateStatus = FALSE; /* 2. apply to DB */ db_item_state_update (item); /* 3. propagate to vfolders */ vfolder_foreach (node_update_counters); /* 4. update item list GUI state */ itemlist_update_item (item); /* 5. updated feed list unread counters */ node = node_from_id (item->nodeId); node_update_counters (node); /* 6. duplicate state propagation */ if (item->validGuid) { GSList *duplicates, *iter; duplicates = iter = db_item_get_duplicates (item->sourceId); while (iter) { itemPtr duplicate = item_load (GPOINTER_TO_UINT (iter->data)); /* The check on node_from_id() is an evil workaround to handle "lost" items in the DB that have no associated node in the feed list. This should be fixed by having the feed list in the DB too, so we can clean up correctly after crashes. */ if (duplicate && duplicate->id != item->id && node_from_id (duplicate->nodeId)) { item_set_read_state (duplicate, newState); } if (duplicate) item_unload (duplicate); iter = g_slist_next (iter); } g_slist_free (duplicates); } debug_end_measurement (DEBUG_GUI, "set read status"); } /** * In difference to all the other item state handling methods * item_state_set_all_read does not immediately apply the * changes to the GUI because it is usually called recursively * and would be to slow. Instead the node structure flag for * recounting is set. By calling feedlist_update() afterwards * those recounts are executed and applied to the GUI. */ void itemset_mark_read (nodePtr node) { itemSetPtr itemSet; itemSet = node_get_itemset (node); GList *iter = itemSet->ids; while (iter) { gulong id = GPOINTER_TO_UINT (iter->data); itemPtr item = item_load (id); if (item) { if (!item->readStatus) { nodePtr node = node_from_id (item->nodeId); if (node) { item_state_set_recount_flag (node); node_source_item_mark_read (node, item, TRUE); } debug_start_measurement (DEBUG_GUI); GSList *duplicates = db_item_get_duplicate_nodes (item->sourceId); GSList *duplicate = duplicates; while (duplicate) { gchar *nodeId = (gchar *)duplicate->data; nodePtr affectedNode = node_from_id (nodeId); if (affectedNode) item_state_set_recount_flag (affectedNode); g_free (nodeId); duplicate = g_slist_next (duplicate); } g_slist_free(duplicates); debug_end_measurement (DEBUG_GUI, "mark read of duplicates"); } item_unload (item); } iter = g_list_next (iter); } // FIXME: why not call itemset_free (itemSet); here? Crashes! } void item_state_set_all_popup (const gchar *nodeId) { db_itemset_mark_all_popup (nodeId); /* No GUI updating necessary... */ } liferea-1.13.7/src/item_state.h000066400000000000000000000041211415350204600163060ustar00rootroot00000000000000/** * @file item_state.c item state controller interface * * Copyright (C) 2007-2008 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEM_STATE_H #define _ITEM_STATE_H #include "item.h" #include "node.h" /** * Request to change the flag state of the given item. * * @param item the item * @param newState new flag state */ void item_set_flag_state (itemPtr item, gboolean newState); /** * Notifies the item list controller that the flag * state of the given item has changed. * * @param item the item * @param newState new flag state */ void item_flag_state_changed (itemPtr item, gboolean newState); /** * Request to change the read state of the given item. * * @param item the item * @param newState new read state */ void item_set_read_state (itemPtr item, gboolean newState); /** * Notifies the item list controller that the read * state of the given item has changed. * * @param item the item * @param newState new read status */ void item_read_state_changed (itemPtr item, gboolean newState); /** * Requests to mark read all items in the given nodes item list. * * @param nodeId the node whose item list is to be modified */ void itemset_mark_read (nodePtr node); /** * Resets the popup flag for all items of the given item set. * * @param nodeId the node whose item list is to be modified */ void item_state_set_all_popup (const gchar *nodeId); #endif liferea-1.13.7/src/itemlist.c000066400000000000000000000477431415350204600160160ustar00rootroot00000000000000/* * @file itemlist.c item list handling * * Copyright (C) 2004-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include "comments.h" #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "item_history.h" #include "item_state.h" #include "itemlist.h" #include "itemset.h" #include "metadata.h" #include "node.h" #include "rule.h" #include "vfolder.h" #include "ui/item_list_view.h" #include "ui/itemview.h" #include "ui/liferea_shell.h" #include "ui/feed_list_view.h" #include "ui/liferea_htmlview.h" /* The 'item list' is a controller for 'item view' and database backend. It manages the currently displayed 'node', realizes filtering by node and 'item set' rules, also duplicate elimination and provides synchronisation for backend and GUI access to the current itemset. The 'item list' provides methods to add/remove items from the 'item view' synchronously and allows registering 'item loaders' for asynchronous adding of item batches. The 'item list' does not handle item list rendering variants, which is done by the 'item view'. */ #define ITEMLIST_GET_PRIVATE itemlist_get_instance_private struct ItemListPrivate { GHashTable *guids; /*<< list of GUID to avoid having duplicates in currently loaded list */ itemSetPtr filter; /*<< currently active filter rules */ nodePtr currentNode; /*<< the node whose own or its child items are currently displayed */ gulong selectedId; /*<< the currently selected (and displayed) item id */ nodeViewType viewMode; /*<< current viewing mode */ guint loading; /*<< if >0 prevents selection effects when loading the item list */ itemPtr invalidSelection; /*<< if set then the next selection might need to do an unselect first */ gboolean deferredRemove; /*<< TRUE if selected item needs to be removed from cache on unselecting */ gboolean deferredFilter; /*<< TRUE if selected item needs to be filtered on unselecting */ }; static GObjectClass *parent_class = NULL; static ItemList *itemlist = NULL; G_DEFINE_TYPE_WITH_CODE (ItemList, itemlist, G_TYPE_OBJECT, G_ADD_PRIVATE (ItemList)); static void itemlist_init (ItemList *il) { /* 1. Prepare globally accessible singleton */ g_assert (NULL == itemlist); itemlist = il; itemlist->priv = ITEMLIST_GET_PRIVATE (il); } static void itemlist_duplicate_list_remove_item (itemPtr item) { if (!item->validGuid) return; if (!itemlist->priv->guids) return; g_hash_table_remove (itemlist->priv->guids, item->sourceId); } static void itemlist_duplicate_list_add_item (itemPtr item) { if (!item->validGuid) return; if (!itemlist->priv->guids) itemlist->priv->guids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); g_hash_table_insert (itemlist->priv->guids, g_strdup (item->sourceId), GUINT_TO_POINTER (item->id)); } static gboolean itemlist_duplicate_list_check_item (itemPtr item) { if (!itemlist->priv->guids || !item->validGuid) return TRUE; return (NULL == g_hash_table_lookup (itemlist->priv->guids, item->sourceId)); } static void itemlist_duplicate_list_free (void) { if (itemlist->priv->guids) { g_hash_table_destroy (itemlist->priv->guids); itemlist->priv->guids = NULL; } } static void itemlist_finalize (GObject *object) { itemset_free (itemlist->priv->filter); itemlist_duplicate_list_free (); G_OBJECT_CLASS (parent_class)->finalize (object); } static void itemlist_class_init (ItemListClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); parent_class = g_type_class_peek_parent (klass); object_class->finalize = itemlist_finalize; } /* member wrappers */ itemPtr itemlist_get_selected (void) { return item_load(itemlist->priv->selectedId); } gulong itemlist_get_selected_id (void) { return itemlist->priv->selectedId; } static void itemlist_set_selected (itemPtr item) { itemlist->priv->selectedId = item?item->id:0; } nodePtr itemlist_get_displayed_node (void) { return itemlist->priv->currentNode; } /* called when unselecting the item or unloading the item list */ static void itemlist_check_for_deferred_action (void) { gulong id = itemlist->priv->selectedId; itemPtr item; if (id) { itemlist_set_selected (NULL); /* check for removals caused by itemlist filter rule */ if (itemlist->priv->deferredFilter) { itemlist->priv->deferredFilter = FALSE; item = item_load (id); itemview_remove_item (item); feed_list_view_update_node (item->nodeId); } /* check for removals caused by vfolder rules */ if (itemlist->priv->deferredRemove) { itemlist->priv->deferredRemove = FALSE; item = item_load (id); itemlist_remove_item (item); } } } // FIXME: is this an itemset method? static gboolean itemlist_filter_check_item (itemPtr item) { if (itemlist->priv->currentNode && IS_VFOLDER (itemlist->priv->currentNode)) { vfolderPtr vfolder = (vfolderPtr)itemlist->priv->currentNode->data; /* Use hide read for search folders */ if (vfolder->unreadOnly && item->readStatus) return FALSE; /* Use search folder rule list in case of a search folder */ return itemset_check_item (vfolder->itemset, item); } /* apply the item list filter if available */ if (itemlist->priv->filter) return itemset_check_item (itemlist->priv->filter, item); /* otherwise keep the item */ return TRUE; } static void itemlist_merge_item (itemPtr item) { if (!itemlist_duplicate_list_check_item (item)) return; if (!itemlist_filter_check_item (item)) return; itemlist_duplicate_list_add_item (item); itemview_add_item (item); } /* Helper method checking if the passed item set is relevant for the currently item list content. */ static gboolean itemlist_itemset_is_valid (itemSetPtr itemSet) { gint folder_display_mode; nodePtr node; node = node_from_id (itemSet->nodeId); if (!itemlist->priv->currentNode) return FALSE; /* Nothing to do if nothing is displayed */ if (!IS_VFOLDER (itemlist->priv->currentNode) && (itemlist->priv->currentNode != node) && !node_is_ancestor (itemlist->priv->currentNode, node)) return FALSE; /* Nothing to do if the item set does not belong to this node, or this is a search folder */ conf_get_int_value (FOLDER_DISPLAY_MODE, &folder_display_mode); if (IS_FOLDER (itemlist->priv->currentNode) && !folder_display_mode) return FALSE; /* Bail out if it is a folder without the recursive display preference set */ debug1 (DEBUG_GUI, "reloading item list with node \"%s\"", node_get_title (node)); return TRUE; } void itemlist_merge_itemset (itemSetPtr itemSet) { debug_enter ("itemlist_merge_itemset"); if (itemlist_itemset_is_valid (itemSet)) { debug_start_measurement (DEBUG_GUI); itemset_foreach (itemSet, itemlist_merge_item); itemview_update (); debug_end_measurement (DEBUG_GUI, "itemlist merge"); } debug_exit ("itemlist_merge_itemset"); } void itemlist_load (nodePtr node) { itemSetPtr itemSet; gint folder_display_mode; gboolean folder_display_hide_read; debug_enter ("itemlist_load"); g_return_if_fail (NULL != node); debug1 (DEBUG_GUI, "loading item list with node \"%s\"", node_get_title (node)); g_assert (!itemlist->priv->guids); g_assert (!itemlist->priv->filter); /* 1. Filter check. Don't continue if folder is selected and no folder viewing is configured. If folder viewing is enabled set up a "unread items only" rule depending on the prefences. */ /* for folders and other heirarchic nodes do filtering */ if (IS_FOLDER (node) || node->children) { liferea_shell_update_allitems_actions (FALSE, 0 != node->unreadCount); conf_get_int_value (FOLDER_DISPLAY_MODE, &folder_display_mode); if (!folder_display_mode) return; conf_get_bool_value (FOLDER_DISPLAY_HIDE_READ, &folder_display_hide_read); if (folder_display_hide_read) { itemlist->priv->filter = g_new0(struct itemSet, 1); itemlist->priv->filter->anyMatch = TRUE; itemset_add_rule (itemlist->priv->filter, "unread", "", TRUE); } } else { liferea_shell_update_allitems_actions (0 != node->itemCount, 0 != node->unreadCount); } itemlist->priv->loading++; itemlist->priv->viewMode = node_get_view_mode (node); itemview_set_layout (itemlist->priv->viewMode); /* Set the new displayed node... */ itemlist->priv->currentNode = node; itemview_set_displayed_node (itemlist->priv->currentNode); itemview_set_mode (ITEMVIEW_NODE_INFO); itemSet = node_get_itemset (itemlist->priv->currentNode); itemlist_merge_itemset (itemSet); if (!IS_VFOLDER (node)) /* FIXME: this is ugly! */ itemset_free (itemSet); itemlist->priv->loading--; debug_exit("itemlist_load"); } void itemlist_unload (gboolean markRead) { /* Always clear to ensure clearing on search */ itemview_clear (); if (itemlist->priv->currentNode) { itemview_set_displayed_node (NULL); /* 1. Postprocessing for previously selected node, this is necessary to realize reliable read marking when using condensed mode. It's important to do this only when the selection really changed. */ if (markRead && (2 == node_get_view_mode (itemlist->priv->currentNode))) feedlist_mark_all_read (itemlist->priv->currentNode); itemlist_check_for_deferred_action (); } itemlist_set_selected (NULL); itemlist_duplicate_list_free (); itemlist->priv->currentNode = NULL; itemset_free (itemlist->priv->filter); itemlist->priv->filter = NULL; } void itemlist_select_next_unread (void) { itemPtr result = NULL; /* If we are in combined mode we have to mark everything read or else we would never jump to the next feed, because no item will be selected and marked read... */ if (itemlist->priv->currentNode) { if (NODE_VIEW_MODE_COMBINED == node_get_view_mode (itemlist->priv->currentNode)) node_mark_all_read (itemlist->priv->currentNode); } itemlist->priv->loading++; /* prevent unwanted selections */ /* before scanning the feed list, we test if there is a unread item in the currently selected feed! */ result = itemview_find_unread_item (itemlist->priv->selectedId); /* If none is found we continue searching in the feed list */ if (!result) { nodePtr node; /* scan feed list and find first feed with unread items */ node = feedlist_find_unread_feed (feedlist_get_root ()); if (node) { /* load found feed */ feed_list_view_select (node); if (NODE_VIEW_MODE_COMBINED != node_get_view_mode (node)) result = itemview_find_unread_item (0); /* find first unread item */ } else { /* if we don't find a feed with unread items do nothing */ liferea_shell_set_status_bar (_("There are no unread items")); } } itemlist->priv->loading--; if (result) itemview_select_item (result); } /* menu commands */ void itemlist_toggle_flag (itemPtr item) { item_set_flag_state (item, !(item->flagStatus)); /* No itemview_update() to avoid disturbing HTML scroll state and media content */ } void itemlist_toggle_read_status (itemPtr item) { item_set_read_state (item, !(item->readStatus)); /* No itemview_update() to avoid disturbing HTML scroll state and media content */ } /* function to remove items due to item list filtering */ static void itemlist_hide_item (itemPtr item) { /* if the currently selected item should be removed we don't do it and set a flag to do it when unselecting */ if (itemlist->priv->selectedId != item->id) { itemview_remove_item (item); feed_list_view_update_node (item->nodeId); } else { itemlist->priv->deferredFilter = TRUE; /* update the item to show new state that forces later removal */ itemview_update_item (item); } } /* function to cancel deferred removal of selected item */ static void itemlist_unhide_item (itemPtr item) { itemlist->priv->deferredFilter = FALSE; } /* functions to remove items on remove requests */ /* hard unconditional item remove */ void itemlist_remove_item (itemPtr item) { if (itemlist->priv->selectedId == item->id) { itemlist_set_selected (NULL); itemlist->priv->deferredFilter = FALSE; itemlist->priv->deferredRemove = FALSE; } itemlist_duplicate_list_remove_item (item); itemview_remove_item (item); itemview_update (); db_item_remove (item->id); /* update feed list counters*/ vfolder_foreach (node_update_counters); node_update_counters (node_from_id (item->nodeId)); item_unload (item); } /* soft possibly delayed item remove */ static void itemlist_request_remove_item (itemPtr item) { /* if the currently selected item should be removed we don't do it and set a flag to do it when unselecting */ if (itemlist->priv->selectedId != item->id) { itemlist_remove_item (item); } else { itemlist->priv->deferredRemove = TRUE; /* update the item to show new state that forces later removal */ itemview_update_item (item); } } void itemlist_remove_items (itemSetPtr itemSet, GList *items) { GList *iter = items; while (iter) { itemPtr item = (itemPtr) iter->data; if (itemlist->priv->selectedId != item->id) { /* don't call itemlist_remove_item() here, because it's to slow */ itemview_remove_item (item); db_item_remove (item->id); } else { /* go the normal and selection-safe way to avoid disturbing the user */ itemlist_request_remove_item (item); } item_unload (item); iter = g_list_next (iter); } itemview_update (); vfolder_foreach (node_update_counters); node_update_counters (node_from_id (itemSet->nodeId)); } void itemlist_remove_all_items (nodePtr node) { if (node == itemlist->priv->currentNode) itemview_clear (); db_itemset_remove_all (node->id); if (node == itemlist->priv->currentNode) { itemview_update (); itemlist_duplicate_list_free (); } vfolder_foreach (node_update_counters); node_update_counters (node); } void itemlist_update_item (itemPtr item) { if (!itemlist_filter_check_item (item)) { itemlist_hide_item (item); return; } else { itemlist_unhide_item (item); } /* FIXME: this is tricky. It's possible that the item is * selected, but the itemview contains a webpage. In * that case, we don't want to reload the item. * So how to know whether the itemview contains the item? */ itemview_update_item (item); } /* mouse/keyboard interaction callbacks */ void itemlist_selection_changed (itemPtr item) { debug_enter ("itemlist_selection_changed"); debug_start_measurement (DEBUG_GUI); if (0 == itemlist->priv->loading) { /* folder&vfolder postprocessing to remove/filter unselected items no more matching the display rules because they have changed state */ itemlist_check_for_deferred_action (); debug1 (DEBUG_GUI, "item list selection changed to \"%s\"", item?item_get_title (item):"(null)"); itemlist_set_selected (item); /* set read and unset update status when selecting */ if (item) { gchar *link = NULL; nodePtr node = node_from_id (item->nodeId); item_set_read_state (item, TRUE); itemview_set_mode (ITEMVIEW_SINGLE_ITEM); if (node->loadItemLink && (link = item_make_link (item))) { itemview_launch_URL (link, TRUE /* force internal */); g_free (link); } else { if (IS_FEED(node) && !((feedPtr)node->data)->ignoreComments) comments_refresh (item); itemview_select_item (item); itemview_update (); } feed_list_view_update_node (item->nodeId); } feedlist_reset_new_item_count (); } if (item) item_unload (item); debug_end_measurement (DEBUG_GUI, "itemlist selection"); debug_exit ("itemlist_selection_changed"); } /* viewing mode callbacks */ guint itemlist_get_view_mode (void) { return itemlist->priv->viewMode; } static void itemlist_set_view_mode (nodeViewType newMode) { nodePtr node; itemPtr item; itemlist->priv->viewMode = newMode; node = itemlist_get_displayed_node (); item = itemlist_get_selected (); if (node) { itemlist_unload (FALSE); node_set_view_mode (node, itemlist->priv->viewMode); itemview_set_layout (itemlist->priv->viewMode); itemlist_load (node); /* If there was an item selected, select it again since * itemlist_unload() unselects it. */ if (item && itemlist->priv->viewMode != NODE_VIEW_MODE_COMBINED) itemview_select_item (item); } if (item) item_unload (item); } void on_view_activate (GSimpleAction *action, GVariant *value, gpointer user_data) { const gchar *s_val = g_variant_get_string (value, NULL); GVariant *cur_state = g_action_get_state (G_ACTION(action)); const gchar *s_cur_state = g_variant_get_string (cur_state,NULL); /* If requested state is the same as current state, leave without doing * anything. */ if (!g_strcmp0 (s_val,s_cur_state)) { g_variant_unref (cur_state); return; } g_variant_unref (cur_state); nodeViewType val = 0; if (!g_strcmp0 ("normal",s_val)) { val = NODE_VIEW_MODE_NORMAL; } if (!g_strcmp0 ("wide",s_val)) { val = NODE_VIEW_MODE_WIDE; } if (!g_strcmp0 ("combined",s_val)) { /* Combined is removed : default to normal */ val = NODE_VIEW_MODE_NORMAL; } itemlist_set_view_mode (val); /* Getting the actual value to reflect current state even if for some * reason, other functions couldn't make the requested change. * May be overkill. */ val = itemlist_get_view_mode (); switch (val) { case NODE_VIEW_MODE_NORMAL: case NODE_VIEW_MODE_DEFAULT: case NODE_VIEW_MODE_COMBINED: g_simple_action_set_state (action, g_variant_new_string("normal")); break; case NODE_VIEW_MODE_WIDE: g_simple_action_set_state (action, g_variant_new_string("wide")); break; } } static void itemlist_select_from_history (gboolean back) { itemPtr item; nodePtr node; if (back) item = item_history_get_previous (); else item = item_history_get_next (); if (!item) return; node = node_from_id (item->parentNodeId); if (!node) return; if (node != feedlist_get_selected ()) feed_list_view_select (node); if (NODE_VIEW_MODE_COMBINED == itemlist_get_view_mode ()) itemlist_set_view_mode (NODE_VIEW_MODE_NORMAL); itemview_select_item (item); item_unload (item); } void on_prev_read_item_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemlist_select_from_history (TRUE); } void on_next_read_item_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemlist_select_from_history (FALSE); } /* item loader methods */ static void itemlist_item_batch_fetched_cb (ItemLoader *il, GSList *items, gpointer user_data) { GSList *iter; if (item_loader_get_node (il) != itemlist->priv->currentNode) return; /* Bail on loader not matching selection */ debug0 (DEBUG_CACHE, "itemlist_item_batch_fetched_cb()"); iter = items; while (iter) { itemPtr item = (itemPtr)iter->data; itemlist_merge_item (item); item_unload (item); iter= g_slist_next (iter); } itemview_update(); g_slist_free (items); } static void itemlist_add_loader (ItemLoader *loader) { g_signal_connect (G_OBJECT (loader), "item-batch-fetched", G_CALLBACK (itemlist_item_batch_fetched_cb), NULL); item_loader_start (loader); } void itemlist_add_search_result (ItemLoader *loader) { nodeViewType viewMode; /* Ensure that we are in a useful viewing mode (3 paned) */ itemlist_unload (FALSE); viewMode = itemlist_get_view_mode (); if ((NODE_VIEW_MODE_NORMAL != viewMode) && (NODE_VIEW_MODE_WIDE != viewMode)) itemview_set_layout (NODE_VIEW_MODE_NORMAL); itemview_set_mode (ITEMVIEW_SINGLE_ITEM); /* Set current node to search result dummy node so that we except only items from the respective loader for the item view. */ itemlist->priv->currentNode = item_loader_get_node (loader); itemlist_add_loader (loader); } ItemList * itemlist_create (void) { return ITEMLIST (g_object_new (ITEMLIST_TYPE, NULL)); } liferea-1.13.7/src/itemlist.h000066400000000000000000000137571415350204600160210ustar00rootroot00000000000000/* * @file itemlist.h itemlist handling * * Copyright (C) 2004-2011 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEMLIST_H #define _ITEMLIST_H #include #include "item.h" #include "item_loader.h" #include "itemset.h" G_BEGIN_DECLS #define ITEMLIST_TYPE (itemlist_get_type ()) #define ITEMLIST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), ITEMLIST_TYPE, ItemList)) #define ITEMLIST_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ITEMLIST_TYPE, ItemListClass)) #define IS_ITEMLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ITEMLIST_TYPE)) #define IS_ITEMLIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ITEMLIST_TYPE)) typedef struct ItemList ItemList; typedef struct ItemListClass ItemListClass; typedef struct ItemListPrivate ItemListPrivate; struct ItemList { GObject parent; /*< private >*/ ItemListPrivate *priv; }; struct ItemListClass { GObjectClass parent_class; }; GType itemlist_get_type (void); /** * itemlist_create: (skip) * * Set up the item list. * * Returns: (transfer full): the item list instance */ ItemList * itemlist_create (void); /** * itemlist_get_displayed_node: (skip) * * Returns the currently displayed node. * * Returns: (transfer none) (nullable): displayed node (or NULL) */ struct node * itemlist_get_displayed_node (void); /** * itemlist_get_selected: (skip) * * Returns the currently selected and displayed item. * * Returns: (transfer full) (nullable): displayed item (or NULL) to be free'd using item_unload() */ itemPtr itemlist_get_selected (void); /** * itemlist_get_selected_id: * * Returns the id of the currently selected item. * * Returns: displayed item id (or 0) */ gulong itemlist_get_selected_id (void); /** * itemlist_merge_itemset: (skip) * @itemSet: the item set to be merged * * To be called whenever a feed was updated. If it is a somehow * displayed feed it is loaded this method decides if the * and how the item list GUI needs to be updated. * */ void itemlist_merge_itemset (itemSetPtr itemSet); /** * itemlist_load: (skip) * @node: the node * * Loads the passed nodes items into the item list. */ void itemlist_load (struct node *node); /** * itemlist_unload: (skip) * @markRead: if TRUE all items are marked as read * * Clears the item list. Unsets the currently * displayed item set. Optionally marks every item read. */ void itemlist_unload (gboolean markRead); /** * itemlist_get_view_mode: * * Returns the viewing mode property of the currently displayed item set. * * Returns: viewing mode (0 = normal, 1 = wide, 2 = combined view) */ guint itemlist_get_view_mode (void); /** * on_view_activate: (skip) * @action: the action that emitted the signal * @value: string in a GVariant, representing the requested mode. * @user_data: unused * * Menu callback that toggles the different viewing modes * This is the 'change_state' callback for the "SetViewMode" action. */ void on_view_activate (GSimpleAction *action, GVariant *value, gpointer user_data); /** * on_prev_read_item_activate: (skip) * * Menu callback to select the previously read item from the item history */ void on_prev_read_item_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * on_next_read_item_activate: (skip) * * Menu callback to select the next read item from the item history */ void on_next_read_item_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data); /* item handling function */ void itemlist_update_item (itemPtr item); /** * itemlist_remove_item: (skip) * @item: the item * * To be called whenever the user wants to remove * a single item. If necessary the item will be unselected. * The item will be removed immediately. */ void itemlist_remove_item (itemPtr item); /** * itemlist_remove_items: (skip) * @itemSet: the item set from which items are to be removed * @items: the items to be removed * * To be called whenever some of the items of an item set * are to be removed. In difference to itemlist_remove_item() * this function will remove all items first and update the * GUI once. */ void itemlist_remove_items (itemSetPtr itemSet, GList *items); /** * itemlist_remove_all_items: (skip) * @node: the node whose item list is to be removed * * To be called whenever the user wants to remove * all items of a node. Item list selection will be * resetted. All items are removed immediately. */ void itemlist_remove_all_items (struct node *node); /** * itemlist_selection_changed: (skip) * @item: new selected item * * Called from GUI when item list selection changes. */ void itemlist_selection_changed (itemPtr item); /** * itemlist_select_next_unread: * * Tries to select the next unread item that is currently in the * item list. Or does nothing if there are no unread items left. */ void itemlist_select_next_unread (void); /** * itemlist_toggle_flag: (skip) * @item: the item * * Toggle the flag of the given item. */ void itemlist_toggle_flag (itemPtr item); /** * itemlist_toggle_read_status: (skip) * @item: the item * * Toggle the read status of the given item. */ void itemlist_toggle_read_status (itemPtr item); /** * itemlist_add_search_result: (skip) * @loader: the search result item loader * * Register a search result item loader. * */ void itemlist_add_search_result (ItemLoader *loader); G_END_DECLS #endif liferea-1.13.7/src/itemset.c000066400000000000000000000357051415350204600156310ustar00rootroot00000000000000/* * @file itemset.c handling sets of items * * Copyright (C) 2005-2017 Lars Windolf * Copyright (C) 2005-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "enclosure.h" #include "feed.h" #include "itemlist.h" #include "itemset.h" #include "metadata.h" #include "node.h" #include "rule.h" #include "vfolder.h" #include "fl_sources/node_source.h" void itemset_foreach (itemSetPtr itemSet, itemActionFunc callback) { GList *iter = itemSet->ids; while(iter) { itemPtr item = item_load (GPOINTER_TO_UINT (iter->data)); if (item) { (*callback) (item); item_unload (item); } iter = g_list_next (iter); } } // FIXME: this ought to be a subscription property! static guint itemset_get_max_item_count (itemSetPtr itemSet) { nodePtr node = node_from_id (itemSet->nodeId); if (node && IS_FEED (node)) return feed_get_max_item_count (node); return G_MAXUINT; } /** * itemset_generic_merge_check: (skip) * @items: existing items * @newItem: new item to merge * @maxChecks: maximum number of item checks * @allowUpdates: TRUE if item content update is to be * allowed for existing items * @allowStateChanges: TRUE if item state shall be * overwritten by source * * Generic merge logic suitable for feeds * * Returns: TRUE if merging instead of updating is necessary) */ static gboolean itemset_generic_merge_check (GList *items, itemPtr newItem, gint maxChecks, gboolean allowUpdates, gboolean allowStateChanges) { GList *oldItemIdIter = items; itemPtr oldItem = NULL; gboolean found, equal = FALSE; guint reason = 0; /* determine if we should add it... */ debug3 (DEBUG_CACHE, "check new item for merging: \"%s\", %i, %i", item_get_title (newItem), allowUpdates, allowStateChanges); /* compare to every existing item in this feed */ found = FALSE; while (oldItemIdIter) { oldItem = (itemPtr)(oldItemIdIter->data); /* try to compare the two items */ /* trivial case: one item has id the other doesn't -> they can't be equal */ if (((item_get_id (oldItem) == NULL) && (item_get_id (newItem) != NULL)) || ((item_get_id (oldItem) != NULL) && (item_get_id (newItem) == NULL))) { /* cannot be equal (different ids) so compare to next old item */ oldItemIdIter = g_list_next (oldItemIdIter); continue; } /* just for the case there are no ids: compare titles and HTML descriptions */ equal = TRUE; if (((item_get_title (oldItem) != NULL) && (item_get_title (newItem) != NULL)) && (0 != strcmp (item_get_title (oldItem), item_get_title (newItem)))) { equal = FALSE; reason |= 1; } if (((item_get_description (oldItem) != NULL) && (item_get_description (newItem) != NULL)) && (0 != strcmp (item_get_description(oldItem), item_get_description (newItem)))) { equal = FALSE; reason |= 2; } /* best case: they both have ids (position important: id check is useless without knowing if the items are different!) */ if (item_get_id (oldItem)) { if (0 == strcmp (item_get_id (oldItem), item_get_id (newItem))) { found = TRUE; if (allowStateChanges) { /* found corresponding item, check if they are REALLY equal (eg, read status may have changed) */ if(oldItem->readStatus != newItem->readStatus) { equal = FALSE; reason |= 4; } if(oldItem->flagStatus != newItem->flagStatus) { equal = FALSE; reason |= 8; } } break; } else { /* different ids, but the content might be still equal (e.g. empty) so we need to explicitly unset the equal flag !!! */ equal = FALSE; reason |= 16; } } if (equal) { found = TRUE; break; } oldItemIdIter = g_list_next (oldItemIdIter); } if (!found) { debug0 (DEBUG_CACHE, "-> item is to be added"); } else { /* if the item was found but has other contents -> update contents */ if (!equal) { if (allowUpdates) { /* no item_set_new_status() - we don't treat changed items as new items! */ item_set_title (oldItem, item_get_title (newItem)); /* don't use item_set_description as it does some unwanted length handling and we want to enforce the new description */ g_free (oldItem->description); oldItem->description = newItem->description; newItem->description = NULL; oldItem->time = newItem->time; oldItem->updateStatus = TRUE; // FIXME: this does not remove metadata from DB metadata_list_free (oldItem->metadata); oldItem->metadata = newItem->metadata; newItem->metadata = NULL; /* Only update item state for feed sources where it is necessary which means online accounts we sync against, but not normal online feeds where items have no read status. */ if (allowStateChanges) { /* To avoid notification spam from external sources: never set read items to unread again! */ if ((!oldItem->readStatus) && (newItem->readStatus)) oldItem->readStatus = newItem->readStatus; oldItem->flagStatus = newItem->flagStatus; } db_item_update (oldItem); debug1 (DEBUG_CACHE, "-> item already existing and was updated, reason %x", reason); } else { debug0 (DEBUG_CACHE, "-> item updates not merged because of parser errors"); } } else { debug0 (DEBUG_CACHE, "-> item already exists"); } } return !found; } static gboolean itemset_merge_item (itemSetPtr itemSet, GList *items, itemPtr item, gint maxChecks, gboolean allowUpdates) { gboolean allowStateChanges = FALSE; gboolean merge; nodePtr node; debug2 (DEBUG_UPDATE, "trying to merge \"%s\" to node id \"%s\"", item_get_title (item), itemSet->nodeId); g_assert (itemSet->nodeId); node = node_from_id (itemSet->nodeId); if (node) allowStateChanges = NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC; /* first try to merge with existing item */ merge = itemset_generic_merge_check (items, item, maxChecks, allowUpdates, allowStateChanges); /* if it is a new item add it to the item set */ if (merge) { g_assert (!item->nodeId); g_assert (!item->id); item->nodeId = g_strdup (itemSet->nodeId); if (!item->parentNodeId) item->parentNodeId = g_strdup (itemSet->nodeId); /* step 1: write item to DB */ db_item_update (item); /* step 2: add to itemset */ itemSet->ids = g_list_prepend (itemSet->ids, GUINT_TO_POINTER (item->id)); /* step 3: trigger async enrichment of item description */ if (node && IS_FEED (node) && ((feedPtr)node->data)->html5Extract) feed_enrich_item (node->subscription, item); debug3 (DEBUG_UPDATE, "-> added \"%s\" (id=%d) to item set %p...", item_get_title (item), item->id, itemSet); /* step 4: duplicate detection, mark read if it is a duplicate */ if (item->validGuid) { GSList *iter, *duplicates; duplicates = iter = db_item_get_duplicates (item->sourceId); while (iter) { debug1 (DEBUG_UPDATE, "-> duplicate guid exists: #%lu", GPOINTER_TO_UINT (iter->data)); iter = g_slist_next (iter); } if (g_slist_length (duplicates) > 1) { item->readStatus = TRUE; /* no unread counting... */ item->popupStatus = FALSE; /* no notification... */ } g_slist_free (duplicates); } /* step 5: Check item for new enclosures to download */ if (node && (((feedPtr)node->data)->encAutoDownload)) { GSList *iter = metadata_list_get_values (item->metadata, "enclosure"); while (iter) { enclosurePtr enc = enclosure_from_string (iter->data); debug1 (DEBUG_UPDATE, "download enclosure (%s)", (gchar *)iter->data); enclosure_download (NULL, enc->url, FALSE /* non interactive */); iter = g_slist_next (iter); enclosure_free (enc); } } } else { debug2 (DEBUG_UPDATE, "-> not adding \"%s\" to node id \"%s\"...", item_get_title (item), itemSet->nodeId); item_unload (item); } return merge; } static gint itemset_sort_by_date (gconstpointer a, gconstpointer b) { itemPtr item1 = (itemPtr)a; itemPtr item2 = (itemPtr)b; g_assert(item1 && item2); /* We have a problem here if all items of the feed do have no date, then this comparison is useless. To avoid such a case we alternatively compare by item id (which should be an ever-increasing number) and thereby indicate merge order as a secondary order criterion */ if (item1->time == item2->time) { if (item1->id < item2->id) return 1; if (item1->id > item2->id) return -1; return 0; } if (item1->time < item2->time) return 1; if (item1->time > item2->time) return -1; return 0; } guint itemset_merge_items (itemSetPtr itemSet, GList *list, gboolean allowUpdates, gboolean markAsRead) { GList *iter, *droppedItems = NULL, *items = NULL; guint i, max, length, toBeDropped, newCount = 0, flagCount = 0; nodePtr node; debug_start_measurement (DEBUG_UPDATE); debug2 (DEBUG_UPDATE, "old item set %p of (node id=%s):", itemSet, itemSet->nodeId); /* 1. Preparation: determine effective maximum cache size The problem here is that the configured maximum cache size might not always be sufficient. We need to check border use cases in the following. */ length = g_list_length (list); max = itemset_get_max_item_count (itemSet); /* Preload all items for flag counting and later merging comparison */ iter = itemSet->ids; while (iter) { itemPtr item = item_load (GPOINTER_TO_UINT (iter->data)); if (item) { items = g_list_append (items, item); if (item->flagStatus) flagCount++; } iter = g_list_next (iter); } debug1(DEBUG_UPDATE, "current cache size: %d", g_list_length(itemSet->ids)); debug1(DEBUG_UPDATE, "current cache limit: %d", max); debug1(DEBUG_UPDATE, "downloaded feed size: %d", g_list_length(list)); debug1(DEBUG_UPDATE, "flag count: %d", flagCount); /* Case #1: Avoid having too many flagged items. We count the flagged items and check if they are fewer than - to ensure that all new items fit in a feed cache full of flagged items. This handling MUST NOT be invoked when the number of items is larger then the cache size, otherwise we would never remove any items for large feeds. */ if ((length < max) && (max < length + flagCount)) { max = flagCount + length; debug2 (DEBUG_UPDATE, "too many flagged items -> increasing cache limit to %u (node id=%s)", max, itemSet->nodeId); } /* 2. Avoid cache wrapping (if feed size > cache size) Truncate the new itemset if it is longer than the maximum cache size which could cause items to be dropped and added again on subsequent merges with the same feed content */ if (length > max) { debug2 (DEBUG_UPDATE, "item list too long (%u, max=%u) for merging!", length, max); /* reach max element */ for(i = 0, iter = list; (i < max) && iter; ++i) iter = g_list_next (iter); /* and remove all following elements */ while (iter) { itemPtr item = (itemPtr) iter->data; debug2 (DEBUG_UPDATE, "ignoring item nr %u (%s)...", ++i, item_get_title (item)); item_unload (item); iter = g_list_next (iter); list = g_list_remove (list, item); } } /* 3. Merge received items to existing item set Items are given in top to bottom display order. Adding them in this order would mean to reverse their order in the merged list, so merging needs to be done bottom to top. During this step the item list (items) may exceed the cache limit. */ iter = g_list_last (list); while (iter) { itemPtr item = (itemPtr)iter->data; if (markAsRead) item->readStatus = TRUE; if (itemset_merge_item (itemSet, items, item, length, allowUpdates)) { newCount++; items = g_list_prepend (items, iter->data); } iter = g_list_previous (iter); } g_list_free (list); vfolder_foreach (node_update_counters); node = node_from_id (itemSet->nodeId); if (node && (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ITEM_STATE_SYNC)) node_update_counters (node); debug1(DEBUG_UPDATE, "added %d new items", newCount); /* 4. Apply cache limit for effective item set size and unload older items as necessary. In this step it is important never to drop flagged items and to drop the oldest items only. */ if (g_list_length (items) > max) toBeDropped = g_list_length (items) - max; else toBeDropped = 0; debug3 (DEBUG_UPDATE, "%u new items, cache limit is %u -> dropping %u items", newCount, max, toBeDropped); items = g_list_sort (items, itemset_sort_by_date); iter = g_list_last (items); while (iter) { itemPtr item = (itemPtr) iter->data; if (toBeDropped > 0 && !item->flagStatus) { debug2 (DEBUG_UPDATE, "dropping item nr %u (%s)....", item->id, item_get_title (item)); droppedItems = g_list_append (droppedItems, item); /* no unloading here, it's done in itemlist_remove_items() */ toBeDropped--; } else { item_unload (item); } iter = g_list_previous (iter); } if (droppedItems) { itemlist_remove_items (itemSet, droppedItems); g_list_free (droppedItems); } /* 5. Sanity check to detect merging bugs */ if (g_list_length (items) > itemset_get_max_item_count (itemSet) + flagCount) debug0 (DEBUG_CACHE, "Fatal: Item merging bug! Resulting item list is too long! Cache limit does not work. This is a severe program bug!"); g_list_free (items); debug_end_measurement (DEBUG_UPDATE, "merge itemset"); return newCount; } gboolean itemset_check_item (itemSetPtr itemSet, itemPtr item) { gboolean result = TRUE; GSList *iter = itemSet->rules; while (iter) { rulePtr rule = (rulePtr) iter->data; ruleCheckFunc func = rule->ruleInfo->checkFunc; gboolean ruleResult = FALSE; ruleResult = (*func) (rule, item); result &= (rule->additive)?ruleResult:!ruleResult; if (itemSet->anyMatch && ruleResult) return TRUE; iter = g_slist_next (iter); } return result; } void itemset_add_rule (itemSetPtr itemSet, const gchar *ruleId, const gchar *value, gboolean additive) { rulePtr rule; rule = rule_new (ruleId, value, additive); if (rule) itemSet->rules = g_slist_append (itemSet->rules, rule); else g_warning ("unknown search folder rule id: \"%s\"", ruleId); } void itemset_free (itemSetPtr itemSet) { GSList *rule; if (!itemSet) return; rule = itemSet->rules; while (rule) { rule_free (rule->data); rule = g_slist_next (rule); } g_slist_free (itemSet->rules); g_list_free (itemSet->ids); g_free (itemSet); } liferea-1.13.7/src/itemset.h000066400000000000000000000061211415350204600156240ustar00rootroot00000000000000/* * @file itemset.h interface to handle sets of items * * Copyright (C) 2005-2012 Lars Windolf * Copyright (C) 2005-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEMSET_H #define _ITEMSET_H #include "item.h" #include "rule.h" /* * The itemset interface processes item list actions * based on the item set type specified by the node * the item set belongs to. */ typedef struct itemSet { GSList *rules; /*<< list of rules each item matches */ gboolean anyMatch; /*<< TRUE means only one of the rules must match for item inclusion */ GList *ids; /*<< the list of item ids */ gchar *nodeId; /*<< the feed list node id this item set belongs to */ } *itemSetPtr; /* item set iterating interface */ typedef void (*itemActionFunc) (itemPtr item); /** * itemset_foreach: (skip) * @itemSet: the item set * @callback: the callback * * Calls the given callback for each of the items in the item set. */ void itemset_foreach (itemSetPtr itemSet, itemActionFunc callback); /** * itemset_merge_items: (skip) * @itemSet: the item set to merge into * @items: a list of items to merge * @allowUpdates: TRUE if older items may be replaced * @markAsRead: TRUE if all new items should be marked as read * * Merges the given item set into the item set of * the given node. Used for node updating. * * Returns: the number of new merged items */ guint itemset_merge_items(itemSetPtr itemSet, GList *items, gboolean allowUpdates, gboolean markAsRead); /** * itemset_check_item: (skip) * @itemSet: the itemSet * @item: the item * * Checks if the given item matches the rules of the given item set. * * Returns: TRUE if the item matches the rules of the item set */ gboolean itemset_check_item (itemSetPtr itemSet, itemPtr item); /** * itemset_add_rule: (skip) * @itemSet: the item set * @ruleId: id string for this rule type * @value: argument string for this rule * @additive: indicates positive or negative logic * * Method that creates and adds a rule to an item set. To be used * on loading time, when creating searches or when editing * search folder properties. */ void itemset_add_rule (itemSetPtr itemSet, const gchar *ruleId, const gchar *value, gboolean additive); /** * itemset_free: (skip) * @itemSet: the item set to free * * Frees the given item set and all items it contains. */ void itemset_free(itemSetPtr itemSet); #endif liferea-1.13.7/src/json.c000066400000000000000000000036411415350204600151220ustar00rootroot00000000000000/** * @file json.h simplification wrappers for libjson-glib * * Copyright (C) 2010 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "json.h" JsonNode * json_get_node (JsonNode *node, const gchar *keyName) { JsonObject *obj; JsonNode *key; obj = json_node_get_object (node); if (!obj) return NULL; key = json_object_get_member (obj, keyName); if (!key) return NULL; return key; } const gchar * json_get_string (JsonNode *node, const gchar *keyName) { JsonObject *obj; JsonNode *key; obj = json_node_get_object (node); if (!obj) return NULL; key = json_object_get_member (obj, keyName); if (!key) return NULL; return json_node_get_string (key); } gint64 json_get_int (JsonNode *node, const gchar *keyName) { JsonObject *obj; JsonNode *key; obj = json_node_get_object (node); if (!obj) return 0; key = json_object_get_member (obj, keyName); if (!key) return 0; return json_node_get_int (key); } gboolean json_get_bool (JsonNode *node, const gchar *keyName) { JsonObject *obj; JsonNode *key; obj = json_node_get_object (node); if (!obj) return FALSE; key = json_object_get_member (obj, keyName); if (!key) return FALSE; return json_node_get_boolean (key); } liferea-1.13.7/src/json.h000066400000000000000000000036501415350204600151270ustar00rootroot00000000000000/** * @file json.h simplification wrappers for libjson-glib * * Copyright (C) 2010 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _JSON_H #define _JSON_H #include #include /** * Query a simple json object node for a given subnode. * * @param node the Json node to check * @param keyName the name of the subnode * * @returns subnode (or NULL) */ JsonNode *json_get_node (JsonNode *node, const gchar *keyName); /** * Query a simple json object node for a given string key. * * @param obj the Json node to check * @param key the key to look up * * @returns value of the key (or NULL) */ const gchar * json_get_string (JsonNode *node, const gchar *key); /** * Query a simple json object node for a given numeric key. * * @param obj the Json node to check * @param key the key to look up * * @returns value of the key (no error handling!) */ gint64 json_get_int (JsonNode *node, const gchar *key); /** * Query a simple json object node for a given boolean key. * * @param obj the Json node to check * @param key the key to look up * * @returns value of the key (no error handling!) */ gboolean json_get_bool (JsonNode *node, const gchar *key); #endif liferea-1.13.7/src/liferea-add-feed.in000066400000000000000000000007461415350204600173760ustar00rootroot00000000000000#!/bin/sh # This script can be used to automatically add # subscriptions to Liferea. Just supply a valid # and correctly escaped feed URL as parameter. if [ $# -ne 1 ]; then echo "Wrong parameter count!" echo "" echo "Syntax: $0 " echo "" exit 1 fi URL=$1 if ! pgrep -x liferea >/dev/null 2>&1; then echo "Liferea is not running! You need to start it first." exit 1 fi NEWURL=`echo "$URL" | sed 's/feed\:https/https/'` @prefix@/bin/liferea --add-feed "$NEWURL" liferea-1.13.7/src/liferea_application.c000066400000000000000000000306501415350204600201430ustar00rootroot00000000000000/** * @file main.c Liferea startup * * Copyright (C) 2003-2020 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * Some code like the command line handling was inspired by * * Pan - A Newsreader for Gtk+ * Copyright (C) 2002 Charles Kerr * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "liferea_application.h" #ifdef HAVE_CONFIG_H # include #endif #include #include "conf.h" #include "common.h" #include "db.h" #include "dbus.h" #include "debug.h" #include "feedlist.h" #include "social.h" #include "update.h" #include "xml.h" #include "ui/liferea_shell.h" #include struct _LifereaApplication { GtkApplication parent; gchar *initialStateOption; gint pluginsDisabled; LifereaDBus *dbus; gulong debug_flags; }; G_DEFINE_TYPE (LifereaApplication, liferea_application, GTK_TYPE_APPLICATION) static LifereaApplication *liferea_app = NULL; static void liferea_application_finalize (GObject *gobject) { LifereaApplication *self = LIFEREA_APPLICATION(gobject); g_clear_object (&self->dbus); /* Chaining finalize from parent class. */ G_OBJECT_CLASS(liferea_application_parent_class)->finalize(gobject); } /* GApplication "open" callback for receiving feed-add requests from remote --add-feed option or * adding feeds passed as argument. */ static void on_app_open (GApplication *application, gpointer files, gint n_files, gchar *hint, gpointer user_data) { int i; GFile **uris = (GFile **)files; GtkApplication *gtk_app = GTK_APPLICATION (application); GList *list = gtk_application_get_windows (gtk_app); LifereaApplication *app = LIFEREA_APPLICATION (application); if (!list) liferea_shell_create (gtk_app, app->initialStateOption, app->pluginsDisabled); for (i=0;(idata)); gtk_window_present (GTK_WINDOW (list->data)); } else { liferea_shell_create (gtk_app, app->initialStateOption, app->pluginsDisabled); } css_filename = g_build_filename (PACKAGE_DATA_DIR, PACKAGE, "liferea.css", NULL); css_file = g_file_new_for_path (css_filename); provider = gtk_css_provider_new (); gtk_css_provider_load_from_file(provider, css_file, &error); if (G_UNLIKELY (!gtk_css_provider_load_from_file(provider, css_file, &error))) { g_critical ("Could not load CSS data: %s", error->message); g_clear_error (&error); } else { gtk_style_context_add_provider_for_screen ( gdk_screen_get_default(), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } g_object_unref (provider); g_object_unref (css_file); g_free (css_filename); } /* Callback to the startup signal emitted only by the primary instance upon registration. */ static void on_app_startup (GApplication *gapp, gpointer user_data) { LifereaApplication *app = LIFEREA_APPLICATION (gapp); set_debug_level (app->debug_flags); /* Configuration necessary for network options, so it has to be initialized before update_init() */ conf_init (); /* We need to do the network initialization here to allow network-manager to be setup before gtk_init() */ update_init (); /* order is important! */ db_init (); /* initialize sqlite */ xml_init (); /* initialize libxml2 */ social_init (); /* initialize social bookmarking */ app->dbus = liferea_dbus_new (); } /* Callback to the "shutdown" signal emitted only on the primary instance; */ static void on_app_shutdown (GApplication *app, gpointer user_data) { GList *list; debug_enter ("liferea_shutdown"); /* order is important ! */ update_deinit (); /* When application is started as a service, it waits 10 seconds for a message. * If no message arrives, it will shutdown without having created a window. */ list = gtk_application_get_windows (GTK_APPLICATION (app)); if (list) { liferea_shell_destroy (); } db_deinit (); social_free (); conf_deinit (); xml_deinit (); debug_exit ("liferea_shutdown"); } static void show_version () { printf ("Liferea %s\n", VERSION); } static gboolean debug_entries_parse_callback (const gchar *option_name, const gchar *value, gpointer data, GError **error) { gulong *debug_flags = data; if (g_str_equal (option_name, "--debug-all")) { *debug_flags = 0xffff - DEBUG_VERBOSE - DEBUG_TRACE; } else if (g_str_equal (option_name, "--debug-cache")) { *debug_flags |= DEBUG_CACHE; } else if (g_str_equal (option_name, "--debug-conf")) { *debug_flags |= DEBUG_CONF; } else if (g_str_equal (option_name, "--debug-db")) { *debug_flags |= DEBUG_DB; } else if (g_str_equal (option_name, "--debug-gui")) { *debug_flags |= DEBUG_GUI; } else if (g_str_equal (option_name, "--debug-html")) { *debug_flags |= DEBUG_HTML; } else if (g_str_equal (option_name, "--debug-net")) { *debug_flags |= DEBUG_NET; } else if (g_str_equal (option_name, "--debug-parsing")) { *debug_flags |= DEBUG_PARSING; } else if (g_str_equal (option_name, "--debug-performance")) { *debug_flags |= DEBUG_PERF; } else if (g_str_equal (option_name, "--debug-trace")) { *debug_flags |= DEBUG_TRACE; } else if (g_str_equal (option_name, "--debug-update")) { *debug_flags |= DEBUG_UPDATE; } else if (g_str_equal (option_name, "--debug-vfolder")) { *debug_flags |= DEBUG_VFOLDER; } else if (g_str_equal (option_name, "--debug-verbose")) { *debug_flags |= DEBUG_VERBOSE; } else { return FALSE; } return TRUE; } static gint on_handle_local_options (GApplication *app, GVariantDict *options, gpointer user_data) { gchar *uri; if (g_variant_dict_lookup_value (options, "version", NULL)) { show_version (); return 0; /* Show version and exit */ } if (g_variant_dict_lookup (options, "add-feed", "s", &uri)) { GFile *uris[2]; uris[0] = g_file_new_for_uri (uri); uris[1] = NULL; g_application_register (app, NULL, NULL); g_application_open (G_APPLICATION (app), uris, 1, "feed-add"); g_object_unref (uris[0]); return -1; } return -1; } static void liferea_application_class_init (LifereaApplicationClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->finalize = liferea_application_finalize; } static void liferea_application_init (LifereaApplication *self) { GOptionGroup *debug; GOptionEntry entries[] = { { "mainwindow-state", 'w', 0, G_OPTION_ARG_STRING, &self->initialStateOption, N_("Start Liferea with its main window in STATE. STATE may be `shown' or `hidden'"), N_("STATE") }, { "version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Show version information and exit"), NULL }, { "add-feed", 'a', 0, G_OPTION_ARG_STRING, NULL, N_("Add a new subscription"), N_("uri") }, { "disable-plugins", 'p', 0, G_OPTION_FLAG_NONE, &self->pluginsDisabled, N_("Start with all plugins disabled"), NULL }, { NULL, 0, 0, 0, NULL, NULL, NULL } }; GOptionEntry debug_entries[] = { { "debug-all", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of all types"), NULL }, { "debug-cache", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages for the cache handling"), NULL }, { "debug-conf", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages for the configuration handling"), NULL }, { "debug-db", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of the database handling"), NULL }, { "debug-gui", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of all GUI functions"), NULL }, { "debug-html", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Enables HTML rendering debugging. Each time Liferea renders HTML output it will also dump the generated HTML into ~/.cache/liferea/output.html"), NULL }, { "debug-net", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of all network activity"), NULL }, { "debug-parsing", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of all parsing functions"), NULL }, { "debug-performance", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages when a function takes too long to process"), NULL }, { "debug-trace", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages when entering/leaving functions"), NULL }, { "debug-update", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of the feed update processing"), NULL }, { "debug-vfolder", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print debugging messages of the search folder matching"), NULL }, { "debug-verbose", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, debug_entries_parse_callback, N_("Print verbose debugging messages"), NULL }, { NULL, 0, 0, 0, NULL, NULL, NULL } }; debug = g_option_group_new ("debug", _("Print debugging messages for the given topic"), _("Print debugging messages for the given topic"), &self->debug_flags, NULL); g_option_group_set_translation_domain(debug, GETTEXT_PACKAGE); g_option_group_add_entries (debug, debug_entries); g_application_add_main_option_entries (G_APPLICATION (self), entries); g_application_add_option_group (G_APPLICATION (self), debug); g_application_add_option_group (G_APPLICATION (self), g_irepository_get_option_group ()); g_signal_connect (G_OBJECT (self), "activate", G_CALLBACK (on_app_activate), NULL); g_signal_connect (G_OBJECT (self), "open", G_CALLBACK (on_app_open), NULL); g_signal_connect (G_OBJECT (self), "shutdown", G_CALLBACK (on_app_shutdown), NULL); g_signal_connect (G_OBJECT (self), "startup", G_CALLBACK (on_app_startup), NULL); g_signal_connect (G_OBJECT (self), "handle-local-options", G_CALLBACK (on_handle_local_options), NULL); } static gboolean liferea_application_shutdown_source_func (gpointer userdata) { g_application_quit (G_APPLICATION (liferea_app)); return FALSE; } void liferea_application_shutdown (void) { g_idle_add (liferea_application_shutdown_source_func, NULL); } gint liferea_application_new (int argc, char *argv[]) { gint status; g_assert (NULL == liferea_app); liferea_app = g_object_new (LIFEREA_APPLICATION_TYPE, "flags", G_APPLICATION_HANDLES_OPEN, "application-id", "net.sourceforge.liferea", NULL); g_set_prgname ("liferea"); g_set_application_name (_("Liferea")); gtk_window_set_default_icon_name ("net.sourceforge.liferea"); /* GTK theme support */ status = g_application_run (G_APPLICATION (liferea_app), argc, argv); g_object_unref (liferea_app); return status; } void liferea_application_rebuild_css(void) { liferea_shell_rebuild_css (); } liferea-1.13.7/src/liferea_application.h000066400000000000000000000027711415350204600201530ustar00rootroot00000000000000/* * @file liferea_application.h LifereaApplication type * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_APPLICATION_H #define _LIFEREA_APPLICATION_H #include #include G_BEGIN_DECLS #define LIFEREA_APPLICATION_TYPE (liferea_application_get_type ()) G_DECLARE_FINAL_TYPE (LifereaApplication, liferea_application, LIFEREA, APPLICATION, GtkApplication) /** * liferea_application_new: (skip) * @argc: number of arguments * @argv: arguments * * Start a new GApplication representing Liferea * * Returns: exit code */ gint liferea_application_new (int argc, char *argv[]); /** * liferea_application_shutdown: * * Shutdown GApplication */ void liferea_application_shutdown (void); G_END_DECLS void liferea_application_rebuild_css (void); #endif liferea-1.13.7/src/main.c000066400000000000000000000030631415350204600150730ustar00rootroot00000000000000/** * @file main.c Liferea startup * * Copyright (C) 2003-2020 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include "common.h" #include "liferea_application.h" static void signal_handler (int sig) { liferea_application_shutdown (); } static void rebuild_css (int sig) { liferea_application_rebuild_css (); } int main (int argc, char *argv[]) { signal (SIGTERM, signal_handler); signal (SIGINT, signal_handler); #ifdef SIGHUP signal (SIGHUP, rebuild_css); #endif #ifdef ENABLE_NLS bindtextdomain (GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); #endif return liferea_application_new (argc, argv); } liferea-1.13.7/src/metadata.c000066400000000000000000000240471415350204600157340ustar00rootroot00000000000000/** * @file metadata.c handling of typed item and feed meta data * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2014 Lars Windolf * Copyright (C) 2015 Rich Coe * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include "common.h" #include "debug.h" #include "metadata.h" #include "xml.h" /* Metadata in Liferea are ordered lists of key/value list pairs. Both feed list nodes and items can have a list of metadata assigned. Metadata date values are always text values but maybe of different type depending on their usage type. */ static GHashTable *metadataTypes = NULL; /**< hash table with all registered meta data types */ struct pair { gchar *strid; /** metadata type id */ GSList *data; /** list of metadata values */ }; /* register metadata types to check validity on adding */ static void metadata_init (void) { g_assert (NULL == metadataTypes); metadataTypes = g_hash_table_new (g_str_hash, g_str_equal); /* generic types */ metadata_type_register ("author", METADATA_TYPE_HTML); metadata_type_register ("contributor", METADATA_TYPE_HTML); metadata_type_register ("copyright", METADATA_TYPE_HTML); metadata_type_register ("language", METADATA_TYPE_HTML); metadata_type_register ("pubDate", METADATA_TYPE_TEXT); metadata_type_register ("contentUpdateDate", METADATA_TYPE_TEXT); metadata_type_register ("managingEditor", METADATA_TYPE_HTML); metadata_type_register ("webmaster", METADATA_TYPE_HTML); metadata_type_register ("feedgenerator", METADATA_TYPE_HTML); metadata_type_register ("imageUrl", METADATA_TYPE_URL); metadata_type_register ("icon", METADATA_TYPE_URL); metadata_type_register ("homepage", METADATA_TYPE_URL); metadata_type_register ("textInput", METADATA_TYPE_HTML); metadata_type_register ("errorReportsTo", METADATA_TYPE_HTML); metadata_type_register ("feedgeneratorUri", METADATA_TYPE_URL); metadata_type_register ("category", METADATA_TYPE_HTML); metadata_type_register ("enclosure", METADATA_TYPE_TEXT); metadata_type_register ("commentsUri", METADATA_TYPE_URL); metadata_type_register ("commentFeedUri", METADATA_TYPE_URL); metadata_type_register ("feedTitle", METADATA_TYPE_HTML); metadata_type_register ("description", METADATA_TYPE_HTML); metadata_type_register ("richContent", METADATA_TYPE_HTML5); /* types for aggregation NS */ metadata_type_register ("agSource", METADATA_TYPE_URL); metadata_type_register ("agTimestamp", METADATA_TYPE_TEXT); /* types for blog channel */ metadata_type_register ("blogChannel", METADATA_TYPE_HTML); /* types for creative commons */ metadata_type_register ("license", METADATA_TYPE_HTML); /* types for Dublin Core (some dc tags are mapped to feed and item metadata types) */ metadata_type_register ("creator", METADATA_TYPE_HTML); metadata_type_register ("publisher", METADATA_TYPE_HTML); metadata_type_register ("type", METADATA_TYPE_HTML); metadata_type_register ("format", METADATA_TYPE_HTML); metadata_type_register ("identifier", METADATA_TYPE_HTML); metadata_type_register ("source", METADATA_TYPE_URL); metadata_type_register ("coverage", METADATA_TYPE_HTML); /* types for slash */ metadata_type_register ("slash", METADATA_TYPE_HTML); /* type for gravatars */ metadata_type_register ("gravatar", METADATA_TYPE_URL); /* for RSS 2.0 real source and newsbin real source info */ metadata_type_register ("realSourceUrl", METADATA_TYPE_URL); metadata_type_register ("realSourceTitle", METADATA_TYPE_URL); /* for trackback URL */ metadata_type_register ("related", METADATA_TYPE_URL); metadata_type_register ("via", METADATA_TYPE_URL); /* for georss:point */ metadata_type_register ("point", METADATA_TYPE_TEXT); /* for mediaRSS */ metadata_type_register ("mediadescription", METADATA_TYPE_HTML); metadata_type_register ("mediathumbnail", METADATA_TYPE_URL); metadata_type_register ("mediastarRatingcount", METADATA_TYPE_TEXT); metadata_type_register ("mediastarRatingavg", METADATA_TYPE_TEXT); metadata_type_register ("mediastarRatingmax", METADATA_TYPE_TEXT); metadata_type_register ("mediaviews", METADATA_TYPE_TEXT); return; } void metadata_type_register (const gchar *name, gint type) { if (!metadataTypes) metadata_init (); g_hash_table_insert (metadataTypes, (gpointer)name, GINT_TO_POINTER (type)); } gboolean metadata_is_type_registered (const gchar *strid) { if (!metadataTypes) metadata_init (); if (g_hash_table_lookup (metadataTypes, strid)) return TRUE; else return FALSE; } static gint metadata_get_type (const gchar *name) { gint type; if (!metadataTypes) metadata_init (); type = GPOINTER_TO_INT (g_hash_table_lookup (metadataTypes, (gpointer)name)); if (0 == type) g_warning ("Unknown metadata type: %s, please report this Liferea bug!", name); return type; } static gint metadata_value_cmp (gconstpointer a, gconstpointer b) { if (g_str_equal ((gchar *)a, (gchar *)b)) return 0; return 1; } GSList * metadata_list_append (GSList *metadata, const gchar *strid, const gchar *data) { GSList *iter = metadata; gchar *tmp, *checked_data = NULL; struct pair *p; if (!data) return metadata; /* lookup type and check format */ switch (metadata_get_type (strid)) { case METADATA_TYPE_TEXT: /* No check because renderer will process further */ checked_data = g_strdup (data); break; case METADATA_TYPE_URL: /* Simple sanity check to see if it doesn't break XML */ if (!strchr(data, '<') && !(strchr (data, '>')) && !(strchr (data, '&'))) { checked_data = g_strdup (data); } else { checked_data = (gchar *)common_uri_escape ((xmlChar *)data); } /* finally strip whitespace */ checked_data = g_strchomp (checked_data); break; default: g_warning ("Unknown metadata type: %s (id=%d), please report this Liferea bug! Treating as HTML.", strid, metadata_get_type (strid)); case METADATA_TYPE_HTML: /* Needs to check for proper XHTML */ if (xhtml_is_well_formed (data)) { tmp = g_strdup (data); } else { debug1 (DEBUG_PARSING, "not well formed HTML: %s", data); tmp = g_markup_escape_text (data, -1); debug1 (DEBUG_PARSING, "escaped as: %s", tmp); } /* And needs to remove DHTML */ checked_data = xhtml_strip_dhtml (tmp); g_free (tmp); break; case METADATA_TYPE_HTML5: /* Remove DHTML */ checked_data = g_strdup (data); break; } while (iter) { p = (struct pair*)iter->data; if (g_str_equal (p->strid, strid)) { /* Avoid duplicate values */ if (NULL == g_slist_find_custom (p->data, checked_data, metadata_value_cmp)) p->data = g_slist_append (p->data, checked_data); else g_free (checked_data); return metadata; } iter = iter->next; } p = g_new (struct pair, 1); p->strid = g_strdup (strid); p->data = g_slist_append (NULL, checked_data); metadata = g_slist_append (metadata, p); return metadata; } void metadata_list_set (GSList **metadata, const gchar *strid, const gchar *data) { GSList *iter = *metadata; struct pair *p; while (iter) { p = (struct pair*)iter->data; if (g_str_equal (p->strid, strid)) { g_slist_free_full (p->data, g_free); p->data = g_slist_append (NULL, g_strdup (data)); return; } iter = iter->next; } p = g_new (struct pair, 1); p->strid = g_strdup (strid); p->data = g_slist_append (NULL, g_strdup (data)); *metadata = g_slist_append (*metadata, p); } void metadata_list_foreach (GSList *metadata, metadataForeachFunc func, gpointer user_data) { GSList *list = metadata; guint index = 0; while (list) { struct pair *p = (struct pair*)list->data; GSList *values = (GSList *)p->data; while (values) { index++; (*func)(p->strid, values->data, index, user_data); values = g_slist_next (values); } list = list->next; } } GSList * metadata_list_get_values (GSList *metadata, const gchar *strid) { GSList *list = metadata; while (list) { struct pair *p = (struct pair*)list->data; if (g_str_equal (p->strid, strid)) return p->data; list = list->next; } return NULL; } const gchar * metadata_list_get (GSList *metadata, const gchar *strid) { GSList *values; values = metadata_list_get_values (metadata, strid); return values?values->data:NULL; } GSList * metadata_list_copy (GSList *list) { GSList *copy = NULL; GSList *iter2, *iter = list; struct pair *p; while (iter) { p = (struct pair*)iter->data; iter2 = p->data; while (iter2) { copy = metadata_list_append (copy, p->strid, iter2->data); iter2 = iter2->next; } iter = iter->next; } return copy; } void metadata_list_free (GSList *metadata) { GSList *iter = metadata; while (iter) { struct pair *p = (struct pair*)iter->data; g_slist_free_full (p->data, g_free); g_free (p->strid); g_free (p); iter = iter->next; } g_slist_free (metadata); } void metadata_add_xml_nodes (GSList *metadata, xmlNodePtr parentNode) { GSList *list = metadata; xmlNodePtr attribute; xmlNodePtr metadataNode = xmlNewChild (parentNode, NULL, BAD_CAST"attributes", NULL); while (list) { struct pair *p = (struct pair*)list->data; GSList *list2 = p->data; while (list2) { attribute = xmlNewTextChild (metadataNode, NULL, BAD_CAST"attribute", (xmlChar *)list2->data); xmlNewProp (attribute, BAD_CAST"name", (xmlChar *)p->strid); list2 = list2->next; } list = list->next; } } liferea-1.13.7/src/metadata.h000066400000000000000000000122571415350204600157410ustar00rootroot00000000000000/** * @file metadata.h handling of typed item and feed metadata * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2008-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _METADATA_H #define _METADATA_H #include #include #include "feed_parser.h" /* -------------------------------------------------------- */ /* interface definitions for namespace parsing handler */ /* -------------------------------------------------------- */ struct NsHandler; /** definition of various namespace tag handler */ typedef void (*registerNsFunc) (struct NsHandler *nsh, GHashTable *prefixhash, GHashTable *urihash); typedef void (*parseChannelTagFunc) (feedParserCtxtPtr ctxt, xmlNodePtr cur); typedef void (*parseItemTagFunc) (feedParserCtxtPtr ctxt, xmlNodePtr cur); /** struct used to register namespace handler */ typedef struct NsHandler { const gchar *prefix; /**< namespace prefix */ registerNsFunc registerNs; parseItemTagFunc parseItemTag; /**< item tag parsing method */ parseChannelTagFunc parseChannelTag; /**< channel tag parsing method */ } NsHandler; /** Metadata value types */ enum { METADATA_TYPE_TEXT = 1, /**< metadata can be any character data and needs escaping */ METADATA_TYPE_URL = 2, /**< metadata is an URL and guaranteed to be valid for use in XML */ METADATA_TYPE_HTML = 3, /**< metadata is XHTML content and valid to be embedded in output with Readability.js */ METADATA_TYPE_HTML5 = 4 /**< metadata is trusted HTML5 content and can be embedded in output with Readability.js */ }; /** * Register a metadata type. This allows type specific * sanity handling and detecting invalid metadata. * * @param name key name * @param type key type */ void metadata_type_register (const gchar *name, gint); /** * Checks whether a metadata type is registered * * @param strid the metadata type identifier * * @returns TRUE if the metadata type is registered, otherwise FALSE */ gboolean metadata_is_type_registered (const gchar *strid); /** * Appends a value to the value list of a specific metadata type * Don't mix this function with metadata_list_set() ! * * @param metadata the metadata list * @param strid the metadata type identifier * @param data data to add * * @returns the changed meta data list */ GSList * metadata_list_append(GSList *metadata, const gchar *strid, const gchar *data); /** * Sets (and overwrites if necessary) the value of a specific metadata type. * Don't mix this function with metadata_list_append() ! * * @param metadata the metadata list * @param strid the metadata type identifier * @param data data to add */ void metadata_list_set(GSList **metadata, const gchar *strid, const gchar *data); /** * Returns the first value of a given type from a specified metadata list. * Do use this function only for single instance types. * * @param metadata the metadata list * @param strid the metadata type identifier * * @returns the first value (or NULL) */ const gchar * metadata_list_get(GSList *metadata, const gchar *strid); /** * Definition of metadata foreach function * * @param key metadata type id * @param value metadata value * @param index metadata list ordering index * @param user_data user data */ typedef void (*metadataForeachFunc) (const gchar *key, const gchar *value, guint index, gpointer user_data); /** * Can be used to iterate over all key/value pairs of the given metadata list. * * @param metadata the metadata list * @param func callback function * @param user_data data to be passed to func */ void metadata_list_foreach(GSList *metadata, metadataForeachFunc func, gpointer user_data); /** * Returns a list of all values of a given type from a specified metadata list. * * @param metadata the metadata list * @param strid the metadata type identifier * * @returns a list of values (or NULL) */ GSList * metadata_list_get_values(GSList *metadata, const gchar *strid); /** * Creates a copy of a given metadata list. * * @param metadata the metadata list * * @returns the new list */ GSList * metadata_list_copy(GSList *list); /** * Frees all memory allocated by the given metadata list. * * @param metadata the metadata list */ void metadata_list_free(GSList *metadata); /** * Adds the given metadata list to a given XML document node. * To be used for saving feed metadata to cache. * * @param metadata the metadata list * @param parentNode the XML node */ void metadata_add_xml_nodes(GSList *metadata, xmlNodePtr parentNode); #endif liferea-1.13.7/src/migrate.c000066400000000000000000000103161415350204600155760ustar00rootroot00000000000000/** * @file migrate.c migration between different cache versions * * Copyright (C) 2007-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "migrate.h" #include #include #include #include "common.h" #include "db.h" #include "debug.h" #include "export.h" /** * Copy a from $HOME//subdir to a target directory /subdir. * * @param from relative base path in $HOME (e.g. ".liferea_1.4") * @param to absolute target base path (e.g. "/home/joe/.config") * @param subdir subdir to copy from source to destination (can be empty string) */ static void migrate_copy_dir (const gchar *from, const gchar *to, const gchar *subdir) { gchar *fromDirname, *toDirname; gchar *srcfile, *destfile; GDir *dir; g_print ("Processing %s%s...\n", from, subdir); fromDirname = g_build_filename (g_get_home_dir (), from, subdir, NULL); toDirname = g_build_filename (to, subdir, NULL); dir = g_dir_open (fromDirname, 0, NULL); while (NULL != (srcfile = (gchar *)g_dir_read_name (dir))) { destfile = g_build_filename (toDirname, srcfile, NULL); srcfile = g_build_filename (fromDirname, srcfile, NULL); if (g_file_test (srcfile, G_FILE_TEST_IS_REGULAR)) { g_print ("copying %s\n to %s\n", srcfile, destfile); common_copy_file (srcfile, destfile); } else { g_print("skipping %s\n", srcfile); } g_free (destfile); g_free (srcfile); } g_dir_close(dir); g_free (fromDirname); g_free (toDirname); } static void migrate_from_14plus (const gchar *oldBaseDir, nodePtr node) { GFile *sourceDbFile, *targetDbFile; gchar *newConfigDir, *newCacheDir, *newDataDir, *oldCacheDir, *filename; g_print("Performing %s -> XDG cache migration...\n", oldBaseDir); /* 1.) Close already loaded DB */ db_deinit (); /* 2.) Copy all files */ newCacheDir = g_build_filename (g_get_user_cache_dir(), "liferea", NULL); newConfigDir = g_build_filename (g_get_user_config_dir(), "liferea", NULL); newDataDir = g_build_filename (g_get_user_data_dir(), "liferea", NULL); oldCacheDir = g_build_filename (oldBaseDir, "cache", NULL); migrate_copy_dir (oldBaseDir, newConfigDir, ""); migrate_copy_dir (oldCacheDir, newCacheDir, G_DIR_SEPARATOR_S "favicons"); migrate_copy_dir (oldCacheDir, newCacheDir, G_DIR_SEPARATOR_S "plugins"); /* 3.) Move DB to from new config dir to cache dir instead (this is caused by the batch copy in step 2.) */ sourceDbFile = g_file_new_for_path (g_build_filename (newConfigDir, "liferea.db", NULL)); targetDbFile = g_file_new_for_path (g_build_filename (newDataDir, "liferea.db", NULL)); g_file_move (sourceDbFile, targetDbFile, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, NULL); g_object_unref (sourceDbFile); g_object_unref (targetDbFile); /* 3.) And reopen the copied DB */ db_init (); /* 4.) Migrate file feed list into DB */ filename = common_create_config_filename ("feedlist.opml"); if (!import_OPML_feedlist (filename, node, FALSE, TRUE)) g_error ("Fatal: Feed list migration failed!"); g_free (filename); g_free (newConfigDir); g_free (newCacheDir); g_free (oldCacheDir); } void migration_execute (migrationMode mode, nodePtr node) { switch (mode) { case MIGRATION_FROM_14: migrate_from_14plus (".liferea_1.4", node); break; case MIGRATION_FROM_16: migrate_from_14plus (".liferea_1.6", node); break; case MIGRATION_FROM_18: migrate_from_14plus (".liferea_1.8", node); break; case MIGRATION_MODE_INVALID: default: g_error ("Invalid migration mode!"); return; break; } } liferea-1.13.7/src/migrate.h000066400000000000000000000023171415350204600156050ustar00rootroot00000000000000/** * @file migrate.h migration between different cache versions * * Copyright (C) 2007-2011 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _MIGRATE_H #define _MIGRATE_H #include "node.h" typedef enum { MIGRATION_MODE_INVALID = 0, MIGRATION_FROM_14, MIGRATION_FROM_16, MIGRATION_FROM_18 } migrationMode; /** * Performs a migration for the given migration mode. * * @param mode migration mode * @param node feed list root node */ void migration_execute (migrationMode mode, nodePtr node); #endif liferea-1.13.7/src/net.c000066400000000000000000000406551415350204600147450ustar00rootroot00000000000000/** * @file net.c HTTP network access using libsoup * * Copyright (C) 2007-2021 Lars Windolf * Copyright (C) 2009 Emilio Pozuelo Monfort * Copyright (C) 2021 Lorenzo L. Ancora * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "net.h" #include #include #include #include #include #include #include #include #include "common.h" #include "conf.h" #include "debug.h" #define HOMEPAGE "https://lzone.de/liferea/" static SoupSession *session = NULL; /* Session configured for preferences */ static SoupSession *session2 = NULL; /* Session for "Don't use proxy feature" */ static ProxyDetectMode proxymode = PROXY_DETECT_MODE_AUTO; static gchar *proxyname = NULL; static gchar *proxyusername = NULL; static gchar *proxypassword = NULL; static int proxyport = 0; static void network_process_redirect_callback (SoupMessage *msg, gpointer user_data) { updateJobPtr job = (updateJobPtr)user_data; const gchar *location = NULL; SoupURI *newuri; if (301 == msg->status_code || 308 == msg->status_code) { location = soup_message_headers_get_one (msg->response_headers, "Location"); newuri = soup_uri_new (location); if (SOUP_URI_IS_VALID (newuri) && ! soup_uri_equal (newuri, soup_message_get_uri (msg))) { debug2 (DEBUG_NET, "\"%s\" permanently redirects to new location \"%s\"", soup_uri_to_string (soup_message_get_uri (msg), FALSE), soup_uri_to_string (newuri, FALSE)); job->result->httpstatus = msg->status_code; job->result->source = soup_uri_to_string (newuri, FALSE); } } } static void network_process_callback (SoupSession *session, SoupMessage *msg, gpointer user_data) { updateJobPtr job = (updateJobPtr)user_data; SoupDate *last_modified; const gchar *tmp = NULL; GHashTable *params; gboolean revalidated = FALSE; gint maxage; gint age; job->result->source = soup_uri_to_string (soup_message_get_uri(msg), FALSE); job->result->httpstatus = msg->status_code; /* keep some request headers for revalidated responses */ revalidated = (304 == job->result->httpstatus); debug1 (DEBUG_NET, "download status code: %d", msg->status_code); debug1 (DEBUG_NET, "source after download: >>>%s<<<", job->result->source); #ifdef HAVE_G_MEMDUP2 job->result->data = g_memdup2 (msg->response_body->data, msg->response_body->length+1); #else job->result->data = g_memdup (msg->response_body->data, msg->response_body->length+1); #endif job->result->size = (size_t)msg->response_body->length; debug1 (DEBUG_NET, "%d bytes downloaded", job->result->size); job->result->contentType = g_strdup (soup_message_headers_get_content_type (msg->response_headers, NULL)); /* Update last-modified date */ if (revalidated) { job->result->updateState->lastModified = update_state_get_lastmodified (job->request->updateState); } else { tmp = soup_message_headers_get_one (msg->response_headers, "Last-Modified"); if (tmp) { /* The string may be badly formatted, which will make * soup_date_new_from_string() return NULL */ last_modified = soup_date_new_from_string (tmp); if (last_modified) { job->result->updateState->lastModified = soup_date_to_time_t (last_modified); soup_date_free (last_modified); } } } /* Update ETag value */ if (revalidated) { job->result->updateState->etag = g_strdup (update_state_get_etag (job->request->updateState)); } else { tmp = soup_message_headers_get_one (msg->response_headers, "ETag"); if (tmp) { job->result->updateState->etag = g_strdup (tmp); } } /* Update cache max-age */ tmp = soup_message_headers_get_list (msg->response_headers, "Cache-Control"); if (tmp) { params = soup_header_parse_param_list (tmp); if (params) { tmp = g_hash_table_lookup (params, "max-age"); if (tmp) { maxage = atoi (tmp); if (0 < maxage) { /* subtract Age from max-age */ tmp = soup_message_headers_get_one (msg->response_headers, "Age"); if (tmp) { age = atoi (tmp); if (0 < age) { maxage = maxage - age; } } if (0 < maxage) { job->result->updateState->maxAgeMinutes = ceil ( (float) (maxage / 60)); } } } } soup_header_free_param_list (params); } update_process_finished_job (job); } /* Downloads a feed specified in the request structure, returns the downloaded data or NULL in the request structure. If the webserver reports a permanent redirection, the feed url will be modified and the old URL 'll be freed. The request structure will also contain the HTTP status and the last modified string. */ void network_process_request (const updateJobPtr job) { SoupMessage *msg; SoupDate *date; gboolean do_not_track = FALSE; g_assert (NULL != job->request); debug1 (DEBUG_NET, "downloading %s", job->request->source); if (job->request->postdata && (debug_level & DEBUG_VERBOSE) && (debug_level & DEBUG_NET)) debug1 (DEBUG_NET, " postdata=>>>%s<<<", job->request->postdata); /* Prepare the SoupMessage */ msg = soup_message_new (job->request->postdata ? SOUP_METHOD_POST : SOUP_METHOD_GET, job->request->source); if (!msg) { g_warning ("The request for %s could not be parsed!", job->request->source); return; } /* Set the postdata for the request */ if (job->request->postdata) { soup_message_set_request (msg, "application/x-www-form-urlencoded", SOUP_MEMORY_STATIC, /* libsoup won't free the postdata */ job->request->postdata, strlen (job->request->postdata)); } /* Set the If-Modified-Since: header */ if (job->request->updateState && update_state_get_lastmodified (job->request->updateState)) { gchar *datestr; date = soup_date_new_from_time_t (update_state_get_lastmodified (job->request->updateState)); datestr = soup_date_to_string (date, SOUP_DATE_HTTP); soup_message_headers_append (msg->request_headers, "If-Modified-Since", datestr); g_free (datestr); soup_date_free (date); } /* Set the If-None-Match header */ if (job->request->updateState && update_state_get_etag (job->request->updateState)) { soup_message_headers_append(msg->request_headers, "If-None-Match", update_state_get_etag (job->request->updateState)); } /* Set the I-AM header */ if (job->request->updateState && (update_state_get_lastmodified (job->request->updateState) || update_state_get_etag (job->request->updateState))) { soup_message_headers_append(msg->request_headers, "A-IM", "feed"); } /* Support HTTP content negotiation */ soup_message_headers_append(msg->request_headers, "Accept", "application/atom+xml,application/xml;q=0.9,text/xml;q=0.8,*/*;q=0.7"); /* Set the authentication */ if (!job->request->authValue && job->request->options && job->request->options->username && job->request->options->password) { SoupURI *uri = soup_message_get_uri (msg); soup_uri_set_user (uri, job->request->options->username); soup_uri_set_password (uri, job->request->options->password); } if (job->request->authValue) { soup_message_headers_append (msg->request_headers, "Authorization", job->request->authValue); } /* Add requested cookies */ if (job->request->updateState && job->request->updateState->cookies) { soup_message_headers_append (msg->request_headers, "Cookie", job->request->updateState->cookies); soup_message_disable_feature (msg, SOUP_TYPE_COOKIE_JAR); } /* TODO: Right now we send the msg, and if it requires authentication and * we didn't provide one, the petition fails and when the job is processed * it sees it needs authentication and displays a dialog, and if credentials * are entered, it queues a new job with auth credentials. Instead of that, * we should probably handle authentication directly here, connecting the * msg to a callback in case of 401 (see soup_message_add_status_code_handler()) * displaying the dialog ourselves, and requeing the msg if we get credentials */ /* Add Do Not Track header according to settings */ conf_get_bool_value (DO_NOT_TRACK, &do_not_track); if (do_not_track) soup_message_headers_append (msg->request_headers, "DNT", "1"); /* Process permanent redirects (update feed location) */ soup_message_add_status_code_handler (msg, "got_body", 301, (GCallback) network_process_redirect_callback, job); soup_message_add_status_code_handler (msg, "got_body", 308, (GCallback) network_process_redirect_callback, job); /* If the feed has "dont use a proxy" selected, use 'session2' which is non-proxy */ if (job->request->options && job->request->options->dontUseProxy) soup_session_queue_message (session2, msg, network_process_callback, job); else soup_session_queue_message (session, msg, network_process_callback, job); } static void network_authenticate ( SoupSession *session, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer data) { if (!retrying && msg->status_code == SOUP_STATUS_PROXY_UNAUTHORIZED) { soup_auth_authenticate (auth, g_strdup (proxyusername), g_strdup (proxypassword)); } // FIXME: Handle HTTP 401 too } static void network_set_soup_session_proxy (SoupSession *session, ProxyDetectMode mode, const gchar *host, guint port, const gchar *user, const gchar *password) { SoupURI *uri = NULL; switch (mode) { case PROXY_DETECT_MODE_AUTO: /* Sets proxy-resolver to the default resolver, this unsets proxy-uri. */ g_object_set (G_OBJECT (session), SOUP_SESSION_PROXY_RESOLVER, g_proxy_resolver_get_default (), NULL); break; case PROXY_DETECT_MODE_NONE: /* Sets proxy-resolver to NULL, this unsets proxy-uri. */ g_object_set (G_OBJECT (session), SOUP_SESSION_PROXY_RESOLVER, NULL, NULL); break; case PROXY_DETECT_MODE_MANUAL: uri = soup_uri_new (NULL); soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTP); soup_uri_set_host (uri, host); soup_uri_set_port (uri, port); soup_uri_set_user (uri, user); soup_uri_set_password (uri, password); soup_uri_set_path (uri, "/"); if (SOUP_URI_IS_VALID (uri)) { /* Sets proxy-uri, this unsets proxy-resolver. */ g_object_set (G_OBJECT (session), SOUP_SESSION_PROXY_URI, uri, NULL); } soup_uri_free (uri); break; } } void network_init (void) { gchar *useragent; SoupCookieJar *cookies; gchar *filename; SoupLogger *logger; gchar const *sysua = g_getenv("LIFEREA_UA"); if(sysua == NULL) { bool anonua = g_getenv("LIFEREA_UA_ANONYMOUS") != NULL; if(anonua == false) { /* Set an appropriate user agent, * e.g. "Liferea/1.10.0 (Linux; https://lzone.de/liferea/) AppleWebKit (KHTML, like Gecko)" */ useragent = g_strdup_printf ("Liferea/%s (%s; %s) AppleWebKit (KHTML, like Gecko)", VERSION, OSNAME, HOMEPAGE); } else { /* Set an anonymized, randomic user agent, * e.g. "Liferea/0.28.0 (https://lzone.de/liferea/) AppleWebKit (KHTML, like Gecko)" */ useragent = g_strdup_printf ("Liferea/%.2f.0 (%s) AppleWebKit (KHTML, like Gecko)", g_random_double(), HOMEPAGE); } } else { /* Set an arbitrary user agent from the environment variable LIFEREA_UA */ useragent = g_strdup (sysua); } sysua = NULL; g_assert_nonnull(useragent); debug1 (DEBUG_NET, "HTTP/S user-agent set to \"%s\"", useragent); /* Session cookies */ filename = common_create_config_filename ("session_cookies.txt"); cookies = soup_cookie_jar_text_new (filename, TRUE); g_free (filename); /* Initialize libsoup */ session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, useragent, SOUP_SESSION_TIMEOUT, 120, SOUP_SESSION_IDLE_TIMEOUT, 30, SOUP_SESSION_ADD_FEATURE, cookies, NULL); session2 = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, useragent, SOUP_SESSION_TIMEOUT, 120, SOUP_SESSION_IDLE_TIMEOUT, 30, SOUP_SESSION_ADD_FEATURE, cookies, SOUP_SESSION_PROXY_URI, NULL, SOUP_SESSION_PROXY_RESOLVER, NULL, NULL); /* Only 'session' gets proxy, 'session2' is for non-proxy requests */ network_set_soup_session_proxy (session, network_get_proxy_detect_mode(), network_get_proxy_host (), network_get_proxy_port (), network_get_proxy_username (), network_get_proxy_password ()); g_signal_connect (session, "authenticate", G_CALLBACK (network_authenticate), NULL); /* Soup debugging */ if (debug_level & DEBUG_NET) { logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS, -1); soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger)); } g_free (useragent); } void network_deinit (void) { g_free (proxyname); g_free (proxyusername); g_free (proxypassword); } ProxyDetectMode network_get_proxy_detect_mode (void) { return proxymode; } const gchar * network_get_proxy_host (void) { return proxyname; } guint network_get_proxy_port (void) { return proxyport; } const gchar * network_get_proxy_username (void) { return proxyusername; } const gchar * network_get_proxy_password (void) { return proxypassword; } extern void network_monitor_proxy_changed (void); void network_set_proxy (ProxyDetectMode mode, gchar *host, guint port, gchar *user, gchar *password) { g_free (proxyname); g_free (proxyusername); g_free (proxypassword); proxymode = mode; proxyname = host; proxyport = port; proxyusername = user; proxypassword = password; /* session will be NULL if we were called from conf_init() as that's called * before net_init() */ if (session) network_set_soup_session_proxy (session, mode, host, port, user, password); debug4 (DEBUG_NET, "proxy set to http://%s:%s@%s:%d", user, password, host, port); network_monitor_proxy_changed (); } const char * network_strerror (gint status) { const gchar *tmp = NULL; switch (status) { /* Some libsoup transport errors */ case SOUP_STATUS_NONE: tmp = _("The update request was cancelled"); break; case SOUP_STATUS_CANT_RESOLVE: tmp = _("Unable to resolve destination host name"); break; case SOUP_STATUS_CANT_RESOLVE_PROXY: tmp = _("Unable to resolve proxy host name"); break; case SOUP_STATUS_CANT_CONNECT: tmp = _("Unable to connect to remote host"); break; case SOUP_STATUS_CANT_CONNECT_PROXY: tmp = _("Unable to connect to proxy"); break; case SOUP_STATUS_SSL_FAILED: tmp = _("SSL/TLS negotiation failed. Possible outdated or unsupported encryption algorithm. Check your operating system settings."); break; /* http 3xx redirection */ case SOUP_STATUS_MOVED_PERMANENTLY: tmp = _("The resource moved permanently to a new location"); break; /* http 4xx client error */ case SOUP_STATUS_UNAUTHORIZED: tmp = _("You are unauthorized to download this feed. Please update your username and " "password in the feed properties dialog box"); break; case SOUP_STATUS_PAYMENT_REQUIRED: tmp = _("Payment required"); break; case SOUP_STATUS_FORBIDDEN: tmp = _("You're not allowed to access this resource"); break; case SOUP_STATUS_NOT_FOUND: tmp = _("Resource Not Found"); break; case SOUP_STATUS_METHOD_NOT_ALLOWED: tmp = _("Method Not Allowed"); break; case SOUP_STATUS_NOT_ACCEPTABLE: tmp = _("Not Acceptable"); break; case SOUP_STATUS_PROXY_UNAUTHORIZED: tmp = _("Proxy authentication required"); break; case SOUP_STATUS_REQUEST_TIMEOUT: tmp = _("Request timed out"); break; case SOUP_STATUS_GONE: tmp = _("The webserver indicates this feed is discontinued. It's no longer available. Liferea won't update it anymore but you can still access the cached headlines."); break; } if (!tmp) { if (SOUP_STATUS_IS_TRANSPORT_ERROR (status)) { tmp = _("There was an internal error in the update process"); } else if (SOUP_STATUS_IS_REDIRECTION (status)) { tmp = _("Feed not available: Server requested unsupported redirection!"); } else if (SOUP_STATUS_IS_CLIENT_ERROR (status)) { tmp = _("Client Error"); } else if (SOUP_STATUS_IS_SERVER_ERROR (status)) { tmp = _("Server Error"); } else { tmp = _("An unknown networking error happened!"); } } g_assert (tmp); return tmp; } liferea-1.13.7/src/net.h000066400000000000000000000054761415350204600147540ustar00rootroot00000000000000/** * @file net.h HTTP network access interface * * Copyright (C) 2007-2021 Lars Windolf * Copyright (C) 2009 Emilio Pozuelo Monfort * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NET_H #define _NET_H #include #include "update.h" /* Simple glue layer to abstract network code */ /** * Initialize HTTP client networking support. */ void network_init (void); /** * Cleanup HTTP client networking support. */ void network_deinit (void); typedef enum { PROXY_DETECT_MODE_AUTO = 0, /* Use system settings */ PROXY_DETECT_MODE_NONE, /* No Proxy */ PROXY_DETECT_MODE_MANUAL /* Manually configured proxy */ } ProxyDetectMode; /** * Configures the network client to use the given proxy * settings. If the host name is NULL then no proxy will * be used. * * @param mode indicate whether to use the system setting, no proxy or the following parameters. * @param host the new proxy host * @param port the new proxy port * @param user the new proxy username or NULL * @param password the new proxy password or NULL */ void network_set_proxy (ProxyDetectMode mode, gchar *host, guint port, gchar *user, gchar *password); /** * Returns the proxy detect mode. * */ ProxyDetectMode network_get_proxy_detect_mode (void); /** * Returns the currently configured proxy host. * * @returns the proxy host */ const gchar * network_get_proxy_host (void); /** * Returns the currently configured proxy port. * * @returns the proxy port */ guint network_get_proxy_port (void); /** * Returns the currently configured proxy user name * * @returns the proxy user name (or NULL) */ const gchar * network_get_proxy_username (void); /** * Returns the currently configured proxy password. * * @returns the proxy password (or NULL) */ const gchar * network_get_proxy_password (void); /** * Process the given update job. * * @param request the update request */ void network_process_request (const updateJobPtr job); /** * Returns explanation string for the given network error code. * * @param status libsoup status code * * @returns explanation string */ const char * network_strerror (gint status); #endif liferea-1.13.7/src/net_monitor.c000066400000000000000000000126631415350204600165120ustar00rootroot00000000000000/** * @file network_monitor.c network status monitor * * Copyright (C) 2009-2020 Lars Windolf * Copyright (C) 2010 Emilio Pozuelo Monfort * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "net_monitor.h" #include #include "debug.h" #include "net.h" struct NetworkMonitorPrivate { GDBusConnection *conn; guint subscription_id; gboolean online; }; G_DEFINE_TYPE_WITH_CODE (NetworkMonitor, network_monitor, G_TYPE_OBJECT, G_ADD_PRIVATE (NetworkMonitor)) enum { ONLINE_STATUS_CHANGED, PROXY_CHANGED, LAST_SIGNAL }; static guint network_monitor_signals[LAST_SIGNAL] = { 0 }; static GObjectClass *parent_class = NULL; static NetworkMonitor *network_monitor = NULL; static void network_monitor_finalize (GObject *object) { NetworkMonitor *self = NETWORK_MONITOR (object); debug0 (DEBUG_NET, "network manager: unregistering network state change callback"); if (self->priv->conn && self->priv->subscription_id) { g_dbus_connection_signal_unsubscribe (self->priv->conn, self->priv->subscription_id); } network_deinit (); G_OBJECT_CLASS (parent_class)->finalize (object); } static void network_monitor_class_init (NetworkMonitorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); parent_class = g_type_class_peek_parent (klass); object_class->finalize = network_monitor_finalize; network_monitor_signals [ONLINE_STATUS_CHANGED] = g_signal_new ("online-status-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN); network_monitor_signals [PROXY_CHANGED] = g_signal_new ("proxy-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static gboolean is_nm_connected (guint state) { if (state == 60 || /* NM_STATE_CONNECTED_SITE */ state == 70) /* NM_STATE_CONNECTED_GLOBAL */ return TRUE; return FALSE; } static void on_network_state_changed_cb (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { gboolean online = network_monitor_is_online (); guint state; g_variant_get (parameters, "(u)", &state); if (online && !is_nm_connected (state)) { debug0 (DEBUG_NET, "network manager: no network connection -> going offline"); network_monitor_set_online (FALSE); } else if (!online && is_nm_connected (state)) { debug0 (DEBUG_NET, "network manager: active connection -> going online"); network_monitor_set_online (TRUE); } } void network_monitor_set_online (gboolean mode) { if (network_monitor->priv->online != mode) { network_monitor->priv->online = mode; debug1 (DEBUG_NET, "Changing online mode to %s", mode?"online":"offline"); g_signal_emit (network_monitor, network_monitor_signals[ONLINE_STATUS_CHANGED], 0, mode); } } gboolean network_monitor_is_online (void) { if (!network_monitor) return FALSE; return network_monitor->priv->online; } void network_monitor_proxy_changed (void) { if (!network_monitor) return; g_signal_emit (network_monitor, network_monitor_signals[PROXY_CHANGED], 0, NULL); } static void on_bus_get_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { NetworkMonitor *self = NETWORK_MONITOR (user_data); GError *error = NULL; self->priv->conn = g_bus_get_finish (result, &error); if (!self->priv->conn) { debug1 (DEBUG_NET, "Could not connect to system bus: %s", error->message); g_error_free (error); return; } g_dbus_connection_set_exit_on_close (self->priv->conn, FALSE); debug0 (DEBUG_NET, "network manager: connecting to StateChanged signal"); self->priv->subscription_id = g_dbus_connection_signal_subscribe (self->priv->conn, "org.freedesktop.NetworkManager", "org.freedesktop.NetworkManager", "StateChanged", NULL, NULL, G_DBUS_SIGNAL_FLAGS_NONE, on_network_state_changed_cb, self, NULL); debug1 (DEBUG_NET, "network manager: connected to StateChanged signal: %s", self->priv->subscription_id ? "yes" : "no"); } static void network_monitor_init (NetworkMonitor *nm) { nm->priv = network_monitor_get_instance_private (nm); nm->priv->online = TRUE; /* For now accessing the network monitor also sets up the network! */ network_init (); g_bus_get (G_BUS_TYPE_SYSTEM, NULL, on_bus_get_cb, nm); } NetworkMonitor * network_monitor_get (void) { if (G_UNLIKELY (!network_monitor)) network_monitor = NETWORK_MONITOR (g_object_new (NETWORK_MONITOR_TYPE, NULL)); return network_monitor; } liferea-1.13.7/src/net_monitor.h000066400000000000000000000046561415350204600165220ustar00rootroot00000000000000/** * @file network_monitor.c network status monitor * * Copyright (C) 2009 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NETWORK_MONITOR_H #define _NETWORK_MONITOR_H #include #include G_BEGIN_DECLS typedef struct NetworkMonitor NetworkMonitor; typedef struct NetworkMonitorClass NetworkMonitorClass; typedef struct NetworkMonitorPrivate NetworkMonitorPrivate; struct NetworkMonitor { GObject parent; /*< private >*/ NetworkMonitorPrivate *priv; }; struct NetworkMonitorClass { GObjectClass parent; }; GType network_monitor_get_type (void); #define NETWORK_MONITOR_TYPE (network_monitor_get_type ()) #define NETWORK_MONITOR(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), NETWORK_MONITOR_TYPE, NetworkMonitor)) #define NETWORK_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NETWORK_MONITOR_TYPE, NetworkMonitorClass)) #define IS_NETWORK_MONITOR(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), NETWORK_MONITOR_TYPE)) #define IS_NETWORK_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NETWORK_MONITOR_TYPE)) #define NETWORK_MONITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NETWORK_MONITOR_TYPE, NetworkMonitorClass)) /** * Returns the network monitor object. Creates it if * necessary first. * * @returns the network monitor */ NetworkMonitor* network_monitor_get (void); /** * Sets the online status according to mode. * * @param mode TRUE for online, FALSE for offline */ void network_monitor_set_online (gboolean mode); /** * Queries the online status. * * @return TRUE if online */ gboolean network_monitor_is_online (void); /** * Called by networking when proxy was changed. */ void network_monitor_proxy_changed (void); #endif liferea-1.13.7/src/newsbin.c000066400000000000000000000151651415350204600156220ustar00rootroot00000000000000/** * @file newsbin.c news bin node type implementation * * Copyright (C) 2006-2016 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "newsbin.h" #include #include "common.h" #include "db.h" #include "feed.h" #include "feedlist.h" #include "itemlist.h" #include "metadata.h" #include "render.h" #include "ui/icons.h" #include "ui/feed_list_view.h" #include "ui/liferea_dialog.h" static GSList * newsbin_list = NULL; GSList * newsbin_get_list (void) { return newsbin_list; } static void newsbin_import (nodePtr node, nodePtr parent, xmlNodePtr cur, gboolean trusted) { xmlChar *tmp; feed_get_node_type ()->import (node, parent, cur, trusted); /* but we don't need a subscription (created by feed_import()) */ g_free (node->subscription); node->subscription = NULL; tmp = xmlGetProp (cur, BAD_CAST"alwaysShowInReducedMode"); if (tmp && !xmlStrcmp (tmp, BAD_CAST"true")) ((feedPtr)node->data)->alwaysShowInReduced = TRUE; xmlFree (tmp); ((feedPtr)node->data)->cacheLimit = CACHE_UNLIMITED; newsbin_list = g_slist_append(newsbin_list, node); } static void newsbin_export (nodePtr node, xmlNodePtr xml, gboolean trusted) { feedPtr feed = (feedPtr) node->data; if (trusted) { if (feed->alwaysShowInReduced) xmlNewProp (xml, BAD_CAST"alwaysShowInReducedMode", BAD_CAST"true"); } } static void newsbin_remove (nodePtr node) { newsbin_list = g_slist_remove(newsbin_list, node); feed_get_node_type()->remove(node); } static gchar * newsbin_render (nodePtr node) { gchar *output = NULL; xmlDocPtr doc; doc = feed_to_xml(node, NULL); output = render_xml(doc, "newsbin", NULL); xmlFreeDoc(doc); return output; } static void on_newsbin_common_btn_clicked (GtkButton *button, gpointer user_data) { GtkWidget *dialog = gtk_widget_get_toplevel (GTK_WIDGET (button)); GtkWidget *nameentry = liferea_dialog_lookup (dialog, "newsbinnameentry"); GtkWidget *showinreduced = liferea_dialog_lookup (dialog, "newsbinalwaysshowinreduced"); nodePtr newsbin = (nodePtr) user_data; gboolean newly_created = FALSE; if (newsbin == NULL) { newsbin = node_new (newsbin_get_node_type ()); node_set_data (newsbin, (gpointer)feed_new ()); newsbin_list = g_slist_append(newsbin_list, newsbin); newly_created = TRUE; } node_set_title (newsbin, (gchar *)gtk_entry_get_text (GTK_ENTRY (nameentry))); if (newsbin->data) { ((feedPtr)newsbin->data)->alwaysShowInReduced = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (showinreduced)); } if (newly_created) { feedlist_node_added (newsbin); } feedlist_schedule_save (); gtk_widget_destroy (dialog); } static gboolean ui_newsbin_common (nodePtr node) { GtkWidget *dialog = liferea_dialog_new ("new_newsbin"); GtkWidget *nameentry = liferea_dialog_lookup (dialog, "newsbinnameentry"); GtkWidget *showinreduced = liferea_dialog_lookup (dialog, "newsbinalwaysshowinreduced"); GtkWidget *okbutton = liferea_dialog_lookup (dialog, "newnewsbinbtn"); if (node) { gtk_window_set_title (GTK_WINDOW (dialog), _("News Bin Properties")); gtk_entry_set_text (GTK_ENTRY (nameentry), node_get_title (node)); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (showinreduced), ((feedPtr)node->data)->alwaysShowInReduced); } else { gtk_window_set_title (GTK_WINDOW (dialog), _("Create News Bin")); gtk_entry_set_text (GTK_ENTRY (nameentry), ""); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (showinreduced), FALSE); } g_signal_connect (G_OBJECT (okbutton), "clicked", G_CALLBACK (on_newsbin_common_btn_clicked), node); gtk_window_present (GTK_WINDOW (dialog)); return TRUE; } static gboolean ui_newsbin_add (void) { return ui_newsbin_common(NULL); } static void ui_newsbin_properties (nodePtr node) { ui_newsbin_common(node); } void on_action_copy_to_newsbin (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr newsbin; itemPtr item = NULL, copy; guint32 newsbin_index; guint64 item_id; gboolean maybe_item_id; g_variant_get (parameter, "(umt)", &newsbin_index, &maybe_item_id, &item_id); if (maybe_item_id) item = item_load (item_id); else item = itemlist_get_selected(); newsbin = g_slist_nth_data(newsbin_list, newsbin_index); if(item) { copy = item_copy(item); copy->nodeId = newsbin->id; /* necessary to become independent of original item */ copy->parentNodeId = g_strdup (item->nodeId); /* To avoid item doubling in vfolders we reset simple vfolder match attributes */ copy->readStatus = TRUE; copy->flagStatus = FALSE; /* To provide a hint in the rendered output what the orginial feed was the original website link/title are added */ if(!metadata_list_get (copy->metadata, "realSourceUrl")) metadata_list_set (&(copy->metadata), "realSourceUrl", node_get_base_url(node_from_id(item->nodeId))); if(!metadata_list_get (copy->metadata, "realSourceTitle")) metadata_list_set (&(copy->metadata), "realSourceTitle", node_get_title(node_from_id(item->nodeId))); /* do the same as in node_merge_item(s) */ db_item_update(copy); node_update_counters(newsbin); } } nodeTypePtr newsbin_get_node_type (void) { static nodeTypePtr nodeType; if (!nodeType) { /* derive the plugin node type from the folder node type */ nodeType = g_new0 (struct nodeType, 1); nodeType->capabilities = NODE_CAPABILITY_RECEIVE_ITEMS | NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_SHOW_ITEM_COUNT; nodeType->id = "newsbin"; nodeType->icon = icon_get (ICON_NEWSBIN); nodeType->load = feed_get_node_type()->load; nodeType->import = newsbin_import; nodeType->export = newsbin_export; nodeType->save = feed_get_node_type()->save; nodeType->update_counters = feed_get_node_type()->update_counters; nodeType->remove = newsbin_remove; nodeType->render = newsbin_render; nodeType->request_add = ui_newsbin_add; nodeType->request_properties = ui_newsbin_properties; nodeType->free = feed_get_node_type()->free; } return nodeType; } liferea-1.13.7/src/newsbin.h000066400000000000000000000033021415350204600156150ustar00rootroot00000000000000/** * @file newsbin.h news bin node type implementation * * Copyright (C) 2006 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NEWSBIN_H #define _NEWSBIN_H #include "node_type.h" /** * Returns a list of the names of all news bins */ GSList * newsbin_get_list(void); /** * on_action_copy_to_newsbin: (skip) * @action: the action that emitted the signal * @parameter: a GVariant of type "(umt)", first value is the index of the * newsbin in the list, second is optionnal item id. If no item id is * given the selected item is used. * @user_data: unused * * Activate callback for the "copy-item-to-newsbin" action. * Copy the selected item to the specified newsbin. */ void on_action_copy_to_newsbin(GSimpleAction *action, GVariant *parameter, gpointer user_data); /* implementation of the node type interface */ #define IS_NEWSBIN(node) (node->type == newsbin_get_node_type ()) /** * Returns the news bin node type implementation. */ nodeTypePtr newsbin_get_node_type (void); #endif liferea-1.13.7/src/node.c000066400000000000000000000315251415350204600151000ustar00rootroot00000000000000/* * @file node.c hierarchic feed list node handling * * Copyright (C) 2003-2018 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "favicon.h" #include "feedlist.h" #include "itemlist.h" #include "itemset.h" #include "item_state.h" #include "node.h" #include "node_view.h" #include "render.h" #include "subscription_icon.h" #include "update.h" #include "vfolder.h" #include "fl_sources/node_source.h" #include "ui/feed_list_view.h" #include "ui/liferea_shell.h" static GHashTable *nodes = NULL; /*<< node id -> node lookup table */ #define NODE_ID_LEN 7 nodePtr node_is_used_id (const gchar *id) { if (!id || !nodes) return NULL; return (nodePtr)g_hash_table_lookup (nodes, id); } gchar * node_new_id (void) { gchar *id; id = g_new0 (gchar, NODE_ID_LEN + 1); do { int i; for (i = 0; i < NODE_ID_LEN; i++) id[i] = (gchar)g_random_int_range ('a', 'z'); } while (NULL != node_is_used_id (id)); return id; } nodePtr node_from_id (const gchar *id) { nodePtr node; node = node_is_used_id (id); if (!node) debug1 (DEBUG_GUI, "Fatal: no node with id \"%s\" found!", id); return node; } nodePtr node_new (nodeTypePtr type) { nodePtr node; gchar *id; g_assert (NULL != type); node = g_new0 (struct node, 1); node->type = type; node->viewMode = NODE_VIEW_MODE_DEFAULT; node->sortColumn = NODE_VIEW_SORT_BY_TIME; node->sortReversed = TRUE; /* default sorting is newest date at top */ node->available = TRUE; id = node_new_id (); node_set_id (node, id); g_free (id); return node; } void node_set_data (nodePtr node, gpointer data) { g_assert (NULL == node->data); g_assert (NULL != node->type); node->data = data; } void node_set_subscription (nodePtr node, subscriptionPtr subscription) { g_assert (NULL == node->subscription); g_assert (NULL != node->type); node->subscription = subscription; subscription->node = node; /* Besides the favicon age we have no persistent update state field, so everything else goes NULL */ if (node->iconFile && !strstr(node->iconFile, "default.svg")) { subscription->updateState->lastFaviconPoll = (guint64)(common_get_mod_time (node->iconFile)) * G_USEC_PER_SEC; debug2 (DEBUG_UPDATE, "Setting last favicon poll time for %s to %lu", node->id, subscription->updateState->lastFaviconPoll / G_USEC_PER_SEC); } } void node_update_subscription (nodePtr node, gpointer user_data) { if (node->source->root == node) { node_source_update (node); return; } if (node->subscription) subscription_update (node->subscription, GPOINTER_TO_UINT (user_data)); node_foreach_child_data (node, node_update_subscription, user_data); } void node_auto_update_subscription (nodePtr node) { if (node->source->root == node) { node_source_auto_update (node); return; } if (node->subscription) subscription_auto_update (node->subscription); node_foreach_child (node, node_auto_update_subscription); } void node_reset_update_counter (nodePtr node, guint64 *now) { subscription_reset_update_counter (node->subscription, now); node_foreach_child_data (node, node_reset_update_counter, now); } gboolean node_is_ancestor (nodePtr node1, nodePtr node2) { nodePtr tmp; tmp = node2->parent; while (tmp) { if (node1 == tmp) return TRUE; tmp = tmp->parent; } return FALSE; } void node_free (nodePtr node) { if (node->data && NODE_TYPE (node)->free) NODE_TYPE (node)->free (node); g_assert (NULL == node->children); g_hash_table_remove (nodes, node->id); update_job_cancel_by_owner (node); if (node->subscription) subscription_free (node->subscription); if (node->icon) g_object_unref (node->icon); g_free (node->iconFile); g_free (node->title); g_free (node->id); g_free (node); } static void node_calc_counters (nodePtr node) { /* Order is important! First update all children so that hierarchical nodes (folders and feed list sources) can determine their own unread count as the sum of all childs afterwards */ node_foreach_child (node, node_calc_counters); NODE_TYPE (node)->update_counters (node); } static void node_update_parent_counters (nodePtr node) { guint old; if (!node) return; old = node->unreadCount; NODE_TYPE (node)->update_counters (node); if (old != node->unreadCount) { feed_list_view_update_node (node->id); feedlist_new_items (0); /* add 0 new items, as 'new-items' signal updates unread items also */ } if (node->parent) node_update_parent_counters (node->parent); } void node_update_counters (nodePtr node) { guint oldUnreadCount = node->unreadCount; guint oldItemCount = node->itemCount; /* Update the node itself and its children */ node_calc_counters (node); if ((oldUnreadCount != node->unreadCount) || (oldItemCount != node->itemCount)) feed_list_view_update_node (node->id); /* Update the unread count of the parent nodes, usually they just add all child unread counters */ if (!IS_VFOLDER (node)) node_update_parent_counters (node->parent); } void node_update_favicon (nodePtr node) { if (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE_FAVICON) { debug1 (DEBUG_UPDATE, "favicon of node %s needs to be updated...", node->title); subscription_icon_update (node->subscription); } /* Recursion */ if (node->children) node_foreach_child (node, node_update_favicon); } itemSetPtr node_get_itemset (nodePtr node) { return NODE_TYPE (node)->load (node); } void node_mark_all_read (nodePtr node) { if (!node) return; if ((node->unreadCount > 0) || (IS_VFOLDER (node))) { itemset_mark_read (node); node->unreadCount = 0; node->needsUpdate = TRUE; } if (node->children) node_foreach_child (node, node_mark_all_read); } gchar * node_render(nodePtr node) { return NODE_TYPE (node)->render (node); } /* import callbacks and helper functions */ void node_set_parent (nodePtr node, nodePtr parent, gint position) { g_assert (NULL != parent); parent->children = g_slist_insert (parent->children, node, position); node->parent = parent; /* new nodes may be provided by another node source, if not they are handled by the parents node source */ if (!node->source) node->source = parent->source; } void node_reparent (nodePtr node, nodePtr new_parent) { nodePtr old_parent; g_assert (NULL != new_parent); g_assert (NULL != node); debug2 (DEBUG_GUI, "Reparenting node '%s' to a parent '%s'", node_get_title(node), node_get_title(new_parent)); old_parent = node->parent; if (NULL != old_parent) old_parent->children = g_slist_remove (old_parent->children, node); new_parent->children = g_slist_insert (new_parent->children, node, -1); node->parent = new_parent; feed_list_view_remove_node (node); feed_list_view_add_node (node); } void node_remove (nodePtr node) { /* using itemlist_remove_all_items() ensures correct unread and item counters for all parent folders and matching search folders */ itemlist_remove_all_items (node); NODE_TYPE (node)->remove (node); } static xmlDocPtr node_to_xml (nodePtr node) { xmlDocPtr doc; xmlNodePtr rootNode; gchar *tmp; doc = xmlNewDoc (BAD_CAST"1.0"); rootNode = xmlNewDocNode (doc, NULL, BAD_CAST"node", NULL); xmlDocSetRootElement (doc, rootNode); xmlNewTextChild (rootNode, NULL, BAD_CAST"title", BAD_CAST node_get_title (node)); tmp = g_strdup_printf ("%u", node->unreadCount); xmlNewTextChild (rootNode, NULL, BAD_CAST"unreadCount", BAD_CAST tmp); g_free (tmp); tmp = g_strdup_printf ("%u", g_slist_length (node->children)); xmlNewTextChild (rootNode, NULL, BAD_CAST"children", BAD_CAST tmp); g_free (tmp); return doc; } gchar * node_default_render (nodePtr node) { gchar *result; xmlDocPtr doc; doc = node_to_xml (node); result = render_xml (doc, NODE_TYPE(node)->id, NULL); xmlFreeDoc (doc); return result; } /* helper functions to be used with node_foreach* */ void node_save(nodePtr node) { NODE_TYPE(node)->save(node); } /* node attributes encapsulation */ void node_set_title (nodePtr node, const gchar *title) { g_free (node->title); node->title = g_strstrip (g_strdelimit (g_strdup (title), "\r\n", ' ')); } const gchar * node_get_title (nodePtr node) { return node->title; } void node_load_icon (nodePtr node) { /* Load pixbuf for all widget based rendering */ if (node->icon) g_object_unref (node->icon); // FIXME: don't use constant size, but size corresponding to GTK icon // size used in wide view node->icon = favicon_load_from_cache (node->id, 128); /* Create filename for HTML rendering */ g_free (node->iconFile); if (node->icon) node->iconFile = common_create_cache_filename ("favicons", node->id, "png"); else node->iconFile = g_build_filename (PACKAGE_DATA_DIR, PACKAGE, "pixmaps", "default.svg", NULL); } /* determines the nodes favicon or default icon */ gpointer node_get_icon (nodePtr node) { if (!node->icon) return (gpointer) NODE_TYPE(node)->icon; return node->icon; } const gchar * node_get_favicon_file (nodePtr node) { return node->iconFile; } void node_set_id (nodePtr node, const gchar *id) { if (!nodes) nodes = g_hash_table_new(g_str_hash, g_str_equal); if (node->id) { g_hash_table_remove (nodes, node->id); g_free (node->id); } node->id = g_strdup (id); g_hash_table_insert (nodes, node->id, node); } const gchar * node_get_id (nodePtr node) { return node->id; } gboolean node_set_sort_column (nodePtr node, nodeViewSortType sortColumn, gboolean reversed) { if (node->sortColumn == sortColumn && node->sortReversed == reversed) return FALSE; node->sortColumn = sortColumn; node->sortReversed = reversed; return TRUE; } void node_set_view_mode (nodePtr node, nodeViewType viewMode) { gint defaultViewMode; /* To allow users to select a default viewing mode for the layout we need to store only exceptions from this mode, which is why we compare the mode to be set with the default and if it's equal we just set NODE_VIEW_MODE_DEFAULT. This allows to not OPML export the viewMode attribute for nodes the are in default viewing mode, which then allows to follow a switch in the preference to a new default viewing mode. This of course also means that the we use some state on each changing of the view mode preference. */ conf_get_int_value (DEFAULT_VIEW_MODE, &defaultViewMode); if (viewMode != (nodeViewType)defaultViewMode) node->viewMode = viewMode; else node->viewMode = NODE_VIEW_MODE_DEFAULT; } nodeViewType node_get_view_mode (nodePtr node) { gint defaultViewMode; conf_get_int_value (DEFAULT_VIEW_MODE, &defaultViewMode); if (NODE_VIEW_MODE_DEFAULT == node->viewMode) return defaultViewMode; else return node->viewMode; } const gchar * node_get_base_url(nodePtr node) { const gchar *baseUrl = NULL; if (node->subscription) { baseUrl = subscription_get_homepage (node->subscription); if (!baseUrl) baseUrl = subscription_get_source (node->subscription); } /* prevent feed scraping commands to end up as base URI */ if (!((baseUrl != NULL) && (baseUrl[0] != '|') && (strstr(baseUrl, "://") != NULL))) baseUrl = NULL; return baseUrl; } gboolean node_can_add_child_feed (nodePtr node) { g_assert (node->source->root); if (!(NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) return FALSE; return (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ADD_FEED); } gboolean node_can_add_child_folder (nodePtr node) { g_assert (node->source->root); if (!(NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS)) return FALSE; return (NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_ADD_FOLDER); } /* node children iterating interface */ void node_foreach_child_full (nodePtr node, gpointer func, gint params, gpointer user_data) { GSList *children, *iter; g_assert (NULL != node); /* We need to copy because func might modify the list */ iter = children = g_slist_copy (node->children); while (iter) { nodePtr childNode = (nodePtr)iter->data; /* Apply the method to the child */ if (0 == params) ((nodeActionFunc)func) (childNode); else ((nodeActionDataFunc)func) (childNode, user_data); /* Never descend! */ iter = g_slist_next (iter); } g_slist_free (children); } liferea-1.13.7/src/node.h000066400000000000000000000306401415350204600151020ustar00rootroot00000000000000/* * @file node.h hierarchic feed list node interface * * Copyright (C) 2003-2015 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NODE_H #define _NODE_H #include "itemset.h" #include "node_view.h" /* Liferea's GUI consists of three parts. Feed list, item list and HTML view. The feed list is a view of some or all available nodes. The feed list allows nodes of different types defining different UI behaviours. According to the node's type this interface propagates user interaction to the feed list node type implementation and allows the implementation to change the nodes state. This interface is to hide the node type and node source type complexity for the GUI, scripting and updating functionality. */ /* generic feed list node structure */ typedef struct node { gpointer data; /*<< node type specific data structure */ struct subscription *subscription; /*<< subscription attached to this node (or NULL) */ struct nodeType *type; /*<< node type implementation */ struct nodeSource *source; /*<< the feed list source handling this node */ gchar *iconFile; /*<< the path of the favicon file */ /* feed list state properties of this node */ struct node *parent; /*<< the parent node (or NULL if at root level) */ GSList *children; /*<< ordered list of node children */ gchar *id; /*<< unique node identifier string */ guint itemCount; /*<< number of items */ guint unreadCount; /*<< number of unread items */ guint popupCount; /*<< number of items to be notified */ guint newCount; /*<< number of recently downloaded items */ gchar *title; /*<< the label of the node in the feed list */ gpointer icon; /*<< favicon GdkPixBuf (or NULL) */ gboolean available; /*<< availability of this node (usually the last downloading state) */ gboolean expanded; /*<< expansion state (for nodes with childs) */ /* item list state properties of this node */ nodeViewType viewMode; /*<< Viewing mode for this node (one of NODE_VIEW_MODE_*) */ nodeViewSortType sortColumn; /*<< Node specific item view sort attribute. */ gboolean sortReversed; /*<< Sort in the reverse order? */ /* rendering behaviour of this node */ gboolean loadItemLink; /*<< if TRUE do automatically load the item link into the HTML pane */ /* current state of this node */ gboolean needsUpdate; /*<< if TRUE: the item list has changed and the nodes feed list representation needs to be updated */ gboolean needsRecount; /*<< if TRUE: the number of unread/total items is currently unknown and needs recounting */ } *nodePtr; /** * node_new: (skip) * Creates a new node structure. * * Returns: (transfer full): the new node */ nodePtr node_new (struct nodeType *type); /** * node_is_used_id: (skip) * @id: the node id to check * * Can be used to check whether an id is used or not. * * Returns: (transfer none) (nullable): the node with the given id (or NULL) */ nodePtr node_is_used_id (const gchar *id); /** * node_from_id: (skip) * @id: the node id to look up * * Node lookup by node id. Will report an error if the queried * id does not exist. * * Returns: (transfer none) (nullable): the node with the given id (or NULL) */ nodePtr node_from_id (const gchar *id); /** * node_set_parent: (skip) * @node: the node * @parent: (nullable): the parent node (optional can be NULL) * @position: insert position (optional can be 0) * * Sets a nodes parent. If no parent node is given the * parent node of the currently selected feed or the * selected folder will be used. * * To be used before calling feedlist_node_added() */ void node_set_parent (nodePtr node, nodePtr parent, gint position); /** * node_reparent: (skip) * @node: the node * @new_parent: nodes new parent * * Set a node's new parent and update UI. If a node already has a parent, * it will be removed from its parent children list. */ void node_reparent (nodePtr node, nodePtr new_parent); /** * node_remove: (skip) * @node: the node * * Removes all data associated with the given node. */ void node_remove (nodePtr node); /** * node_set_data: (skip) * @node: the node to attach to * @data: the structure * * Attaches a data structure to the given node. */ void node_set_data(nodePtr node, gpointer data); /** * node_set_subscription: (skip) * @node: the node * @subscription: the subscription * * Attaches the subscription to the given node. */ void node_set_subscription (nodePtr node, struct subscription *subscription); /** * node_update_subscription: (skip) * @node: the node * @user_data: update flags * * Helper function to be used with node_foreach_child() * to mass-update subscriptions. */ void node_update_subscription (nodePtr node, gpointer user_data); /** * node_auto_update_subscription: (skip) * @node: the node * * Helper function to be used with node_foreach_child() * to mass-auto-update subscriptions. */ void node_auto_update_subscription (nodePtr node); /** * node_reset_update_counter: (skip) * @node: the node * @now: the current timestamp * * Helper function to be used with node_foreach_child() * to mass-auto-update subscriptions. */ void node_reset_update_counter (nodePtr node, guint64 *now); /** * node_is_ancestor: (skip) * @node1: the possible ancestor * @node2: the possible child * * Determines whether node1 is an ancestor of node2 * * Returns: TRUE if node1 is ancestor of node2 */ gboolean node_is_ancestor(nodePtr node1, nodePtr node2); /** * node_get_title: (skip) * @node: the node * * Query the node's title for the feed list. * * Returns: the title */ const gchar * node_get_title(nodePtr node); /** * node_set_title: (skip) * @node: the node * @title: the title * * Sets the node's title for the feed list. */ void node_set_title(nodePtr node, const gchar *title); /** * node_update_counters: (skip) * @node: the node * * Update the number of items and unread items of a node from * the DB. This method ensures propagation to parent folders. */ void node_update_counters(nodePtr node); /** * node_mark_all_read: (skip) * @node: the node to process * * Recursively marks all items of the given node as read. */ void node_mark_all_read(nodePtr node); /** * node_set_icon: (skip) * @node: the node * @icon: (nullable): a pixmap or NULL * * Assigns a new pixmaps as the favicon representing this node. */ void node_set_icon(nodePtr node, gpointer icon); /** * node_get_icon: (skip) * * Returns an appropriate icon for the given node. If the node * is unavailable the "unavailable" icon will be returned. If * the node is available an existing favicon or the node type * specific default icon will be returned. * * Returns: (nullable): a pixmap or NULL */ gpointer node_get_icon (nodePtr node); /** * node_get_large_icon: (skip) * * Returns a large icon for the node. Does not return any default * icons like node_get_icon() does. * * Returns: (nullable): a pixmap or NULL */ gpointer node_get_large_icon (nodePtr node); /** * node_get_favicon_file: (skip) * @node: the node * * Returns the name of the favicon cache file for the given node. * If there is no favicon a default icon file name will be returned. * * Returns: a file name */ const gchar * node_get_favicon_file(nodePtr node); /** * node_new_id: * * Returns a new unused unique node id. * * Returns: (transfer full): new id (to be free'd using g_free) */ gchar * node_new_id (void); /** * node_get_id: (skip) * @node: the node * * Query the unique id string of the node. * * Returns: id string */ const gchar *node_get_id (nodePtr node); /** * node_set_id: (skip) * @node: the node * @id: the id string * * Set the unique id string of the node. */ void node_set_id(nodePtr node, const gchar *id); /** * node_free: (skip) * @node: the node to free * * Frees a given node structure. */ void node_free(nodePtr node); /** * node_default_render: (skip) * @node: the node to render * * Helper function for generic node rendering. Performs * a generic node serialization to XML and passes the * generated XML source document to the XSLT stylesheet * with the same name as the node type id. * * Returns: XHTML string */ gchar * node_default_render(nodePtr node); /** * node_save: (skip) * @node: the node * * Saves the given node to cache. */ void node_save(nodePtr node); /** * node_get_itemset: (skip) * @node: the node * * Loads all items of the given node into memory. * The caller needs to free the item set using itemset_free() * * Returns: the item set */ itemSetPtr node_get_itemset(nodePtr node); /** * node_render: (skip) * @node: the node * * Node content rendering * * Returns: string with node rendered in HTML */ gchar * node_render(nodePtr node); /** * node_update_favicon: (skip) * @node: the node * * Called when updating favicons is requested. */ void node_update_favicon (nodePtr node); /** * node_load_icon: (skip) * @node: the node * * Load node icon in memory. Should be called only once on startup * and when the node icon has changed. */ void node_load_icon (nodePtr node); /** * node_set_sort_column: (skip) * @node: the node * @sortColumn: sort column id * @reversed: TRUE if order should be reversed * * Change/Set the sort column of a given node. * * Returns: TRUE if the passed settings were different from the previous ones */ gboolean node_set_sort_column (nodePtr node, nodeViewSortType sortColumn, gboolean reversed); /** * node_set_view_mode: (skip) * @node: the node * @newMode: viewing mode (NODE_VIEW_MODE_*) * * Change/Set the viewing mode of a given node. */ void node_set_view_mode(nodePtr node, nodeViewType newMode); /** * node_get_view_mode: (skip) * @node: the node * * Query the effective viewing mode setting of a given mode. * When node viewing mode is set to default it will return the * configured default. * * Returns: viewing mode (NODE_VIEW_MODE_*) */ nodeViewType node_get_view_mode(nodePtr node); /** * node_get_base_url: (skip) * @node: the node * * Returns the base URL for the given node. * If it is a mixed item set NULL will be returned. * * Returns: base URL */ const gchar * node_get_base_url(nodePtr node); /** * node_can_add_child_feed: (skip) * @node: the node * * Query whether a feed be added to the given node. * * Returns: TRUE if a feed can be added */ gboolean node_can_add_child_feed (nodePtr node); /** * node_can_add_child_folder: (skip) * @node: the node * * Query whether a folder be added to the given node. * * Returns: TRUE if a folder can be added */ gboolean node_can_add_child_folder (nodePtr node); /* child nodes iterating interface */ typedef void (*nodeActionFunc) (nodePtr node); typedef void (*nodeActionDataFunc) (nodePtr node, gpointer user_data); /* * Do not call this method directly! Do use * node_foreach_child() or node_foreach_child_data()! */ void node_foreach_child_full(nodePtr ptr, gpointer func, gint params, gpointer user_data); /** * node_foreach_child: (skip) * @node: node pointer whose children should be processed * @func: the function to process all found elements * * Helper function to call node methods for all * children of a given node. The given function may * modify the children list. */ #define node_foreach_child(node, func) node_foreach_child_full(node,func,0,NULL) /** * node_foreach_child_data: (skip) * @node: node pointer whose children should be processed * @func: the function to process all found elements * @user_data: specifies the second argument that func should be passed * * Helper function to call node methods for all * children of a given node. The given function may * modify the children list. */ #define node_foreach_child_data(node, func, user_data) node_foreach_child_full(node,func,1,user_data) #endif liferea-1.13.7/src/node_type.c000066400000000000000000000056641415350204600161460ustar00rootroot00000000000000/** * @file node_type.c node type handling * * Copyright (C) 2007-2008 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "node_type.h" #include "feed.h" #include "feed_parser.h" #include "feedlist.h" #include "folder.h" #include "newsbin.h" #include "node.h" #include "vfolder.h" #include "fl_sources/node_source.h" static GSList *nodeTypes = NULL; static void node_type_register (nodeTypePtr nodeType) { /* all attributes and methods except free() are mandatory! */ g_assert (nodeType->id); g_assert (nodeType->import); g_assert (nodeType->export); g_assert (nodeType->load); g_assert (nodeType->save); g_assert (nodeType->update_counters); g_assert (nodeType->remove); g_assert (nodeType->render); g_assert (nodeType->request_add); g_assert (nodeType->request_properties); nodeTypes = g_slist_append (nodeTypes, (gpointer)nodeType); } const gchar * node_type_to_str (nodePtr node) { /* To distinguish different feed formats (Atom, RSS...) we do return different type identifiers for feed subscriptions... */ if (IS_FEED (node)) { g_assert (NULL != node->data); return feed_type_fhp_to_str (((feedPtr)(node->data))->fhp); } return NODE_TYPE (node)->id; } nodeTypePtr node_str_to_type (const gchar *str) { GSList *iter; g_assert (NULL != str); /* initialize known node types the first time... */ if (!nodeTypes) { node_type_register (feed_get_node_type ()); node_type_register (root_get_node_type ()); node_type_register (folder_get_node_type ()); node_type_register (vfolder_get_node_type ()); node_type_register (node_source_get_node_type ()); node_type_register (newsbin_get_node_type ()); } if (g_str_equal (str, "")) /* type maybe "" if initial download is not yet done */ return feed_get_node_type (); if (NULL != feed_type_str_to_fhp (str)) return feed_get_node_type (); /* check against all node types */ iter = nodeTypes; while (iter) { if (g_str_equal (str, ((nodeTypePtr)iter->data)->id)) return (nodeTypePtr)iter->data; iter = g_slist_next (iter); } return NULL; } /* Interactive node adding (e.g. feed menu->new subscription) */ gboolean node_type_request_interactive_add (nodeTypePtr nodeType) { if (!feedlist_is_writable ()) return FALSE; return nodeType->request_add (); } liferea-1.13.7/src/node_type.h000066400000000000000000000077451415350204600161550ustar00rootroot00000000000000/** * @file node_type.h node type interface * * Copyright (C) 2007-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NODE_TYPE_H #define _NODE_TYPE_H #include "node.h" #include #include /** node type capabilities */ enum { NODE_CAPABILITY_SHOW_ITEM_FAVICONS = (1<<0), /**< display favicons in item list (useful for recursively viewed leaf node) */ NODE_CAPABILITY_ADD_CHILDS = (1<<1), /**< allows adding new childs */ NODE_CAPABILITY_REMOVE_CHILDS = (1<<2), /**< allows removing it's childs */ NODE_CAPABILITY_SUBFOLDERS = (1<<3), /**< allows creating/removing sub folders */ NODE_CAPABILITY_REMOVE_ITEMS = (1<<5), /**< allows removing of single items */ NODE_CAPABILITY_RECEIVE_ITEMS = (1<<6), /**< is a DnD target for item copies */ NODE_CAPABILITY_REORDER = (1<<7), /**< allows DnD to reorder childs */ NODE_CAPABILITY_SHOW_UNREAD_COUNT = (1<<8), /**< display the unread item count in the feed list */ NODE_CAPABILITY_SHOW_ITEM_COUNT = (1<<9), /**< display the absolute item count in the feed list */ NODE_CAPABILITY_UPDATE = (1<<10), /**< node type always has a subscription and can be updated */ NODE_CAPABILITY_UPDATE_CHILDS = (1<<11), /**< childs of this node type can be updated */ NODE_CAPABILITY_UPDATE_FAVICON = (1<<12), /**< this node allows downloading a favicon */ NODE_CAPABILITY_EXPORT = (1<<13) /**< nodes of this type can be exported safely to OPML */ }; /** * Liferea supports different types of nodes in the feed * list. The type of a feed list node determines how the user * can interact with it. */ /** node type interface */ typedef struct nodeType { gulong capabilities; /**< bitmask of node type capabilities */ const gchar *id; /**< type id (used for type attribute in OPML export) */ const GIcon *icon; /**< default icon for nodes of this type (if no favicon available) */ /* For method documentation see the wrappers defined below! All methods are mandatory for each node type. */ void (*import) (nodePtr node, nodePtr parent, xmlNodePtr cur, gboolean trusted); void (*export) (nodePtr node, xmlNodePtr cur, gboolean trusted); itemSetPtr (*load) (nodePtr node); void (*save) (nodePtr node); void (*update_counters) (nodePtr node); void (*remove) (nodePtr node); gchar * (*render) (nodePtr node); gboolean (*request_add) (void); void (*request_properties) (nodePtr node); /** * Called to allow node type to clean up it's specific data. * The node structure itself is destroyed after this call. * * @param node the node */ void (*free) (nodePtr node); } *nodeTypePtr; #define NODE_TYPE(node) (node->type) /** * Maps node type to string. For feed nodes * it maps to the feed type string. * * @param node the node * * @returns type string (or NULL if unknown) */ const gchar *node_type_to_str (nodePtr node); /** * Maps node type string to type constant. * * @param type str the node type as string * * @returns node type */ nodeTypePtr node_str_to_type (const gchar *str); /** * Interactive node adding (e.g. feed menu->new subscription), * launches some dialog that upon success adds a feed of the * given type. * * @param nodeType the node type * * @returns TRUE on success */ gboolean node_type_request_interactive_add (nodeTypePtr nodeType); #endif liferea-1.13.7/src/node_view.h000066400000000000000000000022271415350204600161340ustar00rootroot00000000000000/* * @file node_view.h node view modes * * Copyright (C) 2009-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _NODE_VIEW_H #define _NODE_VIEW_H typedef enum { NODE_VIEW_MODE_NORMAL = 0, NODE_VIEW_MODE_WIDE = 1, NODE_VIEW_MODE_COMBINED = 2, NODE_VIEW_MODE_DEFAULT, } nodeViewType; typedef enum { NODE_VIEW_SORT_BY_TIME = 0, /* default */ NODE_VIEW_SORT_BY_TITLE, NODE_VIEW_SORT_BY_PARENT, NODE_VIEW_SORT_BY_STATE } nodeViewSortType; #endif liferea-1.13.7/src/parsers/000077500000000000000000000000001415350204600154605ustar00rootroot00000000000000liferea-1.13.7/src/parsers/Makefile.am000066400000000000000000000014201415350204600175110ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in AM_CPPFLAGS = \ -DPACKAGE_DATA_DIR=\""$(datadir)"\" \ -DPACKAGE_LIB_DIR=\""$(libdir)"\" \ -DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \ -I$(top_srcdir)/src \ $(PACKAGE_CFLAGS) noinst_LIBRARIES = libliparsers.a libliparsers_a_CFLAGS = $(PACKAGE_FLAGS) libliparsers_a_SOURCES = \ atom10.c atom10.h \ html5_feed.c html5_feed.h \ ldjson_feed.c ldjson_feed.h \ ns_admin.c ns_admin.h \ ns_ag.c ns_ag.h \ ns_cC.c ns_cC.h \ ns_content.c ns_content.h \ ns_dc.c ns_dc.h \ ns_georss.c ns_georss.h \ ns_itunes.c ns_itunes.h \ ns_media.c ns_media.h \ ns_slash.c ns_slash.h \ ns_syn.c ns_syn.h \ ns_trackback.c ns_trackback.h \ ns_wfw.c ns_wfw.h \ rss_channel.c rss_channel.h \ rss_item.c rss_item.h liferea-1.13.7/src/parsers/atom10.c000066400000000000000000000640711415350204600167350ustar00rootroot00000000000000/** * @file atom10.c Atom 1.0 Parser * * Copyright (C) 2005-2006 Nathan Conrad * Copyright (C) 2003-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "atom10.h" #include #include #include "common.h" #include "date.h" #include "debug.h" #include "enclosure.h" #include "feed_parser.h" #include "feedlist.h" #include "ns_admin.h" #include "ns_ag.h" #include "ns_cC.h" #include "ns_content.h" #include "ns_dc.h" #include "ns_georss.h" #include "ns_itunes.h" #include "ns_media.h" #include "ns_slash.h" #include "ns_syn.h" #include "ns_trackback.h" #include "ns_wfw.h" #include "metadata.h" #include "subscription.h" #include "xml.h" #define ATOM10_NS BAD_CAST"http://www.w3.org/2005/Atom" /* to store the ATOMNsHandler structs for all supported RDF namespace handlers */ GHashTable *atom10_nstable = NULL; GHashTable *ns_atom10_ns_uri_table = NULL; struct atom10ParserState { gboolean errorDetected; }; typedef void (*atom10ElementParserFunc) (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state); static gchar * atom10_mark_up_text_content (gchar* content) { gchar **tokens; gchar **token; gchar *str, *old_str; if (!content) return NULL; if (!*content) return g_strdup (content); tokens = g_strsplit (content, "\n\n", 0); if (!tokens[0]) { /* No tokens */ str = g_strdup(""); } else if (!tokens[1]) { /* One token */ str = g_markup_escape_text (tokens[0], -1); } else { /* Many tokens */ token = tokens; while (*token) { old_str = *token; str = g_strchug (g_strchomp (*token)); /* WARNING: modifies the token string*/ if (str[0] != '\0') { *token = g_markup_printf_escaped ("

%s

", str); g_free (old_str); } else { **token = '\0'; /* Erase the particular token because it is blank */ } token++; } str = g_strjoinv ("\n", tokens); } g_strfreev (tokens); return str; } /** * This parses an Atom content construct. * * @param cur the XML node to be parsed * @param ctxt a valid feed parser context * @returns g_strduped string which must be freed by the caller. */ static gchar * atom10_parse_content_construct (xmlNodePtr cur, feedParserCtxtPtr ctxt) { gchar *ret = NULL; if (xmlHasNsProp (cur, BAD_CAST"src", NULL )) { /* RFC 4287 says a feed must have a summary when there's a src attribute in the content (and the content therefore empty). We are already parsing the summary separately. RFC 4287 also says an entry must contain one link element with rel="alternate", so there's no point in parsing src and setting it as link. */ ret = NULL; } else { gchar *type; /* determine encoding mode */ type = xml_get_ns_attribute (cur, "type", NULL); /* Contents need to be de-encoded and should not contain sub-tags.*/ if (type && (g_str_equal (type,"html") || !g_ascii_strcasecmp (type, "text/html"))) { ret = xhtml_extract (cur, 0, NULL); } else if (!type || !strcmp (type, "text") || !strncasecmp (type, "text/",5)) { gchar *tmp; /* Assume that "text/ *" files can be directly displayed.. kinda stated in the RFC */ ret = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); g_strchug (g_strchomp (ret)); if (!type || !strcasecmp (type, "text")) tmp = atom10_mark_up_text_content (ret); else tmp = g_markup_printf_escaped ("
%s
", ret); g_free (ret); ret = tmp; } else if (!strcmp(type,"xhtml") || !g_ascii_strcasecmp (type, "application/xhtml+xml")) { /* The spec says to only show the contents of the div tag that MUST be present */ ret = xhtml_extract (cur, 2, NULL); } else { /* Do nothing on unsupported content types. This allows summaries to be used. */ ret = NULL; } g_free (type); } return ret; } /** * Parse Atom 1.0 text tags of all sorts. * * @param htmlified If set to 1, then HTML is returned. * When set to 0, All HTML tags are removed * * @returns an escaped version of a text construct. */ static gchar * atom10_parse_text_construct (xmlNodePtr cur, gboolean htmlified) { gchar *type, *tmp, *ret = NULL; /* determine encoding mode */ type = xml_get_ns_attribute (cur, "type", NULL); /* not sure what MIME types are necessary... */ /* This that need to be de-encoded and should not contain sub-tags.*/ if (!type || !strcmp(type, "text")) { ret = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (ret) { g_strchug (g_strchomp (ret)); if (htmlified) { tmp = atom10_mark_up_text_content (ret); g_free (ret); ret = tmp; } } } else if (!strcmp(type, "html")) { ret = xhtml_extract (cur, 0, NULL); if (!htmlified) ret = unhtmlize (unxmlize (ret)); } else if (!strcmp (type, "xhtml")) { /* The spec says to show the contents of the div tag that MUST be present */ ret = xhtml_extract (cur, 2, NULL); if (!htmlified) ret = unhtmlize (ret); } else { /* Invalid Atom feed */ ret = g_strdup ("This attribute was invalidly specified in this Atom feed."); } g_free (type); return ret; } static gchar * atom10_parse_person_construct (xmlNodePtr cur) { gchar *tmp = NULL; gchar *name = NULL, *uri = NULL, *email = NULL; gboolean invalid = FALSE; cur = cur->xmlChildrenNode; while (cur) { if (NULL == cur->name || cur->type != XML_ELEMENT_NODE || cur->ns == NULL || cur->ns->href == NULL) { cur = cur->next; continue; } if (xmlStrEqual (cur->ns->href, ATOM10_NS)) { if (xmlStrEqual (cur->name, BAD_CAST"name")) { g_free (name); name = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); } if (xmlStrEqual (cur->name, BAD_CAST"email")) { if (email) invalid = TRUE; g_free(email); tmp = (gchar *)xmlNodeListGetString(cur->doc, cur->xmlChildrenNode, 1); email = g_markup_printf_escaped (" - %s", tmp, tmp); g_free(tmp); } if (xmlStrEqual(cur->name, BAD_CAST"uri")) { if (uri) invalid = TRUE; g_free (uri); tmp = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); uri = g_markup_printf_escaped (" (%s)", tmp, _("Website")); g_free (tmp); } } else { /* FIXME: handle extension elements here */ } cur = cur->next; } if (!name) invalid = TRUE; if (!invalid) tmp = g_strdup_printf ("%s%s%s", name, uri?uri:"", email?email:""); else tmp = NULL; g_free (uri); g_free (email); g_free (name); return tmp; } /* Note: this function is called for both item and feed context */ static gchar * atom10_parse_link (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *href, *alternate = NULL; href = xml_get_ns_attribute (cur, "href", NULL); if (href) { xmlChar *baseURL = xmlNodeGetBase (cur->doc, cur); gchar *url, *relation, *type, *escTitle = NULL, *title; const gchar *feedURL = subscription_get_homepage (ctxt->subscription); if (!baseURL && feedURL && feedURL[0] != '|' && strstr (feedURL, "://")) baseURL = xmlStrdup (BAD_CAST (feedURL)); url = (gchar *)common_build_url (href, (gchar *)baseURL); type = xml_get_ns_attribute (cur, "type", NULL); relation = xml_get_ns_attribute (cur, "rel", NULL); title = xml_get_ns_attribute (cur, "title", NULL); if (title) escTitle = g_markup_escape_text (title, -1); if (!xmlHasNsProp (cur, BAD_CAST"rel", NULL) || !relation || g_str_equal (relation, BAD_CAST"alternate")) { alternate = g_strdup (url); } else if (g_str_equal (relation, "self")) { alternate = g_strdup (url); } else if (g_str_equal (relation, "replies")) { if (!type || g_str_equal (type, BAD_CAST"application/atom+xml")) { gchar *commentUri = (gchar *)common_build_url ((gchar *)url, subscription_get_homepage (ctxt->subscription)); if (ctxt->item) metadata_list_set (&ctxt->item->metadata, "commentFeedUri", commentUri); g_free (commentUri); } } else if (g_str_equal (relation, "enclosure")) { if (ctxt->item) { gsize length = 0; gchar *lengthStr = xml_get_ns_attribute (cur, "length", NULL); if (lengthStr) length = atol (lengthStr); g_free (lengthStr); gchar *encStr = enclosure_values_to_string (url, type, length, FALSE /* not yet downloaded */); ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "enclosure", encStr); ctxt->item->hasEnclosure = TRUE; g_free (encStr); } } else if (g_str_equal (relation, "related") || g_str_equal (relation, "via")) { if (ctxt->item) ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, relation, url); } else { /* g_warning ("Unhandled Atom link with unexpected relation \"%s\"\n", relation); */ } xmlFree (title); xmlFree (baseURL); g_free (escTitle); g_free (url); g_free(relation); g_free(type); g_free(href); } else { /* FIXME: @href is required, this document is not valid Atom */; } return alternate; } static void atom10_parse_entry_author (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *author; author = atom10_parse_person_construct (cur); if (author) { ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "author", author); g_free (author); } } static void atom10_parse_entry_category (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *category = NULL; category = xml_get_ns_attribute (cur, "label", NULL); if (!category) category = xml_get_ns_attribute (cur, "term", NULL); if (category) { gchar *escaped = g_markup_escape_text (category, -1); /* Black-list some categories used by Google Reader clone online readers that should not be visible to the end-user */ if (!g_str_equal (category, "reading-list") && !g_str_equal (category, "read") && !strstr(category, "user/-/label/")) ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "category", escaped); g_free (escaped); xmlFree (category); } } static void atom10_parse_entry_content (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *content; content = atom10_parse_content_construct (cur, ctxt); if (content) { item_set_description (ctxt->item, content); g_free (content); } } static void atom10_parse_entry_contributor (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *contributor; contributor = atom10_parse_person_construct (cur); if (contributor) { ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "contributor", contributor); g_free (contributor); } } static void atom10_parse_entry_id (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *id; id = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (id) { if (strlen (id) > 0) { item_set_id (ctxt->item, id); ctxt->item->validGuid = TRUE; } g_free (id); } } static void atom10_parse_entry_link (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *href; href = atom10_parse_link (cur, ctxt, state); if (href) { item_set_source (ctxt->item, href); g_free (href); } } static void atom10_parse_entry_published (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *datestr; datestr = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (datestr) { ctxt->item->time = date_parse_ISO8601 (datestr); ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "pubDate", datestr); g_free (datestr); } } static void atom10_parse_entry_rights (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *rights; rights = atom10_parse_text_construct (cur, FALSE); if (rights) { ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "copyright", rights); g_free (rights); } } /* can be used for short text descriptions, if there is no description we show the content */ static void atom10_parse_entry_summary (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *summary; summary = atom10_parse_text_construct (cur, TRUE); if (summary) { item_set_description (ctxt->item, summary); g_free (summary); } /* FIXME: set a flag to show a "Read more" link to the user; but where? */ } static void atom10_parse_entry_title (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *title; title = atom10_parse_text_construct(cur, FALSE); if (title) { item_set_title (ctxt->item, title); g_free (title); } } static void atom10_parse_entry_updated (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *datestr; datestr = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); /* if pubDate is already set, don't overwrite it */ if (datestr && !metadata_list_get(ctxt->item->metadata, "pubDate")) { ctxt->item->time = date_parse_ISO8601 (datestr); ctxt->item->metadata = metadata_list_append (ctxt->item->metadata, "contentUpdateDate", datestr); } g_free (datestr); } /* tag support, FIXME: base64 not supported */ /* method to parse standard tags for each item element */ static itemPtr atom10_parse_entry (feedParserCtxtPtr ctxt, xmlNodePtr cur) { NsHandler *nsh; parseItemTagFunc pf; atom10ElementParserFunc func; static GHashTable *entryElementHash = NULL; if (!entryElementHash) { entryElementHash = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (entryElementHash, "author", &atom10_parse_entry_author); g_hash_table_insert (entryElementHash, "category", &atom10_parse_entry_category); g_hash_table_insert (entryElementHash, "content", &atom10_parse_entry_content); g_hash_table_insert (entryElementHash, "contributor", &atom10_parse_entry_contributor); g_hash_table_insert (entryElementHash, "id", &atom10_parse_entry_id); g_hash_table_insert (entryElementHash, "link", &atom10_parse_entry_link); g_hash_table_insert (entryElementHash, "published", &atom10_parse_entry_published); g_hash_table_insert (entryElementHash, "rights", &atom10_parse_entry_rights); /* FIXME: Parse "source" */ g_hash_table_insert (entryElementHash, "summary", &atom10_parse_entry_summary); g_hash_table_insert (entryElementHash, "title", &atom10_parse_entry_title); g_hash_table_insert (entryElementHash, "updated", &atom10_parse_entry_updated); } ctxt->item = item_new (); cur = cur->xmlChildrenNode; while (cur) { if (cur->type != XML_ELEMENT_NODE || cur->name == NULL || cur->ns == NULL) { cur = cur->next; continue; } if ((cur->ns->href && (nsh = (NsHandler *)g_hash_table_lookup (ns_atom10_ns_uri_table, (gpointer)cur->ns->href))) || (cur->ns->prefix && (nsh = (NsHandler *)g_hash_table_lookup (atom10_nstable, (gpointer)cur->ns->prefix)))) { pf = nsh->parseItemTag; if (pf) (*pf) (ctxt, cur); cur = cur->next; continue; } /* check namespace of this tag */ if (!cur->ns->href) { /* This is an invalid feed... no idea what to do with the current element */ debug1 (DEBUG_PARSING, "element with no namespace found in atom feed (%s)!", cur->name); cur = cur->next; continue; } if (xmlStrcmp(cur->ns->href, ATOM10_NS)) { debug1(DEBUG_PARSING, "unknown namespace %s found!", cur->ns->href); cur = cur->next; continue; } /* At this point, the namespace must be the Atom 1.0 namespace */ func = g_hash_table_lookup (entryElementHash, cur->name); if (func) { (*func) (cur, ctxt, NULL); } else { debug1 (DEBUG_PARSING, "unknown entry element \"%s\" found", cur->name); } cur = cur->next; } /* after parsing we fill the infos into the itemPtr structure */ ctxt->item->readStatus = FALSE; if (0 == ctxt->item->time) ctxt->item->time = ctxt->feed->time; return ctxt->item; } static void atom10_parse_feed_author (xmlNodePtr cur, feedParserCtxtPtr ctxt, itemPtr ip, struct atom10ParserState *state) { /* parse feed author */ gchar *author = atom10_parse_person_construct (cur); if (author) { ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "author", author); g_free (author); } /* FIXME: make item parsing use this author if not specified elsewhere */ } static void atom10_parse_feed_category (xmlNodePtr cur, feedParserCtxtPtr ctxt, itemPtr ip, struct atom10ParserState *state) { gchar *label = NULL; label = xml_get_ns_attribute (cur, "label", NULL); if (!label) label = xml_get_ns_attribute (cur, "term", NULL); if (label) { gchar *escaped = g_markup_escape_text (label, -1); ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "category", escaped); g_free (escaped); xmlFree (label); } } static void atom10_parse_feed_contributor (xmlNodePtr cur, feedParserCtxtPtr ctxt, itemPtr ip, struct atom10ParserState *state) { /* parse feed contributors */ gchar *contributer = atom10_parse_person_construct (cur); if (contributer) { ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "contributor", contributer); g_free (contributer); } } static void atom10_parse_feed_generator (xmlNodePtr cur, feedParserCtxtPtr ctxt, itemPtr ip, struct atom10ParserState *state) { gchar *ret, *version, *tmp = NULL, *uri; ret = unhtmlize ((gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1)); if (ret && ret[0] != '\0') { version = xml_get_ns_attribute (cur, "version", NULL); if (version) { tmp = g_strdup_printf ("%s %s", ret, version); g_free (ret); g_free (version); ret = tmp; } uri = xml_get_ns_attribute (cur, "uri", NULL); if (uri) { tmp = g_markup_printf_escaped ("%s", uri, ret); g_free (uri); g_free (ret); ret = tmp; } ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "feedgenerator", tmp); } g_free (ret); } static void atom10_parse_feed_icon (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *icon_uri; icon_uri = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (icon_uri) { debug1 (DEBUG_PARSING, "icon URI found in atom feed: %s", icon_uri); ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "icon", icon_uri); } } static void atom10_parse_feed_id (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { /* FIXME: Parse ID, but I'm not sure where Liferea would use it */ } static void atom10_parse_feed_link (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *href; href = atom10_parse_link (cur, ctxt, state); if (href) { xmlChar *baseURL = xmlNodeGetBase (cur->doc, xmlDocGetRootElement (cur->doc)); subscription_set_homepage (ctxt->subscription, href); /* Set the default base to the feed's HTML URL if not set yet */ if (baseURL == NULL) xmlNodeSetBase (xmlDocGetRootElement (cur->doc), (xmlChar *)href); else xmlFree (baseURL); g_free (href); } } static void atom10_parse_feed_logo (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *logoUrl; logoUrl = atom10_parse_text_construct (cur, FALSE); if (logoUrl) { metadata_list_set (&ctxt->subscription->metadata, "imageUrl", logoUrl); g_free (logoUrl); } } static void atom10_parse_feed_rights (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *rights; rights = atom10_parse_text_construct (cur, FALSE); if (rights) { ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "copyright", rights); g_free (rights); } } static void atom10_parse_feed_subtitle (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *subtitle; subtitle = atom10_parse_text_construct (cur, TRUE); if (subtitle) { metadata_list_set (&ctxt->subscription->metadata, "description", subtitle); g_free (subtitle); } } static void atom10_parse_feed_title (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *title; title = atom10_parse_text_construct(cur, FALSE); if (title) { if (ctxt->title) g_free (ctxt->title); ctxt->title = title; } } /* Sort items in descending date order (newer items first). */ static gint atom10_item_sort_by_date (gconstpointer a, gconstpointer b) { itemPtr item1 = (itemPtr)a; itemPtr item2 = (itemPtr)b; g_assert (item1 && item2); if (item1->time == item2->time) { /* Items identical.. can we distinguish further? */ return 0; } if (item1->time < item2->time) return 1; if (item1->time > item2->time) return -1; return 0; } static void atom10_parse_feed_updated (xmlNodePtr cur, feedParserCtxtPtr ctxt, struct atom10ParserState *state) { gchar *timestamp; timestamp = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (timestamp) { ctxt->subscription->metadata = metadata_list_append (ctxt->subscription->metadata, "contentUpdateDate", timestamp); ctxt->feed->time = date_parse_ISO8601 (timestamp); g_free (timestamp); } } /* reads a Atom feed URL and returns a new channel structure (even if the feed could not be read) */ static void atom10_parse_feed (feedParserCtxtPtr ctxt, xmlNodePtr cur) { NsHandler *nsh; parseChannelTagFunc pf; atom10ElementParserFunc func; static GHashTable *feedElementHash = NULL; if(!feedElementHash) { feedElementHash = g_hash_table_new (g_str_hash, g_str_equal); g_hash_table_insert (feedElementHash, "author", &atom10_parse_feed_author); g_hash_table_insert (feedElementHash, "category", &atom10_parse_feed_category); g_hash_table_insert (feedElementHash, "contributor", &atom10_parse_feed_contributor); g_hash_table_insert (feedElementHash, "generator", &atom10_parse_feed_generator); g_hash_table_insert (feedElementHash, "icon", &atom10_parse_feed_icon); g_hash_table_insert (feedElementHash, "id", &atom10_parse_feed_id); g_hash_table_insert (feedElementHash, "link", &atom10_parse_feed_link); g_hash_table_insert (feedElementHash, "logo", &atom10_parse_feed_logo); g_hash_table_insert (feedElementHash, "rights", &atom10_parse_feed_rights); g_hash_table_insert (feedElementHash, "subtitle", &atom10_parse_feed_subtitle); g_hash_table_insert (feedElementHash, "title", &atom10_parse_feed_title); g_hash_table_insert (feedElementHash, "updated", &atom10_parse_feed_updated); } while (TRUE) { if (xmlStrcmp (cur->name, BAD_CAST"feed")) { g_string_append (ctxt->feed->parseErrors, "

Could not find Atom 1.0 header!

"); break; } /* parse feed contents */ cur = cur->xmlChildrenNode; while (cur) { if (!cur->name || cur->type != XML_ELEMENT_NODE || !cur->ns) { cur = cur->next; continue; } /* check if supported namespace should handle the current tag by trying to determine a namespace handler */ nsh = NULL; if (cur->ns->href) nsh = (NsHandler *)g_hash_table_lookup (ns_atom10_ns_uri_table, (gpointer)cur->ns->href); if (cur->ns->prefix && !nsh) nsh = (NsHandler *)g_hash_table_lookup (atom10_nstable, (gpointer)cur->ns->prefix); if(nsh) { pf = nsh->parseChannelTag; if(pf) (*pf)(ctxt, cur); cur = cur->next; continue; } /* check namespace of this tag */ if (!cur->ns->href) { /* This is an invalid feed... no idea what to do with the current element */ debug1 (DEBUG_PARSING, "element with no namespace found in atom feed (%s)!", cur->name); cur = cur->next; continue; } if (xmlStrcmp (cur->ns->href, ATOM10_NS)) { debug1 (DEBUG_PARSING, "unknown namespace %s found in atom feed!", cur->ns->href); cur = cur->next; continue; } /* At this point, the namespace must be the Atom 1.0 namespace */ func = g_hash_table_lookup (feedElementHash, cur->name); if (func) { (*func) (cur, ctxt, NULL); } else if (xmlStrEqual (cur->name, BAD_CAST"entry")) { ctxt->item = atom10_parse_entry (ctxt, cur); if (ctxt->item) ctxt->items = g_list_insert_sorted (ctxt->items, ctxt->item, atom10_item_sort_by_date); } cur = cur->next; } /* FIXME: Maybe check to see that the required information was actually provided (persuant to the RFC). */ /* after parsing we fill in the infos into the feedPtr structure */ break; } } static gboolean atom10_format_check (xmlDocPtr doc, xmlNodePtr cur) { if (cur->name == NULL || cur->ns == NULL || cur->ns->href == NULL) return FALSE; return xmlStrEqual (cur->name, BAD_CAST"feed") && xmlStrEqual (cur->ns->href, ATOM10_NS); } static void atom10_add_ns_handler (NsHandler *handler) { g_assert (NULL != atom10_nstable); g_hash_table_insert (atom10_nstable, (gpointer)handler->prefix, handler); g_assert (handler->registerNs != NULL); handler->registerNs (handler, atom10_nstable, ns_atom10_ns_uri_table); } feedHandlerPtr atom10_init_feed_handler (void) { feedHandlerPtr fhp; fhp = g_new0 (struct feedHandler, 1); if (!atom10_nstable) { atom10_nstable = g_hash_table_new (g_str_hash, g_str_equal); ns_atom10_ns_uri_table = g_hash_table_new (g_str_hash, g_str_equal); /* register name space handlers */ atom10_add_ns_handler (ns_dc_get_handler ()); atom10_add_ns_handler (ns_slash_get_handler ()); atom10_add_ns_handler (ns_content_get_handler ()); atom10_add_ns_handler (ns_syn_get_handler ()); atom10_add_ns_handler (ns_admin_get_handler ()); atom10_add_ns_handler (ns_ag_get_handler ()); atom10_add_ns_handler (ns_cC_get_handler ()); atom10_add_ns_handler (ns_wfw_get_handler ()); atom10_add_ns_handler (ns_media_get_handler ()); atom10_add_ns_handler (ns_trackback_get_handler ()); atom10_add_ns_handler (ns_georss_get_handler ()); } /* prepare feed handler structure */ fhp->typeStr = "atom"; fhp->feedParser = atom10_parse_feed; fhp->checkFormat = atom10_format_check; return fhp; } liferea-1.13.7/src/parsers/atom10.h000066400000000000000000000017701415350204600167370ustar00rootroot00000000000000/** * @file atom10.h Atom 1.0 Parser * * Copyright (C) 2005-2006 Nathan Conrad * Copyright (C) 2003-2008 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ATOM10_H #define _ATOM10_H #include "feed_parser.h" feedHandlerPtr atom10_init_feed_handler(void); #endif liferea-1.13.7/src/parsers/html5_feed.c000066400000000000000000000115371415350204600176470ustar00rootroot00000000000000/** * @file html5_feed.c Parsing semantic annotated HTML5 webpages like feeds * * Copyright (C) 2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "html5_feed.h" #include "common.h" #include "date.h" #include "feed_parser.h" #include "metadata.h" #include "xml.h" static void html5_feed_parse_article_title (xmlNodePtr cur, feedParserCtxtPtr ctxt) { gchar *tmp = unxmlize (xhtml_extract (cur, 2, NULL)); if (tmp) { g_strchug (g_strchomp (tmp)); item_set_title (ctxt->item, tmp); g_free(tmp); } } static void html5_feed_parse_article (xmlNodePtr itemNode, gpointer userdata) { feedParserCtxtPtr ctxt = (feedParserCtxtPtr)userdata; xmlNodePtr cur; gchar *tmp; ctxt->item = item_new (); // Get article time if ((cur = xpath_find (itemNode, ".//time/@datetime"))) { tmp = xhtml_extract (cur, 0, NULL); if (tmp) { ctxt->item->time = date_parse_RFC822 (tmp); g_free(tmp); } } // or default to current feed timestamp if (0 == ctxt->item->time) ctxt->item->time = ctxt->feed->time; // get link if ((cur = xpath_find (itemNode, ".//a/@href"))) { tmp = (gchar *)xmlNodeListGetString (cur->doc, cur->xmlChildrenNode, 1); if (tmp) { xmlChar *link = common_build_url (tmp, ctxt->subscription->source); item_set_source (ctxt->item, (gchar *)link); xmlFree (link); // we use the link as id, as on websites link point to unique // content in 99% of the cases ctxt->item->sourceId = tmp; } } // extract title if ((cur = xpath_find (itemNode, ".//h1"))) html5_feed_parse_article_title (cur, ctxt); else if ((cur = xpath_find (itemNode, ".//h2"))) html5_feed_parse_article_title (cur, ctxt); else if ((cur = xpath_find (itemNode, ".//h3"))) html5_feed_parse_article_title (cur, ctxt); // Extract the actual article tmp = xhtml_extract (itemNode, 1, NULL); if (tmp) { item_set_description (ctxt->item, tmp); g_free(tmp); } if (ctxt->item->sourceId && ctxt->item->title && ctxt->item->description) ctxt->items = g_list_append (ctxt->items, ctxt->item); else item_unload (ctxt->item); } /** * Parses given data as a HTML5 document * * @param ctxt the feed parser context * @param cur the root node of the XML document */ static void html5_feed_parse (feedParserCtxtPtr ctxt, xmlNodePtr root) { gchar *tmp; xmlNodePtr cur; xmlChar *baseURL = xmlNodeGetBase (root->doc, root); ctxt->feed->time = time(NULL); /* For HTML5 source the homepage is the source */ subscription_set_homepage (ctxt->subscription, ctxt->subscription->source); /* Set the default base to the feed's HTML URL if not set yet */ if (baseURL == NULL) xmlNodeSetBase (root, (xmlChar *)ctxt->subscription->source); if ((cur = xpath_find (root, "/html/head/title"))) { ctxt->title = unxmlize (xhtml_extract (cur, 0, NULL)); } if ((cur = xpath_find (root, "/html/head/meta[@name = 'description']"))) { tmp = xhtml_extract (cur, 0, NULL); if (tmp) { metadata_list_set (&ctxt->subscription->metadata, "description", tmp); g_free (tmp); } } if(!xpath_foreach_match (root, "/html/body//article", html5_feed_parse_article, ctxt)) { g_string_append(ctxt->feed->parseErrors, "

Could not find HTML5 tags!

"); return; } } static void html5_feed_check_article (xmlNodePtr cur, gpointer userdata) { gint *articleCount = (gint *)userdata; if (xpath_find (cur, ".//h1") || xpath_find (cur, ".//h2") || xpath_find (cur, ".//h3")) (*articleCount)++; } static gboolean html5_feed_check (xmlDocPtr doc, xmlNodePtr root) { gint articleCount = 0; /* A HTML5 website that we can parse like a feed must meet the following criteria - XHTML - multiple
tags - inside the
an

or

tag that we can use as title */ xpath_foreach_match (root, "/html/body//article", html5_feed_check_article, &articleCount); // let's say 3 suffices for "multiple" articles if (articleCount >= 3) return TRUE; return FALSE; } feedHandlerPtr html5_init_feed_handler (void) { feedHandlerPtr fhp; fhp = g_new0 (struct feedHandler, 1); /* prepare feed handler structure */ fhp->typeStr = "html5"; fhp->feedParser = html5_feed_parse; fhp->checkFormat = html5_feed_check; fhp->html = TRUE; return fhp; } liferea-1.13.7/src/parsers/html5_feed.h000066400000000000000000000017321415350204600176500ustar00rootroot00000000000000/** * @file html5_feed.h Parsing semantic annotated HTML5 webpages like feeds * * Copyright (C) 2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _HTML5_FEED_H #define _HTML5_FEED_H #include "feed_parser.h" feedHandlerPtr html5_init_feed_handler(void); #endif liferea-1.13.7/src/parsers/ldjson_feed.c000066400000000000000000000235771415350204600201160ustar00rootroot00000000000000/** * @file ldjson_feed.c Parsing LD+JSON snippets in HTML5 webpages like feeds * * Copyright (C) 2020-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ldjson_feed.h" #include "common.h" #include "date.h" #include "feed_parser.h" #include "json.h" #include "metadata.h" #include "xml.h" static void ldjson_feed_parse_json_website (JsonNode *node, feedParserCtxtPtr ctxt) { JsonNode *logo; const gchar *tmp; /* Try to extract subscription details (title, logo...) Example: { "@context" : "http://schema.org", "type" : "WebPage", "url" : "https://www.meinestadt.de/", "@type" : "Organization", "name": "meinestadt.de GmbH", "logo" : { "@type" : "ImageObject", "url" : "https://unternehmen.meinestadt.de/logos/meinestadt_de_logo.jpg", "width" : 500, "height" : 500 }, "sameAs" : [ "https://www.facebook.com/meinestadt.de", "https://www.facebook.com/meinestadt.destellenmarkt", "https://www.facebook.com/meinestadt.deapp", "https://www.xing.com/company/meinestadt-de", "https://www.linkedin.com/company/allesklar.com-ag", "https://de.wikipedia.org/wiki/Meinestadt.de", "https://www.youtube.com/user/meinestadtde", "https://www.instagram.com/meinestadt.de/", "https://twitter.com/meinestadt_de" ] } */ if ((tmp = json_get_string (node, "name"))) node_set_title (ctxt->subscription->node, tmp); if ((tmp = json_get_string (node, "url"))) subscription_set_homepage (ctxt->subscription, tmp); if ((logo = json_get_node (node, "logo"))) if ((tmp = json_get_string (logo, "url"))) metadata_list_set (&ctxt->subscription->metadata, "imageUrl", tmp); } static void ldjson_feed_parse_json_event (JsonNode *node, feedParserCtxtPtr ctxt) { const gchar *tmp; const gchar *eventStatus, *type; ctxt->item = item_new (); /* what we expect here: 1.) MusicEvent { "name": "Band name", "image": "...", "startDate": "2021-06-12T17:00:00+01:00", "endDate": "2021-06-12", "previousStartDate": "2020-06-17T16:00:00Z", "url": "https://example.com", "description": "detailed description", "offers": { "url": "https://example.com/abc", "availabilityStarts": "2019-07-05T10:00:00Z", "priceCurrency": "GBP", "price": "89", "validFrom": "2019-07-05T10:00:00Z", "@type": "Offer" }, "performer": [ { "name": "Performer 1", "sameAs": "https://example.com/performer1", "@type": "MusicGroup" }, { "name": "Performer 2", "sameAs": "https://example.com/performer2", "@type": "MusicGroup" } ], "@type": "MusicEvent" } */ /* eventStatus is a schema.org enum e.g. "https://schema.org/EventCancelled" let's strip the prefix and use the enum name as is */ type = json_get_string (node, "@type"); eventStatus = json_get_string (node, "eventStatus"); if (eventStatus && eventStatus == g_strstr_len (eventStatus, -1, "https://schema.org/Event")) eventStatus += 24; else eventStatus = NULL; if ((tmp = json_get_string (node, "startDate"))) { // schema.org says startDate should be ISO8601, but RFC822 // is seen to often. So fuzzy match date format. if (strstr (tmp, " ")) ctxt->item->time = date_parse_RFC822 (tmp); else ctxt->item->time = date_parse_ISO8601 (tmp); } else { // or default to current feed timestamp ctxt->item->time = ctxt->feed->time; } if ((tmp = json_get_string (node, "url"))) { xmlChar *link = common_build_url (tmp, ctxt->subscription->source); item_set_source (ctxt->item, (const gchar *)link); xmlFree (link); // we use the link as id, as on websites link point to unique // content in 99% of the cases ctxt->item->sourceId = g_strdup (tmp); } if ((tmp = json_get_string (node, "name"))) { gchar *title; if (eventStatus && !g_str_equal (eventStatus, "Scheduled")) title = g_strdup_printf("%s %s (%s)", g_str_equal(type, "MusicEvent")?"🎹":"🎪", tmp, eventStatus?eventStatus:""); else title = g_strdup (tmp); item_set_title (ctxt->item, title); g_free (title); } if ((tmp = json_get_string (node, "description"))) { GString *description = g_string_new (NULL); const gchar *image = json_get_string (node, "image"); if (image) g_string_append_printf (description, "

", image); g_string_append (description, tmp); if (eventStatus) g_string_append_printf (description, "

Status: %s

", eventStatus); item_set_description (ctxt->item, description->str); g_string_free (description, TRUE); } // FIXME: extract 'location' // FIXME: extract 'performers' // FIXME: extract offer if (ctxt->item->sourceId && ctxt->item->title) ctxt->items = g_list_append (ctxt->items, ctxt->item); else item_unload (ctxt->item); } /* demux different LD+JSON types */ static void ldjson_feed_parse_json_by_type (JsonNode *node, feedParserCtxtPtr ctxt) { const gchar *type = json_get_string (node, "@type"); if (!type) return; if (g_str_equal (type, "Event") || g_str_equal (type, "MusicEvent")) ldjson_feed_parse_json_event (node, ctxt); if (g_str_equal (type, "WebPage")) ldjson_feed_parse_json_website (node, ctxt); // FIXME: implement offer // FIXME: implement job posting } static void ldjson_feed_parse_json (xmlNodePtr xml, gpointer userdata) { feedParserCtxtPtr ctxt = (feedParserCtxtPtr)userdata; JsonParser *json = json_parser_new (); JsonNode *node; xmlChar *data = xmlNodeListGetString (xml->doc, xml->xmlChildrenNode, 1); if (data) { json_parser_load_from_data (json, (gchar *)data, -1, NULL); node = json_parser_get_root (json); if (node) { /* Loop over array objects or directly use object as is */ if (JSON_NODE_OBJECT == json_node_get_node_type (node)) ldjson_feed_parse_json_by_type (node, ctxt); if (JSON_NODE_ARRAY == json_node_get_node_type (node)) { GList *list = json_array_get_elements (json_node_get_array (node)); while (list) { ldjson_feed_parse_json_by_type ((JsonNode *)list->data, ctxt); list = g_list_next (list); } g_list_free (list); } } g_object_unref (json); } xmlFree (data); } /** * Parses given data as a HTML document containing LD+JSON data * * @param ctxt the feed parser context * @param cur the root node of the XML document */ static void ldjson_feed_parse (feedParserCtxtPtr ctxt, xmlNodePtr root) { gchar *tmp; xmlNodePtr cur; xmlChar *baseURL = xmlNodeGetBase (root->doc, root); ctxt->feed->time = time(NULL); /* For homepage the HTML page itself is the default source */ subscription_set_homepage (ctxt->subscription, ctxt->subscription->source); /* Set the default base to the feed's HTML URL if not set yet */ if (baseURL == NULL) xmlNodeSetBase (root, (xmlChar *)ctxt->subscription->source); if ((cur = xpath_find (root, "/html/head/title"))) { ctxt->title = unxmlize (xhtml_extract (cur, 0, NULL)); } if ((cur = xpath_find (root, "/html/head/meta[@name = 'description']"))) { tmp = xhtml_extract (cur, 0, NULL); if (tmp) { metadata_list_set (&ctxt->subscription->metadata, "description", tmp); g_free (tmp); } } if(!xpath_foreach_match (root, "//script[@type='application/ld+json']", ldjson_feed_parse_json, ctxt)) { g_string_append(ctxt->feed->parseErrors, "

Could not find any ld+json tags!

"); return; } } static void ldjson_feed_check_json_type (JsonNode *node, gpointer userdata) { const gchar *type; gint *entryCount = (gint *)userdata; type = json_get_string (node, "@type"); if (type) { /* Let us not count 'WebPage' here as it does not indicate items, but only subscription infos */ if (g_str_equal (type, "MusicEvent") || g_str_equal (type, "Event")) (*entryCount)++; } } static void ldjson_feed_check_json (xmlNodePtr xml, gpointer userdata) { xmlChar *data; JsonParser *json = json_parser_new (); JsonNode *node; data = xmlNodeListGetString (xml->doc, xml->xmlChildrenNode, 1); if (data) { json_parser_load_from_data (json, (gchar *)data, -1, NULL); node = json_parser_get_root (json); if (node) { /* Loop over array objects or directly use object as is */ if (JSON_NODE_OBJECT == json_node_get_node_type (node)) ldjson_feed_check_json_type (node, userdata); if (JSON_NODE_ARRAY == json_node_get_node_type (node)) { GList *list = json_array_get_elements (json_node_get_array (node)); while (list) { ldjson_feed_check_json_type ((JsonNode *)list->data, userdata); list = g_list_next (list); } g_list_free (list); } } g_object_unref (json); } xmlFree (data); } static gboolean ldjson_feed_check (xmlDocPtr doc, xmlNodePtr root) { gint entryCount = 0; /* A HTML website with LD+JSON that we can parse like a feed must meet the following criteria - XML readable XHTML/HTML5/HTML - multiple

1

", "

1

\n" }; gchar *tc_article_micro_format[] = { "

1

", "

1

\n" }; gchar *tc_article_cms_content_id[] = { "

1

", "

1

\n" }; gchar *tc_article_missing[] = { "

1

", NULL }; /* this test case is about an empty tag "" not being collapsed to "" but to be output as " " instead */ gchar *tc_article_empty_tags[] = { "

1

", "

1

\n" }; /* this test case is about nested empty tags "" being expanded as " " */ gchar *tc_article_empty_tags_nested[] = { "

1

", "

1

\n
\n
\n" }; /* this test case is about empty XHTML tags "" being expanded */ gchar *tc_article_self_closed_tags[] = { "

1

", "

1

\n" }; /* this test case is about nested empty XHTML tags "" being expanded */ gchar *tc_article_self_closed_tags_nested[] = { "

1

", "

1

\n
\n
\n" }; /* this test case is about stripping inline script and CSS */ gchar *tc_article_strip_inline_code[] = { "

1

", "

1

\n" }; static void tc_auto_discover_link (gconstpointer user_data) { gchar **tc = (gchar **)user_data; GSList *result; guint i = 2; result = html_auto_discover_feed (g_strdup (tc[0]), tc[1]); do { if (!tc[i]) { g_assert_null (result); } else { g_assert_cmpstr (tc[i], ==, result->data); result = g_slist_next (result); } } while(tc[i++]); } static void tc_get_article (gconstpointer user_data) { gchar **tc = (gchar **)user_data; gchar *result = html_get_article (tc[0], "https://example.com"); if (!tc[1]) g_assert_null (result); else g_assert_cmpstr (tc[1], ==, result); g_free (result); } int main (int argc, char *argv[]) { g_test_init (&argc, &argv, NULL); g_test_add_data_func ("/html/auto_discover_link_xml", &tc_xml, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_xml_base_url", &tc_xml_base_url, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_rss", &tc_rss, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_rdf", &tc_rdf, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_atom", &tc_atom, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_atom2", &tc_atom2, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_broken_tag", &tc_broken_tag, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_garbage", &tc_garbage, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_xml_atom", &tc_xml_atom, &tc_auto_discover_link); g_test_add_data_func ("/html/auto_discover_link_xml_atom2", &tc_xml_atom2, &tc_auto_discover_link); g_test_add_data_func ("/html/html5_extract_article", &tc_article, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_micro_format", &tc_article_micro_format, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_cms_content_id", &tc_article_cms_content_id, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_missing", &tc_article_missing, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_empty_tags", &tc_article_empty_tags, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_empty_tags_nested", &tc_article_empty_tags_nested, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_self_closed_tags", &tc_article_self_closed_tags, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_self_closed_tags_nested", &tc_article_self_closed_tags_nested, &tc_get_article); g_test_add_data_func ("/html/html5_extract_article_strip_inline_code", &tc_article_strip_inline_code, &tc_get_article); return g_test_run(); } liferea-1.13.7/src/tests/parse_xml.c000066400000000000000000000076751415350204600173200ustar00rootroot00000000000000/** * @file parse_xml.c Test cases for XML helpers * * Copyright (C) 2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "xml.h" typedef struct tcXPath { gchar *name; gchar *xml_string; gchar *xpath_expression; gboolean result; } *tcXPathPtr; struct tcXPath tc_xpath[][4] = { { "/parse_xml/xpath_find_empty_doc", "\n", "/html/body", TRUE // counter-intuitive, but a body is automatically added by libxml2! }, { "/parse_xml/xpath_find_empty_doc2", "\n", "/html/body", TRUE }, { "/parse_xml/xpath_find_real_doc", "\n\nTitle\njssj", "/html/body", TRUE }, { "/parse_xml/xpath_find_atom_feed", "", "/html/head/link[@rel='alternate' and @type='application/atom+xml']/@href", TRUE }, NULL }; typedef struct tcStripper { gchar *name; gchar *xml_string; gchar *xpath_expression; // expression that must not be found } *tcStripperPtr; struct tcStripper tc_strippers[][3] = { { "/xhtml_strip/onload", "
< div onload=\"alert('Hallo');\">", "//div/@onload" }, { "/xhtml_strip/meta", "", "//meta" }, { "/xhtml_strip/wbr", "
", "//wbr" }, { "/xhtml_strip/extra_body", "
abc
", "//div/body" }, { "/xhtml_strip/script", "
", "//script" }, { "/xhtml_strip/iframe", "
", "//iframe" }, NULL }; static void tc_xpath_find (gconstpointer user_data) { tcXPathPtr tc = (tcXPathPtr)user_data; xmlDocPtr doc = xhtml_parse ((gchar *)tc->xml_string, (size_t)strlen (tc->xml_string)); xmlNodePtr root; g_assert_false (!doc); root = xmlDocGetRootElement (doc); g_assert_false (!root); g_assert_true ((xpath_find (root, tc->xpath_expression) != NULL) == tc->result); xmlFreeDoc (doc); } static void tc_strip (gconstpointer user_data) { tcStripperPtr tc = (tcStripperPtr)user_data; xmlDocPtr doc; xmlNodePtr root; gchar *stripped; stripped = xhtml_strip_dhtml ((const gchar *)tc->xml_string); g_assert_true (stripped != NULL); doc = xhtml_parse (stripped, (size_t)strlen (stripped)); g_free (stripped); g_assert_false (!doc); root = xmlDocGetRootElement (doc); g_assert_false (!root); g_assert_true (xpath_find (root, tc->xpath_expression) == NULL); xmlFreeDoc (doc); } int main (int argc, char *argv[]) { gint result; xml_init (); g_test_init (&argc, &argv, NULL); for (int i = 0; tc_xpath[i]->xml_string != NULL; i++) { g_test_add_data_func (tc_xpath[i]->name, &tc_xpath[i], &tc_xpath_find); } for (int i = 0; tc_strippers[i]->xml_string != NULL; i++) { g_test_add_data_func (tc_strippers[i]->name, &tc_strippers[i], &tc_strip); } result = g_test_run(); xml_deinit (); return result; } liferea-1.13.7/src/tests/test_a11y.sh000077500000000000000000000005721415350204600173200ustar00rootroot00000000000000#!/bin/bash if command -vp gla11y >/dev/null; then output=$(gla11y ../../glade/*.ui) echo "$output" # For now lets prevent only fatals if echo "$output" | grep -q FATAL; then printf "ERROR: Fatal accessibility issues were found!\n" exit 1 else printf "Accessibility looks fine.\n" fi else printf "WARNING: gla11y is not installed, cannot test accessibility\n" fi liferea-1.13.7/src/ui/000077500000000000000000000000001415350204600144165ustar00rootroot00000000000000liferea-1.13.7/src/ui/Makefile.am000066400000000000000000000023051415350204600164520ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in AM_CPPFLAGS = \ -DPACKAGE_DATA_DIR=\""$(datadir)"\" \ -DPACKAGE_LIB_DIR=\""$(pkglibdir)"\" \ -DPACKAGE_LOCALE_DIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \ -I$(top_srcdir)/src noinst_LIBRARIES = libliui.a libliui_a_CFLAGS = $(PACKAGE_CFLAGS) $(LIBINDICATE_CFLAGS) libliui_a_SOURCES = \ auth_dialog.c auth_dialog.h \ browser_tabs.c browser_tabs.h \ enclosure_list_view.c enclosure_list_view.h \ feed_list_view.c feed_list_view.h \ gedit-close-button.c gedit-close-button.h \ icons.c icons.h \ item_list_view.c item_list_view.h \ itemview.c itemview.h \ liferea_dialog.c liferea_dialog.h \ liferea_htmlview.c liferea_htmlview.h \ liferea_shell.c liferea_shell.h \ liferea_shell_activatable.c liferea_shell_activatable.h \ media_player.c media_player.h \ media_player_activatable.c media_player_activatable.h \ popup_menu.c popup_menu.h \ preferences_dialog.c preferences_dialog.h \ rule_editor.c rule_editor.h \ search_dialog.c search_dialog.h \ search_folder_dialog.c search_folder_dialog.h \ subscription_dialog.c subscription_dialog.h \ ui_common.c ui_common.h \ ui_dnd.c ui_dnd.h \ ui_folder.c ui_folder.h \ ui_update.c ui_update.h liferea-1.13.7/src/ui/auth_dialog.c000066400000000000000000000074731415350204600170550ustar00rootroot00000000000000/** * @file auth_dialog.c authentication dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/auth_dialog.h" #include #include "common.h" #include "debug.h" #include "ui/liferea_dialog.h" struct _AuthDialog { GObject parentInstance; subscriptionPtr subscription; GtkWidget *dialog; GtkWidget *username; GtkWidget *password; gint flags; }; G_DEFINE_TYPE (AuthDialog, auth_dialog, G_TYPE_OBJECT); static void auth_dialog_finalize (GObject *object) { AuthDialog *ad = AUTH_DIALOG (object); if (ad->subscription != NULL) ad->subscription->activeAuth = FALSE; gtk_widget_destroy (ad->dialog); } static void auth_dialog_class_init (AuthDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = auth_dialog_finalize; } static void on_authdialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { AuthDialog *ad = AUTH_DIALOG (user_data); if (response_id == GTK_RESPONSE_OK) { subscription_set_auth_info (ad->subscription, gtk_entry_get_text (GTK_ENTRY (ad->username)), gtk_entry_get_text (GTK_ENTRY (ad->password))); subscription_update (ad->subscription, ad->flags); } g_object_unref (ad); } static void auth_dialog_load (AuthDialog *ad, subscriptionPtr subscription, gint flags) { gchar *promptStr; gchar *source = NULL; xmlURIPtr uri; subscription->activeAuth = TRUE; ad->subscription = subscription; ad->flags = flags; uri = xmlParseURI (subscription_get_source (ad->subscription)); if (uri) { if (uri->user) { gchar *user = uri->user; gchar *pass = strstr (user, ":"); if(pass) { pass[0] = '\0'; pass++; gtk_entry_set_text (GTK_ENTRY (ad->password), pass); } gtk_entry_set_text (GTK_ENTRY (ad->username), user); xmlFree (uri->user); uri->user = NULL; } xmlFree (uri->user); uri->user = NULL; source = (gchar *) xmlSaveUri (uri); xmlFreeURI (uri); } promptStr = g_strdup_printf ( _("Enter the username and password for \"%s\" (%s):"), node_get_title (ad->subscription->node), source?source:_("Unknown source")); gtk_label_set_text (GTK_LABEL (liferea_dialog_lookup (ad->dialog, "prompt")), promptStr); g_free (promptStr); if (source) xmlFree (source); } static void auth_dialog_init (AuthDialog *ad) { ad->dialog = liferea_dialog_new ("auth"); ad->username = liferea_dialog_lookup (ad->dialog, "usernameEntry"); ad->password = liferea_dialog_lookup (ad->dialog, "passwordEntry"); g_signal_connect (G_OBJECT (ad->dialog), "response", G_CALLBACK (on_authdialog_response), ad); gtk_widget_show_all (ad->dialog); } AuthDialog * auth_dialog_new (subscriptionPtr subscription, gint flags) { AuthDialog *ad; if (subscription->activeAuth) { debug0 (DEBUG_UPDATE, "Missing/wrong authentication. Skipping, as a dialog is already active."); return NULL; } ad = AUTH_DIALOG (g_object_new (AUTH_DIALOG_TYPE, NULL)); auth_dialog_load(ad, subscription, flags); return ad; } liferea-1.13.7/src/ui/auth_dialog.h000066400000000000000000000027211415350204600170510ustar00rootroot00000000000000/** * @file auth_dialog.h authentication support dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_AUTH_H #define _UI_AUTH_H #include #include "subscription.h" G_BEGIN_DECLS #define AUTH_DIALOG_TYPE (auth_dialog_get_type ()) G_DECLARE_FINAL_TYPE (AuthDialog, auth_dialog, AUTH, DIALOG, GObject) /** * auth_dialog_new: * Create a new authentication dialog if there is not already one for * the given subscription. * * @subscription: the subscription whose authentication info is needed * @flags: the flags for the update request after authenticating * * Returns: (transfer none): new dialog */ AuthDialog * auth_dialog_new (subscriptionPtr subscription, gint flags); G_END_DECLS #endif /* _UI_AUTH_H */ liferea-1.13.7/src/ui/browser_tabs.c000066400000000000000000000225451415350204600172660ustar00rootroot00000000000000/* * @file browser_tabs.c internal browsing using multiple tabs * * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2006 Nathan Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/browser_tabs.h" #include #include #include "common.h" #include "ui/liferea_shell.h" #include "ui/item_list_view.h" #include "ui/gedit-close-button.h" /* All widget elements and state of a tab */ typedef struct _tabInfo tabInfo; struct _tabInfo { GtkWidget *label; /*<< the tab label */ GtkWidget *widget; /*<< the embedded child widget */ LifereaHtmlView *htmlview; /*<< the tabs HTML view widget */ }; /** * tab_info_copy: (skip) * * Creates a copy of @tabInfo */ static gpointer tab_info_copy (gpointer orig) { tabInfo *a = orig; tabInfo *b; b = g_new0 (tabInfo, 1); b->label = a->label; b->widget = a->widget; b->htmlview = a->htmlview; // Shall we? g_object_ref(b->label); g_object_ref(b->widget); g_object_ref(b->htmlview); return b; } /** * tab_info_free: (skip) * * free @tabInfo */ static void tab_info_free (gpointer orig) { tabInfo *a = orig; g_object_unref(a->label); g_object_unref(a->widget); g_object_unref(a->htmlview); g_free(orig); } G_DEFINE_BOXED_TYPE (tabInfo, tab_info, tab_info_copy, tab_info_free) // gslist type for gproperty //https://git.gnome.org/browse/gobject-introspection/tree/tests/gimarshallingtests.c static GType gi_marshalling_boxed_gslist_get_type (void) { static GType type = 0; if (type == 0) { type = g_boxed_type_register_static ("GIMarshallingBoxedGSList", (GBoxedCopyFunc) g_slist_copy, (GBoxedFreeFunc) g_slist_free); } return type; } /* tab callbacks */ static void browser_tabs_close_tab (tabInfo *tab); static gboolean on_tab_key_press (GtkWidget *widget, GdkEventKey *event, gpointer data) { guint modifiers; modifiers = gtk_accelerator_get_default_mod_mask (); if ((event->keyval == GDK_KEY_w) && ((event->state & modifiers) == GDK_CONTROL_MASK)) { browser_tabs_close_tab ((tabInfo *)data); return TRUE; } return FALSE; } /* browser tabs object */ enum { PROP_NONE, PROP_NOTEBOOK, PROP_HEAD_LINES, PROP_TAB_INFO_LIST }; struct _BrowserTabs { GObject parentInstance; GtkNotebook *notebook; GtkWidget *headlines; /*<< widget of the headlines tab */ GSList *list; /*<< tabInfo structures for all tabs */ }; static BrowserTabs *tabs = NULL; G_DEFINE_TYPE (BrowserTabs, browser_tabs, G_TYPE_OBJECT); /* Removes tab info structure */ static void browser_tabs_remove_tab (tabInfo *tab) { tabs->list = g_slist_remove (tabs->list, tab); g_object_unref (tab->htmlview); g_free (tab); } static void browser_tabs_finalize (GObject *object) { BrowserTabs *bt = BROWSER_TABS (object); GSList *iter = bt->list; while (iter) { browser_tabs_remove_tab (iter->data); iter = g_slist_next (iter); } } static void browser_tabs_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BrowserTabs *bt = BROWSER_TABS (object); switch (prop_id) { case PROP_NOTEBOOK: g_value_set_object (value, bt->notebook); break; case PROP_HEAD_LINES: g_value_set_object (value, bt->headlines); break; case PROP_TAB_INFO_LIST: g_value_set_boxed (value, bt->list); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void browser_tabs_class_init (BrowserTabsClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = browser_tabs_get_property; object_class->finalize = browser_tabs_finalize; /* BrowserTabs:notebook: */ g_object_class_install_property ( object_class, PROP_NOTEBOOK, g_param_spec_object ( "notebook", "GtkNotebook", "GtkNotebook object", GTK_TYPE_NOTEBOOK, G_PARAM_READABLE)); /* BrowserTabs:headlines: */ g_object_class_install_property ( object_class, PROP_HEAD_LINES, g_param_spec_object ( "head-lines", "GtkWidget", "GtkWidget object", GTK_TYPE_WIDGET, G_PARAM_READABLE)); /** * BrowserTabs:tab-info-list: (type GSList(tabInfo)) (transfer none): */ g_object_class_install_property ( object_class, PROP_TAB_INFO_LIST, g_param_spec_boxed( "tab-info-list", "SList of browser tab info", "A GList of tab info containing htmlviews", gi_marshalling_boxed_gslist_get_type(), G_PARAM_READABLE)); } static void browser_tabs_init (BrowserTabs *bt) { /* globally accessible singleton */ g_assert (NULL == tabs); tabs = bt; } BrowserTabs * browser_tabs_create (GtkNotebook *notebook) { g_object_new (BROWSER_TABS_TYPE, NULL); tabs->notebook = notebook; tabs->headlines = gtk_notebook_get_nth_page (notebook, 0); gtk_notebook_set_show_tabs (tabs->notebook, FALSE); return tabs; } /* HTML view signal handlers */ static const gchar * remove_string_prefix (const gchar *string, const gchar *prefix) { int len; len = strlen (prefix); if (!strncmp (string, prefix, len)) string += len; return string; } static const gchar * create_label_text (const gchar *title) { const gchar *tmp; tmp = (title && *title) ? title : _("Untitled"); tmp = remove_string_prefix (tmp, "http://"); tmp = remove_string_prefix (tmp, "https://"); return tmp; } static void on_htmlview_title_changed (gpointer object, gchar *title, gpointer user_data) { tabInfo *tab = (tabInfo *)user_data; gtk_label_set_text (GTK_LABEL(tab->label), create_label_text (title)); } static void on_htmlview_close_tab (gpointer object, gpointer user_data) { browser_tabs_close_tab((tabInfo *)user_data); } /* Close tab and removes tab info structure */ static void browser_tabs_close_tab (tabInfo *tab) { int n = 0; GList *iter, *list; /* Find the tab index that needs to be closed */ iter = list = gtk_container_get_children (GTK_CONTAINER (tabs->notebook)); while (iter) { if (tab->widget == GTK_WIDGET (iter->data)) break; n++; iter = g_list_next (iter); } g_list_free (list); if (iter) { gtk_notebook_remove_page (tabs->notebook, n); browser_tabs_remove_tab (tab); } /* check if all tabs are closed */ if (1 == gtk_notebook_get_n_pages (tabs->notebook)) gtk_notebook_set_show_tabs (tabs->notebook, FALSE); } static void on_htmlview_status_message (gpointer obj, gchar *url) { liferea_shell_set_important_status_bar ("%s", url); } /* single tab creation */ LifereaHtmlView * browser_tabs_add_new (const gchar *url, const gchar *title, gboolean activate) { GtkWidget *close_button, *labelBox; tabInfo *tab; int i; tab = g_new0 (tabInfo, 1); tab->htmlview = liferea_htmlview_new (TRUE /* internal browsing */); tab->widget = liferea_htmlview_get_widget (tab->htmlview); tabs->list = g_slist_append (tabs->list, tab); g_object_set_data (G_OBJECT (tab->widget), "tabInfo", tab); g_signal_connect (tab->htmlview, "title-changed", G_CALLBACK (on_htmlview_title_changed), tab); g_signal_connect (tab->htmlview, "statusbar-changed", G_CALLBACK (on_htmlview_status_message), NULL); /* create tab widgets */ tab->label = gtk_label_new (create_label_text (title)); gtk_label_set_ellipsize (GTK_LABEL (tab->label), PANGO_ELLIPSIZE_END); gtk_label_set_width_chars (GTK_LABEL (tab->label), 17); labelBox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); gtk_box_pack_start (GTK_BOX (labelBox), tab->label, FALSE, FALSE, 0); close_button = gedit_close_button_new (); gtk_box_pack_end (GTK_BOX (labelBox), close_button, FALSE, FALSE, 0); g_signal_connect ((gpointer)close_button, "clicked", G_CALLBACK (on_htmlview_close_tab), (gpointer)tab); gtk_widget_show_all (labelBox); i = gtk_notebook_append_page (tabs->notebook, tab->widget, labelBox); g_signal_connect (gtk_notebook_get_nth_page (tabs->notebook, i), "key-press-event", G_CALLBACK (on_tab_key_press), (gpointer)tab); gtk_notebook_set_show_tabs (tabs->notebook, TRUE); gtk_notebook_set_tab_reorderable (tabs->notebook, tab->widget, TRUE); if (activate && (i != -1)) gtk_notebook_set_current_page (tabs->notebook, i); if (url) liferea_htmlview_launch_URL_internal (tab->htmlview, (gchar *)url); return tab->htmlview; } void browser_tabs_show_headlines (void) { gtk_notebook_set_current_page (tabs->notebook, gtk_notebook_page_num (tabs->notebook, tabs->headlines)); } LifereaHtmlView * browser_tabs_get_active_htmlview (void) { tabInfo *tab; gint current; current = gtk_notebook_get_current_page (tabs->notebook); if (0 == current) return NULL; /* never return the first page widget (because it is the item view) */ tab = g_object_get_data (G_OBJECT (gtk_notebook_get_nth_page (tabs->notebook, current)), "tabInfo"); return tab->htmlview; } void browser_tabs_do_zoom (gint zoom) { liferea_htmlview_do_zoom (browser_tabs_get_active_htmlview (), zoom); } liferea-1.13.7/src/ui/browser_tabs.h000066400000000000000000000045001415350204600172620ustar00rootroot00000000000000/* * @file browser_tabs.h internal browsing using multiple tabs * * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2006 Nathan Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _BROWSER_TABS_H #define _BROWSER_TABS_H #include #include "liferea_htmlview.h" G_BEGIN_DECLS #define BROWSER_TABS_TYPE (browser_tabs_get_type ()) G_DECLARE_FINAL_TYPE (BrowserTabs, browser_tabs, BROWSER, TABS, GObject) /** * browser_tabs_create: (skip) * @notebook: GtkNotebook widget to use * * Returns: the singleton browser tabs instance. */ BrowserTabs * browser_tabs_create (GtkNotebook *notebook); /** * browser_tabs_add_new: * @url: URL to be loaded in new tab (can be NULL to do nothing) * @title: title of the tab to be created * @activate: Should the new tab be put in the foreground? * * Adds a new tab with the specified URL and title. * * Returns: the newly created HTML view */ LifereaHtmlView * browser_tabs_add_new (const gchar *url, const gchar *title, gboolean activate); /** * browser_tabs_show_headlines: * * makes the headline tab visible */ void browser_tabs_show_headlines (void); /** * browser_tabs_get_active_htmlview: * * Used to determine which HTML view (a tab or the headlines view) * is currently visible and can be used to display HTML that * is to be loaded * * Returns: (transfer none) (nullable): HTML view widget */ LifereaHtmlView * browser_tabs_get_active_htmlview (void); /** * browser_tabs_do_zoom: * @zoom: 1 for zoom in, -1 for zoom out, 0 for reset * * Requests the tab to change zoom level. */ void browser_tabs_do_zoom (gint zoom); G_END_DECLS #endif liferea-1.13.7/src/ui/enclosure_list_view.c000066400000000000000000000414011415350204600206460ustar00rootroot00000000000000/** * @file enclosure-list-view.c enclosures/podcast handling GUI * * Copyright (C) 2005-2019 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "enclosure_list_view.h" #include #include "common.h" #include "conf.h" #include "debug.h" #include "enclosure.h" #include "item.h" #include "metadata.h" #include "ui/liferea_dialog.h" #include "ui/media_player.h" #include "ui/popup_menu.h" #include "ui/ui_common.h" /* enclosure list view implementation */ enum { ES_NAME_STR, ES_MIME_STR, ES_DOWNLOADED, ES_SIZE, ES_SIZE_STR, ES_SERIALIZED, ES_LEN }; struct _EnclosureListView { GObject parentInstance; GSList *enclosures; /**< list of currently presented enclosures */ GtkWidget *container; /**< container the list is embedded in */ GtkWidget *expander; /**< expander that shows/hides the list */ GtkWidget *treeview; GtkTreeStore *treestore; }; G_DEFINE_TYPE (EnclosureListView, enclosure_list_view, G_TYPE_OBJECT); static void enclosure_list_view_finalize (GObject *object) { g_slist_free_full (ENCLOSURE_LIST_VIEW (object)->enclosures, (GDestroyNotify)enclosure_free); } static void enclosure_list_view_destroy_cb (GtkWidget *widget, EnclosureListView *elv) { g_object_unref (elv); } static void enclosure_list_view_class_init (EnclosureListViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = enclosure_list_view_finalize; } static void enclosure_list_view_init (EnclosureListView *elv) { } static enclosurePtr enclosure_list_view_get_selected_enclosure (EnclosureListView *elv, GtkTreeIter *iter) { gchar *str; enclosurePtr enclosure; gtk_tree_model_get (GTK_TREE_MODEL (elv->treestore), iter, ES_SERIALIZED, &str, -1); enclosure = enclosure_from_string (str); g_free (str); return enclosure; } static gboolean on_enclosure_list_button_press (GtkWidget *treeview, GdkEvent *event, gpointer user_data) { GdkEventButton *eb = (GdkEventButton *)event; GtkTreePath *path; GtkTreeIter iter; EnclosureListView *elv = (EnclosureListView *)user_data; if ((event->type != GDK_BUTTON_PRESS) || (3 != eb->button)) return FALSE; /* avoid handling header clicks */ if (eb->window != gtk_tree_view_get_bin_window (GTK_TREE_VIEW (treeview))) return FALSE; if (!gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), (gint)eb->x, (gint)eb->y, &path, NULL, NULL, NULL)) return FALSE; if (gtk_tree_model_get_iter (GTK_TREE_MODEL (elv->treestore), &iter, path)) ui_popup_enclosure_menu (enclosure_list_view_get_selected_enclosure (elv, &iter), event); return TRUE; } static gboolean on_enclosure_list_popup_menu (GtkWidget *widget, gpointer user_data) { GtkTreeView *treeview = GTK_TREE_VIEW (widget); GtkTreeModel *model; GtkTreeIter iter; EnclosureListView *elv = (EnclosureListView *)user_data; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (treeview), &model, &iter)) { ui_popup_enclosure_menu (enclosure_list_view_get_selected_enclosure (elv, &iter), NULL); return TRUE; } return FALSE; } static gboolean on_enclosure_list_activate (GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data) { GtkTreeIter iter; GtkTreeModel *model; EnclosureListView *elv = (EnclosureListView *)user_data; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (treeview), &model, &iter)) { on_popup_open_enclosure (enclosure_list_view_get_selected_enclosure (elv, &iter)); return TRUE; } return FALSE; } EnclosureListView * enclosure_list_view_new () { EnclosureListView *elv; GtkCellRenderer *renderer; GtkTreeViewColumn *column; GtkWidget *widget; elv = ENCLOSURE_LIST_VIEW (g_object_new (ENCLOSURE_LIST_VIEW_TYPE, NULL)); /* Use a vbox to allow media player insertion */ elv->container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_name (GTK_WIDGET (elv->container), "enclosureview"); elv->expander = gtk_expander_new (_("Attachments")); gtk_box_pack_end (GTK_BOX (elv->container), elv->expander, TRUE, TRUE, 0); widget = gtk_scrolled_window_new (NULL, NULL); /* FIXME: Setting a fixed size is not nice, but a workaround for the enclosure list view being hidden as 1px size in Ubuntu */ gtk_widget_set_size_request (widget, -1, 75); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (widget), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); gtk_container_add (GTK_CONTAINER (elv->expander), widget); elv->treeview = gtk_tree_view_new (); gtk_container_add (GTK_CONTAINER (widget), elv->treeview); gtk_widget_show (elv->treeview); elv->treestore = gtk_tree_store_new (ES_LEN, G_TYPE_STRING, /* ES_NAME_STR */ G_TYPE_STRING, /* ES_MIME_STR */ G_TYPE_BOOLEAN, /* ES_DOWNLOADED */ G_TYPE_ULONG, /* ES_SIZE */ G_TYPE_STRING, /* ES_SIZE_STRING */ G_TYPE_STRING /* ES_SERIALIZED */ ); gtk_tree_view_set_model (GTK_TREE_VIEW (elv->treeview), GTK_TREE_MODEL(elv->treestore)); /* explicitely no translation for invisible column headers... */ renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Size", renderer, "text", ES_SIZE_STR, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (elv->treeview), column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("URL", renderer, "text", ES_NAME_STR, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (elv->treeview), column); gtk_tree_view_column_set_sort_column_id (column, ES_NAME_STR); gtk_tree_view_column_set_expand (column, TRUE); g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("MIME", renderer, "text", ES_MIME_STR, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (elv->treeview), column); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (elv->treeview), FALSE); g_signal_connect (G_OBJECT (elv->treeview), "button_press_event", G_CALLBACK (on_enclosure_list_button_press), (gpointer)elv); g_signal_connect (G_OBJECT (elv->treeview), "row-activated", G_CALLBACK (on_enclosure_list_activate), (gpointer)elv); g_signal_connect (G_OBJECT (elv->treeview), "popup_menu", G_CALLBACK (on_enclosure_list_popup_menu), (gpointer)elv); g_signal_connect_object (elv->container, "destroy", G_CALLBACK (enclosure_list_view_destroy_cb), elv, 0); return elv; } GtkWidget * enclosure_list_view_get_widget (EnclosureListView *elv) { return elv->container; } void enclosure_list_view_load (EnclosureListView *elv, itemPtr item) { GSList *list, *filteredList; guint len; /* Ugly workaround to prevent race on startup when item is selected but enclosure list view not yet initialized. */ if (!elv) return; /* cleanup old content */ gtk_tree_store_clear (elv->treestore); g_slist_free_full (elv->enclosures, (GDestroyNotify)enclosure_free); elv->enclosures = NULL; /* load list into tree view */ filteredList = NULL; list = metadata_list_get_values (item->metadata, "enclosure"); while (list) { enclosurePtr enclosure = enclosure_from_string (list->data); if (enclosure) { GtkTreeIter iter; gchar *sizeStr; guint size = enclosure->size; /* The following literals are the enclosure list size units */ gchar *unit = _(" Bytes"); if (size > 1024) { size /= 1024; unit = _("kB"); } if (size > 1024) { size /= 1024; unit = _("MB"); } if (size > 1024) { size /= 1024; unit = _("GB"); } /* The following literal is the format string for enclosure sizes (number + unit string) */ if (size > 0) sizeStr = g_strdup_printf (_("%d%s"), size, unit); else sizeStr = g_strdup (""); gtk_tree_store_append (elv->treestore, &iter, NULL); gtk_tree_store_set (elv->treestore, &iter, ES_NAME_STR, enclosure->url, ES_MIME_STR, enclosure->mime?enclosure->mime:"", ES_DOWNLOADED, enclosure->downloaded, ES_SIZE, enclosure->size, ES_SIZE_STR, sizeStr, ES_SERIALIZED, list->data, -1); g_free (sizeStr); elv->enclosures = g_slist_append (elv->enclosures, enclosure); // Filter unwanted MIME types (we only want audio/* and video/*) if (enclosure->mime && (g_str_has_prefix (enclosure->mime, "video/") || (g_str_has_prefix (enclosure->mime, "audio/")))) { filteredList = g_slist_append (filteredList, list->data); } } list = g_slist_next (list); } /* decide visibility of the list */ len = g_slist_length (elv->enclosures); if (len == 0) { enclosure_list_view_hide (elv); return; } gtk_widget_show_all (elv->container); /* update list title */ gchar *text = g_strdup_printf (ngettext("%d attachment", "%d attachments", len), len); gtk_expander_set_label (GTK_EXPANDER (elv->expander), text); g_free (text); /* Load the optional media player plugin */ if (g_slist_length (filteredList) > 0) { liferea_media_player_load (elv->container, filteredList); } } void enclosure_list_view_select (EnclosureListView *elv, guint position) { GtkTreeIter iter; if (!gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (elv->treestore), &iter, NULL, position)) return; gtk_tree_selection_select_iter (gtk_tree_view_get_selection (GTK_TREE_VIEW (elv->treeview)), &iter); } void enclosure_list_view_select_next (EnclosureListView *elv) { GtkTreeIter selected_iter; GtkTreeSelection *selection; GtkTreeModel *model; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (elv->treeview)); if (gtk_tree_selection_get_selected (selection, &model, &selected_iter) && gtk_tree_model_iter_next (model, &selected_iter)) gtk_tree_selection_select_iter (selection, &selected_iter); else enclosure_list_view_select (elv, 0); } void enclosure_list_view_open_next (EnclosureListView *elv) { GtkTreeIter selected_iter; enclosure_list_view_select_next (elv); if (gtk_tree_selection_get_selected ( gtk_tree_view_get_selection (GTK_TREE_VIEW (elv->treeview)), NULL, &selected_iter)) { enclosurePtr enclosure; enclosure = enclosure_list_view_get_selected_enclosure (elv, &selected_iter); on_popup_open_enclosure ((gpointer) enclosure); } } void enclosure_list_view_hide (EnclosureListView *elv) { if (!elv) return; gtk_widget_hide (GTK_WIDGET (elv->container)); } /* callback for preferences and enclosure type handling */ static void on_selectcmdok_clicked (const gchar *filename, gpointer user_data) { GtkWidget *dialog = (GtkWidget *)user_data; gchar *utfname; if (!filename) return; utfname = g_filename_to_utf8 (filename, -1, NULL, NULL, NULL); if (utfname) { gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (dialog, "enc_cmd_entry")), utfname); g_free (utfname); } } static void on_selectcmd_pressed (GtkButton *button, gpointer user_data) { GtkWidget *dialog = (GtkWidget *)user_data; const gchar *utfname; gchar *name; utfname = gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (dialog,"enc_cmd_entry"))); name = g_filename_from_utf8 (utfname, -1, NULL, NULL, NULL); if (name) { ui_choose_file (_("Choose File"), "gtk-open", FALSE, on_selectcmdok_clicked, name, NULL, NULL, NULL, dialog); g_free (name); } } /* dialog used for both changing and adding new definitions */ static void on_adddialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { gchar *typestr; gboolean new = FALSE; enclosurePtr enclosure; encTypePtr etp; if (response_id == GTK_RESPONSE_OK) { etp = g_object_get_data (G_OBJECT (dialog), "type"); typestr = g_object_get_data (G_OBJECT (dialog), "typestr"); enclosure = g_object_get_data (G_OBJECT (dialog), "enclosure"); if (!etp) { new = TRUE; etp = g_new0 (struct encType, 1); if (!strchr (typestr, '/')) etp->extension = g_strdup (typestr); else etp->mime = g_strdup (typestr); } else { g_free (etp->cmd); } etp->cmd = g_strdup (gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "enc_cmd_entry")))); etp->permanent = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "enc_always_btn"))); if (new) enclosure_mime_type_add (etp); else enclosure_mime_types_save (); /* now we have ensured an existing type configuration and can launch the URL for which we configured the type */ if (enclosure) on_popup_open_enclosure (enclosure); g_free (typestr); } gtk_widget_destroy (GTK_WIDGET (dialog)); } /* either type or url and typestr are optional */ static void ui_enclosure_type_setup (encTypePtr type, enclosurePtr enclosure, gchar *typestr) { GtkWidget *dialog; gchar *tmp; dialog = liferea_dialog_new ("enclosure_handler"); if (type) { typestr = type->mime?type->mime:type->extension; gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (dialog, "enc_cmd_entry")), type->cmd); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (dialog, "enc_always_btn")), TRUE); } if (!strchr(typestr, '/')) tmp = g_strdup_printf (_("File Extension .%s"), typestr); else tmp = g_strdup_printf ("%s", typestr); gtk_label_set_text (GTK_LABEL (liferea_dialog_lookup (dialog, "enc_type_label")), tmp); g_free (tmp); g_object_set_data (G_OBJECT(dialog), "typestr", g_strdup (typestr)); g_object_set_data (G_OBJECT(dialog), "enclosure", enclosure); g_object_set_data (G_OBJECT(dialog), "type", type); g_signal_connect (G_OBJECT(dialog), "response", G_CALLBACK(on_adddialog_response), type); g_signal_connect (G_OBJECT(liferea_dialog_lookup(dialog, "enc_cmd_select_btn")), "clicked", G_CALLBACK(on_selectcmd_pressed), dialog); gtk_widget_show (dialog); } void on_popup_open_enclosure (gpointer callback_data) { gchar *typestr, *tmp = NULL; enclosurePtr enclosure = (enclosurePtr)callback_data; encTypePtr etp_tmp = NULL, etp_found = NULL; GSList *iter; /* 1.) Always try to determine the file extension... */ /* find extension by looking for last '.' */ typestr = strrchr (enclosure->url, '.'); if (typestr) typestr = tmp = g_strdup (typestr + 1); /* handle case where there is a slash after the '.' */ if (typestr && strrchr (typestr, '/')) typestr = strrchr (typestr, '/'); /* handle case where there is no '.' at all */ if (!typestr && strrchr (enclosure->url, '/')) typestr = strrchr (enclosure->url, '/'); /* if we found no extension we map to dummy type "data" */ if (!typestr) typestr = tmp = g_strdup ("data"); /* strip GET parameters from typestr */ g_strdelimit (typestr, "?", 0); debug2 (DEBUG_CACHE, "url:%s, mime:%s", enclosure->url, enclosure->mime); /* 2.) Search for type configuration based on MIME or file extension... */ iter = (GSList *)enclosure_mime_types_get (); while (iter) { etp_tmp = (encTypePtr)(iter->data); if (enclosure->mime && etp_tmp->mime) { /* match know MIME types and stop looking if found */ if (!strcmp(enclosure->mime, etp_tmp->mime)) { etp_found = etp_tmp; break; } } else if (etp_tmp->extension) { /* match known file extensions and keep looking for matching MIME type */ if (!strcmp(typestr, etp_tmp->extension)) { etp_found = etp_tmp; } } iter = g_slist_next (iter); } if (etp_found) { enclosure_download (etp_found, enclosure->url, TRUE); } else { if (enclosure->mime) ui_enclosure_type_setup (NULL, enclosure, enclosure->mime); else ui_enclosure_type_setup (NULL, enclosure, typestr); } g_free (tmp); } void on_popup_save_enclosure (gpointer callback_data) { enclosurePtr enclosure = (enclosurePtr)callback_data; enclosure_download (NULL, enclosure->url, TRUE); } void ui_enclosure_change_type (encTypePtr type) { ui_enclosure_type_setup (type, NULL, NULL); } void on_popup_copy_enclosure (gpointer callback_data) { enclosurePtr enclosure = (enclosurePtr)callback_data; gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY), enclosure->url, -1); gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), enclosure->url, -1); } liferea-1.13.7/src/ui/enclosure_list_view.h000066400000000000000000000057201415350204600206570ustar00rootroot00000000000000/** * @file enclosure-list-view.h enclosures list view * * Copyright (C) 2005-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ENCLOSURE_LIST_VIEW_H #define _ENCLOSURE_LIST_VIEW_H #include G_BEGIN_DECLS #include "item.h" #include "enclosure.h" // FIXME: should not be necessary #define ENCLOSURE_LIST_VIEW_TYPE (enclosure_list_view_get_type ()) G_DECLARE_FINAL_TYPE (EnclosureListView, enclosure_list_view, ENCLOSURE_LIST, VIEW, GObject) /** * enclosure_list_view_new: * Sets up a new enclosure list view. * * Returns: (transfer none): a new enclosure list view */ EnclosureListView * enclosure_list_view_new (void); /** * enclosure_list_view_get_widget: (skip) * * Returns the rendering widget for a HTML view. Only * to be used by ui_mainwindow.c for widget reparenting. */ GtkWidget * enclosure_list_view_get_widget (EnclosureListView *elv); /** * enclosure_list_view_load: * Loads the enclosure list of the given item into the * given enclosure list view widget. * * @elv: the enclosure list view * @item: the item */ void enclosure_list_view_load (EnclosureListView *elv, itemPtr item); /** * enclosure_list_view_select: * @elv: the enclosure list view * @position: the position to select * * Select the nth enclosure in the enclosure list. */ void enclosure_list_view_select (EnclosureListView *elv, guint position); /** * enclosure_list_view_select_next: * @elv: the enclosure list view * * Select the next enclosure in the list, or the first if none was * selected or the end of the list was reached. */ void enclosure_list_view_select_next (EnclosureListView *elv); /** * enclosure_list_view_open_next: * @elv: the enclosure list view * * Select the next enclosure in the list and open it. */ void enclosure_list_view_open_next (EnclosureListView *elv); /** * enclosure_list_view_hide: * Hides the enclosure list view. * * @elv: the enclosure list view */ void enclosure_list_view_hide (EnclosureListView *elv); /* related menu creation and callbacks */ void on_popup_open_enclosure(gpointer callback_data); void on_popup_save_enclosure(gpointer callback_data); void on_popup_copy_enclosure(gpointer callback_data); // FIXME: this does not belong here! void ui_enclosure_change_type (encTypePtr type); G_END_DECLS #endif liferea-1.13.7/src/ui/feed_list_view.c000066400000000000000000000675511415350204600175700ustar00rootroot00000000000000/** * @file feed_list_view.c the feed list in a GtkTreeView * * Copyright (C) 2004-2019 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2005 Raphael Slinckx * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/feed_list_view.h" #include #include #include "common.h" #include "conf.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "net_monitor.h" #include "newsbin.h" #include "render.h" #include "vfolder.h" #include "ui/icons.h" #include "ui/liferea_dialog.h" #include "ui/liferea_shell.h" #include "ui/subscription_dialog.h" #include "ui/ui_dnd.h" #include "fl_sources/node_source.h" struct _FeedListView { GObject parentInstance; GtkTreeView *treeview; GtkTreeModel *filter; GtkTreeStore *feedstore; GHashTable *flIterHash; /**< hash table used for fast node id <-> tree iter lookup */ gboolean feedlist_reduced_unread; /**< TRUE when feed list is in reduced mode (no folders, only unread feeds) */ }; enum { SELECTION_CHANGED, LAST_SIGNAL }; static FeedListView *flv = NULL; // singleton static guint feed_list_view_signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE (FeedListView, feed_list_view, G_TYPE_OBJECT); static void feed_list_view_finalize (GObject *object) { } static void feed_list_view_class_init (FeedListViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = feed_list_view_finalize; feed_list_view_signals[SELECTION_CHANGED] = g_signal_new ("selection-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); } static void feed_list_view_init (FeedListView *f) { } static void feed_list_view_row_changed_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter) { nodePtr node; gtk_tree_model_get (model, iter, FS_PTR, &node, -1); if (node) feed_list_view_update_iter (node->id, iter); } static void feed_list_view_selection_changed_cb (GtkTreeSelection *selection, gpointer data) { GtkTreeIter iter; GtkTreeModel *model; nodePtr node; if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, FS_PTR, &node, -1); debug1 (DEBUG_GUI, "feed list selection changed to \"%s\"", node?node_get_title (node):"Empty node"); if (!node) { /* The selected iter is an "empty" node added to an empty folder. We get the parent's node * to set it as the selected node. This is useful if the user adds a feed, the folder will * be used as location for the new node. */ GtkTreeIter parent; if (gtk_tree_model_iter_parent (model, &parent, &iter)) gtk_tree_model_get (model, &parent, FS_PTR, &node, -1); else { debug0 (DEBUG_GUI, "A selected null node has no parent. This should not happen."); return; } liferea_shell_update_feed_menu (TRUE, FALSE, FALSE); } else { gboolean allowModify = (NODE_SOURCE_TYPE (node->source->root)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST); liferea_shell_update_update_menu ((NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE) || (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE_CHILDS)); liferea_shell_update_feed_menu (allowModify, TRUE, allowModify); } /* 1.) update feed list and item list states */ g_signal_emit_by_name (FEED_LIST_VIEW (flv), "selection-changed", node->id); /* 2.) Refilter the GtkTreeView to get rid of nodes with 0 unread messages when in reduced mode. */ gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (flv->filter)); } else { /* If we cannot get the new selection we keep the old one this happens when we're doing drag&drop for example. */ } } static void feed_list_view_row_activated_cb (GtkTreeView *tv, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) { GtkTreeIter iter; nodePtr node; gtk_tree_model_get_iter (gtk_tree_view_get_model (tv), &iter, path); gtk_tree_model_get (gtk_tree_view_get_model (tv), &iter, FS_PTR, &node, -1); if (node && IS_FOLDER (node)) { if (gtk_tree_view_row_expanded (tv, path)) gtk_tree_view_collapse_row (tv, path); else gtk_tree_view_expand_row (tv, path, FALSE); } } static gboolean feed_list_view_key_press_cb (GtkWidget *widget, GdkEventKey *event, gpointer data) { if ((event->type == GDK_KEY_PRESS) && (event->state == 0) && (event->keyval == GDK_KEY_Delete)) { nodePtr node = feedlist_get_selected (); if(node) { if (event->state & GDK_SHIFT_MASK) feedlist_remove_node (node); else feed_list_view_remove (node); return TRUE; } } return FALSE; } static gboolean feed_list_view_filter_visible_function (GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { gint count; nodePtr node; if (!flv->feedlist_reduced_unread) return TRUE; gtk_tree_model_get (model, iter, FS_PTR, &node, FS_UNREAD, &count, -1); if (!node) return FALSE; if (IS_NEWSBIN(node) && node->data && ((feedPtr)node->data)->alwaysShowInReduced) return TRUE; if (IS_FOLDER (node) || IS_NODE_SOURCE (node)) return FALSE; if (IS_VFOLDER (node)) return TRUE; /* Do not hide in any case if the node is selected, otherwise the last unread item of a feed causes the feed to vanish when clicking it */ if (feedlist_get_selected () == node) return TRUE; if (count > 0) return TRUE; return FALSE; } static void feed_list_view_expand (nodePtr node) { if (node->parent) feed_list_view_expand (node->parent); feed_list_view_set_expansion (node, TRUE); } static void feed_list_view_restore_folder_expansion (nodePtr node) { if (node->expanded) feed_list_view_expand (node); node_foreach_child (node, feed_list_view_restore_folder_expansion); } static void feed_list_view_reduce_mode_changed (void) { if (flv->feedlist_reduced_unread) { gtk_tree_view_set_reorderable (flv->treeview, FALSE); gtk_tree_view_set_model (flv->treeview, GTK_TREE_MODEL (flv->filter)); gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (flv->filter)); } else { gtk_tree_view_set_reorderable (flv->treeview, TRUE); gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (flv->filter)); gtk_tree_view_set_model (flv->treeview, GTK_TREE_MODEL (flv->feedstore)); feedlist_foreach (feed_list_view_restore_folder_expansion); } } static void feed_list_view_set_reduce_mode (gboolean newReduceMode) { flv->feedlist_reduced_unread = newReduceMode; conf_set_bool_value (REDUCED_FEEDLIST, flv->feedlist_reduced_unread); feed_list_view_reduce_mode_changed (); feed_list_view_reload_feedlist (); } static gint feed_list_view_sort_folder_compare (gconstpointer a, gconstpointer b) { nodePtr n1 = (nodePtr)a; nodePtr n2 = (nodePtr)b; gchar *s1 = g_utf8_casefold (n1->title, -1); gchar *s2 = g_utf8_casefold (n2->title, -1); gint result = strcmp (s1, s2); g_free (s1); g_free (s2); return result; } void feed_list_view_sort_folder (nodePtr folder) { GtkTreeView *treeview; treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist")); /* Unset the model from the view before clearing it and rebuilding it.*/ gtk_tree_view_set_model (treeview, NULL); folder->children = g_slist_sort (folder->children, feed_list_view_sort_folder_compare); feed_list_view_reload_feedlist (); /* Reduce mode didn't actually change but we need to set the * correct model according to the setting in the same way : */ feed_list_view_reduce_mode_changed (); feedlist_foreach (feed_list_view_restore_folder_expansion); feedlist_schedule_save (); } FeedListView * feed_list_view_create (GtkTreeView *treeview) { GtkCellRenderer *titleRenderer, *countRenderer; GtkCellRenderer *iconRenderer; GtkTreeViewColumn *column, *column2; GtkTreeSelection *select; debug_enter ("feed_list_view_create"); /* Set up store */ g_assert (NULL == flv); flv = FEED_LIST_VIEW (g_object_new (FEED_LIST_VIEW_TYPE, NULL)); flv->treeview = treeview; flv->feedstore = gtk_tree_store_new (FS_LEN, G_TYPE_STRING, G_TYPE_ICON, G_TYPE_POINTER, G_TYPE_UINT, G_TYPE_STRING); gtk_tree_view_set_model (GTK_TREE_VIEW (flv->treeview), GTK_TREE_MODEL (flv->feedstore)); /* Prepare filter */ flv->filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (flv->feedstore), NULL); gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (flv->filter), feed_list_view_filter_visible_function, NULL, NULL); g_signal_connect (G_OBJECT (flv->feedstore), "row-changed", G_CALLBACK (feed_list_view_row_changed_cb), flv); /* we render the icon/state, the feed title and the unread count */ iconRenderer = gtk_cell_renderer_pixbuf_new (); titleRenderer = gtk_cell_renderer_text_new (); countRenderer = gtk_cell_renderer_text_new (); gtk_cell_renderer_set_alignment (countRenderer, 1.0, 0); column = gtk_tree_view_column_new (); column2 = gtk_tree_view_column_new (); gtk_tree_view_column_pack_start (column, iconRenderer, FALSE); gtk_tree_view_column_pack_start (column, titleRenderer, TRUE); gtk_tree_view_column_pack_end (column2, countRenderer, FALSE); gtk_tree_view_column_add_attribute (column, iconRenderer, "gicon", FS_ICON); gtk_tree_view_column_add_attribute (column, titleRenderer, "markup", FS_LABEL); gtk_tree_view_column_add_attribute (column2, countRenderer, "markup", FS_COUNT); gtk_tree_view_column_set_expand (column, TRUE); gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_GROW_ONLY); gtk_tree_view_column_set_sizing (column2, GTK_TREE_VIEW_COLUMN_GROW_ONLY); gtk_tree_view_append_column (flv->treeview, column); gtk_tree_view_append_column (flv->treeview, column2); g_object_set (titleRenderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); g_signal_connect (G_OBJECT (flv->treeview), "row-activated", G_CALLBACK (feed_list_view_row_activated_cb), flv); g_signal_connect (G_OBJECT (flv->treeview), "key-press-event", G_CALLBACK (feed_list_view_key_press_cb), flv); select = gtk_tree_view_get_selection (flv->treeview); gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); g_signal_connect (G_OBJECT (select), "changed", G_CALLBACK (feed_list_view_selection_changed_cb), flv); conf_get_bool_value (REDUCED_FEEDLIST, &flv->feedlist_reduced_unread); if (flv->feedlist_reduced_unread) feed_list_view_reduce_mode_changed (); /* before menu setup for reduced mode check box to be correct */ ui_dnd_setup_feedlist (flv->feedstore); liferea_shell_update_feed_menu (TRUE, FALSE, FALSE); liferea_shell_update_allitems_actions (FALSE, FALSE); debug_exit ("feed_list_view_create"); return flv; } void feed_list_view_select (nodePtr node) { GtkTreeModel *model = gtk_tree_view_get_model (flv->treeview); if (model && node && node != feedlist_get_root ()) { GtkTreePath *path; /* in filtered mode we need to convert the iterator */ if (flv->feedlist_reduced_unread) { GtkTreeIter iter; gtk_tree_model_filter_convert_child_iter_to_iter (GTK_TREE_MODEL_FILTER (flv->filter), &iter, feed_list_view_to_iter (node->id)); path = gtk_tree_model_get_path (model, &iter); } else { path = gtk_tree_model_get_path (model, feed_list_view_to_iter (node->id)); } if (node->parent) feed_list_view_expand (node->parent); if (path) { gtk_tree_view_scroll_to_cell (flv->treeview, path, NULL, FALSE, 0.0, 0.0); gtk_tree_view_set_cursor (flv->treeview, path, NULL, FALSE); gtk_tree_path_free (path); } } else { GtkTreeSelection *selection = gtk_tree_view_get_selection (flv->treeview); gtk_tree_selection_unselect_all (selection); } } // Callbacks void on_menu_properties (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr node = feedlist_get_selected (); NODE_TYPE (node)->request_properties (node); } void on_menu_delete(GSimpleAction *action, GVariant *parameter, gpointer user_data) { feed_list_view_remove (feedlist_get_selected ()); } static void do_menu_update (nodePtr node) { if (network_monitor_is_online ()) node_update_subscription (node, GUINT_TO_POINTER (FEED_REQ_PRIORITY_HIGH)); else liferea_shell_set_status_bar (_("Liferea is in offline mode. No update possible.")); } void on_menu_update (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr node = NULL; if (user_data) node = (nodePtr) user_data; else node = feedlist_get_selected (); if (node) do_menu_update (node); else g_warning ("on_menu_update: no feedlist selected"); } void on_menu_update_all(GSimpleAction *action, GVariant *parameter, gpointer user_data) { do_menu_update (feedlist_get_root ()); } void on_action_mark_all_read (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr feedlist; gboolean confirm_mark_read; gboolean do_mark_read = TRUE; if (!g_strcmp0 (g_action_get_name (G_ACTION (action)), "mark-all-feeds-read")) feedlist = feedlist_get_root (); else if (user_data) feedlist = (nodePtr) user_data; else feedlist = feedlist_get_selected (); conf_get_bool_value (CONFIRM_MARK_ALL_READ, &confirm_mark_read); if (confirm_mark_read) { gint result; GtkMessageDialog *confirm_dialog = GTK_MESSAGE_DIALOG (liferea_dialog_new ("mark_read_dialog")); GtkWidget *dont_ask_toggle = liferea_dialog_lookup (GTK_WIDGET (confirm_dialog), "dontAskAgainToggle"); const gchar *feed_title = (feedlist_get_root () == feedlist) ? _("all feeds"):node_get_title (feedlist); gchar *primary_message = g_strdup_printf (_("Mark %s as read ?"), feed_title); g_object_set (confirm_dialog, "text", primary_message, NULL); g_free (primary_message); gtk_message_dialog_format_secondary_text (confirm_dialog, _("Are you sure you want to mark all items in %s as read ?"), feed_title); conf_bind (CONFIRM_MARK_ALL_READ, dont_ask_toggle, "active", G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN); result = gtk_dialog_run (GTK_DIALOG (confirm_dialog)); if (result != GTK_RESPONSE_OK) do_mark_read = FALSE; gtk_widget_destroy (GTK_WIDGET (confirm_dialog)); } if (do_mark_read) feedlist_mark_all_read (feedlist); } void on_menu_feed_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data) { node_type_request_interactive_add (feed_get_node_type ()); } void on_new_plugin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data) { node_type_request_interactive_add (node_source_get_node_type ()); } void on_new_newsbin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data) { node_type_request_interactive_add (newsbin_get_node_type ()); } void on_menu_folder_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data) { node_type_request_interactive_add (folder_get_node_type ()); } void on_new_vfolder_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data) { node_type_request_interactive_add (vfolder_get_node_type ()); } void on_feedlist_reduced_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GVariant *state = g_action_get_state (G_ACTION (action)); gboolean val = !g_variant_get_boolean (state); feed_list_view_set_reduce_mode (val); g_simple_action_set_state (action, g_variant_new_boolean (val)); g_object_unref (state); } // Handling feed list nodes GtkTreeIter * feed_list_view_to_iter (const gchar *nodeId) { if (!flv->flIterHash) return NULL; return (GtkTreeIter *)g_hash_table_lookup (flv->flIterHash, (gpointer)nodeId); } void feed_list_view_update_iter (const gchar *nodeId, GtkTreeIter *iter) { GtkTreeIter *old; if (!flv->flIterHash) return; old = (GtkTreeIter *)g_hash_table_lookup (flv->flIterHash, (gpointer)nodeId); if (old) *old = *iter; } static void feed_list_view_add_iter (const gchar *nodeId, GtkTreeIter *iter) { if (!flv->flIterHash) flv->flIterHash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); g_hash_table_insert (flv->flIterHash, (gpointer)nodeId, (gpointer)iter); } /* Expansion & Collapsing */ gboolean feed_list_view_is_expanded (const gchar *nodeId) { GtkTreeIter *iter; gboolean expanded = FALSE; if (flv->feedlist_reduced_unread) return FALSE; iter = feed_list_view_to_iter (nodeId); if (iter) { GtkTreePath *path = gtk_tree_model_get_path (gtk_tree_view_get_model (flv->treeview), iter); expanded = gtk_tree_view_row_expanded (flv->treeview, path); gtk_tree_path_free (path); } return expanded; } void feed_list_view_set_expansion (nodePtr folder, gboolean expanded) { GtkTreeIter *iter; GtkTreePath *path; if (flv->feedlist_reduced_unread) return; iter = feed_list_view_to_iter (folder->id); if (!iter) return; path = gtk_tree_model_get_path (gtk_tree_view_get_model (flv->treeview), iter); if (expanded) gtk_tree_view_expand_row (flv->treeview, path, FALSE); else gtk_tree_view_collapse_row (flv->treeview, path); gtk_tree_path_free (path); } /* Folder expansion workaround using "empty" nodes */ void feed_list_view_add_empty_node (GtkTreeIter *parent) { GtkTreeIter iter; gtk_tree_store_append (flv->feedstore, &iter, parent); gtk_tree_store_set (flv->feedstore, &iter, FS_LABEL, _("(Empty)"), FS_PTR, NULL, FS_UNREAD, 0, FS_COUNT, "", -1); } void feed_list_view_remove_empty_node (GtkTreeIter *parent) { GtkTreeIter iter; nodePtr node; gboolean valid; gtk_tree_model_iter_children (GTK_TREE_MODEL (flv->feedstore), &iter, parent); do { gtk_tree_model_get (GTK_TREE_MODEL (flv->feedstore), &iter, FS_PTR, &node, -1); if (!node) { gtk_tree_store_remove (flv->feedstore, &iter); return; } valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (flv->feedstore), &iter); } while (valid); } /* this function is a workaround to the cant-drop-rows-into-emtpy- folders-problem, so we simply pack an "(empty)" entry into each empty folder like Nautilus does... */ static void feed_list_view_check_if_folder_is_empty (const gchar *nodeId) { GtkTreeIter *iter; int count; debug1 (DEBUG_GUI, "folder empty check for node id \"%s\"", nodeId); /* this function does two things: 1. add "(empty)" entry to an empty folder 2. remove an "(empty)" entry from a non empty folder (this state is possible after a drag&drop action) */ iter = feed_list_view_to_iter (nodeId); if (!iter) return; count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (flv->feedstore), iter); /* case 1 */ if (0 == count) { feed_list_view_add_empty_node (iter); return; } if (1 == count) return; /* else we could have case 2 */ feed_list_view_remove_empty_node (iter); } void feed_list_view_add_node (nodePtr node) { gint position; GtkTreeIter *iter, *parentIter = NULL; debug2 (DEBUG_GUI, "adding node \"%s\" as child of parent=\"%s\"", node_get_title(node), (NULL != node->parent)?node_get_title(node->parent):"feed list root"); g_assert (NULL != node->parent); g_assert (NULL == feed_list_view_to_iter (node->id)); /* if parent is NULL we have the root folder and don't create a new row! */ iter = g_new0 (GtkTreeIter, 1); /* if reduced feedlist, show flat treeview */ if (flv->feedlist_reduced_unread) parentIter = NULL; else if (node->parent != feedlist_get_root ()) parentIter = feed_list_view_to_iter (node->parent->id); position = g_slist_index (node->parent->children, node); if (flv->feedlist_reduced_unread || position < 0) gtk_tree_store_append (flv->feedstore, iter, parentIter); else gtk_tree_store_insert (flv->feedstore, iter, parentIter, position); gtk_tree_store_set (flv->feedstore, iter, FS_PTR, node, -1); feed_list_view_add_iter (node->id, iter); feed_list_view_update_node (node->id); if (node->parent != feedlist_get_root ()) feed_list_view_check_if_folder_is_empty (node->parent->id); if (IS_FOLDER (node)) feed_list_view_check_if_folder_is_empty (node->id); } static void feed_list_view_load_feedlist (nodePtr node) { GSList *iter; iter = node->children; while (iter) { node = (nodePtr)iter->data; feed_list_view_add_node (node); if (IS_FOLDER (node) || IS_NODE_SOURCE (node)) feed_list_view_load_feedlist (node); iter = g_slist_next(iter); } } static void feed_list_view_clear_feedlist () { gtk_tree_store_clear (flv->feedstore); g_hash_table_remove_all (flv->flIterHash); } void feed_list_view_reload_feedlist () { feed_list_view_clear_feedlist (); feed_list_view_load_feedlist (feedlist_get_root ()); } void feed_list_view_remove_node (nodePtr node) { GtkTreeIter *iter; gboolean parentExpanded = FALSE; iter = feed_list_view_to_iter (node->id); if (!iter) return; /* must be tolerant because of DnD handling */ if (node->parent) parentExpanded = feed_list_view_is_expanded (node->parent->id); /* If the folder becomes empty, the folder would collapse */ gtk_tree_store_remove (flv->feedstore, iter); g_hash_table_remove (flv->flIterHash, node->id); if (node->parent) { feed_list_view_check_if_folder_is_empty (node->parent->id); if (parentExpanded) feed_list_view_set_expansion (node->parent, TRUE); feed_list_view_update_node (node->parent->id); } } void feed_list_view_update_node (const gchar *nodeId) { GtkTreeIter *iter; gchar *label, *count = NULL; guint labeltype; nodePtr node; static gchar *countColor = NULL; node = node_from_id (nodeId); iter = feed_list_view_to_iter (nodeId); if (!iter) return; /* Initialize unread item color Pango CSS */ if (!countColor) { const gchar *bg = NULL, *fg = NULL; bg = render_get_theme_color ("FEEDLIST_UNREAD_BG"); fg = render_get_theme_color ("FEEDLIST_UNREAD_FG"); if (fg && bg) { countColor = g_strdup_printf ("foreground='#%s' background='#%s'", fg, bg); debug1 (DEBUG_HTML, "Feed list unread CSS: %s\n", countColor); } } labeltype = NODE_TYPE (node)->capabilities; labeltype &= (NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_SHOW_ITEM_COUNT); if (node->unreadCount == 0 && (labeltype & NODE_CAPABILITY_SHOW_UNREAD_COUNT)) labeltype &= ~NODE_CAPABILITY_SHOW_UNREAD_COUNT; label = g_markup_escape_text (node_get_title (node), -1); switch (labeltype) { case NODE_CAPABILITY_SHOW_UNREAD_COUNT | NODE_CAPABILITY_SHOW_ITEM_COUNT: /* treat like show unread count */ case NODE_CAPABILITY_SHOW_UNREAD_COUNT: count = g_strdup_printf (" %u ", countColor?countColor:"", node->unreadCount); break; case NODE_CAPABILITY_SHOW_ITEM_COUNT: count = g_strdup_printf (" %u ", countColor?countColor:"", node->itemCount); break; default: break; } if (IS_VFOLDER (node) && node->data) { /* Extra message for search folder rebuilds */ if (((vfolderPtr)node->data)->reloading) { gchar *tmp = label; label = g_strdup_printf (_("%s\nRebuilding"), label); g_free (tmp); } } gtk_tree_store_set (flv->feedstore, iter, FS_LABEL, label, FS_UNREAD, node->unreadCount, FS_ICON, node->available?node_get_icon (node):icon_get (ICON_UNAVAILABLE), FS_COUNT, count, -1); g_free (label); g_free (count); if (node->parent) feed_list_view_update_node (node->parent->id); } /* node renaming dialog */ static void on_nodenamedialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { nodePtr node = (nodePtr)user_data; if (response_id == GTK_RESPONSE_OK) { node_set_title (node, (gchar *) gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "nameentry")))); feed_list_view_update_node (node->id); feedlist_schedule_save (); } gtk_widget_destroy (GTK_WIDGET (dialog)); } void feed_list_view_rename_node (nodePtr node) { GtkWidget *nameentry, *dialog; dialog = liferea_dialog_new ("rename_node"); nameentry = liferea_dialog_lookup (dialog, "nameentry"); gtk_entry_set_text (GTK_ENTRY (nameentry), node_get_title (node)); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_nodenamedialog_response), node); gtk_widget_show (dialog); } /* node deletion dialog */ static void feed_list_view_remove_cb (GtkDialog *dialog, gint response_id, gpointer user_data) { if (GTK_RESPONSE_ACCEPT == response_id) feedlist_remove_node ((nodePtr)user_data); gtk_widget_destroy (GTK_WIDGET (dialog)); } void feed_list_view_remove (nodePtr node) { GtkWidget *dialog; GtkWindow *mainwindow; gchar *text; g_assert (node == feedlist_get_selected ()); liferea_shell_set_status_bar ("%s \"%s\"", _("Deleting entry"), node_get_title (node)); text = g_strdup_printf (IS_FOLDER (node)?_("Are you sure that you want to delete \"%s\" and its contents?"):_("Are you sure that you want to delete \"%s\"?"), node_get_title (node)); mainwindow = GTK_WINDOW (liferea_shell_get_window ()); dialog = gtk_message_dialog_new (mainwindow, GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", text); gtk_dialog_add_buttons (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Delete"), GTK_RESPONSE_ACCEPT, NULL); gtk_window_set_title (GTK_WINDOW (dialog), _("Deletion Confirmation")); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); gtk_window_set_transient_for (GTK_WINDOW (dialog), mainwindow); g_free (text); gtk_widget_show_all (dialog); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (feed_list_view_remove_cb), node); } static void feed_list_view_add_duplicate_url_cb (GtkDialog *dialog, gint response_id, gpointer user_data) { subscriptionPtr tempSubscription = (subscriptionPtr) user_data; if (GTK_RESPONSE_ACCEPT == response_id) { feedlist_add_subscription ( subscription_get_source (tempSubscription), subscription_get_filter (tempSubscription), update_options_copy (tempSubscription->updateOptions), FEED_REQ_PRIORITY_HIGH ); } subscription_free (tempSubscription); gtk_widget_destroy (GTK_WIDGET (dialog)); } void feed_list_view_add_duplicate_url_subscription (subscriptionPtr tempSubscription, nodePtr exNode) { GtkWidget *dialog; GtkWindow *mainwindow; gchar *text; text = g_strdup_printf ( _("Are you sure that you want to add a new subscription with URL \"%s\"? Another subscription with the same URL already exists (\"%s\")."), tempSubscription->source, node_get_title (exNode) ); mainwindow = GTK_WINDOW (liferea_shell_get_window ()); dialog = gtk_message_dialog_new (mainwindow, GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "%s", text); gtk_dialog_add_buttons (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Add"), GTK_RESPONSE_ACCEPT, NULL); gtk_window_set_title (GTK_WINDOW (dialog), _("Adding Duplicate Subscription Confirmation")); gtk_window_set_transient_for (GTK_WINDOW (dialog), mainwindow); g_free (text); gtk_widget_show_all (dialog); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (feed_list_view_add_duplicate_url_cb), tempSubscription); } liferea-1.13.7/src/ui/feed_list_view.h000066400000000000000000000125061415350204600175630ustar00rootroot00000000000000/** * @file feed_list_view.h the feed list in a GtkTreeView * * Copyright (C) 2004-2019 Lars Windolf * Copyright (C) 2004-2005 Nathan J. Conrad * Copyright (C) 2005 Raphael Slinckx * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _FEED_LIST_VIEW_H #define _FEED_LIST_VIEW_H #include #include "feed.h" #define FEED_LIST_VIEW_TYPE (feed_list_view_get_type ()) G_DECLARE_FINAL_TYPE (FeedListView, feed_list_view, FEED_LIST, VIEW, GObject) /* constants for attributes in feedstore */ enum { FS_LABEL, /**< Displayed name */ FS_ICON, /**< Icon to use */ FS_PTR, /**< pointer to the folder or feed */ FS_UNREAD, /**< Number of unread items */ FS_COUNT, /**< Number of unread items as string */ FS_LEN }; /** * feed_list_view_select: * * Selects the given node in the feed list. * * @node: the node to select */ void feed_list_view_select (nodePtr node); /** * feed_list_view_create: (skip) * * Initializes the feed list. To be called only once. * * @treeview: A treeview widget to use * * Returns: new FeedListView */ FeedListView * feed_list_view_create (GtkTreeView *treeview); /** * feed_list_view_sort_folder: * * Sort the feeds of the given folder node. * * @folder: the folder */ void feed_list_view_sort_folder (nodePtr folder); void on_menu_delete (GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_menu_update (GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_menu_update_all (GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_action_mark_all_read (GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_menu_properties (GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_menu_feed_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data); void on_menu_folder_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data); void on_new_plugin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data); void on_new_newsbin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data); void on_new_vfolder_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data); void on_feedlist_reduced_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * Determines the tree iter of a given node. * * @param nodeId the node id */ GtkTreeIter * feed_list_view_to_iter (const gchar *nodeId); /** * Updates the tree iter of a given node. * * @param nodeId the node * @param iter the new iter */ void feed_list_view_update_iter (const gchar *nodeId, GtkTreeIter *iter); /** * Add a node to the feedlist tree view * s * @param node the node to add */ void feed_list_view_add_node (nodePtr node); /** * Reload the UI feedlist by removing and readding each node */ void feed_list_view_reload_feedlist (void); /** * Remove a node from the feedlist and free its ui_data. * * @param node the node to free */ void feed_list_view_remove_node (nodePtr node); /** * Adds an "empty" node to the given tree iter. * * @param parent a tree iter */ void feed_list_view_add_empty_node (GtkTreeIter *parent); /** * Removes an "empty" node from the given tree iter. * * @param parent a tree iter */ void feed_list_view_remove_empty_node (GtkTreeIter *parent); /** * Determines the expansion state of a feed list tree node. * * @param nodeId the node id * * @returns TRUE if the node is expanded */ gboolean feed_list_view_is_expanded (const gchar *nodeId); /** * Change the expansion/collapsing of the given folder node. * * @param folder the folder node * @param expanded new expansion state */ void feed_list_view_set_expansion (nodePtr folder, gboolean expanded); /** * Updates the tree view entry of the given node. * * @param nodeId the node id */ void feed_list_view_update_node (const gchar *nodeId); /** * Open dialog to rename a given node. * * @param node the node to rename */ void feed_list_view_rename_node (nodePtr node); /** * Prompt the user for confirmation of a folder or feed, and * recursively remove the feed or folder if the user accepts. This * function does not block, so the folder/feeds will not have * been deleted when this function returns. * * @param node the node to remove */ void feed_list_view_remove (nodePtr node); /** * Prompt the user for confirmation and forces adding the node, * even though another node with the same URL exists. * * @param tempSubscription the duplicate URL subscription * @param exNode the existing node */ void feed_list_view_add_duplicate_url_subscription (subscriptionPtr tempSubscription, nodePtr exNode); #endif liferea-1.13.7/src/ui/gedit-close-button.c000066400000000000000000000052261415350204600202770ustar00rootroot00000000000000/* * gedit-close-button.c * This file is part of gedit * * Copyright (C) 2010 - Paolo Borelli * Copyright (C) 2011 - Ignacio Casal Quinteiro * * gedit is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * gedit 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "gedit-close-button.h" struct _GeditCloseButtonClassPrivate { GtkCssProvider *css; }; G_DEFINE_TYPE_WITH_CODE (GeditCloseButton, gedit_close_button, GTK_TYPE_BUTTON, g_type_add_class_private (g_define_type_id, sizeof (GeditCloseButtonClassPrivate))) static void gedit_close_button_class_init (GeditCloseButtonClass *klass) { static const gchar button_style[] = "* {\n" "-GtkButton-default-border : 0;\n" "-GtkButton-default-outside-border : 0;\n" "-GtkButton-inner-border: 0;\n" "-GtkWidget-focus-line-width : 0;\n" "-GtkWidget-focus-padding : 0;\n" "padding: 0;\n" "}"; klass->priv = G_TYPE_CLASS_GET_PRIVATE (klass, GEDIT_TYPE_CLOSE_BUTTON, GeditCloseButtonClassPrivate); klass->priv->css = gtk_css_provider_new (); gtk_css_provider_load_from_data (klass->priv->css, button_style, -1, NULL); } static void gedit_close_button_init (GeditCloseButton *button) { GtkStyleContext *context; GtkWidget *image; GIcon *icon; icon = g_themed_icon_new_with_default_fallbacks ("window-close-symbolic"); image = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_MENU); gtk_widget_show (image); g_object_unref (icon); gtk_container_add (GTK_CONTAINER (button), image); /* make it small */ context = gtk_widget_get_style_context (GTK_WIDGET (button)); gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (GEDIT_CLOSE_BUTTON_GET_CLASS (button)->priv->css), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } GtkWidget * gedit_close_button_new () { return GTK_WIDGET (g_object_new (GEDIT_TYPE_CLOSE_BUTTON, "relief", GTK_RELIEF_NONE, "focus-on-click", FALSE, NULL)); } /* ex:set ts=8 noet: */ liferea-1.13.7/src/ui/gedit-close-button.h000066400000000000000000000042541415350204600203040ustar00rootroot00000000000000/* * gedit-close-button.h * This file is part of gedit * * Copyright (C) 2010 - Paolo Borelli * * gedit is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * gedit 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef __GEDIT_CLOSE_BUTTON_H__ #define __GEDIT_CLOSE_BUTTON_H__ #include G_BEGIN_DECLS #define GEDIT_TYPE_CLOSE_BUTTON (gedit_close_button_get_type ()) #define GEDIT_CLOSE_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GEDIT_TYPE_CLOSE_BUTTON, GeditCloseButton)) #define GEDIT_CLOSE_BUTTON_CONST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GEDIT_TYPE_CLOSE_BUTTON, GeditCloseButton const)) #define GEDIT_CLOSE_BUTTON_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GEDIT_TYPE_CLOSE_BUTTON, GeditCloseButtonClass)) #define GEDIT_IS_CLOSE_BUTTON(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GEDIT_TYPE_CLOSE_BUTTON)) #define GEDIT_IS_CLOSE_BUTTON_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GEDIT_TYPE_CLOSE_BUTTON)) #define GEDIT_CLOSE_BUTTON_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GEDIT_TYPE_CLOSE_BUTTON, GeditCloseButtonClass)) typedef struct _GeditCloseButton GeditCloseButton; typedef struct _GeditCloseButtonClass GeditCloseButtonClass; typedef struct _GeditCloseButtonClassPrivate GeditCloseButtonClassPrivate; struct _GeditCloseButton { GtkButton parent; }; struct _GeditCloseButtonClass { GtkButtonClass parent_class; GeditCloseButtonClassPrivate *priv; }; GType gedit_close_button_get_type (void) G_GNUC_CONST; GtkWidget *gedit_close_button_new (void); G_END_DECLS #endif /* __GEDIT_CLOSE_BUTTON_H__ */ /* ex:set ts=8 noet: */ liferea-1.13.7/src/ui/icons.c000066400000000000000000000055251415350204600157040ustar00rootroot00000000000000/* * @file icons.c Using icons from theme and package pixmaps * * Copyright (C) 2010-2014 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "icons.h" #ifdef HAVE_CONFIG_H # include #endif #include "common.h" static GIcon *icons[MAX_ICONS]; /*<< list of icon assignments */ static gchar * icon_find_pixmap_file (const gchar *filename) { gchar *pathname = g_build_filename (PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "pixmaps", filename, NULL); if (g_file_test (pathname, G_FILE_TEST_EXISTS)) return pathname; g_free (pathname); return NULL; } GdkPixbuf * icon_create_from_file (const gchar *filename) { gchar *pathname = NULL; GdkPixbuf *pixbuf; GError *error = NULL; if (!filename || !filename[0]) return NULL; pathname = icon_find_pixmap_file (filename); if (!pathname) { g_warning (_("Couldn't find pixmap file: %s"), filename); return NULL; } pixbuf = gdk_pixbuf_new_from_file (pathname, &error); if (!pixbuf) { fprintf (stderr, "Failed to load pixbuf file: %s: %s\n", pathname, error->message); g_error_free (error); } g_free (pathname); return pixbuf; } void icons_load (void) { GtkIconTheme *icon_theme; gint i; gchar *path; path = g_build_filename (PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "pixmaps", NULL); icon_theme = gtk_icon_theme_get_default (); gtk_icon_theme_append_search_path (icon_theme, path); static const gchar *iconNames[] = { "unread", /* ICON_UNREAD */ "emblem-important", /* ICON_FLAG */ "available", /* ICON_AVAILABLE */ "available_offline", /* ICON_AVAILABLE_OFFLINE */ "dialog-error", /* ICON_UNAVAILABLE */ "default", /* ICON_DEFAULT */ "folder", /* ICON_FOLDER */ "folder-saved-search", /* ICON_VFOLDER */ "newsbin", /* ICON_NEWSBIN */ "empty", /* ICON_EMPTY */ "empty_offline", /* ICON_EMPTY_OFFLINE */ "gtk-connect", /* ICON_ONLINE */ "gtk-disconnect", /* ICON_OFFLINE */ "mail-attachment", /* ICON_ENCLOSURE */ NULL }; for (i = 0; i < MAX_ICONS; i++) icons[i] = g_themed_icon_new (iconNames[i]); } const GIcon * icon_get (lifereaIcon icon) { g_assert (NULL != *icons); return icons[icon]; } liferea-1.13.7/src/ui/icons.h000066400000000000000000000036501415350204600157060ustar00rootroot00000000000000/* * @file icons.h Using icons from theme and package pixmaps * * Copyright (C) 2010-2013 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ICONS_H #define _ICONS_H #include /* list of all icons used in Liferea */ typedef enum { ICON_UNREAD, ICON_FLAG, ICON_AVAILABLE, ICON_AVAILABLE_OFFLINE, ICON_UNAVAILABLE, ICON_DEFAULT, ICON_FOLDER, ICON_VFOLDER, ICON_NEWSBIN, ICON_EMPTY, ICON_EMPTY_OFFLINE, ICON_ONLINE, ICON_OFFLINE, ICON_ENCLOSURE, MAX_ICONS } lifereaIcon; /** * icons_load: (skip) * * Load all icons from theme and Liferea pixmaps. * * Must be called once before icon_get() may be used! * Must be called only after GTK theme was initialized! */ void icons_load (void); /** * icon_get: * @icon: the icon * * Returns a GIcon for the requested item. * * Returns: (transfer none): GIcon */ const GIcon * icon_get (lifereaIcon icon); /** * icon_create_from_file: * @filename: the name of the file * * Takes a file name relative to "pixmaps" directory and tries to load the * image into a GdkPixbuf. Can be used to load icons not in lifereaIcon * on demand. * * Returns: (transfer full): a new pixbuf or NULL */ GdkPixbuf * icon_create_from_file (const gchar *filename); #endif liferea-1.13.7/src/ui/item_list_view.c000066400000000000000000001040771415350204600176160ustar00rootroot00000000000000/* * @file item_list_view.c presenting items in a GtkTreeView * * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/item_list_view.h" #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include "browser.h" #include "common.h" #include "conf.h" #include "date.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "item.h" #include "itemlist.h" #include "item_state.h" #include "itemview.h" #include "newsbin.h" #include "social.h" #include "xml.h" #include "ui/browser_tabs.h" #include "ui/icons.h" #include "ui/liferea_shell.h" #include "ui/popup_menu.h" #include "ui/ui_common.h" /* * Important performance considerations: Early versions had performance problems * with the item list loading because of the following two problems: * * 1.) Mass-adding items to a sorting enabled tree store. * 2.) Mass-loading items to an attached tree store. * * To avoid both problems we merge against a visible tree store only for single * items that are added/removed by background updates and load complete feeds or * collections of feeds only by adding items to a new unattached tree store. */ /* Enumeration of the columns in the itemstore. */ enum is_columns { IS_TIME, /*<< Time of item creation */ IS_TIME_STR, /*<< Time of item creation as a string*/ IS_LABEL, /*<< Displayed name */ IS_STATEICON, /*<< Pixbuf reference to the item's state icon */ IS_NR, /*<< Item id, to lookup item ptr from parent feed */ IS_PARENT, /*<< Parent node pointer */ IS_FAVICON, /*<< Pixbuf reference to the item's feed's icon */ IS_ENCICON, /*<< Pixbuf reference to the item's enclosure icon */ IS_ENCLOSURE, /*<< Flag whether enclosure is attached or not */ IS_SOURCE, /*<< Source node pointer */ IS_STATE, /*<< Original item state (unread, flagged...) for sorting */ ITEMSTORE_WEIGHT, /*<< Flag whether weight is to be bold and "unread" icon is to be shown */ ITEMSTORE_ALIGN, /*<< How to align title (RTL support) */ ITEMSTORE_LEN /*<< Number of columns in the itemstore */ }; typedef enum { DEFAULT, INTERNAL, TAB, EXTERNAL } open_link_target_type; static void launch_item (itemPtr item, open_link_target_type open_link_target) { if (item) { gchar *link = item_make_link (item); if (link) { switch (open_link_target) { case DEFAULT: itemview_launch_URL (link, FALSE); break; case INTERNAL: itemview_launch_URL (link, TRUE); break; case TAB: browser_tabs_add_new (link, link, FALSE); break; case EXTERNAL: browser_launch_URL_external (link); break; } item_set_read_state (item, TRUE); g_free (link); } else ui_show_error_box (_("This item has no link specified!")); } } struct _ItemListView { GObject parentInstance; GtkTreeView *treeview; GtkWidget *ilscrolledwindow; /*<< The complete ItemListView widget */ GSList *item_ids; /*<< list of all currently known item ids */ gboolean batch_mode; /*<< TRUE if we are in batch adding mode */ GtkTreeStore *batch_itemstore; /*<< GtkTreeStore prepared unattached and to be set on update() */ GHashTable *columns; /*<< Named GtkTreeViewColumns */ gboolean wideView; /*<< TRUE if date has to be rendered into headline column (because date column is invisible) */ }; G_DEFINE_TYPE (ItemListView, item_list_view, G_TYPE_OBJECT); static void item_list_view_finalize (GObject *object) { ItemListView *ilv = ITEM_LIST_VIEW (object); /* Disconnect the treeview signals to avoid spurious calls during teardown */ g_signal_handlers_disconnect_by_data (G_OBJECT (ilv->treeview), object); g_hash_table_destroy (ilv->columns); g_slist_free (ilv->item_ids); if (ilv->batch_itemstore) g_object_unref (ilv->batch_itemstore); if (ilv->ilscrolledwindow) g_object_unref (ilv->ilscrolledwindow); } static void item_list_view_class_init (ItemListViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = item_list_view_finalize; } /* helper functions for item <-> iter conversion */ static gulong item_list_view_iter_to_id (ItemListView *ilv, GtkTreeIter *iter) { gulong id = 0; gtk_tree_model_get (gtk_tree_view_get_model (ilv->treeview), iter, IS_NR, &id, -1); return id; } gboolean item_list_view_contains_id (ItemListView *ilv, gulong id) { return (NULL != g_slist_find (ilv->item_ids, GUINT_TO_POINTER (id))); } static gboolean item_list_view_id_to_iter (ItemListView *ilv, gulong id, GtkTreeIter *iter) { gboolean valid; GtkTreeIter old_iter; GtkTreeModel *model; /* Problem here is batch insertion using batch_itemstore so the item can be in the GtkTreeView attached store or in the batch_itemstore */ if (item_list_view_contains_id (ilv, id)) { /* First search the tree view */ model = gtk_tree_view_get_model (ilv->treeview); valid = gtk_tree_model_get_iter_first (model, &old_iter); while (valid) { gulong current_id = item_list_view_iter_to_id (ilv, &old_iter); if(current_id == id) { *iter = old_iter; return TRUE; } valid = gtk_tree_model_iter_next (model, &old_iter); } /* Next search the batch store */ model = GTK_TREE_MODEL (ilv->batch_itemstore); valid = gtk_tree_model_get_iter_first (model, &old_iter); while (valid) { gulong current_id; gtk_tree_model_get (model, &old_iter, IS_NR, ¤t_id, -1); if(current_id == id) { *iter = old_iter; return TRUE; } valid = gtk_tree_model_iter_next (model, &old_iter); } } return FALSE; } static gint item_list_view_date_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data) { guint64 timea, timeb; double diff; gtk_tree_model_get (model, a, IS_TIME, &timea, -1); gtk_tree_model_get (model, b, IS_TIME, &timeb, -1); diff = difftime ((time_t)timeb, (time_t)timea); if (diff < 0) return 1; if (diff > 0) return -1; return 0; } static gint item_list_view_favicon_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data) { nodePtr node1, node2; gtk_tree_model_get (model, a, IS_SOURCE, &node1, -1); gtk_tree_model_get (model, b, IS_SOURCE, &node2, -1); if (!node1->id || !node2->id) return 0; return strcmp (node1->id, node2->id); } void item_list_view_set_sort_column (ItemListView *ilv, nodeViewSortType sortType, gboolean sortReversed) { gint sortColumn; switch (sortType) { case NODE_VIEW_SORT_BY_TITLE: /* Some ugly switching here, because in wide view we do sort headlines by date */ if (ilv->wideView) sortColumn = IS_TIME; else sortColumn = IS_LABEL; break; case NODE_VIEW_SORT_BY_PARENT: sortColumn = IS_SOURCE; break; case NODE_VIEW_SORT_BY_STATE: sortColumn = IS_STATE; break; case NODE_VIEW_SORT_BY_TIME: default: sortColumn = IS_TIME; break; } gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (gtk_tree_view_get_model (ilv->treeview)), sortColumn, sortReversed?GTK_SORT_DESCENDING:GTK_SORT_ASCENDING); } /* * Creates a GtkTreeStore to be filled with ui_itemlist_set_items * and to be set with ui_itemlist_set_tree_store(). */ static GtkTreeStore * item_list_view_create_tree_store (void) { return gtk_tree_store_new (ITEMSTORE_LEN, G_TYPE_INT64, /* IS_TIME */ G_TYPE_STRING, /* IS_TIME_STR */ G_TYPE_STRING, /* IS_LABEL */ G_TYPE_ICON, /* IS_STATEICON */ G_TYPE_ULONG, /* IS_NR */ G_TYPE_POINTER, /* IS_PARENT */ G_TYPE_ICON, /* IS_FAVICON */ G_TYPE_ICON, /* IS_ENCICON */ G_TYPE_BOOLEAN, /* IS_ENCLOSURE */ G_TYPE_POINTER, /* IS_SOURCE */ G_TYPE_UINT, /* IS_STATE */ G_TYPE_INT, /* ITEMSTORE_WEIGHT */ G_TYPE_FLOAT /* ITEMSTORE_ALIGN */ ); } static void on_itemlist_selection_changed (GtkTreeSelection *selection, gpointer user_data) { GtkTreeIter iter; GtkTreeModel *model; itemPtr item = NULL; if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gulong id = item_list_view_iter_to_id (ITEM_LIST_VIEW (user_data), &iter); if (id != itemlist_get_selected_id ()) { item = item_load (id); liferea_shell_update_item_menu (NULL != item); if (item) itemlist_selection_changed (item); } } else { liferea_shell_update_item_menu (FALSE); } } static void itemlist_sort_column_changed_cb (GtkTreeSortable *treesortable, gpointer user_data) { gint sortColumn, nodeSort; GtkSortType sortType; gboolean sorted, changed; if (feedlist_get_selected () == NULL) return; sorted = gtk_tree_sortable_get_sort_column_id (treesortable, &sortColumn, &sortType); if (!sorted) return; switch (sortColumn) { case IS_TIME: default: nodeSort = NODE_VIEW_SORT_BY_TIME; break; case IS_LABEL: nodeSort = NODE_VIEW_SORT_BY_TITLE; break; case IS_STATE: nodeSort = NODE_VIEW_SORT_BY_STATE; break; case IS_PARENT: case IS_SOURCE: nodeSort = NODE_VIEW_SORT_BY_PARENT; break; } changed = node_set_sort_column (feedlist_get_selected (), nodeSort, sortType == GTK_SORT_DESCENDING); if (changed) feedlist_schedule_save (); } /* * Sets a GtkTreeView to the active GtkTreeView. */ static void item_list_view_set_tree_store (ItemListView *ilv, GtkTreeStore *itemstore) { GtkTreeModel *model; GtkTreeSelection *select; /* drop old tree store */ model = gtk_tree_view_get_model (ilv->treeview); gtk_tree_view_set_model (ilv->treeview, NULL); if (model) g_object_unref (model); gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_TIME, item_list_view_date_sort_func, NULL, NULL); gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_SOURCE, item_list_view_favicon_sort_func, NULL, NULL); g_signal_connect (G_OBJECT (itemstore), "sort-column-changed", G_CALLBACK (itemlist_sort_column_changed_cb), NULL); gtk_tree_view_set_model (ilv->treeview, GTK_TREE_MODEL (itemstore)); /* Setup the selection handler */ select = gtk_tree_view_get_selection (ilv->treeview); gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE); g_signal_connect (G_OBJECT (select), "changed", G_CALLBACK (on_itemlist_selection_changed), ilv); } void item_list_view_remove_item (ItemListView *ilv, itemPtr item) { GtkTreeIter iter; g_assert (NULL != item); if (item_list_view_id_to_iter (ilv, item->id, &iter)) { /* Using the GtkTreeIter check if it is currently selected. If yes, scroll down by one in the sorted GtkTreeView to ensure something is selected after removing the GtkTreeIter */ if (gtk_tree_selection_iter_is_selected (gtk_tree_view_get_selection (ilv->treeview), &iter)) ui_common_treeview_move_cursor (ilv->treeview, 1); gtk_tree_store_remove (GTK_TREE_STORE (gtk_tree_view_get_model (ilv->treeview)), &iter); } else { g_warning ("Fatal: item to be removed not found in item id list!"); } ilv->item_ids = g_slist_remove (ilv->item_ids, GUINT_TO_POINTER (item->id)); } /* cleans up the item list, sets up the iter hash when called for the first time */ void item_list_view_clear (ItemListView *ilv) { GtkAdjustment *adj; GtkTreeStore *itemstore; GtkTreeSelection *select; itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->treeview)); /* unselecting all items is important to remove items whose removal is deferred until unselecting */ select = gtk_tree_view_get_selection (ilv->treeview); gtk_tree_selection_unselect_all (select); adj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (ilv->treeview)); gtk_adjustment_set_value (adj, 0.0); gtk_scrollable_set_vadjustment (GTK_SCROLLABLE (ilv->treeview), adj); /* Disconnect signal handler to be safe */ g_signal_handlers_disconnect_by_func (G_OBJECT (select), G_CALLBACK (on_itemlist_selection_changed), ilv); if (itemstore) gtk_tree_store_clear (itemstore); if (ilv->item_ids) g_slist_free (ilv->item_ids); ilv->item_ids = NULL; /* enable batch mode for following item adds */ ilv->batch_mode = TRUE; ilv->batch_itemstore = item_list_view_create_tree_store (); gtk_widget_freeze_child_notify((GtkWidget *)ilv->treeview); gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (ilv->batch_itemstore), NULL, NULL, NULL); } static gfloat item_list_title_alignment (gchar *title) { if (!title || strlen(title) == 0) return 0.; /* debug5 (DEBUG_HTML, "title ***%s*** first bytes %02hhx%02hhx%02hhx pango %d", title, title[0], title[1], title[2], pango_find_base_dir (title, -1)); */ int txt_direction = common_find_base_dir (title, -1); int app_direction = gtk_widget_get_default_direction (); if ((txt_direction == PANGO_DIRECTION_LTR && app_direction == GTK_TEXT_DIR_LTR) || (txt_direction == PANGO_DIRECTION_RTL && app_direction == GTK_TEXT_DIR_RTL)) return 0.; /* same direction, regular ("left") alignment */ else return 1.; } static void item_list_view_update_item_internal (ItemListView *ilv, itemPtr item, GtkTreeIter *iter, nodePtr node) { GtkTreeStore *itemstore; gchar *title, *time_str; const GIcon *state_icon; gint state = 0; if (item->flagStatus) state += 2; if (!item->readStatus) state += 1; time_str = (0 != item->time) ? date_format ((time_t)item->time, NULL) : g_strdup (""); title = item->title && strlen (item->title) ? item->title : _("*** No title ***"); title = g_strstrip (g_markup_escape_text (title, -1)); if (ilv->wideView) { const gchar *important = _(" important "); gchar *teaser = item_get_teaser (item); gchar *tmp = title; title = g_strdup_printf ("%s%s\n%s%s — %s", item->readStatus?"normal":"ultrabold", title, item->flagStatus?important:"", item->readStatus?"ultralight":"light", teaser?teaser:"", teaser?"…":"", time_str); g_free (tmp); g_free (teaser); } state_icon = item->flagStatus ? icon_get (ICON_FLAG) : !item->readStatus ? icon_get (ICON_UNREAD) : NULL; if (ilv->batch_mode) itemstore = ilv->batch_itemstore; else itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->treeview)); if (NULL == node) { gtk_tree_store_set (itemstore, iter, IS_LABEL, title, IS_TIME, item->time, IS_TIME_STR, time_str, IS_NR, item->id, IS_STATEICON, state_icon, ITEMSTORE_ALIGN, item_list_title_alignment (title), IS_ENCICON, item->hasEnclosure?icon_get (ICON_ENCLOSURE):NULL, IS_ENCLOSURE, item->hasEnclosure, IS_STATE, state, ITEMSTORE_WEIGHT, item->readStatus ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD, -1); } else { gtk_tree_store_set (itemstore, iter, IS_LABEL, title, IS_TIME, item->time, IS_TIME_STR, time_str, IS_NR, item->id, IS_STATEICON, state_icon, IS_PARENT, node, IS_FAVICON, node_get_icon (node), IS_ENCICON, item->hasEnclosure?icon_get (ICON_ENCLOSURE):NULL, IS_ENCLOSURE, item->hasEnclosure, IS_SOURCE, node, IS_STATE, state, ITEMSTORE_WEIGHT, item->readStatus ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD, -1); } g_free (time_str); g_free (title); } void item_list_view_update_item (ItemListView *ilv, itemPtr item) { GtkTreeIter iter; if (!item_list_view_id_to_iter (ilv, item->id, &iter)) return; item_list_view_update_item_internal (ilv, item, &iter, NULL); } void item_list_view_update_all_items (ItemListView *ilv) { gboolean valid; GtkTreeIter iter; gulong id; GtkTreeModel *model; model = gtk_tree_view_get_model (ilv->treeview); valid = gtk_tree_model_get_iter_first (model, &iter); while (valid) { gtk_tree_model_get (model, &iter, IS_NR, &id, -1); itemPtr item = item_load (id); item_list_view_update_item (ilv, item); item_unload (item); valid = gtk_tree_model_iter_next (model, &iter); } } void item_list_view_update (ItemListView *ilv, gboolean hasEnclosures) { /* we depend on the fact that the third column is the enclosure icon column!!! */ gtk_tree_view_column_set_visible (g_hash_table_lookup(ilv->columns, "enclosure"), hasEnclosures); if (ilv->batch_mode) { gtk_widget_thaw_child_notify((GtkWidget *)ilv->treeview); item_list_view_set_tree_store (ilv, ilv->batch_itemstore); ilv->batch_mode = FALSE; } else { /* Nothing to do in non-batch mode as items were added and updated one-by-one in ui_itemlist_add_item() */ } } static gboolean on_item_list_view_key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer data) { if ((event->type == GDK_KEY_PRESS) && (event->state == 0) && (event->keyval == GDK_KEY_Delete)) on_action_remove_item(NULL, NULL, NULL); return FALSE; } /* Show tooltip when headline's column text (IS_LABEL) is truncated. */ static gint get_cell_renderer_width (GtkWidget *widget, GtkCellRenderer *cell, const gchar *text, gint weight) { PangoLayout *layout = gtk_widget_create_pango_layout (widget, text); PangoAttrList *attrbs = pango_attr_list_new(); PangoRectangle rect; gint xpad = 0; pango_attr_list_insert (attrbs, pango_attr_weight_new (weight)); pango_layout_set_attributes (layout, attrbs); pango_attr_list_unref (attrbs); pango_layout_get_pixel_extents (layout, NULL, &rect); g_object_unref (G_OBJECT (layout)); gtk_cell_renderer_get_padding (cell, &xpad, NULL); return (xpad * 2) + rect.x + rect.width; } static gboolean on_item_list_view_query_tooltip (GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, GtkTreeViewColumn *headline_column) { GtkTreeView *view = GTK_TREE_VIEW (widget); GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; gboolean ret = FALSE; if (gtk_tree_view_get_tooltip_context (view, &x, &y, keyboard_mode, &model, &path, &iter)) { GtkTreeViewColumn *column; gint bx, by; gtk_tree_view_convert_widget_to_bin_window_coords (view, x, y, &bx, &by); gtk_tree_view_get_path_at_pos (view, x, y, NULL, &column, NULL, NULL); if (column == headline_column) { GtkCellRenderer *cell; GList *renderers = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column)); cell = GTK_CELL_RENDERER (renderers->data); g_list_free (renderers); gchar *text; gint weight; gtk_tree_model_get (model, &iter, IS_LABEL, &text, ITEMSTORE_WEIGHT, &weight, -1); gint full_width = get_cell_renderer_width (widget, cell, text, weight); gint column_width = gtk_tree_view_column_get_width (column); if (full_width > column_width) { gtk_tooltip_set_text (tooltip, text); ret = TRUE; } g_free (text); } gtk_tree_view_set_tooltip_row (view, tooltip, path); gtk_tree_path_free (path); } return ret; } static gboolean on_item_list_view_button_press_event (GtkWidget *treeview, GdkEvent *event, gpointer user_data) { ItemListView *ilv = ITEM_LIST_VIEW (user_data); GtkTreePath *path; GtkTreeIter iter; GtkTreeViewColumn *column; GdkEventButton *eb; itemPtr item = NULL; gboolean result = FALSE; if (event->type != GDK_BUTTON_PRESS) return FALSE; eb = (GdkEventButton*) event; /* avoid handling header clicks */ if (eb->window != gtk_tree_view_get_bin_window (ilv->treeview)) return FALSE; if (!gtk_tree_view_get_path_at_pos (ilv->treeview, (gint)eb->x, (gint)eb->y, &path, &column, NULL, NULL)) return FALSE; if (gtk_tree_model_get_iter (gtk_tree_view_get_model (ilv->treeview), &iter, path)) item = item_load (item_list_view_iter_to_id (ilv, &iter)); gtk_tree_path_free (path); if (item) { switch (eb->button) { case 1: if (column == g_hash_table_lookup(ilv->columns, "favicon") || column == g_hash_table_lookup(ilv->columns, "state")) { itemlist_toggle_flag (item); result = TRUE; } break; case 2: /* Middle mouse click toggles read status... */ itemlist_toggle_read_status (item); result = TRUE; break; case 3: ui_popup_item_menu (item, event); result = TRUE; break; default: /* Do nothing on buttons >= 4 */ break; } item_unload (item); } return result; } static gboolean on_item_list_view_popup_menu (GtkWidget *widget, gpointer user_data) { GtkTreeView *treeview = GTK_TREE_VIEW (widget); GtkTreeModel *model; GtkTreeIter iter; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (treeview), &model, &iter)) { itemPtr item = item_load (item_list_view_iter_to_id (ITEM_LIST_VIEW (user_data), &iter)); ui_popup_item_menu (item, NULL); item_unload (item); return TRUE; } return FALSE; } static void on_item_list_row_activated (GtkTreeView *treeview, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data) { GtkTreeModel *model = gtk_tree_view_get_model (treeview); GtkTreeIter iter; if (gtk_tree_model_get_iter (model, &iter, path)) { itemPtr item = item_load (item_list_view_iter_to_id (ITEM_LIST_VIEW (user_data), &iter)); launch_item (item, DEFAULT); item_unload (item); } } static void on_item_list_view_columns_changed (GtkTreeView *treeview, ItemListView *ilv) { gint i = 0; GList *columns; GHashTableIter iter; gpointer colname, colptr; gchar *strv[6]; /* This handler is only used for drag and drop reordering, so it should not be hooked up with less than the full number of columns eg: on item_list_view creation or teardown */ if (gtk_tree_view_get_n_columns(treeview) != 5) return; columns = gtk_tree_view_get_columns (treeview); for (GList *li = columns; li; li = li->next) { g_hash_table_iter_init (&iter, ilv->columns); while (g_hash_table_iter_next (&iter, &colname, &colptr)) { if (li->data == colptr) { strv[i++] = colname; strv[i] = NULL; break; } } } conf_set_strv_value (LIST_VIEW_COLUMN_ORDER, (const gchar **)strv); g_list_free (columns); } GtkWidget * item_list_view_get_widget (ItemListView *ilv) { return ilv->ilscrolledwindow; } void item_list_view_move_cursor (ItemListView *ilv, int step) { ui_common_treeview_move_cursor (ilv->treeview, step); } void item_list_view_move_cursor_to_first (ItemListView *ilv) { ui_common_treeview_move_cursor_to_first (ilv->treeview); } static void item_list_view_init (ItemListView *ilv) { } ItemListView * item_list_view_create (gboolean wide) { ItemListView *ilv; GtkCellRenderer *renderer; GtkTreeViewColumn *column, *headline_column; gchar **conf_column_order; ilv = g_object_new (ITEM_LIST_VIEW_TYPE, NULL); ilv->wideView = wide; ilv->columns = g_hash_table_new (g_str_hash, g_str_equal); ilv->ilscrolledwindow = gtk_scrolled_window_new (NULL, NULL); g_object_ref_sink (ilv->ilscrolledwindow); gtk_widget_show (ilv->ilscrolledwindow); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (ilv->ilscrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (ilv->ilscrolledwindow), GTK_SHADOW_IN); ilv->treeview = GTK_TREE_VIEW (gtk_tree_view_new ()); if (wide) { gtk_tree_view_set_fixed_height_mode (ilv->treeview, FALSE); gtk_tree_view_set_grid_lines (ilv->treeview, GTK_TREE_VIEW_GRID_LINES_HORIZONTAL); } gtk_container_add (GTK_CONTAINER (ilv->ilscrolledwindow), GTK_WIDGET (ilv->treeview)); gtk_widget_show (GTK_WIDGET (ilv->treeview)); gtk_widget_set_name (GTK_WIDGET (ilv->treeview), "itemlist"); item_list_view_set_tree_store (ilv, item_list_view_create_tree_store ()); renderer = gtk_cell_renderer_pixbuf_new (); column = gtk_tree_view_column_new_with_attributes ("", renderer, "gicon", IS_STATEICON, NULL); g_object_set (renderer, "stock-size", wide?GTK_ICON_SIZE_LARGE_TOOLBAR:GTK_ICON_SIZE_SMALL_TOOLBAR, NULL); g_hash_table_insert (ilv->columns, "state", column); gtk_tree_view_column_set_sort_column_id (column, IS_STATE); if (wide) gtk_tree_view_column_set_visible (column, FALSE); renderer = gtk_cell_renderer_pixbuf_new (); column = gtk_tree_view_column_new_with_attributes ("", renderer, "gicon", IS_FAVICON, NULL); g_object_set (renderer, "stock-size", wide?GTK_ICON_SIZE_DIALOG:GTK_ICON_SIZE_SMALL_TOOLBAR, NULL); gtk_tree_view_column_set_sort_column_id (column, IS_SOURCE); g_hash_table_insert (ilv->columns, "favicon", column); renderer = gtk_cell_renderer_text_new (); headline_column = gtk_tree_view_column_new_with_attributes (_("Headline"), renderer, "markup", IS_LABEL, "xalign", ITEMSTORE_ALIGN, NULL); gtk_tree_view_column_set_expand (headline_column, TRUE); g_hash_table_insert (ilv->columns, "headline", headline_column); g_object_set (headline_column, "resizable", TRUE, NULL); if (wide) { gtk_tree_view_column_set_sort_column_id (headline_column, IS_TIME); g_object_set (renderer, "wrap-mode", PANGO_WRAP_WORD, NULL); g_object_set (renderer, "wrap-width", 300, NULL); } else { gtk_tree_view_column_set_sort_column_id (headline_column, IS_LABEL); g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); gtk_tree_view_column_add_attribute (headline_column, renderer, "weight", ITEMSTORE_WEIGHT); } renderer = gtk_cell_renderer_pixbuf_new (); column = gtk_tree_view_column_new_with_attributes ("", renderer, "gicon", IS_ENCICON, NULL); g_hash_table_insert (ilv->columns, "enclosure", column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes (_("Date"), renderer, "text", IS_TIME_STR, "weight", ITEMSTORE_WEIGHT, NULL); gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY); g_hash_table_insert (ilv->columns, "date", column); gtk_tree_view_column_set_sort_column_id(column, IS_TIME); if (wide) gtk_tree_view_column_set_visible (column, FALSE); conf_get_strv_value (LIST_VIEW_COLUMN_ORDER, &conf_column_order); for (gchar **li = conf_column_order; *li; li++) { column = g_hash_table_lookup (ilv->columns, *li); g_object_set (column, "reorderable", TRUE, NULL); gtk_tree_view_append_column (ilv->treeview, column); } g_strfreev (conf_column_order); /* And connect signals */ g_signal_connect (G_OBJECT (ilv->treeview), "button_press_event", G_CALLBACK (on_item_list_view_button_press_event), ilv); g_signal_connect (G_OBJECT (ilv->treeview), "columns_changed", G_CALLBACK (on_item_list_view_columns_changed), ilv); g_signal_connect (G_OBJECT (ilv->treeview), "row_activated", G_CALLBACK (on_item_list_row_activated), ilv); g_signal_connect (G_OBJECT (ilv->treeview), "key-press-event", G_CALLBACK (on_item_list_view_key_press_event), ilv); g_signal_connect (G_OBJECT (ilv->treeview), "popup_menu", G_CALLBACK (on_item_list_view_popup_menu), ilv); if (!wide) { gtk_widget_set_has_tooltip (GTK_WIDGET (ilv->treeview), TRUE); g_signal_connect (G_OBJECT (ilv->treeview), "query-tooltip", G_CALLBACK (on_item_list_view_query_tooltip), headline_column); } return ilv; } static void item_list_view_add_item_to_tree_store (ItemListView *ilv, GtkTreeStore *itemstore, itemPtr item) { nodePtr node; GtkTreeIter iter; gboolean exists = FALSE; node = node_from_id (item->nodeId); if(!node) return; /* comment items do cause this... maybe filtering them earlier would be a good idea... */ if (!ilv->batch_mode) exists = item_list_view_id_to_iter (ilv, item->id, &iter); if (!exists) { gtk_tree_store_prepend (itemstore, &iter, NULL); ilv->item_ids = g_slist_prepend (ilv->item_ids, GUINT_TO_POINTER (item->id)); } item_list_view_update_item_internal (ilv, item, &iter, node); } void item_list_view_add_item (ItemListView *ilv, itemPtr item) { GtkTreeStore *itemstore; if (ilv->batch_mode) { /* either merge to new unattached GtkTreeStore */ item_list_view_add_item_to_tree_store (ilv, ilv->batch_itemstore, item); } else { /* or merge to visible item store */ itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (ilv->treeview)); item_list_view_add_item_to_tree_store (ilv, itemstore, item); } } void item_list_view_enable_favicon_column (ItemListView *ilv, gboolean enabled) { gtk_tree_view_column_set_visible (g_hash_table_lookup(ilv->columns, "favicon"), enabled); // In wide view we want to save vertical space and hide the state column if (ilv->wideView) gtk_tree_view_column_set_visible (g_hash_table_lookup(ilv->columns, "state"), !enabled); } void on_action_launch_item_in_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { launch_item (item, INTERNAL); item_unload (item); } } void on_action_launch_item_in_tab (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { launch_item (item, TAB); item_unload (item); } } void on_action_launch_item_in_external_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { launch_item (item, EXTERNAL); item_unload (item); } } /* menu callbacks */ void on_toggle_item_flag (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { itemlist_toggle_flag (item); item_unload (item); } } void on_toggle_unread_status (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { itemlist_toggle_read_status (item); item_unload (item); } } void on_remove_items_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr node; node = feedlist_get_selected (); // FIXME: use node type capability check if (node && (IS_FEED (node) || IS_NEWSBIN (node))) itemlist_remove_all_items (node); else ui_show_error_box (_("You must select a feed to delete its items!")); } void on_action_remove_item (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if (item) { itemview_select_item (NULL); itemlist_remove_item (item); } else { liferea_shell_set_important_status_bar (_("No item has been selected")); } } void item_list_view_select (ItemListView *ilv, itemPtr item) { GtkTreeView *treeview = ilv->treeview; GtkTreeSelection *selection; GtkTreeIter iter; selection = gtk_tree_view_get_selection (treeview); if (item && item_list_view_id_to_iter(ilv, item->id, &iter)){ GtkTreePath *path = NULL; path = gtk_tree_model_get_path (gtk_tree_view_get_model (treeview), &iter); if (path) { gtk_tree_view_set_cursor (treeview, path, NULL, FALSE); gtk_tree_view_scroll_to_cell (treeview, path, NULL, FALSE, 0.0, 0.0); gtk_tree_path_free (path); } } else { if (item) g_warning ("item_list_view_select : attempt to select an item which is not present in the view."); gtk_tree_selection_unselect_all (selection); } } itemPtr item_list_view_find_unread_item (ItemListView *ilv, gulong startId) { GtkTreeIter iter; GtkTreeModel *model; gboolean valid = TRUE; model = gtk_tree_view_get_model (ilv->treeview); if (startId) valid = item_list_view_id_to_iter (ilv, startId, &iter); else valid = gtk_tree_model_get_iter_first (model, &iter); while (valid) { itemPtr item = item_load (item_list_view_iter_to_id (ilv, &iter)); if (item) { /* Skip the selected item */ if (!item->readStatus && item->id != startId) return item; item_unload (item); } valid = gtk_tree_model_iter_next (model, &iter); } return NULL; } void on_next_unread_item_activate (GSimpleAction *menuitem, GVariant*parameter, gpointer user_data) { itemlist_select_next_unread (); } void on_popup_copy_URL_clipboard (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item; item = itemlist_get_selected (); if (item) { gchar *link = item_make_link (item); gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY), link, -1); gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), link, -1); g_free (link); item_unload (item); } else { liferea_shell_set_important_status_bar (_("No item has been selected")); } } void on_popup_social_bm_item_selected (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item; item = itemlist_get_selected (); if (item) { social_add_bookmark (item); item_unload (item); } else liferea_shell_set_important_status_bar (_("No item has been selected")); } liferea-1.13.7/src/ui/item_list_view.h000066400000000000000000000171731415350204600176230ustar00rootroot00000000000000/* * @file item_list_view.h presenting items in a GtkTreeView * * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2004-2005 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEM_LIST_VIEW_H #define _ITEM_LIST_VIEW_H #include #include "item.h" #include "node_view.h" /* This class realizes an GtkTreeView based item view. Instances of ItemListView are managed by the ItemListView. This class hides the GtkTreeView and implements performance optimizations. */ G_BEGIN_DECLS #define ITEM_LIST_VIEW_TYPE (item_list_view_get_type ()) G_DECLARE_FINAL_TYPE (ItemListView, item_list_view, ITEM_LIST, VIEW, GObject) /** * item_list_view_create: (skip) * @wide: TRUE if ItemListView should be optimized for wide view(itemview->priv->currentLayoutMode == NODE_VIEW_MODE_WIDE) * * Create a new ItemListView instance. * * Returns: (transfer none): the ItemListView instance */ ItemListView * item_list_view_create (gboolean wide); /** * item_list_view_get_widget: * * Returns the GtkWidget used by the ItemListView instance. * * Returns: (transfer none): a GtkWidget */ GtkWidget * item_list_view_get_widget (ItemListView *ilv); /** * item_list_view_move_cursor: * @ilv: the ItemListView * @step: move distance * * Moves the cursor in the item list step times. * Negative value means moving backwards. */ void item_list_view_move_cursor (ItemListView *ilv, int step); /** * item_list_view_move_cursor_to_first: * @ilv: the ItemListView * * Moves the cursor to the first element. */ void item_list_view_move_cursor_to_first (ItemListView *ilv); /** * item_list_view_contains_id: * @ilv: the ItemListView * @id: the item id * * Checks whether the given id is in the ItemListView. * * Returns: TRUE if the item is in the ItemListView */ gboolean item_list_view_contains_id (ItemListView *ilv, gulong id); /** * item_list_view_set_sort_column: * @ilv: the ItemListView * @sortType: new sort type * @sortReversed: TRUE for ascending order * * Changes the sorting type (and direction). */ void item_list_view_set_sort_column (ItemListView *ilv, nodeViewSortType sortType, gboolean sortReversed); /** * item_list_view_select: (skip) * @ilv: the ItemListView * @item: the item to select * * Selects the given item (if it is in the ItemListView). */ void item_list_view_select (ItemListView *ilv, itemPtr item); /** * item_list_view_add_item: (skip) * @ilv: the ItemListView * @item: the item to add * * Add an item to an ItemListView. This method is expensive and * is to be used only for new items that need to be inserted * by background updates. */ void item_list_view_add_item (ItemListView *ilv, itemPtr item); /** * item_list_view_remove_item: (skip) * @ilv: the ItemListView * @item: the item to remove * * Remove an item from an ItemListView. This method is expensive * and is to be used only for items removed by background updates * (usually cache drops). */ void item_list_view_remove_item (ItemListView *ilv, itemPtr item); /** * item_list_view_enable_favicon: * @ilv: the ItemListView * @enabled: TRUE if column is to be visible * * Enable the favicon column of the currently displayed itemlist. */ void item_list_view_enable_favicon_column (ItemListView *ilv, gboolean enabled); /** * item_list_view_clear: (skip) * @ilv: the ItemListView * * Remove all items and resets a ItemListView. */ void item_list_view_clear (ItemListView *ilv); /** * item_list_view_update: (skip) * @ilv: the ItemListView * @hasEnclosures: TRUE if at least one item has an enclosure * * Update the ItemListView with the newly added items. To be called * after doing a batch of item_list_view_add_item() calls. */ void item_list_view_update (ItemListView *ilv, gboolean hasEnclosures); /* menu callbacks */ /** * on_toggle_unread_status: (skip) * @action: The activated action. * @parameter: The item id as a GVariant of type "t", or NULL for the selected item. * @user_data: unused * * Toggles the unread status of the selected item. This is called from * a menu. */ void on_toggle_unread_status (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * on_toggle_item_flag: (skip) * @action: The activated action. * @parameter: The item id as a GVariant of type "t", or NULL for the selected item. * @user_data: unused * * Toggles the flag of the selected item. This is called from a menu. */ void on_toggle_item_flag (GSimpleAction *action, GVariant *parameter, gpointer user_data); /* * Opens the selected item in a browser. */ void on_action_launch_item_in_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data); /* * Opens the selected item in a browser. */ void on_action_launch_item_in_tab (GSimpleAction *action, GVariant *parameter, gpointer user_data); /* * Opens the selected item in a browser. */ void on_action_launch_item_in_external_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data); /* * Removes all items from the selected feed. */ void on_remove_items_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * on_action_remove_item: (skip) * @action: The activated action. * @parameter: The item id as a GVariant of type "t", or NULL for the selected item. * @user_data: Unused. * * Removes the selected item from the selected feed. */ void on_action_remove_item (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * item_list_view_find_unread_item: (skip) * @ilv: the ItemListView * @startId: 0 or the item id to start from * * Finds and selects the next unread item starting at the given * item in a ItemListView according to the current GtkTreeView sorting order. * * Returns: (nullable): unread item (or NULL) */ itemPtr item_list_view_find_unread_item (ItemListView *ilv, gulong startId); /** * on_next_unread_item_activate: (skip) * @action: The action that was activated. * @user_data: Unused. * * Searches the displayed feed and then all feeds for an unread * item. If one it found, it is displayed. */ void on_next_unread_item_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * item_list_view_update_item: (skip) * @ilv: the ItemListView * @item: the item * * Update a single item of a ItemListView */ void item_list_view_update_item (ItemListView *ilv, itemPtr item); /** * item_list_view_update_all_items: (skip) * @ilv: the ItemListView * * Update all items of the ItemListView. To be used after * initial batch loading. */ void item_list_view_update_all_items (ItemListView *ilv); /** * on_popup_copy_URL_clipboard: (skip) * * Copies the selected items URL to the clipboard. */ void on_popup_copy_URL_clipboard (GSimpleAction *action, GVariant *parameter, gpointer user_data); /** * on_popup_social_bm_item_selected: (skip) * * Bookmarks the selected item to social bookmark service. */ void on_popup_social_bm_item_selected (GSimpleAction *action, GVariant *parameter, gpointer user_data); #endif liferea-1.13.7/src/ui/itemview.c000066400000000000000000000327721415350204600164260ustar00rootroot00000000000000/* * @file itemview.c viewing feed content in different presentation modes * * Copyright (C) 2006-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "conf.h" #include "debug.h" #include "folder.h" #include "item_history.h" #include "itemlist.h" #include "itemview.h" #include "node.h" #include "vfolder.h" #include "ui/ui_common.h" #include "ui/browser_tabs.h" #include "ui/enclosure_list_view.h" #include "ui/liferea_shell.h" #include "ui/item_list_view.h" #include "ui/liferea_htmlview.h" /* The item view is the layer that switches item list presentations: a HTML single item or list and GtkTreeView list presentation. It hides the item loading behaviour of GtkTreeView and HTML view. The item view does not handle item filtering, which is done by the item list implementation. */ struct _ItemView { GObject parent_instance; gboolean htmlOnly; /*<< TRUE if HTML only mode */ guint mode; /*<< current item view mode */ nodePtr node; /*<< the node whose items are displayed */ gboolean browsing; /*<< TRUE if itemview is used as internal browser right now */ gboolean needsHTMLViewUpdate; /*<< flag to be set when HTML rendering is to be updated, used to delay HTML updates */ gboolean hasEnclosures; /*<< TRUE if at least one item of the current itemset has an enclosure */ nodeViewType viewMode; /*<< current viewing mode */ guint currentLayoutMode; /*<< layout mode (3 pane, 2 pane, wide view) */ ItemListView *itemListView; /*<< widget instance used to present items in list mode */ EnclosureListView *enclosureView; /*<< Enclosure list widget */ LifereaHtmlView *htmlview; /*<< HTML rendering widget instance used to render single items and summaries mode */ gfloat zoom; /*<< HTML rendering widget zoom level */ }; enum { PROP_NONE, PROP_ITEM_LIST_VIEW, PROP_HTML_VIEW }; static ItemView *itemview = NULL; G_DEFINE_TYPE (ItemView, itemview, G_TYPE_OBJECT); static void itemview_finalize (GObject *object) { ItemView *itemview = ITEM_VIEW (object); if (itemview->htmlview) { /* save zoom preferences */ conf_set_int_value (LAST_ZOOMLEVEL, (gint)(100.* liferea_htmlview_get_zoom (itemview->htmlview))); g_object_unref (itemview->htmlview); } if (itemview->enclosureView) g_object_unref (itemview->enclosureView); if (itemview->itemListView) g_object_unref (itemview->itemListView); } static void itemview_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { ItemView *itemview = ITEM_VIEW (object); switch (prop_id) { case PROP_ITEM_LIST_VIEW: g_value_set_object (value, itemview->itemListView); break; case PROP_HTML_VIEW: g_value_set_object (value, itemview->htmlview); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void itemview_class_init (ItemViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = itemview_get_property; object_class->finalize = itemview_finalize; /* ItemView:item-list-view: */ g_object_class_install_property (object_class, PROP_ITEM_LIST_VIEW, g_param_spec_object ( "item-list-view", "ItemListView", "ItemListView object", ITEM_LIST_VIEW_TYPE, G_PARAM_READABLE)); /* ItemView:html-view: */ g_object_class_install_property (object_class, PROP_HTML_VIEW, g_param_spec_object ( "html-view", "LifereaHtmlView", "LifereaHtmlView object", LIFEREA_HTMLVIEW_TYPE, G_PARAM_READABLE)); } void itemview_clear (void) { if (itemview->itemListView) item_list_view_clear (itemview->itemListView); enclosure_list_view_hide (itemview->enclosureView); itemview->hasEnclosures = FALSE; itemview->needsHTMLViewUpdate = TRUE; itemview->browsing = FALSE; } void itemview_set_mode (itemViewMode mode) { browser_tabs_show_headlines (); if (itemview->mode != mode) { /* FIXME: Not being able to call itemview_clear() here is awful! */ itemview->mode = mode; } } void itemview_set_displayed_node (nodePtr node) { if (node == itemview->node) return; itemview->node = node; itemview_clear (); } void itemview_add_item (itemPtr item) { itemview->hasEnclosures |= item->hasEnclosure; if (itemview->itemListView) /* add item in 3 pane mode */ item_list_view_add_item (itemview->itemListView, item); } void itemview_remove_item (itemPtr item) { if (item_list_view_contains_id (itemview->itemListView, item->id)) item_list_view_remove_item (itemview->itemListView, item); } void itemview_select_item (itemPtr item) { itemview_set_mode (ITEMVIEW_SINGLE_ITEM); itemview->needsHTMLViewUpdate = TRUE; itemview->browsing = FALSE; if (itemview->itemListView) item_list_view_select (itemview->itemListView, item); if (item) enclosure_list_view_load (itemview->enclosureView, item); else enclosure_list_view_hide (itemview->enclosureView); if (item) item_history_add (item->id); } void itemview_select_enclosure (guint position) { if (itemview->enclosureView) enclosure_list_view_select (itemview->enclosureView, position); } void itemview_open_next_enclosure (ItemView *view) { if (view->enclosureView) enclosure_list_view_open_next (view->enclosureView); } void itemview_update_item (itemPtr item) { /* Always update the GtkTreeView (bail-out done in ui_itemlist_update_item() */ if (itemview->itemListView) item_list_view_update_item (itemview->itemListView, item); /* Bail if we do internal browsing, and no item is shown */ if (itemview->browsing) return; /* Bail out if no HTML update necessary */ switch (itemview->mode) { case ITEMVIEW_SINGLE_ITEM: /* No HTML update needed if 3 pane mode and item not displayed */ if (item->id != itemlist_get_selected_id ()) return; break; default: /* Return in all other display modes */ return; break; } itemview->needsHTMLViewUpdate = TRUE; } void itemview_update_all_items (void) { /* Always update the GtkTreeView (bail-out done in ui_itemlist_update_item() */ if (itemview->itemListView) item_list_view_update_all_items (itemview->itemListView); itemview->needsHTMLViewUpdate = TRUE; } void itemview_update_node_info (nodePtr node) { /* Bail if we do internal browsing, and no item is shown */ if (itemview->browsing) return; if (!itemview->node) return; if (itemview->node != node) return; if (ITEMVIEW_NODE_INFO != itemview->mode) return; itemview->needsHTMLViewUpdate = TRUE; /* Just setting the update flag, because node info is not cached */ } void itemview_update (void) { if (itemview->itemListView) item_list_view_update (itemview->itemListView, itemview->hasEnclosures); if (itemview->itemListView && itemview->node) { item_list_view_enable_favicon_column (itemview->itemListView, NODE_TYPE (itemview->node)->capabilities & NODE_CAPABILITY_SHOW_ITEM_FAVICONS); item_list_view_set_sort_column (itemview->itemListView, itemview->node->sortColumn, itemview->node->sortReversed); } if (itemview->needsHTMLViewUpdate) { itemview->needsHTMLViewUpdate = FALSE; liferea_htmlview_update (itemview->htmlview, itemview->mode); } if (itemview->node) liferea_shell_update_allitems_actions (0 != itemview->node->itemCount, (0 != itemview->node->unreadCount) || IS_VFOLDER (itemview->node)); } void itemview_display_info (const gchar *html) { liferea_htmlview_write (itemview->htmlview, html, NULL); } /* next unread selection logic */ itemPtr itemview_find_unread_item (gulong startId) { itemPtr result = NULL; /* Note: to select in sorting order we need to do it in the ItemListView otherwise we would have to sort the item list here... */ if (!itemview->itemListView) /* If there is no itemListView we are in combined view and all * items are treated as read. */ return NULL; /* First do a scan from the start position (usually the selected item to the end of the sorted item list) if one is given. */ if (startId) result = item_list_view_find_unread_item (itemview->itemListView, startId); /* Now perform a wrap around by searching again from the top */ if (!result) result = item_list_view_find_unread_item (itemview->itemListView, 0); /* Return NULL if not found, or only the selected item is unread */ if (result && result->id == startId) return NULL; return result; } void itemview_scroll (void) { liferea_htmlview_scroll (itemview->htmlview); } void itemview_move_cursor (int step) { if (itemview->itemListView) item_list_view_move_cursor (itemview->itemListView, step); } void itemview_move_cursor_to_first (void) { if (itemview->itemListView) item_list_view_move_cursor_to_first (itemview->itemListView); } static void itemview_init (ItemView *iv) { debug_enter("itemview_init"); /* 0. Prepare globally accessible singleton */ g_assert (NULL == itemview); itemview = iv; debug_exit("itemview_init"); } static void on_important_status_message (gpointer obj, gchar *url) { if (strstr (url, "liferea://") != url) liferea_shell_set_important_status_bar ("%s", url); } void itemview_set_layout (nodeViewType newMode) { GtkWidget *previous_parent = NULL; const gchar *htmlWidgetName, *ilWidgetName, *encViewVBoxName; if (newMode == itemview->currentLayoutMode) return; itemview->currentLayoutMode = newMode; if (!itemview->htmlview) { debug0 (DEBUG_GUI, "Creating HTML widget"); itemview->htmlview = liferea_htmlview_new (FALSE); g_signal_connect (itemview->htmlview, "statusbar-changed", G_CALLBACK (on_important_status_message), NULL); /* Set initial zoom */ liferea_htmlview_set_zoom (itemview->htmlview, itemview->zoom/100.); } else { liferea_htmlview_clear (itemview->htmlview); } debug1 (DEBUG_GUI, "Setting item list layout mode: %d", newMode); switch (newMode) { case NODE_VIEW_MODE_COMBINED: // Not supported anymore, fall through to NORMAL case NODE_VIEW_MODE_NORMAL: htmlWidgetName = "normalViewHtml"; ilWidgetName = "normalViewItems"; encViewVBoxName = "normalViewVBox"; break; case NODE_VIEW_MODE_WIDE: htmlWidgetName = "wideViewHtml"; ilWidgetName = "wideViewItems"; encViewVBoxName = "wideViewVBox"; break; default: g_warning("fatal: illegal viewing mode!"); return; break; } /* Reparenting HTML view. This avoids the overhead of new browser instances. */ g_assert (htmlWidgetName); gtk_notebook_set_current_page (GTK_NOTEBOOK (liferea_shell_lookup ("itemtabs")), newMode); previous_parent = gtk_widget_get_parent (liferea_htmlview_get_widget (itemview->htmlview)); if (previous_parent) gtk_container_remove (GTK_CONTAINER (previous_parent), liferea_htmlview_get_widget (itemview->htmlview)); gtk_container_add (GTK_CONTAINER (liferea_shell_lookup (htmlWidgetName)), liferea_htmlview_get_widget (itemview->htmlview)); /* Recreate the item list view */ if (itemview->itemListView) { previous_parent = gtk_widget_get_parent (item_list_view_get_widget (itemview->itemListView)); if (previous_parent) gtk_container_remove (GTK_CONTAINER (previous_parent), item_list_view_get_widget (itemview->itemListView)); g_clear_object (&itemview->itemListView); } if (ilWidgetName) { itemview->itemListView = item_list_view_create (newMode == NODE_VIEW_MODE_WIDE); gtk_container_add (GTK_CONTAINER (liferea_shell_lookup (ilWidgetName)), item_list_view_get_widget (itemview->itemListView)); } /* Destroy previous enclosure list. */ if (itemview->enclosureView) { gtk_widget_destroy (enclosure_list_view_get_widget (itemview->enclosureView)); itemview->enclosureView = NULL; } /* Create a new enclosure list GtkTreeView. */ if (encViewVBoxName) { itemview->enclosureView = enclosure_list_view_new (); gtk_grid_attach_next_to (GTK_GRID (liferea_shell_lookup (encViewVBoxName)), enclosure_list_view_get_widget (itemview->enclosureView), NULL, GTK_POS_BOTTOM, 1,1); gtk_widget_show_all (liferea_shell_lookup (encViewVBoxName)); } } ItemView * itemview_create (GtkWidget *window) { gint zoom; g_object_new (ITEM_VIEW_TYPE, NULL); /* 1. Load preferences */ conf_get_int_value (LAST_ZOOMLEVEL, &zoom); if (zoom == 0) { zoom = 100; conf_set_int_value (LAST_ZOOMLEVEL, zoom); } itemview->zoom = zoom; /* 2. Set initial layout (because no node selected yet) */ itemview_set_layout (NODE_VIEW_MODE_WIDE); return itemview; } void itemview_launch_URL (const gchar *url, gboolean forceInternal) { gboolean internal; if (forceInternal) { itemview->browsing = TRUE; liferea_htmlview_launch_URL_internal (itemview->htmlview, url); return; } /* Otherwise let the HTML view figure out if we want to browse internally. */ internal = liferea_htmlview_handle_URL (itemview->htmlview, url); if (!internal) liferea_htmlview_launch_URL_internal (itemview->htmlview, url); } void itemview_do_zoom (gint zoom) { if (itemview->htmlview == NULL) return; liferea_htmlview_do_zoom (itemview->htmlview, zoom); } void itemview_style_update (void) { liferea_htmlview_update_stylesheet (itemview->htmlview); } liferea-1.13.7/src/ui/itemview.h000066400000000000000000000140451415350204600164240ustar00rootroot00000000000000/* * @file itemview.h viewing feed content in different presentation modes * * Copyright (C) 2006-2019 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _ITEMVIEW_H #define _ITEMVIEW_H #include #include #include #include "item.h" #include "itemset.h" #include "node.h" #include "ui/liferea_htmlview.h" /* Liferea presents items in a dynamic view. The view layout changes according to the subscription preferences and if the user requests it on-the-fly. Also the view contents are refreshed automatically. The view consist of an optional GtkTreeView presenting the list of the relevant items and a HTML widget rendering a feed info, a single item or multiple items at once. */ G_BEGIN_DECLS #define ITEM_VIEW_TYPE (itemview_get_type ()) G_DECLARE_FINAL_TYPE (ItemView, itemview, ITEM, VIEW, GObject) /** * itemview_clear: (skip) * * Removes all currently loaded items from the item view. */ void itemview_clear (void); /** * itemview_set_displayed_node: (skip) * @node: the node whose items are to be presented * * Prepares the view for displaying items of the given node. */ void itemview_set_displayed_node (nodePtr node); /* item view display mode type */ typedef enum { ITEMVIEW_SINGLE_ITEM, /*<< 3 panes, item view shows the selected item only in HTML view */ ITEMVIEW_NODE_INFO /*<< 3 panes, item view shows the selected node description in HTML view*/ } itemViewMode; /** * itemview_set_mode: * @mode: item view mode constant * * Set/unset the display mode of the item view. */ void itemview_set_mode (itemViewMode mode); /** * itemview_add_item: (skip) * @item: the item to add * * Adds an item to the view for rendering. The item must belong * to the item set that was announced with itemview_set_displayed_node(). * * TODO: use item merger signal instead */ void itemview_add_item (itemPtr item); /** * itemview_remove_item: (skip) * @item: the item to remove * * Removes a given item from the view. * * TODO: use item merger signal instead */ void itemview_remove_item (itemPtr item); /** * itemview_select_item: (skip) * @item: the item to select * * Selects a given item in the view. The item must be * added using itemview_add_item before selecting. */ void itemview_select_item (itemPtr item); /** * itemview_select_enclosure: * @position: the position to select * * Selects the nth enclosure in the enclosure list view currently presented. */ void itemview_select_enclosure (guint position); /** * itemview_open_next_enclosure: * @view: The ItemView * * Selects and open the next enclosure in the list. */ void itemview_open_next_enclosure (ItemView *view); /** * itemview_update_item: (skip) * @item: the item to update * * Requests updating the rendering of a given item. */ void itemview_update_item (itemPtr item); /** * itemview_update_all_items: * * Requests updating the rendering of a all displayed items. */ void itemview_update_all_items (void); /** * itemview_update_node_info: (skip) * @node: the node whose info view is to be updated * * Requests updating the rendering of the node info view. * * TODO: register for signal at feed merger instead */ void itemview_update_node_info (struct node *node); /** * itemview_update: (skip) * * Refreshes the item view. Needs to be called after each * add, remove or update of one or more items. * * TODO: register for signal at item merger instead */ void itemview_update (void); /** * itemview_display_info: * @html: HTML to present * * Sets an info display in the item view HTML widget. * Used for special functionality like search result info. */ void itemview_display_info (const gchar *html); /** * itemview_find_unread_item: (skip) * @startId: the item id to start at (or NULL for starting at the top) * * Finds the next unread item. * * Returns: (transfer none): the item found (or NULL) */ itemPtr itemview_find_unread_item (gulong startId); /** * itemview_scroll: * * Paging/skimming the item view. If possible scrolls * down otherwise it triggers Next-Unread. */ void itemview_scroll (void); /** * itemview_move_cursor: * @step: moving steps * * Moves the cursor in the item list step times. * Negative value means moving backwards. */ void itemview_move_cursor (int step); /** * itemview_move_cursor_to_first: * * Moves the cursor in the item list to the first element. */ void itemview_move_cursor_to_first (void); /** * itemview_set_layout: * @newMode: new view mode (NODE_VIEW_MODE_*) * * Switches the layout for the given viewing mode. */ void itemview_set_layout (nodeViewType newMode); /** * itemview_create: (skip) * @window: parent window widget * * Creates the item view singleton instance. * * Returns: (transfer none): the item view instance */ ItemView * itemview_create (GtkWidget *window); /** * itemview_launch_URL: * @url: the link to load * @internal: TRUE if internal browsing is to be enforced * * Launch the given URL in the currently active HTML view. * */ void itemview_launch_URL (const gchar *url, gboolean internal); /** * itemview_do_zoom: * @zoom: 1 for zoom in, -1 for zoom out, 0 for reset * * Requests the item view to change zoom level. */ void itemview_do_zoom (gint zoom); /** * itemview_style_update: * * Invokes a change of the href attribute in WebView's tag */ void itemview_style_update (void); G_END_DECLS #endif liferea-1.13.7/src/ui/liferea_dialog.c000066400000000000000000000062311415350204600175120ustar00rootroot00000000000000/** * @file ui_dialog.c UI dialog handling * * Copyright (C) 2007-2016 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include #include "ui/liferea_dialog.h" #include "ui/liferea_shell.h" #define LIFEREA_DIALOG_GET_PRIVATE liferea_dialog_get_instance_private struct LifereaDialogPrivate { GtkBuilder *xml; GtkWidget *dialog; }; static GObjectClass *parent_class = NULL; G_DEFINE_TYPE_WITH_CODE (LifereaDialog, liferea_dialog, G_TYPE_OBJECT, G_ADD_PRIVATE (LifereaDialog)); static void liferea_dialog_finalize (GObject *object) { LifereaDialog *ls = LIFEREA_DIALOG (object); g_object_unref (ls->priv->xml); G_OBJECT_CLASS (parent_class)->finalize (object); } static void liferea_dialog_destroy_cb (GtkWidget *widget, LifereaDialog *ld) { g_object_unref (ld); } static void liferea_dialog_class_init (LifereaDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); parent_class = g_type_class_peek_parent (klass); object_class->finalize = liferea_dialog_finalize; } static void liferea_dialog_init (LifereaDialog *ld) { ld->priv = LIFEREA_DIALOG_GET_PRIVATE (ld); } GtkWidget * liferea_dialog_lookup (GtkWidget *widget, const gchar *name) { LifereaDialog *ld; if (!widget) return NULL; ld = LIFEREA_DIALOG (g_object_get_data (G_OBJECT (widget), "LifereaDialog")); if (!IS_LIFEREA_DIALOG (ld)) { g_warning ("Fatal: liferea_dialog_lookup() called with something that is not a Liferea dialog!"); return NULL; } if (ld->priv->xml) return GTK_WIDGET (gtk_builder_get_object (ld->priv->xml, name)); return NULL; } GtkWidget * liferea_dialog_new (const gchar *name) { LifereaDialog *ld; gchar *path; ld = LIFEREA_DIALOG (g_object_new (LIFEREA_DIALOG_TYPE, NULL)); path = g_strdup_printf ("%s%s.ui", PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S, name); ld->priv->xml = gtk_builder_new_from_file (path); g_free (path); g_return_val_if_fail (ld->priv->xml != NULL, NULL); ld->priv->dialog = GTK_WIDGET (gtk_builder_get_object (ld->priv->xml, name)); gtk_window_set_transient_for (GTK_WINDOW (ld->priv->dialog), GTK_WINDOW (liferea_shell_get_window())); gtk_builder_connect_signals (ld->priv->xml, NULL); g_return_val_if_fail (ld->priv->dialog != NULL, NULL); g_object_set_data (G_OBJECT (ld->priv->dialog), "LifereaDialog", ld); g_signal_connect_object (ld->priv->dialog, "destroy", G_CALLBACK (liferea_dialog_destroy_cb), ld, 0); return ld->priv->dialog; } liferea-1.13.7/src/ui/liferea_dialog.h000066400000000000000000000042641415350204600175230ustar00rootroot00000000000000/** * @file ui_dialog.h UI dialog handling * * Copyright (C) 2007-2016 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_DIALOG_H #define _UI_DIALOG_H #include #include #include G_BEGIN_DECLS #define LIFEREA_DIALOG_TYPE (liferea_dialog_get_type ()) #define LIFEREA_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_DIALOG_TYPE, LifereaDialog)) #define LIFEREA_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), LIFEREA_DIALOG_TYPE, LifereaDialogClass)) #define IS_LIFEREA_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_DIALOG_TYPE)) #define IS_LIFEREA_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), LIFEREA_DIALOG_TYPE)) typedef struct LifereaDialog LifereaDialog; typedef struct LifereaDialogClass LifereaDialogClass; typedef struct LifereaDialogPrivate LifereaDialogPrivate; struct LifereaDialog { GObject parent; /*< private >*/ LifereaDialogPrivate *priv; }; struct LifereaDialogClass { GObjectClass parent_class; }; GType liferea_dialog_get_type (void); /** * Convenience wrapper to create a new dialog and set up its GUI * * @param name the dialog name * * @returns the dialog widget */ GtkWidget * liferea_dialog_new (const gchar *name); /** * Helper function to look up child widgets of a dialog window. * * @param widget the dialog widget * @param name widget name * * @returns found widget (or NULL) */ GtkWidget * liferea_dialog_lookup (GtkWidget *widget, const gchar *name); G_END_DECLS #endif liferea-1.13.7/src/ui/liferea_htmlview.c000066400000000000000000000450471415350204600201220ustar00rootroot00000000000000/* * @file liferea_htmlview.c Liferea embedded HTML rendering * * Copyright (C) 2003-2021 Lars Windolf * Copyright (C) 2005-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/liferea_htmlview.h" #include #if !defined (G_OS_WIN32) || defined (HAVE_SYS_WAIT_H) #include #endif #include #include "browser.h" #include "browser_history.h" #include "comments.h" #include "common.h" #include "conf.h" #include "debug.h" #include "enclosure.h" #include "feed.h" #include "feedlist.h" #include "itemlist.h" #include "net_monitor.h" #include "social.h" #include "render.h" #include "ui/browser_tabs.h" #include "ui/item_list_view.h" #include "ui/itemview.h" /* The LifereaHtmlView is a complex widget used to present both internally rendered content as well as serving as a browser widget. It automatically switches on a toolbar for history and URL navigation when browsing external content. When serving internal content it reacts to different internal link schemata to trigger functionality inside Liferea. To avoid websites hijacking this we keep a flag to support the link schema only on liferea_htmlview_write() */ #define RENDERER(htmlview) (htmlview->impl) struct _LifereaHtmlView { GObject parent_instance; GtkWidget *renderWidget; /*<< The HTML widget (e.g. Webkit widget) */ GtkWidget *container; /*<< Outer container including render widget and toolbar */ GtkWidget *toolbar; /*<< The navigation toolbar */ GtkWidget *forward; /*<< The forward button */ GtkWidget *back; /*<< The back button */ GtkWidget *urlentry; /*<< The URL entry widget */ browserHistory *history; /*<< The browser history */ gboolean internal; /*<< TRUE if internal view presenting generated HTML with special links */ gboolean forceInternalBrowsing; /*<< TRUE if clicked links should be force loaded in a new tab (regardless of global preference) */ gboolean readerMode; /*<< TRUE if Readability.js is to be used */ gchar *content; /*<< current HTML content (excluding decorations, content passed to Readability.js) */ htmlviewImplPtr impl; /*<< Browser widget support implementation */ }; enum { STATUSBAR_CHANGED, TITLE_CHANGED, LOCATION_CHANGED, LAST_SIGNAL }; enum { PROP_NONE, PROP_RENDER_WIDGET }; /* LifereaHtmlView toolbar callbacks */ static gboolean on_htmlview_url_entry_activate (GtkWidget *widget, gpointer user_data) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (user_data); gchar *url; url = (gchar *)gtk_entry_get_text (GTK_ENTRY (widget)); liferea_htmlview_launch_URL_internal (htmlview, url); return TRUE; } static void on_htmlview_history_back (GtkWidget *widget, gpointer user_data) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (user_data); gchar *url; url = browser_history_back (htmlview->history); gtk_widget_set_sensitive (htmlview->forward, browser_history_can_go_forward (htmlview->history)); gtk_widget_set_sensitive (htmlview->back, browser_history_can_go_back (htmlview->history)); liferea_htmlview_launch_URL_internal (htmlview, url); gtk_entry_set_text (GTK_ENTRY (htmlview->urlentry), url); } static void on_htmlview_history_forward (GtkWidget *widget, gpointer user_data) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (user_data); gchar *url; url = browser_history_forward (htmlview->history); gtk_widget_set_sensitive (htmlview->forward, browser_history_can_go_forward (htmlview->history)); gtk_widget_set_sensitive (htmlview->back, browser_history_can_go_back (htmlview->history)); liferea_htmlview_launch_URL_internal (htmlview, url); gtk_entry_set_text (GTK_ENTRY (htmlview->urlentry), url); } /* LifereaHtmlView class */ static guint liferea_htmlview_signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE (LifereaHtmlView, liferea_htmlview, G_TYPE_OBJECT); static void liferea_htmlview_finalize (GObject *object) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (object); browser_history_free (htmlview->history); g_clear_object (&htmlview->container); g_free (htmlview->content); g_signal_handlers_disconnect_by_data (network_monitor_get (), object); } static void liferea_htmlview_get_property (GObject *gobject, guint prop_id, GValue *value, GParamSpec *pspec) { LifereaHtmlView *self = LIFEREA_HTMLVIEW (gobject); switch (prop_id) { case PROP_RENDER_WIDGET: g_value_set_object (value, self->renderWidget); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); break; } } static void liferea_htmlview_class_init (LifereaHtmlViewClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = liferea_htmlview_get_property; object_class->finalize = liferea_htmlview_finalize; /* LifereaHtmlView:renderWidget: */ g_object_class_install_property ( object_class, PROP_RENDER_WIDGET, g_param_spec_object ( "renderwidget", "GtkWidget", "GtkWidget object", GTK_TYPE_WIDGET, G_PARAM_READABLE)); liferea_htmlview_signals[STATUSBAR_CHANGED] = g_signal_new ("statusbar-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); liferea_htmlview_signals[TITLE_CHANGED] = g_signal_new ("title-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); liferea_htmlview_signals[LOCATION_CHANGED] = g_signal_new ("location-changed", G_OBJECT_CLASS_TYPE (object_class), (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), 0, NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); htmlview_get_impl ()->init (); } static void liferea_htmlview_init (LifereaHtmlView *htmlview) { GtkWidget *widget, *image; htmlview->content = NULL; htmlview->internal = FALSE; htmlview->readerMode = FALSE; htmlview->impl = htmlview_get_impl (); htmlview->renderWidget = RENDERER (htmlview)->create (htmlview); htmlview->container = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); g_object_ref_sink (htmlview->container); htmlview->history = browser_history_new (); htmlview->toolbar = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); widget = gtk_button_new (); gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); image = gtk_image_new_from_icon_name ("go-previous", GTK_ICON_SIZE_BUTTON); gtk_widget_show (image); gtk_container_add (GTK_CONTAINER (widget), image); gtk_box_pack_start (GTK_BOX (htmlview->toolbar), widget, FALSE, FALSE, 0); g_signal_connect ((gpointer)widget, "clicked", G_CALLBACK (on_htmlview_history_back), (gpointer)htmlview); gtk_widget_set_sensitive (widget, FALSE); htmlview->back = widget; widget = gtk_button_new (); gtk_button_set_relief (GTK_BUTTON(widget), GTK_RELIEF_NONE); image = gtk_image_new_from_icon_name ("go-next", GTK_ICON_SIZE_BUTTON); gtk_widget_show (image); gtk_container_add (GTK_CONTAINER (widget), image); gtk_box_pack_start (GTK_BOX (htmlview->toolbar), widget, FALSE, FALSE, 0); g_signal_connect ((gpointer)widget, "clicked", G_CALLBACK (on_htmlview_history_forward), (gpointer)htmlview); gtk_widget_set_sensitive (widget, FALSE); htmlview->forward = widget; widget = gtk_entry_new (); gtk_box_pack_start (GTK_BOX (htmlview->toolbar), widget, TRUE, TRUE, 0); g_signal_connect ((gpointer)widget, "activate", G_CALLBACK (on_htmlview_url_entry_activate), (gpointer)htmlview); htmlview->urlentry = widget; gtk_box_pack_start (GTK_BOX (htmlview->container), htmlview->toolbar, FALSE, FALSE, 0); gtk_box_pack_end (GTK_BOX (htmlview->container), htmlview->renderWidget, TRUE, TRUE, 0); gtk_widget_show_all (htmlview->container); } static void liferea_htmlview_set_online (LifereaHtmlView *htmlview, gboolean online) { if (RENDERER (htmlview)->setOffLine) (RENDERER (htmlview)->setOffLine) (!online); } static void liferea_htmlview_online_status_changed (NetworkMonitor *nm, gboolean online, gpointer userdata) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (userdata); liferea_htmlview_set_online (htmlview, online); } static void liferea_htmlview_proxy_changed (NetworkMonitor *nm, gpointer userdata) { LifereaHtmlView *htmlview = LIFEREA_HTMLVIEW (userdata); if (RENDERER (htmlview)->setProxy) (RENDERER (htmlview)->setProxy) (network_get_proxy_detect_mode (), network_get_proxy_host (), network_get_proxy_port (), network_get_proxy_username (), network_get_proxy_password ()); } LifereaHtmlView * liferea_htmlview_new (gboolean forceInternalBrowsing) { LifereaHtmlView *htmlview; htmlview = LIFEREA_HTMLVIEW (g_object_new (LIFEREA_HTMLVIEW_TYPE, NULL)); htmlview->forceInternalBrowsing = forceInternalBrowsing; conf_get_bool_value (ENABLE_READER_MODE, &(htmlview->readerMode)); liferea_htmlview_clear (htmlview); g_signal_connect (network_monitor_get (), "online-status-changed", G_CALLBACK (liferea_htmlview_online_status_changed), htmlview); g_signal_connect (network_monitor_get (), "proxy-changed", G_CALLBACK (liferea_htmlview_proxy_changed), htmlview); debug0 (DEBUG_NET, "Setting initial HTML widget proxy..."); liferea_htmlview_proxy_changed (network_monitor_get (), htmlview); return htmlview; } /* Needed when adding widget to a parent and querying GTK theme */ GtkWidget * liferea_htmlview_get_widget (LifereaHtmlView *htmlview) { return htmlview->container; } void liferea_htmlview_write (LifereaHtmlView *htmlview, const gchar *string, const gchar *base) { const gchar *baseURL = base; const gchar *errMsg = "ERROR: Invalid encoded UTF8 buffer passed to HTML widget! This shouldn't happen."; if (!htmlview) return; htmlview->internal = TRUE; /* enables special links */ if (baseURL == NULL) baseURL = "file:///"; if (debug_level & DEBUG_HTML) { gchar *filename = common_create_cache_filename (NULL, "output", "html"); g_file_set_contents (filename, string, -1, NULL); g_free (filename); } if (!g_utf8_validate (string, -1, NULL)) { /* It is really a bug if we get invalid encoded UTF-8 here!!! */ (RENDERER (htmlview)->write) (htmlview->renderWidget, errMsg, strlen (errMsg), baseURL, "text/plain"); } else { (RENDERER (htmlview)->write) (htmlview->renderWidget, string, strlen (string), baseURL, "text/html"); } /* We hide the toolbar as it should only be shown when loading external content */ gtk_widget_hide (htmlview->toolbar); } void liferea_htmlview_clear (LifereaHtmlView *htmlview) { liferea_htmlview_write (htmlview, "", NULL); } struct internalUriType { const gchar *suffix; void (*func)(itemPtr item); }; void liferea_htmlview_on_url (LifereaHtmlView *htmlview, const gchar *url) { g_signal_emit_by_name (htmlview, "statusbar-changed", url); } void liferea_htmlview_title_changed (LifereaHtmlView *htmlview, const gchar *title) { g_signal_emit_by_name (htmlview, "title-changed", title); } void liferea_htmlview_progress_changed (LifereaHtmlView *htmlview, gdouble progress) { double bar_progress = (progress == 1.0)?0.0:progress; gtk_entry_set_progress_fraction (GTK_ENTRY (htmlview->urlentry), bar_progress); } void liferea_htmlview_location_changed (LifereaHtmlView *htmlview, const gchar *location) { if (!g_str_has_prefix (location, "liferea")) { /* A URI different from the locally generated html base url is being loaded. */ htmlview->internal = FALSE; } if (!htmlview->internal) { browser_history_add_location (htmlview->history, location); gtk_widget_set_sensitive (htmlview->forward, browser_history_can_go_forward (htmlview->history)); gtk_widget_set_sensitive (htmlview->back, browser_history_can_go_back (htmlview->history)); gtk_entry_set_text (GTK_ENTRY (htmlview->urlentry), location); /* We show the toolbar as it should be visible when loading external content */ gtk_widget_show_all (htmlview->toolbar); } g_signal_emit_by_name (htmlview, "location-changed", location); } void liferea_htmlview_load_finished (LifereaHtmlView *htmlview, const gchar *location) { /* Add Readability.js handling - for external content: if user chose so - for internal content: always (Readability is enable on demand here) */ if (htmlview->readerMode || (location == strstr (location, "liferea://"))) { g_autoptr(GBytes) b1 = NULL, b2 = NULL; // Return Readability.js and Liferea specific loader code b1 = g_resources_lookup_data ("/org/gnome/liferea/readability/Readability.js", 0, NULL); b2 = g_resources_lookup_data ("/org/gnome/liferea/htmlview.js", 0, NULL); g_assert(b1 != NULL); g_assert(b2 != NULL); // FIXME: pass actual content here too, instead of on render_item()! // this safe us from the trouble to have JS enabled earlier! debug1 (DEBUG_GUI, "Enabling reader mode for '%s'", location); (RENDERER (htmlview)->run_js) (htmlview->renderWidget, g_strdup_printf ("%s\n%s\nloadContent(%s, '%s');\n", (gchar *)g_bytes_get_data (b1, NULL), (gchar *)g_bytes_get_data (b2, NULL), (htmlview->readerMode?"true":"false"), htmlview->content)); } } gboolean liferea_htmlview_handle_URL (LifereaHtmlView *htmlview, const gchar *url) { gboolean browse_inside_application; g_return_val_if_fail (htmlview, TRUE); g_return_val_if_fail (url, TRUE); conf_get_bool_value (BROWSE_INSIDE_APPLICATION, &browse_inside_application); debug3 (DEBUG_GUI, "handle URL: %s %s %s", browse_inside_application?"true":"false", htmlview->forceInternalBrowsing?"true":"false", htmlview->internal?"true":"false"); if(htmlview->forceInternalBrowsing || browse_inside_application) { liferea_htmlview_launch_URL_internal (htmlview, url); } else { (void)browser_launch_URL_external (url); } return TRUE; } void liferea_htmlview_launch_URL_internal (LifereaHtmlView *htmlview, const gchar *url) { /* Reset any intermediate reader mode change via htmlview context menu */ conf_get_bool_value (ENABLE_READER_MODE, &(htmlview->readerMode)); gtk_widget_set_sensitive (htmlview->forward, browser_history_can_go_forward (htmlview->history)); gtk_widget_set_sensitive (htmlview->back, browser_history_can_go_back (htmlview->history)); gtk_entry_set_text (GTK_ENTRY (htmlview->urlentry), url); (RENDERER (htmlview)->launch) (htmlview->renderWidget, url); } void liferea_htmlview_set_zoom (LifereaHtmlView *htmlview, gfloat diff) { (RENDERER (htmlview)->zoomLevelSet) (htmlview->renderWidget, diff); } gfloat liferea_htmlview_get_zoom (LifereaHtmlView *htmlview) { return (RENDERER (htmlview)->zoomLevelGet) (htmlview->renderWidget); } void liferea_htmlview_set_reader_mode (LifereaHtmlView *htmlview, gboolean readerMode) { htmlview->readerMode = readerMode; (RENDERER (htmlview)->reload) (htmlview->renderWidget); } gboolean liferea_htmlview_get_reader_mode (LifereaHtmlView *htmlview) { return htmlview->readerMode; } void liferea_htmlview_scroll (LifereaHtmlView *htmlview) { (RENDERER (htmlview)->scrollPagedown) (htmlview->renderWidget); } void liferea_htmlview_do_zoom (LifereaHtmlView *htmlview, gint zoom) { if (!zoom) liferea_htmlview_set_zoom (htmlview, 1.0); else if (zoom > 0) liferea_htmlview_set_zoom (htmlview, 1.2 * liferea_htmlview_get_zoom (htmlview)); else liferea_htmlview_set_zoom (htmlview, 0.8 * liferea_htmlview_get_zoom (htmlview)); } static void liferea_htmlview_start_output (GString *buffer, const gchar *base) { /* Prepare HTML boilderplate */ g_string_append (buffer, "\n"); g_string_append (buffer, "\n"); g_string_append (buffer, "\nHTML View"); // FIXME: consider adding CSP meta tag here as e.g. Firefox reader mode page does g_string_append (buffer, ""); if (base) { gchar *escBase = g_markup_escape_text (base, -1); g_string_append (buffer, "\n"); g_free (escBase); } g_string_append (buffer, "Loading..."); } void liferea_htmlview_update (LifereaHtmlView *htmlview, guint mode) { GString *output; nodePtr node = NULL; itemPtr item = NULL; gchar *baseURL = NULL; gchar *content = NULL; /* determine base URL */ switch (mode) { case ITEMVIEW_SINGLE_ITEM: item = itemlist_get_selected (); if(item) { baseURL = (gchar *)node_get_base_url (node_from_id (item->nodeId)); item_unload (item); } break; case ITEMVIEW_NODE_INFO: node = feedlist_get_selected (); if (!node) return; baseURL = (gchar *) node_get_base_url (node); break; } if (baseURL) baseURL = g_markup_escape_text (baseURL, -1); output = g_string_new (NULL); liferea_htmlview_start_output (output, baseURL); switch (mode) { case ITEMVIEW_SINGLE_ITEM: item = itemlist_get_selected (); if (item) { content = item_render (item, mode); item_unload (item); } break; case ITEMVIEW_NODE_INFO: if (node) content = node_render (node); break; default: g_warning ("HTML view: invalid viewing mode!!!"); break; } g_free (htmlview->content); htmlview->content = NULL; if (content) { /* URI escape our content for safe transfer to Readability.js URI escaping is needed for UTF-8 conservation and for JS stringification */ htmlview->content = g_uri_escape_string (content, NULL, TRUE); g_free (content); } else { htmlview->content = g_uri_escape_string ("", NULL, TRUE); } debug1 (DEBUG_HTML, "writing %d bytes to HTML view", strlen (output->str)); liferea_htmlview_write (htmlview, output->str, baseURL); g_string_free (output, TRUE); g_free (baseURL); } void liferea_htmlview_update_stylesheet (LifereaHtmlView *htmlview) { (RENDERER (htmlview)->setStylesheet) (htmlview->renderWidget); } liferea-1.13.7/src/ui/liferea_htmlview.h000066400000000000000000000165031415350204600201220ustar00rootroot00000000000000/* * @file liferea_htmlview.h Liferea embedded HTML rendering * * Copyright (C) 2003-2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_HTMLVIEW_H #define _LIFEREA_HTMLVIEW_H #include #include "net.h" G_BEGIN_DECLS #define LIFEREA_HTMLVIEW_TYPE (liferea_htmlview_get_type ()) G_DECLARE_FINAL_TYPE (LifereaHtmlView, liferea_htmlview, LIFEREA, HTMLVIEW, GObject) /** * liferea_htmlview_new: (skip) * @forceInternalBrowsing: TRUE to act as fully fledged browser * * Function to set up a new html view widget for any purpose. * * Returns: a new Liferea HTML widget */ LifereaHtmlView * liferea_htmlview_new (gboolean forceInternalBrowsing); /** * liferea_htmlview_set_headline_view: * * Make this LifereaHtmlView instance a headline view. This causes * an additional "go back" step for the history tab allowing to go back * from Web content to the headline when browsing inline. */ void liferea_htmlview_set_headline_view (LifereaHtmlView *htmlview); /** * liferea_htmlview_get_widget: * @htmlview: the HTML view * * Returns the rendering widget for a HTML view. Only * to be used by liferea_shell.c for widget reparenting. * * Returns: (transfer none): the rendering widget */ GtkWidget *liferea_htmlview_get_widget (LifereaHtmlView *htmlview); /** * liferea_htmlview_clear: (skip) * @htmlview: the HTML view widget to clear * * Loads a emtpy HTML page. Resets any item view state. */ void liferea_htmlview_clear (LifereaHtmlView *htmlview); /** * liferea_htmlview_write: (skip) * @htmlview: the htmlview widget to be set * @string: HTML source * @base: base url for resolving relative links * * Method to display the passed HTML source to the HTML widget. */ void liferea_htmlview_write (LifereaHtmlView *htmlview, const gchar *string, const gchar *base); /** * liferea_html_view_on_url: (skip) * @htmlview: the htmlview causing the event * @url: new URL (or empty string) * * Callback for plugins to process on-url events. Depending on * the link type the link will be copied to the status bar. */ void liferea_htmlview_on_url (LifereaHtmlView *htmlview, const gchar *url); void liferea_htmlview_title_changed (LifereaHtmlView *htmlview, const gchar *title); void liferea_htmlview_progress_changed (LifereaHtmlView *htmlview, gdouble progress); void liferea_htmlview_location_changed (LifereaHtmlView *htmlview, const gchar *location); void liferea_htmlview_load_finished (LifereaHtmlView *htmlview, const gchar *location); /** * liferea_htmlview_handle_URL: (skip) * @htmlview: the HTML view to use * @url: URL to launch * * Launches the specified URL either in external browser or by passing * plain data to Readability.js or by unfiltered rendering. Alternativly it * handles a special URL by triggering HTML generation. * * Returns FALSE to indicate the HTML widget should launch the link itself. * * To enforce a launching behaviour do use * * liferea_htmlview_launch_URL_internal (htmlview, url) * * or * * browser_launch_URL_external (url) * * instead of this method. * * Returns: FALSE if link is to be launched by browser widget */ gboolean liferea_htmlview_handle_URL (LifereaHtmlView *htmlview, const gchar *url); /** * liferea_htmlview_launch_URL_internal: (skip) * @htmlview: the HTML view to use * @url: the URL to load * * Enforces loading of the given URL in the given browser widget. */ void liferea_htmlview_launch_URL_internal (LifereaHtmlView *htmlview, const gchar *url); /** * liferea_htmlview_set_zoom: * @zoom: New zoom * * Function to change the zoom level of the HTML widget. * 1.0 is a 1:1 zoom. * */ void liferea_htmlview_set_zoom (LifereaHtmlView *htmlview, gfloat zoom); /** * liferea_htmlview_get_zoom: * @htmlview: htmlview to examine * * Function to determine the current zoom level. * * Returns: the currently set zoom level */ gfloat liferea_htmlview_get_zoom (LifereaHtmlView *htmlview); /** * liferea_htmlview_set_reader_mode: * @htmlview: htmlview to change * @readerMode: new mode * * Allows to temporarily change the reader mode of the htmlview, will be * reset when navigating to another URL */ void liferea_htmlview_set_reader_mode (LifereaHtmlView *htmlview, gboolean readerMode); /** * liferea_htmlview_get_reader_mode: * @htmlview: htmlview to get mode of * * Allows to query the currently active reader mode setting * * Returns: TRUE if reader mode is on */ gboolean liferea_htmlview_get_reader_mode (LifereaHtmlView *htmlview); /** * liferea_htmlview_scroll: * @htmlview: htmlview to scroll * * Function scrolls down the given HTML view if possible. * */ void liferea_htmlview_scroll (LifereaHtmlView *htmlview); /** * liferea_htmlview_do_zoom: * @htmlview: the html view * @zoom: 1 for zoom in, -1 for zoom out, 0 for reset * * To be called when HTML view needs to change the text size * of the rendering widget implementation. */ void liferea_htmlview_do_zoom (LifereaHtmlView *htmlview, gint zoom); /** * liferea_htmlview_update: * * Renders all added items to the given HTML view. To be called * after one or more calls of htmlview_(add|remove|update)_item. * * @param htmlview HTML view to render to * @param mode item view mode (see type itemViewMode) */ void liferea_htmlview_update (LifereaHtmlView *htmlview, guint mode); /** * liferea_htmlview_update_stylesheet: * @htmlview: the html view * * Update the user stylesheet of the WebView */ void liferea_htmlview_update_stylesheet (LifereaHtmlView *htmlview); G_END_DECLS /* interface for HTML rendering support implementation */ typedef struct htmlviewImpl { void (*init) (void); GtkWidget* (*create) (LifereaHtmlView *htmlview); void (*write) (GtkWidget *widget, const gchar *string, guint length, const gchar *base, const gchar *contentType); void (*run_js) (GtkWidget *widget, gchar *js); void (*launch) (GtkWidget *widget, const gchar *url); gfloat (*zoomLevelGet) (GtkWidget *widget); void (*zoomLevelSet) (GtkWidget *widget, gfloat zoom); gboolean (*hasSelection) (GtkWidget *widget); void (*copySelection) (GtkWidget *widget); void (*setProxy) (ProxyDetectMode mode, const gchar *hostname, guint port, const gchar *username, const gchar *password); void (*scrollPagedown) (GtkWidget *widget); void (*setOffLine) (gboolean offline); void (*setStylesheet) (GtkWidget *widget); void (*reload) (GtkWidget *widget); } *htmlviewImplPtr; /** * htmlview_get_impl: (skip) */ extern htmlviewImplPtr htmlview_get_impl(void); /* Use this macro to declare a html rendering support implementation. */ #define DECLARE_HTMLVIEW_IMPL(impl) \ htmlviewImplPtr htmlview_get_impl(void) { \ return &impl; \ } #endif liferea-1.13.7/src/ui/liferea_shell.c000066400000000000000000001406101415350204600173620ustar00rootroot00000000000000/* * @file liferea_shell.c UI layout handling * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2007-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifdef HAVE_CONFIG_H # include #endif #include "ui/liferea_shell.h" #include #include #include #include #include "browser.h" #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "export.h" #include "feedlist.h" #include "item_history.h" #include "itemlist.h" #include "liferea_application.h" #include "net_monitor.h" #include "newsbin.h" #include "plugins_engine.h" #include "render.h" #include "social.h" #include "vfolder.h" #include "ui/browser_tabs.h" #include "ui/feed_list_view.h" #include "ui/icons.h" #include "ui/itemview.h" #include "ui/item_list_view.h" #include "ui/liferea_dialog.h" #include "ui/liferea_shell_activatable.h" #include "ui/preferences_dialog.h" #include "ui/search_dialog.h" #include "ui/ui_common.h" #include "ui/ui_update.h" extern gboolean searchFolderRebuild; /* db.c */ struct _LifereaShell { GObject parent_instance; GtkBuilder *xml; GtkWindow *window; /*<< Liferea main window */ GtkWidget *toolbar; GtkTreeView *feedlistViewWidget; GtkStatusbar *statusbar; /*<< main window status bar */ gboolean statusbarLocked; /*<< flag locking important message on status bar */ guint statusbarLockTimer; /*<< timer id for status bar lock reset timer */ GtkWidget *statusbar_feedsinfo; GtkWidget *statusbar_feedsinfo_evbox; GActionGroup *generalActions; GActionGroup *addActions; /*<< all types of "New" options */ GActionGroup *feedActions; /*<< update and mark read */ GActionGroup *readWriteActions; /*<< node remove and properties, node itemset items remove */ GActionGroup *itemActions; /*<< item state toggline, single item remove */ ItemList *itemlist; FeedList *feedlist; ItemView *itemview; BrowserTabs *tabs; PeasExtensionSet *extensions; /*<< Plugin management */ gboolean fullscreen; /*<< track fullscreen */ }; enum { PROP_NONE, PROP_FEED_LIST, PROP_ITEM_LIST, PROP_ITEM_VIEW, PROP_BROWSER_TABS, PROP_BUILDER }; static LifereaShell *shell = NULL; G_DEFINE_TYPE (LifereaShell, liferea_shell, G_TYPE_OBJECT); static void liferea_shell_finalize (GObject *object) { LifereaShell *ls = LIFEREA_SHELL (object); g_object_unref (ls->xml); } static void liferea_shell_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { LifereaShell *shell = LIFEREA_SHELL (object); switch (prop_id) { case PROP_FEED_LIST: g_value_set_object (value, shell->feedlist); break; case PROP_ITEM_LIST: g_value_set_object (value, shell->itemlist); break; case PROP_ITEM_VIEW: g_value_set_object (value, shell->itemview); break; case PROP_BROWSER_TABS: g_value_set_object (value, shell->tabs); break; case PROP_BUILDER: g_value_set_object (value, shell->xml); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void liferea_shell_class_init (LifereaShellClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = liferea_shell_get_property; object_class->finalize = liferea_shell_finalize; /* LifereaShell:feed-list: */ g_object_class_install_property (object_class, PROP_FEED_LIST, g_param_spec_object ("feed-list", "LifereaFeedList", "LifereaFeedList object", FEED_LIST_TYPE, G_PARAM_READABLE)); /* LifereaShell:item-list: */ g_object_class_install_property (object_class, PROP_ITEM_LIST, g_param_spec_object ("item-list", "LifereaItemList", "LifereaItemList object", ITEMLIST_TYPE, G_PARAM_READABLE)); /* LifereaShell:item-view: */ g_object_class_install_property (object_class, PROP_ITEM_VIEW, g_param_spec_object ("item-view", "LifereaItemView", "LifereaItemView object", ITEM_VIEW_TYPE, G_PARAM_READABLE)); /* LifereaShell:browser-tabs: */ g_object_class_install_property (object_class, PROP_BROWSER_TABS, g_param_spec_object ("browser-tabs", "LifereaBrowserTabs", "LifereaBrowserTabs object", BROWSER_TABS_TYPE, G_PARAM_READABLE)); /* LifereaShell:builder: */ g_object_class_install_property (object_class, PROP_BUILDER, g_param_spec_object ("builder", "GtkBuilder", "Liferea user interfaces definitions", GTK_TYPE_BUILDER, G_PARAM_READABLE)); } GtkWidget * liferea_shell_lookup (const gchar *name) { g_return_val_if_fail (shell != NULL, NULL); return GTK_WIDGET (gtk_builder_get_object (shell->xml, name)); } static void liferea_shell_init (LifereaShell *ls) { /* globally accessible singleton */ g_assert (NULL == shell); shell = ls; shell->xml = gtk_builder_new_from_file (PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "mainwindow.ui"); if (!shell->xml) g_error ("Loading " PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "mainwindow.ui failed"); gtk_builder_connect_signals (shell->xml, NULL); } /* * Restore the window position from the values saved into gconf. Note * that this does not display/present/show the mainwindow. */ static void liferea_shell_restore_position (void) { /* load window position */ int x, y, w, h; gboolean last_window_maximized; GdkWindow *gdk_window; GdkMonitor *monitor; GdkRectangle work_area; conf_get_int_value (LAST_WINDOW_X, &x); conf_get_int_value (LAST_WINDOW_Y, &y); conf_get_int_value (LAST_WINDOW_WIDTH, &w); conf_get_int_value (LAST_WINDOW_HEIGHT, &h); debug4 (DEBUG_GUI, "Retrieved saved setting: size %dx%d position %d:%d", w, h, x, y); /* Restore position only if the width and height were saved */ if (w != 0 && h != 0) { gdk_window = gtk_widget_get_window (GTK_WIDGET (shell->window)); monitor = gdk_display_get_monitor_at_window (gtk_widget_get_display (GTK_WIDGET(shell->window)), gdk_window); gdk_monitor_get_workarea (monitor, &work_area); if (x >= work_area.width) x = work_area.width - 100; else if (x + w < 0) x = 100; if (y >= work_area.height) y = work_area.height - 100; else if (y + w < 0) y = 100; debug4 (DEBUG_GUI, "Restoring to size %dx%d position %d:%d", w, h, x, y); gtk_window_move (GTK_WINDOW (shell->window), x, y); /* load window size */ gtk_window_resize (GTK_WINDOW (shell->window), w, h); } conf_get_bool_value (LAST_WINDOW_MAXIMIZED, &last_window_maximized); if (last_window_maximized) gtk_window_maximize (GTK_WINDOW (shell->window)); else gtk_window_unmaximize (GTK_WINDOW (shell->window)); } void liferea_shell_save_position (void) { GtkWidget *pane; gint x, y, w, h; gboolean last_window_maximized; GdkWindow *gdk_window; GdkMonitor *monitor; GdkRectangle work_area; /* save pane proportions */ pane = liferea_shell_lookup ("leftpane"); if (pane) { x = gtk_paned_get_position (GTK_PANED (pane)); conf_set_int_value (LAST_VPANE_POS, x); } pane = liferea_shell_lookup ("normalViewPane"); if (pane) { y = gtk_paned_get_position (GTK_PANED (pane)); conf_set_int_value (LAST_HPANE_POS, y); } pane = liferea_shell_lookup ("wideViewPane"); if (pane) { y = gtk_paned_get_position (GTK_PANED (pane)); conf_set_int_value (LAST_WPANE_POS, y); } /* The following needs to be skipped when the window is not visible */ if (!gtk_widget_get_visible (GTK_WIDGET (shell->window))) return; conf_get_bool_value (LAST_WINDOW_MAXIMIZED, &last_window_maximized); if (last_window_maximized) return; gtk_window_get_position (shell->window, &x, &y); gtk_window_get_size (shell->window, &w, &h); gdk_window = gtk_widget_get_window (GTK_WIDGET (shell->window)); monitor = gdk_display_get_monitor_at_window (gtk_widget_get_display (GTK_WIDGET(shell->window)), gdk_window); gdk_monitor_get_workarea (monitor, &work_area); if (x+w<0 || y+h<0 || x > work_area.width || y > work_area.height) return; debug4 (DEBUG_GUI, "Saving window size and position: %dx%d %d:%d", w, h, x, y); /* save window position */ conf_set_int_value (LAST_WINDOW_X, x); conf_set_int_value (LAST_WINDOW_Y, y); /* save window size */ conf_set_int_value (LAST_WINDOW_WIDTH, w); conf_set_int_value (LAST_WINDOW_HEIGHT, h); } void liferea_shell_set_toolbar_style (const gchar *toolbar_style) { if (!toolbar_style) /* default to icons */ gtk_toolbar_set_style (GTK_TOOLBAR (shell->toolbar), GTK_TOOLBAR_ICONS); else if (g_str_equal (toolbar_style, "text")) gtk_toolbar_set_style (GTK_TOOLBAR (shell->toolbar), GTK_TOOLBAR_TEXT); else if (g_str_equal (toolbar_style, "both")) gtk_toolbar_set_style (GTK_TOOLBAR (shell->toolbar), GTK_TOOLBAR_BOTH); else if (g_str_equal (toolbar_style, "both_horiz") || g_str_equal (toolbar_style, "both-horiz") ) gtk_toolbar_set_style (GTK_TOOLBAR (shell->toolbar), GTK_TOOLBAR_BOTH_HORIZ); else /* default to icons */ gtk_toolbar_set_style (GTK_TOOLBAR (shell->toolbar), GTK_TOOLBAR_ICONS); } void liferea_shell_update_toolbar (void) { gboolean disable_toolbar; conf_get_bool_value (DISABLE_TOOLBAR, &disable_toolbar); if (disable_toolbar) gtk_widget_hide (shell->toolbar); else gtk_widget_show (shell->toolbar); } void liferea_shell_update_update_menu (gboolean enabled) { g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (shell->feedActions), "update-selected")), enabled); } void liferea_shell_update_feed_menu (gboolean add, gboolean enabled, gboolean readWrite) { ui_common_simple_action_group_set_enabled (shell->addActions, add); ui_common_simple_action_group_set_enabled (shell->feedActions, enabled); ui_common_simple_action_group_set_enabled (shell->readWriteActions, readWrite); } void liferea_shell_update_item_menu (gboolean enabled) { ui_common_simple_action_group_set_enabled (shell->itemActions, enabled); } void liferea_shell_update_allitems_actions (gboolean isNotEmpty, gboolean isRead) { g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (shell->generalActions), "remove-selected-feed-items")), isNotEmpty); g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (shell->feedActions), "mark-selected-feed-as-read")), isRead); } void liferea_shell_update_history_actions (void) { g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (shell->generalActions), "prev-read-item")), item_history_has_previous ()); g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (shell->generalActions), "next-read-item")), item_history_has_next ()); } static void liferea_shell_update_unread_stats (gpointer user_data) { gint new_items, unread_items; gchar *msg, *tmp; if (!shell) return; new_items = feedlist_get_new_item_count (); unread_items = feedlist_get_unread_item_count (); if (new_items != 0) msg = g_strdup_printf (ngettext (" (%d new)", " (%d new)", new_items), new_items); else msg = g_strdup (""); if (unread_items != 0) tmp = g_strdup_printf (ngettext ("%d unread%s", "%d unread%s", unread_items), unread_items, msg); else tmp = g_strdup (""); gtk_label_set_text (GTK_LABEL (shell->statusbar_feedsinfo), tmp); g_free (tmp); g_free (msg); } /* Do to the unsuitable GtkStatusBar stack handling which doesn't allow to keep messages on top of the stack for some time without overwriting them with newly arriving messages we need some extra handling here. Liferea knows two types of status messages: -> low prio messages (e.g. updating status messages) -> high prio messages (caused by user interaction, e.g. link hovering) The ideas is to keep the high prio messages always visible no matter what low prio messages arrive. To solve this we define the status bar stack is always a stack of two messages at most. At the bottom of the stack is always the latest low prio message and on top of the stack is the latest high prio message (or none at all). To enforce this using GtkStatusBar we use a lock to avoid adding low prio messages on top of high priority ones. This lock is valid for at most 5s which should be enough to read the high priority message. Afterwards new low priority messages will overrule the out-dated high priority message. */ static gboolean liferea_shell_unlock_status_bar_cb (gpointer user_data) { shell->statusbarLocked = FALSE; shell->statusbarLockTimer = 0; return FALSE; } static gboolean liferea_shell_set_status_bar_important_cb (gpointer user_data) { gchar *text = (gchar *)user_data; guint id; GtkStatusbar *statusbar; statusbar = GTK_STATUSBAR (shell->statusbar); id = gtk_statusbar_get_context_id (statusbar, "important"); gtk_statusbar_pop (statusbar, id); gtk_statusbar_push (statusbar, id, text); g_free(text); return FALSE; } static gboolean liferea_shell_set_status_bar_default_cb (gpointer user_data) { gchar *text = (gchar *)user_data; guint id; GtkStatusbar *statusbar; statusbar = GTK_STATUSBAR (shell->statusbar); id = gtk_statusbar_get_context_id (statusbar, "default"); gtk_statusbar_pop (statusbar, id); gtk_statusbar_push (statusbar, id, text); g_free(text); return FALSE; } void liferea_shell_set_status_bar (const char *format, ...) { va_list args; gchar *text; if (shell->statusbarLocked) return; g_return_if_fail (format != NULL); va_start (args, format); text = g_strdup_vprintf (format, args); va_end (args); g_idle_add ((GSourceFunc)liferea_shell_set_status_bar_default_cb, (gpointer)text); } void liferea_shell_set_important_status_bar (const char *format, ...) { va_list args; gchar *text; g_return_if_fail (format != NULL); va_start (args, format); text = g_strdup_vprintf (format, args); va_end (args); shell->statusbarLocked = FALSE; if (shell->statusbarLockTimer) { g_source_remove (shell->statusbarLockTimer); shell->statusbarLockTimer = 0; } /* URL hover messages are reset with an empty string, so we must locking the status bar on empty strings! */ if (!g_str_equal (text, "")) { /* Realize 5s locking for important messages... */ shell->statusbarLocked = TRUE; shell->statusbarLockTimer = g_timeout_add_seconds (5, liferea_shell_unlock_status_bar_cb, NULL); } g_idle_add ((GSourceFunc)liferea_shell_set_status_bar_important_cb, (gpointer)text); } /* For zoom in : zoom = 1, for zoom out : zoom= -1, for reset : zoom = 0 */ static void liferea_shell_do_zoom (gint zoom) { /* We must apply the zoom either to the item view or to an open tab, depending on the browser tabs GtkNotebook page that is active... */ if (!browser_tabs_get_active_htmlview ()) itemview_do_zoom (zoom); else browser_tabs_do_zoom (zoom); } static gboolean on_key_press_event_null_cb (GtkWidget *widget, GdkEventKey *event, gpointer data) { return FALSE; } static gboolean on_notebook_scroll_event_null_cb (GtkWidget *widget, GdkEventScroll *event) { GtkNotebook *notebook = GTK_NOTEBOOK (widget); GtkWidget* child; GtkWidget* originator; if (!gtk_notebook_get_current_page (notebook)) return FALSE; child = gtk_notebook_get_nth_page (notebook, gtk_notebook_get_current_page (notebook)); originator = gtk_get_event_widget ((GdkEvent *)event); /* ignore scroll events from the content of the page */ if (!originator || gtk_widget_is_ancestor (originator, child)) return FALSE; return TRUE; } static gboolean on_close (GtkWidget *widget, GdkEvent *event, gpointer user_data) { liferea_application_shutdown (); return TRUE; } static gboolean on_window_state_event (GtkWidget *widget, GdkEvent *event, gpointer user_data) { if (event->type == GDK_WINDOW_STATE) { GdkWindowState changed = ((GdkEventWindowState*)event)->changed_mask, state = ((GdkEventWindowState*)event)->new_window_state; if (changed == GDK_WINDOW_STATE_MAXIMIZED && !(state & GDK_WINDOW_STATE_WITHDRAWN)) { if (state & GDK_WINDOW_STATE_MAXIMIZED) { conf_set_bool_value (LAST_WINDOW_MAXIMIZED, TRUE); } else { conf_set_bool_value (LAST_WINDOW_MAXIMIZED, FALSE); gtk_container_child_set (GTK_CONTAINER (liferea_shell_lookup ("normalViewPane")), liferea_shell_lookup ("normalViewItems"), "resize", TRUE, NULL); gtk_container_child_set (GTK_CONTAINER (liferea_shell_lookup ("wideViewPane")), liferea_shell_lookup ("wideViewItems"), "resize", TRUE, NULL); } } if (state & GDK_WINDOW_STATE_ICONIFIED) conf_set_int_value (LAST_WINDOW_STATE, MAINWINDOW_ICONIFIED); else if(state & GDK_WINDOW_STATE_WITHDRAWN) conf_set_int_value (LAST_WINDOW_STATE, MAINWINDOW_HIDDEN); else conf_set_int_value (LAST_WINDOW_STATE, MAINWINDOW_SHOWN); } if ((event->window_state.new_window_state & GDK_WINDOW_STATE_FULLSCREEN) == 0) shell->fullscreen = TRUE; else shell->fullscreen = FALSE; return FALSE; } static gboolean on_key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer data) { gboolean modifier_matches = FALSE; guint default_modifiers; const gchar *type = NULL; GtkWidget *focusw = NULL; gint browse_key_setting; if (event->type == GDK_KEY_PRESS) { default_modifiers = gtk_accelerator_get_default_mod_mask (); /* handle [+] headline skimming hotkey */ switch (event->keyval) { case GDK_KEY_space: conf_get_int_value (BROWSE_KEY_SETTING, &browse_key_setting); switch (browse_key_setting) { default: case 0: modifier_matches = ((event->state & default_modifiers) == 0); /* Hack to make space handled in the module. This is necessary because the HTML widget code must be able to catch spaces for input fields. By ignoring the space here it will be passed to the HTML widget which in turn will pass it back if it is not eaten by any input field currently focussed. */ /* pass through space keys only if HTML widget has the focus */ focusw = gtk_window_get_focus (GTK_WINDOW (widget)); if (focusw) type = g_type_name (G_OBJECT_TYPE (focusw)); if (type && (g_str_equal (type, "LifereaWebView"))) return FALSE; break; case 1: modifier_matches = ((event->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK); break; case 2: modifier_matches = ((event->state & GDK_MOD1_MASK) == GDK_MOD1_MASK); break; } if (modifier_matches) { itemview_scroll (); return TRUE; } break; } /* prevent usage of navigation keys in entries */ focusw = gtk_window_get_focus (GTK_WINDOW (widget)); if (!focusw || GTK_IS_ENTRY (focusw)) return FALSE; /* prevent usage of navigation keys in HTML view */ type = g_type_name (G_OBJECT_TYPE (focusw)); if (type && (g_str_equal (type, "LifereaWebView"))) return FALSE; /* check for treeview navigation */ if (0 == (event->state & default_modifiers)) { switch (event->keyval) { case GDK_KEY_KP_Delete: case GDK_KEY_Delete: if (focusw == GTK_WIDGET (shell->feedlistViewWidget)) return FALSE; /* to be handled in feed_list_view_key_press_cb() */ on_action_remove_item (NULL, NULL, NULL); return TRUE; break; case GDK_KEY_n: on_next_unread_item_activate (NULL, NULL, NULL); return TRUE; break; case GDK_KEY_f: itemview_move_cursor (1); return TRUE; break; case GDK_KEY_b: itemview_move_cursor (-1); return TRUE; break; case GDK_KEY_u: ui_common_treeview_move_cursor (shell->feedlistViewWidget, -1); itemview_move_cursor_to_first (); return TRUE; break; case GDK_KEY_d: ui_common_treeview_move_cursor (shell->feedlistViewWidget, 1); itemview_move_cursor_to_first (); return TRUE; break; } } } return FALSE; } static void on_prefbtn_clicked (GSimpleAction *action, GVariant *parameter, gpointer user_data) { preferences_dialog_open (); } static void on_searchbtn_clicked (GSimpleAction *action, GVariant *parameter, gpointer user_data) { simple_search_dialog_open (); } static void on_about_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkWidget *dialog; dialog = liferea_dialog_new ("about"); gtk_about_dialog_set_version (GTK_ABOUT_DIALOG (dialog), VERSION); g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_hide), NULL); gtk_widget_show (dialog); } static void liferea_shell_add_html_tab (const gchar *file, const gchar *name) { gchar *filepattern = g_strdup_printf (PACKAGE_DATA_DIR "/" PACKAGE "/doc/html/%s", file); gchar *filename = common_get_localized_filename (filepattern); gchar *fileuri = g_strdup_printf ("file://%s", filename); browser_tabs_add_new (fileuri, name, TRUE); g_free (filepattern); g_free (filename); g_free (fileuri); } static void on_topics_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_add_html_tab ("topics_%s.html", _("Help Topics")); } static void on_quick_reference_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_add_html_tab ("reference_%s.html", _("Quick Reference")); } static void on_faq_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_add_html_tab ("faq_%s.html", _("FAQ")); } static void on_menu_quit (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_application_shutdown (); } static void on_menu_fullscreen_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { shell->fullscreen == TRUE ? gtk_window_fullscreen(shell->window) : gtk_window_unfullscreen (shell->window); g_simple_action_set_state (action, g_variant_new_boolean (shell->fullscreen)); } static void on_action_zoomin_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_do_zoom (1); } static void on_action_zoomout_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_do_zoom (-1); } static void on_action_zoomreset_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { liferea_shell_do_zoom (0); } static void on_menu_import (GSimpleAction *action, GVariant *parameter, gpointer user_data) { import_OPML_file (); } static void on_menu_export (GSimpleAction *action, GVariant *parameter, gpointer user_data) { export_OPML_file (); } /* methods to receive URLs which were dropped anywhere in the main window */ static void liferea_shell_URL_received (GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *data, guint info, guint time_received) { gchar *tmp1, *tmp2, *freeme; GtkWidget *mainwindow; GtkAllocation alloc; GtkTreeView *treeview; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; nodePtr node; gint tx, ty; g_return_if_fail (gtk_selection_data_get_data (data) != NULL); mainwindow = GTK_WIDGET (shell->window); treeview = GTK_TREE_VIEW (shell->feedlistViewWidget); model = gtk_tree_view_get_model (treeview); /* x and y are relative to the main window, make them relative to the treeview */ gtk_widget_translate_coordinates (mainwindow, GTK_WIDGET (treeview), x, y, &tx, &ty); /* Allow link drops only over feed list widget. This is to avoid the frequent accidental text drops in the HTML view. */ gtk_widget_get_allocation(GTK_WIDGET(treeview), &alloc); if((x > alloc.x+alloc.width) || (x < alloc.x) || (y > alloc.y+alloc.height) || (y < alloc.y)) { gtk_drag_finish (context, FALSE, FALSE, time_received); return; } if ((gtk_selection_data_get_length (data) >= 0) && (gtk_selection_data_get_format (data) == 8)) { /* extra handling to accept multiple drops */ freeme = tmp1 = g_strdup ((gchar *) gtk_selection_data_get_data (data)); while ((tmp2 = strsep (&tmp1, "\n\r"))) { if (strlen (tmp2)) { /* if the drop is over a node, select it so that feedlist_add_subscription() * adds it in the correct folder */ if (gtk_tree_view_get_dest_row_at_pos (treeview, tx, ty, &path, NULL)) { if (gtk_tree_model_get_iter (model, &iter, path)) { gtk_tree_model_get (model, &iter, FS_PTR, &node, -1); /* if node is NULL, feed_list_view_select() will unselect the tv */ feed_list_view_select (node); } gtk_tree_path_free (path); } feedlist_add_subscription (g_strdup (tmp2), NULL, NULL, FEED_REQ_PRIORITY_HIGH); } } g_free (freeme); gtk_drag_finish (context, TRUE, FALSE, time_received); } else { gtk_drag_finish (context, FALSE, FALSE, time_received); } } static void liferea_shell_setup_URL_receiver (void) { GtkWidget *mainwindow; GtkTargetEntry target_table[] = { { "STRING", GTK_TARGET_OTHER_WIDGET, 0 }, { "text/plain", GTK_TARGET_OTHER_WIDGET, 0 }, { "text/uri-list", GTK_TARGET_OTHER_WIDGET, 1 }, { "_NETSCAPE_URL", GTK_TARGET_OTHER_APP, 1 }, { "application/x-rootwin-drop", GTK_TARGET_OTHER_APP, 2 } }; mainwindow = GTK_WIDGET (shell->window); /* doesn't work with GTK_DEST_DEFAULT_DROP... */ gtk_drag_dest_set (mainwindow, GTK_DEST_DEFAULT_ALL, target_table, sizeof (target_table)/sizeof (target_table[0]), GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK); g_signal_connect (G_OBJECT (mainwindow), "drag_data_received", G_CALLBACK (liferea_shell_URL_received), NULL); } static void on_action_open_enclosure (GSimpleAction *action, GVariant *parameter, gpointer user_data) { LifereaShell *shell = LIFEREA_SHELL (user_data); itemview_open_next_enclosure (shell->itemview); } static const GActionEntry liferea_shell_gaction_entries[] = { {"update-all", on_menu_update_all, NULL, NULL, NULL}, {"mark-all-feeds-read", on_action_mark_all_read, NULL, NULL, NULL}, {"import-feed-list", on_menu_import, NULL, NULL, NULL}, {"export-feed-list", on_menu_export, NULL, NULL, NULL}, {"quit", on_menu_quit, NULL, NULL, NULL}, {"remove-selected-feed-items", on_remove_items_activate, NULL, NULL, NULL}, {"prev-read-item", on_prev_read_item_activate, NULL, NULL, NULL}, {"next-read-item", on_next_read_item_activate, NULL, NULL, NULL}, {"next-unread-item", on_next_unread_item_activate, NULL, NULL, NULL}, {"zoom-in", on_action_zoomin_activate, NULL, NULL, NULL}, {"zoom-out", on_action_zoomout_activate, NULL, NULL, NULL}, {"zoom-reset", on_action_zoomreset_activate, NULL, NULL, NULL}, {"show-update-monitor", on_menu_show_update_monitor, NULL, NULL, NULL}, {"show-preferences", on_prefbtn_clicked, NULL, NULL, NULL}, {"search-feeds", on_searchbtn_clicked, NULL, NULL, NULL}, {"show-help-contents", on_topics_activate, NULL, NULL, NULL}, {"show-help-quick-reference", on_quick_reference_activate, NULL, NULL, NULL}, {"show-help-faq", on_faq_activate, NULL, NULL, NULL}, {"show-about", on_about_activate, NULL, NULL, NULL}, /* For mysterious reasons, the radio menu magic seem to only works with a * parameter/state of type string. */ {"set-view-mode", NULL, "s", "@s 'normal'", on_view_activate}, /* Parameter type must be NULL for toggle. */ {"fullscreen", NULL, NULL, "@b false", on_menu_fullscreen_activate}, {"reduced-feed-list", NULL, NULL, "@b false", on_feedlist_reduced_activate}, {"toggle-item-read-status", on_toggle_unread_status, "t", NULL, NULL}, {"toggle-item-flag", on_toggle_item_flag, "t", NULL, NULL}, {"remove-item", on_action_remove_item, "t", NULL, NULL}, {"launch-item-in-tab", on_action_launch_item_in_tab, "t", NULL, NULL}, {"launch-item-in-browser", on_action_launch_item_in_browser, "t", NULL, NULL}, {"launch-item-in-external-browser", on_action_launch_item_in_external_browser, "t", NULL, NULL}, {"open-item-enclosure", on_action_open_enclosure, "t", NULL, NULL}, }; static const GActionEntry liferea_shell_add_gaction_entries[] = { {"new-subscription", on_menu_feed_new, NULL, NULL, NULL}, {"new-folder", on_menu_folder_new, NULL, NULL, NULL}, {"new-vfolder", on_new_vfolder_activate, NULL, NULL, NULL}, {"new-source", on_new_plugin_activate, NULL, NULL, NULL}, {"new-newsbin", on_new_newsbin_activate, NULL, NULL, NULL} }; static const GActionEntry liferea_shell_feed_gaction_entries[] = { {"mark-selected-feed-as-read", on_action_mark_all_read, NULL, NULL, NULL}, {"update-selected", on_menu_update, NULL, NULL, NULL} }; static const GActionEntry liferea_shell_read_write_gaction_entries[] = { {"selected-node-properties", on_menu_properties, NULL, NULL, NULL}, {"delete-selected", on_menu_delete, NULL, NULL, NULL} }; static const GActionEntry liferea_shell_item_gaction_entries[] = { {"toggle-selected-item-read-status", on_toggle_unread_status, NULL, NULL, NULL}, {"toggle-selected-item-flag", on_toggle_item_flag, NULL, NULL, NULL}, {"remove-selected-item", on_action_remove_item, NULL, NULL, NULL}, {"launch-selected-item-in-tab", on_action_launch_item_in_tab, NULL, NULL, NULL}, {"launch-selected-item-in-browser", on_action_launch_item_in_browser, NULL, NULL, NULL}, {"launch-selected-item-in-external-browser", on_action_launch_item_in_external_browser, NULL, NULL, NULL}, {"open-selected-item-enclosure", on_action_open_enclosure, NULL, NULL, NULL} }; static void on_action_open_link_in_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemview_launch_URL (g_variant_get_string (parameter, NULL), TRUE /* use internal browser */); } static void on_action_open_link_in_external_browser (GSimpleAction *action, GVariant *parameter, gpointer user_data) { browser_launch_URL_external (g_variant_get_string (parameter, NULL)); } static void on_action_open_link_in_tab (GSimpleAction *action, GVariant *parameter, gpointer user_data) { browser_tabs_add_new (g_variant_get_string (parameter, NULL), g_variant_get_string (parameter, NULL), FALSE); } static void on_action_social_bookmark_link (GSimpleAction *action, GVariant *parameter, gpointer user_data) { gchar *social_url, *link, *title; g_variant_get (parameter, "(ss)", &link, &title); social_url = social_get_bookmark_url (link, title); (void)browser_tabs_add_new (social_url, social_url, TRUE); g_free (social_url); } static void on_action_copy_link_to_clipboard (GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkClipboard *clipboard; gchar *link = (gchar *) common_uri_sanitize (BAD_CAST g_variant_get_string (parameter, NULL)); clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY); gtk_clipboard_set_text (clipboard, link, -1); clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_text (clipboard, link, -1); g_free (link); } static void email_the_author(GSimpleAction *action, GVariant *parameter, gpointer user_data) { itemPtr item = NULL; if (parameter) item = item_load (g_variant_get_uint64 (parameter)); else item = itemlist_get_selected (); if(item) { const gchar *author, *subject; GError *error = NULL; gchar *argv[5]; author = item_get_author(item); subject = item_get_title (item); g_assert (author != NULL); argv[0] = g_strdup("xdg-email"); argv[1] = g_strdup_printf ("mailto:%s", author); argv[2] = g_strdup("--subject"); argv[3] = g_strdup_printf ("%s", subject); argv[4] = NULL; g_spawn_async (NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error); if (error && (0 != error->code)) { debug2 (DEBUG_GUI, "Email command failed: %s : %s", argv[0], error->message); liferea_shell_set_important_status_bar (_("Email command failed: %s"), error->message); g_error_free (error); } else { liferea_shell_set_status_bar (_("Starting: \"%s\""), argv[0]); } g_free(argv[0]); g_free(argv[1]); g_free(argv[2]); g_free(argv[3]); item_unload(item); } } static const GActionEntry liferea_shell_link_gaction_entries[] = { {"open-link-in-tab", on_action_open_link_in_tab, "s", NULL, NULL}, {"open-link-in-browser", on_action_open_link_in_browser, "s", NULL, NULL}, {"open-link-in-external-browser", on_action_open_link_in_external_browser, "s", NULL, NULL}, /* The parameters are link, then title. */ {"social-bookmark-link", on_action_social_bookmark_link, "(ss)", NULL, NULL}, {"copy-link-to-clipboard", on_action_copy_link_to_clipboard, "s", NULL, NULL}, {"email-the-author", email_the_author, "t", NULL, NULL} }; static void liferea_shell_restore_state (const gchar *overrideWindowState) { gchar *toolbar_style; gint last_vpane_pos, last_hpane_pos, last_wpane_pos; gint resultState; gboolean last_window_maximized; debug0 (DEBUG_GUI, "Setting toolbar style"); toolbar_style = conf_get_toolbar_style (); liferea_shell_set_toolbar_style (toolbar_style); g_free (toolbar_style); debug0 (DEBUG_GUI, "Restoring window position"); /* Realize needed so that the window structure can be accessed... otherwise we get a GTK warning when window is shown by clicking on notification icon or when theme colors are fetched. */ gtk_widget_realize (GTK_WIDGET (shell->window)); liferea_shell_restore_position (); /* Apply horrible window state parameter logic: -> overrideWindowState provides optional command line flags passed by user or the session manager (prio 1) -> lastState provides last shutdown preference (prio 2) */ /* Initialize with last saved state */ conf_get_int_value (LAST_WINDOW_STATE, &resultState); debug2 (DEBUG_GUI, "Previous window state indicators: dconf=%d, CLI switch=%s", resultState, overrideWindowState); /* Override with command line options */ if (!g_strcmp0 (overrideWindowState, "hidden")) resultState = MAINWINDOW_HIDDEN; if (!g_strcmp0 (overrideWindowState, "shown")) resultState = MAINWINDOW_SHOWN; /* And set the window to the resulting state */ switch (resultState) { case MAINWINDOW_HIDDEN: debug0 (DEBUG_GUI, "Restoring window state 'hidden (to tray)'"); gtk_widget_hide (GTK_WIDGET (shell->window)); break; case MAINWINDOW_SHOWN: default: /* Safe default is always to show window */ debug0 (DEBUG_GUI, "Restoring window state 'shown'"); gtk_widget_show (GTK_WIDGET (shell->window)); } /* This only works after the window has been restored, so we do it last. */ debug0 (DEBUG_GUI, "Loading pane proportions"); conf_get_int_value (LAST_VPANE_POS, &last_vpane_pos); if (last_vpane_pos) gtk_paned_set_position (GTK_PANED (liferea_shell_lookup ("leftpane")), last_vpane_pos); conf_get_int_value (LAST_HPANE_POS, &last_hpane_pos); if (last_hpane_pos) gtk_paned_set_position (GTK_PANED (liferea_shell_lookup ("normalViewPane")), last_hpane_pos); conf_get_int_value (LAST_WPANE_POS, &last_wpane_pos); if (last_wpane_pos) gtk_paned_set_position (GTK_PANED (liferea_shell_lookup ("wideViewPane")), last_wpane_pos); conf_get_bool_value (LAST_WINDOW_MAXIMIZED, &last_window_maximized); if (!last_window_maximized) { gtk_container_child_set (GTK_CONTAINER (liferea_shell_lookup ("normalViewPane")), liferea_shell_lookup ("normalViewItems"), "resize", TRUE, NULL); gtk_container_child_set (GTK_CONTAINER (liferea_shell_lookup ("wideViewPane")), liferea_shell_lookup ("wideViewItems"), "resize", TRUE, NULL); } } static const gchar * liferea_accels_update_all[] = {"u", NULL}; static const gchar * liferea_accels_quit[] = {"q", NULL}; static const gchar * liferea_accels_mark_feed_as_read[] = {"r", NULL}; static const gchar * liferea_accels_next_unread_item[] = {"n", NULL}; static const gchar * liferea_accels_prev_read_item[] = {"n", NULL}; static const gchar * liferea_accels_toggle_item_read_status[] = {"m", NULL}; static const gchar * liferea_accels_toggle_item_flag[] = {"t", NULL}; static const gchar * liferea_accels_fullscreen[] = {"F11", NULL}; static const gchar * liferea_accels_zoom_in[] = {"plus", "equal",NULL}; static const gchar * liferea_accels_zoom_out[] = {"minus", NULL}; static const gchar * liferea_accels_zoom_reset[] = {"0", NULL}; static const gchar * liferea_accels_search_feeds[] = {"f", NULL}; static const gchar * liferea_accels_show_help_contents[] = {"F1", NULL}; static const gchar * liferea_accels_open_selected_item_enclosure[] = {"o", NULL}; void liferea_shell_create (GtkApplication *app, const gchar *overrideWindowState, gint pluginsDisabled) { GMenuModel *menubar_model; gboolean toggle; gchar *id; FeedListView *feedListView; debug_enter ("liferea_shell_create"); g_object_new (LIFEREA_SHELL_TYPE, NULL); shell->window = GTK_WINDOW (liferea_shell_lookup ("mainwindow")); gtk_window_set_application (GTK_WINDOW (shell->window), app); /* Add GActions to application */ shell->generalActions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(shell->generalActions), liferea_shell_gaction_entries, G_N_ELEMENTS (liferea_shell_gaction_entries), NULL); ui_common_add_action_group_to_map (shell->generalActions, G_ACTION_MAP (app)); shell->addActions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(shell->addActions), liferea_shell_add_gaction_entries, G_N_ELEMENTS (liferea_shell_add_gaction_entries), NULL); ui_common_add_action_group_to_map (shell->addActions, G_ACTION_MAP (app)); shell->feedActions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(shell->feedActions), liferea_shell_feed_gaction_entries, G_N_ELEMENTS (liferea_shell_feed_gaction_entries), NULL); ui_common_add_action_group_to_map (shell->feedActions, G_ACTION_MAP (app)); shell->itemActions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(shell->itemActions), liferea_shell_item_gaction_entries, G_N_ELEMENTS (liferea_shell_item_gaction_entries), shell); ui_common_add_action_group_to_map (shell->itemActions, G_ACTION_MAP (app)); shell->readWriteActions = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(shell->readWriteActions), liferea_shell_read_write_gaction_entries, G_N_ELEMENTS (liferea_shell_read_write_gaction_entries), NULL); ui_common_add_action_group_to_map (shell->readWriteActions, G_ACTION_MAP (app)); g_action_map_add_action_entries (G_ACTION_MAP(app), liferea_shell_link_gaction_entries, G_N_ELEMENTS (liferea_shell_link_gaction_entries), NULL); /* 1.) menu creation */ debug0 (DEBUG_GUI, "Setting up menus"); shell->itemlist = itemlist_create (); /* Prepare some toggle button states */ conf_get_bool_value (REDUCED_FEEDLIST, &toggle); g_simple_action_set_state ( G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (app), "reduced-feed-list")), g_variant_new_boolean (toggle)); /* Menu creation */ gtk_builder_add_from_file (shell->xml, PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "liferea_menu.ui", NULL); menubar_model = G_MENU_MODEL (gtk_builder_get_object (shell->xml, "menubar")); gtk_application_set_menubar (app, menubar_model); /* Add accelerators */ gtk_application_set_accels_for_action (app, "app.update-all", liferea_accels_update_all); gtk_application_set_accels_for_action (app, "app.quit", liferea_accels_quit); gtk_application_set_accels_for_action (app, "app.mark-selected-feed-as-read", liferea_accels_mark_feed_as_read); gtk_application_set_accels_for_action (app, "app.next-unread-item", liferea_accels_next_unread_item); gtk_application_set_accels_for_action (app, "app.prev-read-item", liferea_accels_prev_read_item); gtk_application_set_accels_for_action (app, "app.toggle-selected-item-read-status", liferea_accels_toggle_item_read_status); gtk_application_set_accels_for_action (app, "app.toggle-selected-item-flag", liferea_accels_toggle_item_flag); gtk_application_set_accels_for_action (app, "app.fullscreen", liferea_accels_fullscreen); gtk_application_set_accels_for_action (app, "app.zoom-in", liferea_accels_zoom_in); gtk_application_set_accels_for_action (app, "app.zoom-out", liferea_accels_zoom_out); gtk_application_set_accels_for_action (app, "app.zoom-reset", liferea_accels_zoom_reset); gtk_application_set_accels_for_action (app, "app.search-feeds", liferea_accels_search_feeds); gtk_application_set_accels_for_action (app, "app.show-help-contents", liferea_accels_show_help_contents); gtk_application_set_accels_for_action (app, "app.open-selected-item-enclosure", liferea_accels_open_selected_item_enclosure); /* Toolbar */ gtk_builder_add_from_file (shell->xml, PACKAGE_DATA_DIR G_DIR_SEPARATOR_S PACKAGE G_DIR_SEPARATOR_S "liferea_toolbar.ui", NULL); shell->toolbar = GTK_WIDGET (gtk_builder_get_object (shell->xml, "maintoolbar")); /* 2.) setup containers */ debug0 (DEBUG_GUI, "Setting up widget containers"); gtk_grid_attach_next_to (GTK_GRID (liferea_shell_lookup ("vbox1")), shell->toolbar, NULL, GTK_POS_TOP, 1,1); gtk_widget_show_all(GTK_WIDGET(shell->toolbar)); g_signal_connect ((gpointer) liferea_shell_lookup ("itemtabs"), "key_press_event", G_CALLBACK (on_key_press_event_null_cb), NULL); g_signal_connect ((gpointer) liferea_shell_lookup ("itemtabs"), "key_release_event", G_CALLBACK (on_key_press_event_null_cb), NULL); g_signal_connect ((gpointer) liferea_shell_lookup ("itemtabs"), "scroll_event", G_CALLBACK (on_notebook_scroll_event_null_cb), NULL); g_signal_connect (G_OBJECT (shell->window), "delete_event", G_CALLBACK(on_close), NULL); g_signal_connect (G_OBJECT (shell->window), "window_state_event", G_CALLBACK(on_window_state_event), shell); g_signal_connect (G_OBJECT (shell->window), "key_press_event", G_CALLBACK(on_key_press_event), shell); g_signal_connect (G_OBJECT (shell->window), "style-updated", G_CALLBACK(liferea_shell_rebuild_css), NULL); /* 3.) setup status bar */ debug0 (DEBUG_GUI, "Setting up status bar"); shell->statusbar = GTK_STATUSBAR (liferea_shell_lookup ("statusbar")); shell->statusbarLocked = FALSE; shell->statusbarLockTimer = 0; shell->statusbar_feedsinfo_evbox = gtk_event_box_new (); shell->statusbar_feedsinfo = gtk_label_new(""); gtk_container_add (GTK_CONTAINER (shell->statusbar_feedsinfo_evbox), shell->statusbar_feedsinfo); gtk_widget_show_all (shell->statusbar_feedsinfo_evbox); gtk_box_pack_start (GTK_BOX (shell->statusbar), shell->statusbar_feedsinfo_evbox, FALSE, FALSE, 5); g_signal_connect (G_OBJECT (shell->statusbar_feedsinfo_evbox), "button_release_event", G_CALLBACK (on_next_unread_item_activate), NULL); /* 4.) setup tabs */ debug0 (DEBUG_GUI, "Setting up tabbed browsing"); shell->tabs = browser_tabs_create (GTK_NOTEBOOK (liferea_shell_lookup ("browsertabs"))); /* 5.) setup feed list */ debug0 (DEBUG_GUI, "Setting up feed list"); shell->feedlistViewWidget = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist")); feedListView = feed_list_view_create (shell->feedlistViewWidget); /* 6.) setup menu sensivity */ debug0 (DEBUG_GUI, "Initialising menus"); /* On start, no item or feed is selected, so Item menu should be insensitive: */ liferea_shell_update_item_menu (FALSE); /* necessary to prevent selection signals when filling the feed list and setting the 2/3 pane mode view */ gtk_widget_set_sensitive (GTK_WIDGET (shell->feedlistViewWidget), FALSE); /* 7.) setup item view */ debug0 (DEBUG_GUI, "Setting up item view"); shell->itemview = itemview_create (GTK_WIDGET (shell->window)); /* 8.) load icons as required */ debug0 (DEBUG_GUI, "Loading icons"); icons_load (); /* 9.) update and restore all menu elements */ liferea_shell_update_toolbar (); liferea_shell_update_history_actions (); liferea_shell_setup_URL_receiver (); liferea_shell_restore_state (overrideWindowState); gtk_widget_set_sensitive (GTK_WIDGET (shell->feedlistViewWidget), TRUE); /* 10.) After main window is realized get theme colors and set up feed list and load WebView stylesheet */ render_init_theme_colors (GTK_WIDGET (shell->window)); shell->feedlist = feedlist_create (feedListView); g_signal_connect (shell->feedlist, "new-items", G_CALLBACK (liferea_shell_update_unread_stats), shell->feedlist); itemview_style_update (); /* 11.) Restore latest selection */ // FIXME: Move to feed list code if (conf_get_str_value (LAST_NODE_SELECTED, &id)) { feed_list_view_select (node_from_id (id)); g_free (id); } /* 12. Setup shell plugins */ if(0 == pluginsDisabled) { shell->extensions = peas_extension_set_new (PEAS_ENGINE (liferea_plugins_engine_get_default ()), LIFEREA_TYPE_SHELL_ACTIVATABLE, "shell", shell, NULL); liferea_plugins_engine_set_default_signals (shell->extensions, shell); } /* 14. Rebuild search folders if needed */ if (searchFolderRebuild) vfolder_foreach (vfolder_rebuild); debug_exit ("liferea_shell_create"); } void liferea_shell_destroy (void) { liferea_shell_save_position (); g_object_unref (shell->extensions); g_object_unref (shell->tabs); g_object_unref (shell->feedlist); g_object_unref (shell->itemview); g_signal_handlers_block_by_func (shell, G_CALLBACK (on_window_state_event), shell); gtk_widget_destroy (GTK_WIDGET (shell->window)); g_object_unref (shell); } void liferea_shell_present (void) { GtkWidget *mainwindow = GTK_WIDGET (shell->window); if ((gdk_window_get_state (gtk_widget_get_window (mainwindow)) & GDK_WINDOW_STATE_ICONIFIED) || !gtk_widget_get_visible (mainwindow)) liferea_shell_restore_position (); gtk_window_present (shell->window); } static gboolean liferea_shell_window_is_on_other_desktop(GdkWindow *gdkwindow) { #ifdef GDK_WINDOWING_X11 return GDK_IS_X11_DISPLAY (gdk_window_get_display (gdkwindow)) && (gdk_x11_window_get_desktop (gdkwindow) != gdk_x11_screen_get_current_desktop (gdk_window_get_screen (gdkwindow))); #else return FALSE; #endif } static void liferea_shell_window_move_to_current_desktop(GdkWindow *gdkwindow) { #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY (gdk_window_get_display (gdkwindow))) gdk_x11_window_move_to_current_desktop (gdkwindow); #endif } void liferea_shell_show_window (void) { GtkWidget *mainwindow = GTK_WIDGET (shell->window); GdkWindow *gdkwindow = gtk_widget_get_window (mainwindow); liferea_shell_window_move_to_current_desktop (gdkwindow); if (!gtk_widget_get_visible (GTK_WIDGET (mainwindow))) liferea_shell_restore_position (); gtk_window_deiconify (GTK_WINDOW (mainwindow)); gtk_window_present (shell->window); } void liferea_shell_toggle_visibility (void) { GtkWidget *mainwindow = GTK_WIDGET (shell->window); GdkWindow *gdkwindow = gtk_widget_get_window (mainwindow); if (liferea_shell_window_is_on_other_desktop (gdkwindow) || !gtk_widget_get_visible (mainwindow)) { liferea_shell_show_window (); } else { liferea_shell_save_position (); gtk_widget_hide (mainwindow); } } GtkWidget * liferea_shell_get_window (void) { return GTK_WIDGET (shell->window); } void liferea_shell_rebuild_css (void) { render_init_theme_colors (GTK_WIDGET (shell->window)); itemview_style_update (); } void liferea_shell_set_view_mode (nodeViewType newMode) { GAction *action; action = g_action_map_lookup_action (G_ACTION_MAP(shell->generalActions), "set-view-mode"); switch (newMode) { case NODE_VIEW_MODE_NORMAL: case NODE_VIEW_MODE_DEFAULT: case NODE_VIEW_MODE_COMBINED: /* Combined is removed, default to normal */ g_action_change_state (action, g_variant_new_string("normal")); break; case NODE_VIEW_MODE_WIDE: g_action_change_state (action, g_variant_new_string("wide")); break; } } liferea-1.13.7/src/ui/liferea_shell.h000066400000000000000000000132141415350204600173660ustar00rootroot00000000000000/* * @file liferea_shell.h UI layout handling * * Copyright (C) 2004-2005 Nathan J. Conrad * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_SHELL_H #define _LIFEREA_SHELL_H #include #include #include #include #include "node.h" /* possible main window states */ enum mainwindowState { MAINWINDOW_SHOWN, /*<< main window is visible */ MAINWINDOW_MAXIMIZED, /*<< main window is visible and maximized */ MAINWINDOW_ICONIFIED, /*<< main window is iconified */ MAINWINDOW_HIDDEN /*<< main window is not visible at all */ }; G_BEGIN_DECLS #define LIFEREA_SHELL_TYPE (liferea_shell_get_type ()) G_DECLARE_FINAL_TYPE (LifereaShell, liferea_shell, LIFEREA, SHELL, GObject) /** * liferea_shell_lookup: * @name: the widget name * * Searches the glade XML UI tree for the given widget * name and returns the found widget. * * Returns: (transfer none) (nullable): the widget found or NULL */ GtkWidget * liferea_shell_lookup (const gchar *name); /** * liferea_shell_save_position * * Save the position of the Liferea main window. */ void liferea_shell_save_position (void); /** * liferea_shell_create: (skip) * @app: the GtkApplication to attach the main window to * @overrideWindowState: optional parameter for window state (or NULL) * @pluginsDisabled 1 if plugins are not to be loaded * * Set up the Liferea main window. */ void liferea_shell_create (GtkApplication *app, const gchar *overrideWindowState, gint pluginsDisabled); /** * liferea_shell_destroy: (skip) * * Destroys the global liferea_shell object. */ void liferea_shell_destroy (void); /** * liferea_shell_present: * * Presents the main window if it is hidden. */ void liferea_shell_present (void); /** * liferea_shell_show_window: * * Show the main window. */ void liferea_shell_show_window (void); /** * liferea_shell_toggle_visibility: * * Toggles main window visibility. */ void liferea_shell_toggle_visibility (void); /** * liferea_shell_set_toolbar_style: * @toolbar_style: text string containing the type of style to use * * Sets the toolbar to a particular style */ void liferea_shell_set_toolbar_style (const gchar *toolbar_style); /** * liferea_shell_update_toolbar: (skip) * * According to the preferences this function enables/disables the toolbar. * * TODO: use signal instead */ void liferea_shell_update_toolbar (void); /** * liferea_shell_update_history_actions: (skip) * * Update item history menu actions and toolbar buttons. * * TODO: use signal instead */ void liferea_shell_update_history_actions (void); /** * liferea_shell_update_feed_menu: (skip) * @add: TRUE if subscribing is to be enabled * @enabled: TRUE if feed actions are to be enabled * @readWrite: TRUE if feed list modifying actions are enabled * * Update the sensitivity of options affecting single feeds. * * TODO: use signal instead */ void liferea_shell_update_feed_menu (gboolean add, gboolean enabled, gboolean readWrite); /** * liferea_shell_update_item_menu: (skip) * @enabled: TRUE if item actions are to be enabled * * Update the sensitivity of options affecting single items. * * TODO: use signal instead */ void liferea_shell_update_item_menu (gboolean enabled); /** * liferea_shell_update_allitems_actions: (skip) * @isNotEmpty: TRUE if there is a non-empty item set active * @isRead: TRUE if there are no unread items in the item set * * Update the sensitivity of options affecting item sets. * * TODO: use signal instead */ void liferea_shell_update_allitems_actions (gboolean isNotEmpty, gboolean isRead); /** * liferea_shell_update_update_menu: (skip) * @enabled: TRUE if menu options are to be enabled * * Set the sensitivity of items in the update menu. * * TODO: use signal instead */ void liferea_shell_update_update_menu (gboolean enabled); /** * liferea_shell_set_status_bar: * * Sets the status bar text. Takes printf() like parameters. */ void liferea_shell_set_status_bar (const char *format, ...); /** * liferea_shell_set_important_status_bar: * * Similar to liferea_shell_set_status_message(), but ensures * that messages stay visible and avoids that those messages * are overwritten by unimportant ones. */ void liferea_shell_set_important_status_bar (const char *format, ...); /** * liferea_shell_get_window: * * Returns the Liferea main window. * * Returns: (transfer none): the main window widget found or NULL */ GtkWidget * liferea_shell_get_window (void); /** * liferea_shell_rebuild_css: * * Invokes a rebuild of the WebView CSS. */ void liferea_shell_rebuild_css (void); /** * liferea_shell_set_view_mode: * @newMode: the new mode * * Changes the view mode programmatically. Used to change the mode when * selecting another feed. Convenience function to trigger the stateful action * set-view-mode. */ void liferea_shell_set_view_mode (nodeViewType newMode); G_END_DECLS #endif liferea-1.13.7/src/ui/liferea_shell_activatable.c000066400000000000000000000070401415350204600217200ustar00rootroot00000000000000/* * @file liferea_shell_activatable.c Shell Plugin Type * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/liferea_shell_activatable.h" #include "ui/liferea_shell.h" /** * SECTION:liferea_shell_activatable * @short_description: Interface for activatable extensions on the shell * @see_also: #PeasExtensionSet * * #LifereaShellActivatable is an interface which should be implemented by * extensions that should be activated on the Liferea main window. **/ G_DEFINE_INTERFACE (LifereaShellActivatable, liferea_shell_activatable, G_TYPE_OBJECT) void liferea_shell_activatable_default_init (LifereaShellActivatableInterface *iface) { static gboolean initialized = FALSE; if (!initialized) { /** * LifereaShellActivatable:window: * * The window property contains the gtr window for this * #LifereaShellActivatable instance. */ g_object_interface_install_property (iface, g_param_spec_object ("shell", "Shell", "The Liferea shell", LIFEREA_SHELL_TYPE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); initialized = TRUE; } } /** * liferea_shell_activatable_activate: * @activatable: A #LifereaShellActivatable. * * Activates the extension on the shell property. */ void liferea_shell_activatable_activate (LifereaShellActivatable * activatable) { LifereaShellActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_SHELL_ACTIVATABLE (activatable)); iface = LIFEREA_SHELL_ACTIVATABLE_GET_IFACE (activatable); if (iface->activate) iface->activate (activatable); } /** * liferea_shell_activatable_deactivate: * @activatable: A #LifereaShellActivatable. * * Deactivates the extension on the shell property. */ void liferea_shell_activatable_deactivate (LifereaShellActivatable * activatable) { LifereaShellActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_SHELL_ACTIVATABLE (activatable)); iface = LIFEREA_SHELL_ACTIVATABLE_GET_IFACE (activatable); if (iface->deactivate) iface->deactivate (activatable); } /** * liferea_shell_activatable_update_state: * @activatable: A #LifereaShellActivatable. * * Triggers an update of the extension internal state to take into account * state changes in the window, due to some event or user action. */ void liferea_shell_activatable_update_state (LifereaShellActivatable * activatable) { LifereaShellActivatableInterface *iface; g_return_if_fail (LIFEREA_IS_SHELL_ACTIVATABLE (activatable)); iface = LIFEREA_SHELL_ACTIVATABLE_GET_IFACE (activatable); if (iface->update_state) iface->update_state (activatable); } liferea-1.13.7/src/ui/liferea_shell_activatable.h000066400000000000000000000044721415350204600217330ustar00rootroot00000000000000/* * @file liferea_shell_activatable.h Shell Plugin Type * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_SHELL_ACTIVATABLE_H__ #define _LIFEREA_SHELL_ACTIVATABLE_H__ #include G_BEGIN_DECLS #define LIFEREA_TYPE_SHELL_ACTIVATABLE (liferea_shell_activatable_get_type ()) #define LIFEREA_SHELL_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_TYPE_SHELL_ACTIVATABLE, LifereaShellActivatable)) #define LIFEREA_SHELL_ACTIVATABLE_IFACE(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), LIFEREA_TYPE_SHELL_ACTIVATABLE, LifereaShellActivatableInterface)) #define LIFEREA_IS_SHELL_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_TYPE_SHELL_ACTIVATABLE)) #define LIFEREA_SHELL_ACTIVATABLE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), LIFEREA_TYPE_SHELL_ACTIVATABLE, LifereaShellActivatableInterface)) typedef struct _LifereaShellActivatable LifereaShellActivatable; typedef struct _LifereaShellActivatableInterface LifereaShellActivatableInterface; struct _LifereaShellActivatableInterface { GTypeInterface g_iface; void (*activate) (LifereaShellActivatable * activatable); void (*deactivate) (LifereaShellActivatable * activatable); void (*update_state) (LifereaShellActivatable * activatable); }; GType liferea_shell_activatable_get_type (void) G_GNUC_CONST; void liferea_shell_activatable_activate (LifereaShellActivatable *activatable); void liferea_shell_activatable_deactivate (LifereaShellActivatable *activatable); void liferea_shell_activatable_update_state (LifereaShellActivatable *activatable); G_END_DECLS #endif /* __LIFEREA_SHELL_ACTIVATABLE_H__ */ liferea-1.13.7/src/ui/media_player.c000066400000000000000000000045241415350204600172220ustar00rootroot00000000000000/* * @file liferea_media_player.c media player helpers * * Copyright (C) 2012-2015 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "media_player.h" #include "media_player_activatable.h" #include "plugins_engine.h" #include // FIXME: This should be a member of some object! static PeasExtensionSet *extensions = NULL; static PeasExtensionSet * liferea_media_player_get_extension_set (void) { if (!extensions) { extensions = peas_extension_set_new (PEAS_ENGINE (liferea_plugins_engine_get_default ()), LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE, NULL); liferea_plugins_engine_set_default_signals (extensions, NULL); } return extensions; } typedef struct mediaPlayerLoadData { GtkWidget *parentWidget; GSList *enclosures; } mediaPlayerLoadData; static void liferea_media_player_load_foreach (PeasExtensionSet *set, PeasPluginInfo *info, PeasExtension *exten, gpointer user_data) { liferea_media_player_activatable_load (LIFEREA_MEDIA_PLAYER_ACTIVATABLE (exten), ((mediaPlayerLoadData *)user_data)->parentWidget, ((mediaPlayerLoadData *)user_data)->enclosures); } void liferea_media_player_load (GtkWidget *parentWidget, GSList *enclosures) { mediaPlayerLoadData user_data; user_data.parentWidget = parentWidget; user_data.enclosures = enclosures; peas_extension_set_foreach (liferea_media_player_get_extension_set (), liferea_media_player_load_foreach, &user_data); } liferea-1.13.7/src/ui/media_player.h000066400000000000000000000024101415350204600172170ustar00rootroot00000000000000/* * @file media_player.h media player helpers * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_MEDIA_PLAYER_H #define _LIFEREA_MEDIA_PLAYER_H #include #include /** * liferea_media_player_load: * @parentWidget: the parent widget for the media player * @enclosures: (element-type gchar*): a list of enclosure strings * * Triggers the creation of a suitable media player and loads a list of * enclosures into it. */ void liferea_media_player_load (GtkWidget *parentWidget, GSList *enclosures); #endif liferea-1.13.7/src/ui/media_player_activatable.c000066400000000000000000000047571415350204600215710ustar00rootroot00000000000000/* * @file liferea_media_player_activatable.c Media Player Plugin * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "media_player_activatable.h" /** * SECTION:liferea_media_player_activatable * @short_description: Interface for activatable extensions providing a media player * @see_also: #PeasExtensionSet * * #LifereaMediaPlayerActivatable is an interface which should be implemented by * extensions that want to provide a media player **/ G_DEFINE_INTERFACE (LifereaMediaPlayerActivatable, liferea_media_player_activatable, G_TYPE_OBJECT) void liferea_media_player_activatable_default_init (LifereaMediaPlayerActivatableInterface *iface) { /* No properties yet */ } void liferea_media_player_activatable_activate (LifereaMediaPlayerActivatable * activatable) { LifereaMediaPlayerActivatableInterface *iface; g_return_if_fail (IS_LIFEREA_MEDIA_PLAYER_ACTIVATABLE (activatable)); iface = LIFEREA_MEDIA_PLAYER_ACTIVATABLE_GET_IFACE (activatable); if (iface->activate) iface->activate (activatable); } void liferea_media_player_activatable_deactivate (LifereaMediaPlayerActivatable * activatable) { LifereaMediaPlayerActivatableInterface *iface; g_return_if_fail (IS_LIFEREA_MEDIA_PLAYER_ACTIVATABLE (activatable)); iface = LIFEREA_MEDIA_PLAYER_ACTIVATABLE_GET_IFACE (activatable); if (iface->deactivate) iface->deactivate (activatable); } void liferea_media_player_activatable_load (LifereaMediaPlayerActivatable * activatable, GtkWidget *parentWidget, GSList *enclosures) { LifereaMediaPlayerActivatableInterface *iface; g_return_if_fail (IS_LIFEREA_MEDIA_PLAYER_ACTIVATABLE (activatable)); iface = LIFEREA_MEDIA_PLAYER_ACTIVATABLE_GET_IFACE (activatable); if (iface->load) iface->load (activatable, parentWidget, enclosures); } liferea-1.13.7/src/ui/media_player_activatable.h000066400000000000000000000055751415350204600215750ustar00rootroot00000000000000/* * @file liferea_media_player_activatable.h Media Player Plugin Type * * Copyright (C) 2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_MEDIA_PLAYER_ACTIVATABLE_H__ #define _LIFEREA_MEDIA_PLAYER_ACTIVATABLE_H__ #include #include G_BEGIN_DECLS #define LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE (liferea_media_player_activatable_get_type ()) #define LIFEREA_MEDIA_PLAYER_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE, LifereaMediaPlayerActivatable)) #define LIFEREA_MEDIA_PLAYER_ACTIVATABLE_IFACE(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE, LifereaMediaPlayerActivatableInterface)) #define IS_LIFEREA_MEDIA_PLAYER_ACTIVATABLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE)) #define LIFEREA_MEDIA_PLAYER_ACTIVATABLE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE ((obj), LIFEREA_MEDIA_PLAYER_ACTIVATABLE_TYPE, LifereaMediaPlayerActivatableInterface)) typedef struct _LifereaMediaPlayerActivatable LifereaMediaPlayerActivatable; typedef struct _LifereaMediaPlayerActivatableInterface LifereaMediaPlayerActivatableInterface; struct _LifereaMediaPlayerActivatableInterface { GTypeInterface g_iface; void (*activate) (LifereaMediaPlayerActivatable * activatable); void (*deactivate) (LifereaMediaPlayerActivatable * activatable); void (*load) (LifereaMediaPlayerActivatable * activatable, GtkWidget *parentWidget, GSList *enclosures); }; GType liferea_media_player_activatable_get_type (void) G_GNUC_CONST; void liferea_media_player_activatable_activate (LifereaMediaPlayerActivatable *activatable); void liferea_media_player_activatable_deactivate (LifereaMediaPlayerActivatable *activatable); /** * liferea_media_player_activatable_load: * @parentWidget: the parent widget for the media player * @enclosures: (element-type gchar*): a list of enclosures * * Triggers the creation of a suitable media player and loads a list of * enclosures into it. */ void liferea_media_player_activatable_load (LifereaMediaPlayerActivatable *activatable, GtkWidget *parentWidget, GSList *enclosures); G_END_DECLS #endif /* __LIFEREA_MEDIA_PLAYER_ACTIVATABLE_H__ */ liferea-1.13.7/src/ui/popup_menu.c000066400000000000000000000344221415350204600167560ustar00rootroot00000000000000/** * @file popup_menu.c popup menus * * Copyright (C) 2003-2013 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2009 Adrian Bunk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/popup_menu.h" #include #include "common.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "net_monitor.h" #include "newsbin.h" #include "node.h" #include "social.h" #include "vfolder.h" #include "ui/enclosure_list_view.h" #include "ui/feed_list_view.h" #include "ui/item_list_view.h" #include "ui/itemview.h" #include "ui/liferea_shell.h" #include "ui/preferences_dialog.h" #include "fl_sources/node_source.h" #define UI_POPUP_ITEM_IS_TOGGLE 1 static void ui_popup_menu (GtkWidget *menu, const GdkEvent *event) { g_signal_connect_after (G_OBJECT(menu), "unmap-event", G_CALLBACK(gtk_widget_destroy), NULL); gtk_widget_show_all (menu); gtk_menu_popup_at_pointer (GTK_MENU(menu), event); } static GtkWidget* ui_popup_add_menuitem (GtkWidget *menu, const gchar *label, gpointer callback, gpointer data, gint toggle) { GtkWidget *item; g_assert (label); if (toggle) { item = gtk_check_menu_item_new_with_mnemonic (label); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM(item), toggle - UI_POPUP_ITEM_IS_TOGGLE); } else { item = gtk_menu_item_new_with_mnemonic (label); } if (callback) g_signal_connect_swapped (G_OBJECT(item), "activate", G_CALLBACK(callback), data); gtk_menu_shell_append (GTK_MENU_SHELL(menu), item); return item; } static const GActionEntry ui_popup_item_gaction_entries[] = { {"copy-item-to-newsbin", on_action_copy_to_newsbin, "(umt)", NULL, NULL}, {"toggle-item-read-status", on_toggle_unread_status, "t", NULL, NULL}, {"toggle-item-flag", on_toggle_item_flag, "t", NULL, NULL}, {"remove-item", on_action_remove_item, "t", NULL, NULL}, {"open-item-in-tab", on_action_launch_item_in_tab, "t", NULL, NULL}, {"open-item-in-browser", on_action_launch_item_in_browser, "t", NULL, NULL}, {"open-item-in-external-browser", on_action_launch_item_in_external_browser, "t", NULL, NULL} }; void ui_popup_item_menu (itemPtr item, const GdkEvent *event) { GtkWidget *menu; GMenu *menu_model, *section; GMenuItem *menu_item; GSimpleActionGroup *action_group; GSList *iter; gchar *text, *item_link; const gchar *author; item_link = item_make_link (item); menu_model = g_menu_new (); menu_item = g_menu_item_new (NULL, NULL); author = item_get_author(item); section = g_menu_new (); g_menu_item_set_label (menu_item, _("Open In _Tab")); g_menu_item_set_action_and_target (menu_item, "item.open-item-in-tab", "t", (guint64) item->id); g_menu_append_item (section, menu_item); g_menu_item_set_label (menu_item, _("_Open In Browser")); g_menu_item_set_action_and_target (menu_item, "item.open-item-in-browser", "t", (guint64) item->id); g_menu_append_item (section, menu_item); g_menu_item_set_label (menu_item, _("Open In _External Browser")); g_menu_item_set_action_and_target (menu_item, "item.open-item-in-external-browser", "t", (guint64) item->id); g_menu_append_item (section, menu_item); if(author){ g_menu_item_set_label (menu_item, _("Email The Author")); g_menu_item_set_action_and_target (menu_item, "app.email-the-author", "t", (guint64) item->id); g_menu_append_item (section, menu_item); } g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); iter = newsbin_get_list (); if (iter) { GMenu *submenu; guint32 i = 0; section = g_menu_new (); submenu = g_menu_new (); while (iter) { nodePtr node = (nodePtr)iter->data; g_menu_item_set_label (menu_item, node_get_title (node)); g_menu_item_set_action_and_target (menu_item, "item.copy-item-to-newsbin", "(umt)", i, TRUE, (guint64) item->id); g_menu_append_item (submenu, menu_item); iter = g_slist_next (iter); i++; } g_menu_append_submenu (section, _("Copy to News Bin"), G_MENU_MODEL (submenu)); g_object_unref (submenu); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); } section = g_menu_new (); text = g_strdup_printf (_("_Bookmark at %s"), social_get_bookmark_site ()); g_menu_item_set_label (menu_item, text); g_menu_item_set_action_and_target (menu_item, "app.social-bookmark-link", "(ss)", item_link, item_get_title (item)); g_menu_append_item (section, menu_item); g_free (text); g_menu_item_set_label (menu_item, _("Copy Item _Location")); g_menu_item_set_action_and_target (menu_item, "app.copy-link-to-clipboard", "s", item_link); g_menu_append_item (section, menu_item); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_item_set_label (menu_item, _("Toggle _Read Status")); g_menu_item_set_action_and_target (menu_item, "item.toggle-item-read-status", "t", (guint64) item->id); g_menu_append_item (section, menu_item); g_menu_item_set_label (menu_item, _("Toggle Item _Flag")); g_menu_item_set_action_and_target (menu_item, "item.toggle-item-flag", "t", (guint64) item->id); g_menu_append_item (section, menu_item); g_menu_item_set_label (menu_item, _("R_emove Item")); g_menu_item_set_action_and_target (menu_item, "item.remove-item", "t", (guint64) item->id); g_menu_append_item (section, menu_item); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); g_object_unref (menu_item); g_free (item_link); g_menu_freeze (menu_model); menu = gtk_menu_new_from_model (G_MENU_MODEL (menu_model)); action_group = g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP(action_group), ui_popup_item_gaction_entries, G_N_ELEMENTS (ui_popup_item_gaction_entries), NULL); gtk_widget_insert_action_group (menu, "item", G_ACTION_GROUP (action_group)); /* The menu has to be attached to an application window or one of its children for access to app actions.*/ gtk_menu_attach_to_widget (GTK_MENU (menu), liferea_shell_lookup ("mainwindow"), NULL); g_object_unref (menu_model); ui_popup_menu (menu, event); } void ui_popup_enclosure_menu (enclosurePtr enclosure, const GdkEvent *event) { GtkWidget *menu; menu = gtk_menu_new (); ui_popup_add_menuitem (menu, _("Open Enclosure..."), on_popup_open_enclosure, enclosure, 0); ui_popup_add_menuitem (menu, _("Save As..."), on_popup_save_enclosure, enclosure, 0); ui_popup_add_menuitem (menu, _("Copy Link Location"), on_popup_copy_enclosure, enclosure, 0); ui_popup_menu (menu, event); } /* popup callback wrappers */ static void ui_popup_rebuild_vfolder (GSimpleAction *action, GVariant *parameter, gpointer user_data) { vfolder_rebuild ((nodePtr)user_data); } static void ui_popup_properties (GSimpleAction *action, GVariant *parameter, gpointer user_data) { nodePtr node = (nodePtr) user_data; NODE_TYPE (node)->request_properties (node); } static void ui_popup_delete (GSimpleAction *action, GVariant *parameter, gpointer user_data) { feed_list_view_remove ((nodePtr)user_data); } static void ui_popup_sort_feeds (GSimpleAction *action, GVariant *parameter, gpointer user_data) { feed_list_view_sort_folder ((nodePtr)user_data); } static void ui_popup_add_convert_to_local (GSimpleAction *action, GVariant *parameter, gpointer user_data) { node_source_convert_to_local ((nodePtr)user_data); } /* Those actions work on the node passed as user_data parameter. */ static const GActionEntry ui_popup_node_gaction_entries[] = { {"node-mark-all-read", on_action_mark_all_read, NULL, NULL, NULL}, {"node-rebuild-vfolder", ui_popup_rebuild_vfolder, NULL, NULL, NULL}, {"node-properties", ui_popup_properties, NULL, NULL, NULL}, {"node-delete", ui_popup_delete, NULL, NULL, NULL}, {"node-sort-feeds", ui_popup_sort_feeds, NULL, NULL, NULL}, {"node-convert-to-local", ui_popup_add_convert_to_local, NULL, NULL, NULL}, {"node-update", on_menu_update, NULL, NULL, NULL} }; /** * Shows popup menus for the feed list depending on the * node type. */ static void ui_popup_node_menu (nodePtr node, gboolean validSelection, const GdkEvent *event) { GtkWidget *menu; GMenu *menu_model, *section; GSimpleActionGroup *action_group; gboolean writeableFeedlist, isRoot, addChildren; if (node->parent) { writeableFeedlist = NODE_SOURCE_TYPE (node->parent->source->root)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST; isRoot = NODE_SOURCE_TYPE (node->source->root)->capabilities & NODE_SOURCE_CAPABILITY_IS_ROOT; addChildren = NODE_TYPE (node->source->root)->capabilities & NODE_CAPABILITY_ADD_CHILDS; } else { /* if we have no parent then we have the root node... */ writeableFeedlist = TRUE; isRoot = TRUE; addChildren = TRUE; } menu_model = g_menu_new (); section = g_menu_new (); if (validSelection) { if (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE) g_menu_append (section, _("_Update"), "node.node-update"); else if (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE_CHILDS) g_menu_append (section, _("_Update Folder"), "node.node-update"); } if (writeableFeedlist) { if (addChildren) { GMenu *submenu; submenu = g_menu_new (); if (node_can_add_child_feed (node)) g_menu_append (submenu, _("New _Subscription..."), "app.new-subscription"); if (node_can_add_child_folder (node)) g_menu_append (submenu, _("New _Folder..."), "app.new-folder"); if (isRoot) { g_menu_append (submenu, _("New S_earch Folder..."), "app.new-vfolder"); g_menu_append (submenu, _("New S_ource..."), "app.new-source"); g_menu_append (submenu, _("New _News Bin..."), "app.new-newsbin"); } g_menu_append_submenu (section, _("_New"), G_MENU_MODEL (submenu)); g_object_unref (submenu); } if (isRoot && node->children) { /* Ending section and starting a new one to get a separator : */ g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("Sort Feeds"), "node.node-sort-feeds"); } } if (validSelection) { g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("_Mark All As Read"), "node.node-mark-all-read"); } if (IS_VFOLDER (node)) { g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("_Rebuild"), "node.node-rebuild-vfolder"); } if (validSelection) { if (writeableFeedlist) { g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("_Delete"), "node.node-delete"); g_menu_append (section, _("_Properties"), "node.node-properties"); } if (IS_NODE_SOURCE (node) && NODE_SOURCE_TYPE (node)->capabilities & NODE_SOURCE_CAPABILITY_CONVERT_TO_LOCAL) { g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("Convert To Local Subscriptions..."), "node.node-convert-to-local"); } } g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); g_menu_freeze (menu_model); action_group = g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP(action_group), ui_popup_node_gaction_entries, G_N_ELEMENTS (ui_popup_node_gaction_entries), node); menu = gtk_menu_new_from_model (G_MENU_MODEL (menu_model)); gtk_widget_insert_action_group (menu, "node", G_ACTION_GROUP (action_group)); gtk_menu_attach_to_widget (GTK_MENU (menu), liferea_shell_lookup ("mainwindow"), NULL); g_object_unref (menu_model); ui_popup_menu (menu, event); } /* mouse button handler */ gboolean on_mainfeedlist_button_press_event (GtkWidget *widget, GdkEvent *event, gpointer user_data) { GdkEventButton *eb; GtkWidget *treeview; GtkTreeModel *model; GtkTreePath *path; GtkTreeIter iter; gboolean selected = TRUE; nodePtr node = NULL; treeview = liferea_shell_lookup ("feedlist"); if (event->type != GDK_BUTTON_PRESS) return FALSE; eb = (GdkEventButton*)event; /* determine node */ if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), eb->x, eb->y, &path, NULL, NULL, NULL)) { model = gtk_tree_view_get_model (GTK_TREE_VIEW (treeview)); gtk_tree_model_get_iter (model, &iter, path); gtk_tree_path_free (path); gtk_tree_model_get (model, &iter, FS_PTR, &node, -1); } else { selected = FALSE; node = feedlist_get_root (); } /* apply action */ switch (eb->button) { default: return FALSE; break; case 2: if (node) { feedlist_mark_all_read (node); itemview_update_node_info (node); itemview_update (); } break; case 3: if (node) { feed_list_view_select (node); } else { /* This happens when an "empty" node or nothing (feed list root) is clicked */ selected = FALSE; node = feedlist_get_root (); } gtk_widget_grab_focus (widget); ui_popup_node_menu (node, selected, event); break; } return TRUE; } /* popup key handler */ gboolean on_mainfeedlist_popup_menu (GtkWidget *widget, gpointer user_data) { GtkWidget *treeview; GtkTreeSelection *selection; GtkTreeModel *model; GtkTreeIter iter; gboolean selected = TRUE; nodePtr node = NULL; treeview = liferea_shell_lookup ("feedlist"); selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, FS_PTR, &node, -1); } else { selected = FALSE; node = feedlist_get_root (); } ui_popup_node_menu (node, selected, NULL); return TRUE; } liferea-1.13.7/src/ui/popup_menu.h000066400000000000000000000036251415350204600167640ustar00rootroot00000000000000/** * @file popup_menu.h popup menus * * Copyright (C) 2003-2011 Lars Windolf * Copyright (C) 2004-2005 Nathan J. Conrad * Copyright (C) 2009 Adrian Bunk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _POPUP_MENU_H #define _POPUP_MENU_H #include #include "item.h" #include "enclosure.h" /** * Shows a popup menu with options for the item list and the * given selected item. * (Open Link, Copy Item, Copy Link...) * * @param item the selected item * @param event The event that triggered the popup */ void ui_popup_item_menu (itemPtr item, const GdkEvent *event); /** * Shows a popup menu for the enclosure list view. * (Save As, Open With...) * * @param enclosure the enclosure * @param event the event that triggered the popup */ void ui_popup_enclosure_menu (enclosurePtr enclosure, const GdkEvent *event); /* GUI callbacks */ gboolean on_mainfeedlist_button_press_event (GtkWidget *widget, GdkEvent *event, gpointer user_data); gboolean on_mainfeedlist_popup_menu (GtkWidget *widget, gpointer user_data); #endif liferea-1.13.7/src/ui/preferences_dialog.c000066400000000000000000000656061415350204600204170ustar00rootroot00000000000000/** * @file preferences_dialog.c Liferea preferences * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2009 Hubert Figuiere * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/preferences_dialog.h" #ifdef HAVE_CONFIG_H # include #endif #include #include "common.h" #include "conf.h" #include "enclosure.h" #include "favicon.h" #include "feedlist.h" #include "folder.h" #include "itemlist.h" #include "social.h" #include "ui/enclosure_list_view.h" #include "ui/item_list_view.h" #include "ui/liferea_dialog.h" #include "ui/liferea_shell.h" #include "ui/ui_common.h" #include "ui/itemview.h" /** common private structure for all subscription dialogs */ struct _PreferencesDialog { GObject parentInstance; GtkWidget *dialog; /**< the GtkDialog widget */ }; G_DEFINE_TYPE (PreferencesDialog, preferences_dialog, G_TYPE_OBJECT); /* file type tree store column ids */ enum fts_columns { FTS_TYPE, /* file type name */ FTS_CMD, /* file cmd name */ FTS_PTR, /* pointer to config entry */ FTS_LEN }; extern GSList *bookmarkSites; /* from social.c */ static PreferencesDialog *prefdialog = NULL; /** download tool commands need to take an URI as %s */ static const gchar * enclosure_download_commands[] = { "steadyflow add %s", "dbus-send --session --print-reply --dest=org.gnome.gwget.ApplicationService /org/gnome/gwget/Gwget org.gnome.gwget.Application.OpenURI string:%s uint32:0", "kget %s", "uget-gtk %s", "transmission-gtk %s", "aria2c %s" }; /** order must match enclosure_download_commands[] */ static const gchar *enclosure_download_tool_options[] = { "Steadyflow", "gwget", "KGet", "uGet", "Transmission (Gtk)", "aria2", NULL }; /** GConf representation of toolbar styles */ static const gchar * gui_toolbar_style_values[] = { "", "both", "both-horiz", "icons", "text", NULL }; static const gchar * gui_toolbar_style_options[] = { N_("GNOME default"), N_("Text below icons"), N_("Text beside icons"), N_("Icons only"), N_("Text only"), NULL }; /* Note: these update interval literal should be kept in sync with the ones in ui_subscription.c! */ static const gchar * default_update_interval_unit_options[] = { N_("minutes"), N_("hours"), N_("days"), NULL }; static const gchar * browser_skim_key_options[] = { N_("Space"), N_(" Space"), N_(" Space"), NULL }; static const gchar * default_view_mode_options[] = { N_("Normal View"), N_("Wide View"), NULL }; gchar * prefs_get_download_command (void) { gboolean use_custom_command; gchar *result = NULL; conf_get_bool_value (DOWNLOAD_USE_CUSTOM_COMMAND, &use_custom_command); if (use_custom_command) conf_get_str_value (DOWNLOAD_CUSTOM_COMMAND, &result); else { gint enclosure_download_tool; conf_get_int_value (DOWNLOAD_TOOL, &enclosure_download_tool); /* FIXME: array boundary check */ result = g_strdup (enclosure_download_commands[enclosure_download_tool]); } return result; } /* Preference dialog class */ static void preferences_dialog_finalize (GObject *object) { PreferencesDialog *pd = PREFERENCES_DIALOG (object); gtk_widget_destroy (pd->dialog); prefdialog = NULL; } static void preferences_dialog_class_init (PreferencesDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = preferences_dialog_finalize; } /* Preference callbacks */ void on_folderdisplaybtn_toggled (GtkToggleButton *togglebutton, gpointer user_data) { gboolean enabled = gtk_toggle_button_get_active(togglebutton); conf_set_int_value(FOLDER_DISPLAY_MODE, (TRUE == enabled)?1:0); } /** * The "Hide read items" button has been clicked. Here we change the * preference and, if the selected node is a folder, we reload the * itemlist. The item selection is lost by this. */ void on_folderhidereadbtn_toggled (GtkToggleButton *togglebutton, gpointer user_data) { nodePtr displayedNode; gboolean enabled; displayedNode = itemlist_get_displayed_node (); enabled = gtk_toggle_button_get_active (togglebutton); conf_set_bool_value (FOLDER_DISPLAY_HIDE_READ, enabled); if (displayedNode && IS_FOLDER (displayedNode)) { itemlist_unload (FALSE); itemlist_load (displayedNode); /* Note: For simplicity when toggling this preference we accept that the current item selection is lost. */ } } void on_startupactionbtn_toggled (GtkButton *button, gpointer user_data) { gboolean enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); conf_set_int_value (STARTUP_FEED_ACTION, enabled?0:1); } void on_browsercmd_changed (GtkEditable *editable, gpointer user_data) { conf_set_str_value (BROWSER_COMMAND, gtk_editable_get_chars (editable,0,-1)); } static void on_browser_changed (GtkComboBox *optionmenu, gpointer user_data) { GtkTreeIter iter; gint num = -1; if (gtk_combo_box_get_active_iter (optionmenu, &iter)) { gtk_tree_model_get (gtk_combo_box_get_model (optionmenu), &iter, 1, &num, -1); gtk_widget_set_sensitive (liferea_dialog_lookup (prefdialog->dialog, "browsercmd"), num != 0); gtk_widget_set_sensitive (liferea_dialog_lookup (prefdialog->dialog, "manuallabel"), num != 0); gtk_widget_set_sensitive (liferea_dialog_lookup (prefdialog->dialog, "urlhintlabel"), num != 0); if (!num) conf_set_str_value (BROWSER_ID, "default"); else conf_set_str_value (BROWSER_ID, "manual"); } } void on_openlinksinsidebtn_clicked (GtkToggleButton *button, gpointer user_data) { conf_set_bool_value (BROWSE_INSIDE_APPLICATION, gtk_toggle_button_get_active (button)); } void on_disablejavascript_toggled (GtkToggleButton *togglebutton, gpointer user_data) { conf_set_bool_value (DISABLE_JAVASCRIPT, gtk_toggle_button_get_active (togglebutton)); } void on_enableplugins_toggled (GtkToggleButton *togglebutton, gpointer user_data) { conf_set_bool_value (ENABLE_PLUGINS, gtk_toggle_button_get_active (togglebutton)); } static void on_socialsite_changed (GtkComboBox *optionmenu, gpointer user_data) { GtkTreeIter iter; if (gtk_combo_box_get_active_iter (optionmenu, &iter)) { gchar * site; gtk_tree_model_get (gtk_combo_box_get_model (optionmenu), &iter, 0, &site, -1); social_set_bookmark_site (site); } } static void on_gui_toolbar_style_changed (gpointer user_data) { gchar *style; gint value = gtk_combo_box_get_active (GTK_COMBO_BOX (user_data)); conf_set_str_value (TOOLBAR_STYLE, gui_toolbar_style_values[value]); style = conf_get_toolbar_style (); liferea_shell_set_toolbar_style (style); g_free (style); } void on_itemCountBtn_value_changed (GtkSpinButton *spinbutton, gpointer user_data) { GtkAdjustment *itemCount; itemCount = gtk_spin_button_get_adjustment (spinbutton); conf_set_int_value (DEFAULT_MAX_ITEMS, gtk_adjustment_get_value (itemCount)); } void on_default_update_interval_value_changed (GtkSpinButton *spinbutton, gpointer user_data) { gint updateInterval, intervalUnit; GtkWidget *unitWidget, *valueWidget; unitWidget = liferea_dialog_lookup (prefdialog->dialog, "globalRefreshIntervalUnitComboBox"); valueWidget = liferea_dialog_lookup (prefdialog->dialog, "globalRefreshIntervalSpinButton"); intervalUnit = gtk_combo_box_get_active (GTK_COMBO_BOX (unitWidget)); updateInterval = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (valueWidget)); if (intervalUnit == 1) updateInterval *= 60; /* hours */ else if (intervalUnit == 2) updateInterval *= 1440; /* days */ conf_set_int_value (DEFAULT_UPDATE_INTERVAL, updateInterval); } static void on_default_update_interval_unit_changed (gpointer user_data) { on_default_update_interval_value_changed (NULL, prefdialog); } static void on_updateallfavicons_clicked (GtkButton *button, gpointer user_data) { feedlist_foreach (node_update_favicon); } static void on_proxyAutoDetect_clicked (GtkButton *button, gpointer user_data) { conf_set_int_value (PROXY_DETECT_MODE, 0); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (prefdialog->dialog, "proxybox")), FALSE); } static void on_noProxy_clicked (GtkButton *button, gpointer user_data) { conf_set_int_value (PROXY_DETECT_MODE, 1); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (prefdialog->dialog, "proxybox")), FALSE); } static void on_manualProxy_clicked (GtkButton *button, gpointer user_data) { conf_set_int_value (PROXY_DETECT_MODE, 2); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (prefdialog->dialog, "proxybox")), TRUE); } void on_useProxyAuth_toggled (GtkToggleButton *button, gpointer user_data) { gboolean enabled = gtk_toggle_button_get_active (button); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (prefdialog->dialog, "proxyauthtable")), enabled); conf_set_bool_value (PROXY_USEAUTH, enabled); } static void on_proxyhostentry_changed (GtkEditable *editable, gpointer user_data) { conf_set_str_value (PROXY_HOST, gtk_editable_get_chars (editable,0,-1)); } static void on_proxyportentry_changed (GtkEditable *editable, gpointer user_data) { conf_set_int_value (PROXY_PORT, atoi (gtk_editable_get_chars (editable,0,-1))); } static void on_proxyusernameentry_changed (GtkEditable *editable, gpointer user_data) { conf_set_str_value (PROXY_USER, gtk_editable_get_chars (editable,0,-1)); } static void on_proxypasswordentry_changed (GtkEditable *editable, gpointer user_data) { conf_set_str_value (PROXY_PASSWD, gtk_editable_get_chars (editable,0,-1)); } static void on_skim_key_changed (gpointer user_data) { conf_set_int_value (BROWSE_KEY_SETTING, gtk_combo_box_get_active (GTK_COMBO_BOX (user_data))); } static void on_default_view_mode_changed (gpointer user_data) { conf_set_int_value (DEFAULT_VIEW_MODE, gtk_combo_box_get_active (GTK_COMBO_BOX (user_data))); } void on_enclosure_download_predefined_toggled (GtkToggleButton *button, gpointer user_data) { gboolean is_active = gtk_toggle_button_get_active (button); gtk_widget_set_sensitive (liferea_dialog_lookup (prefdialog->dialog, "customDownloadEntry"), !is_active); gtk_widget_set_sensitive (liferea_dialog_lookup (prefdialog->dialog, "downloadToolCombo"), is_active); conf_set_bool_value (DOWNLOAD_USE_CUSTOM_COMMAND, !is_active); } static void on_enclosure_download_tool_changed (gpointer user_data) { conf_set_int_value (DOWNLOAD_TOOL, gtk_combo_box_get_active (GTK_COMBO_BOX (user_data))); } void on_enclosure_download_custom_command_changed (GtkEditable *entry, gpointer user_data) { conf_set_str_value (DOWNLOAD_CUSTOM_COMMAND, gtk_entry_get_text (GTK_ENTRY (entry))); } void on_enc_action_change_btn_clicked (GtkButton *button, gpointer user_data) { GtkTreeModel *model; GtkTreeSelection *selection; GtkTreeIter iter; gpointer type; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (liferea_dialog_lookup (prefdialog->dialog, "enc_action_view"))); if(gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, FTS_PTR, &type, -1); ui_enclosure_change_type (type); gtk_tree_store_set (GTK_TREE_STORE (model), &iter, FTS_CMD, ((encTypePtr)type)->cmd, -1); } } void on_enc_action_remove_btn_clicked (GtkButton *button, gpointer user_data) { GtkTreeModel *model; GtkTreeSelection *selection; GtkTreeIter iter; gpointer type; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (liferea_dialog_lookup (prefdialog->dialog, "enc_action_view"))); if (gtk_tree_selection_get_selected (selection, &model, &iter)) { gtk_tree_model_get (model, &iter, FTS_PTR, &type, -1); gtk_tree_store_remove (GTK_TREE_STORE (model), &iter); enclosure_mime_type_remove (type); } } void on_hidetoolbar_toggled (GtkToggleButton *button, gpointer user_data) { conf_set_bool_value (DISABLE_TOOLBAR, gtk_toggle_button_get_active (button)); liferea_shell_update_toolbar (); } void on_readermodebtn_toggled (GtkToggleButton *button, gpointer user_data) { conf_set_bool_value (ENABLE_READER_MODE, gtk_toggle_button_get_active (button)); } void on_donottrackbtn_toggled (GtkToggleButton *button, gpointer user_data) { conf_set_bool_value (DO_NOT_TRACK, gtk_toggle_button_get_active (button)); } void on_itpbtn_toggled (GtkToggleButton *button, gpointer user_data) { conf_set_bool_value (ENABLE_ITP, gtk_toggle_button_get_active (button)); } static void preferences_dialog_destroy_cb (GtkWidget *widget, PreferencesDialog *pd) { g_object_unref (pd); } void preferences_dialog_init (PreferencesDialog *pd) { GtkWidget *widget, *entry; GtkComboBox *combo; GtkListStore *store; GtkTreeIter treeiter; GtkAdjustment *itemCount; GtkTreeStore *treestore; GtkTreeViewColumn *column; GSList *list; gchar *proxyport; gchar *configuredBrowser, *name; gboolean enabled; gint tmp, i, iSetting, proxy_port; gboolean bSetting, manualBrowser; gchar *proxy_host, *proxy_user, *proxy_passwd; gchar *browser_command; gchar *custom_download_command; prefdialog = pd; pd->dialog = liferea_dialog_new ("prefs"); /* Set up browser selection popup */ store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT); gtk_list_store_append (store, &treeiter); gtk_list_store_set (store, &treeiter, 0, _("Default Browser"), 1, 0, -1); gtk_list_store_append (store, &treeiter); gtk_list_store_set (store, &treeiter, 0, _("Manual"), 1, 1, -1); combo = GTK_COMBO_BOX (liferea_dialog_lookup (pd->dialog, "browserpopup")); gtk_combo_box_set_model (combo, GTK_TREE_MODEL (store)); ui_common_setup_combo_text (combo, 0); g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(on_browser_changed), pd); /* ================== panel 1 "feeds" ==================== */ /* check box for feed startup update */ conf_get_int_value (STARTUP_FEED_ACTION, &iSetting); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "startupactionbtn")), (iSetting == 0)); /* cache size setting */ widget = liferea_dialog_lookup (pd->dialog, "itemCountBtn"); itemCount = gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (widget)); conf_get_int_value (DEFAULT_MAX_ITEMS, &iSetting); gtk_adjustment_set_value (itemCount, iSetting); /* set default update interval spin button and unit combo box */ ui_common_setup_combo_menu (liferea_dialog_lookup (pd->dialog, "globalRefreshIntervalUnitComboBox"), default_update_interval_unit_options, G_CALLBACK (on_default_update_interval_unit_changed), -1); widget = liferea_dialog_lookup (pd->dialog, "globalRefreshIntervalUnitComboBox"); conf_get_int_value (DEFAULT_UPDATE_INTERVAL, &tmp); if (tmp % 1440 == 0) { /* days */ gtk_combo_box_set_active (GTK_COMBO_BOX (widget), 2); tmp /= 1440; } else if (tmp % 60 == 0) { /* hours */ gtk_combo_box_set_active (GTK_COMBO_BOX (widget), 1); tmp /= 60; } else { /* minutes */ gtk_combo_box_set_active (GTK_COMBO_BOX (widget), 0); } widget = liferea_dialog_lookup (pd->dialog,"globalRefreshIntervalSpinButton"); gtk_spin_button_set_range (GTK_SPIN_BUTTON (widget), 0, 1000000000); gtk_spin_button_set_value (GTK_SPIN_BUTTON (widget), tmp); g_signal_connect (G_OBJECT (widget), "changed", G_CALLBACK (on_default_update_interval_value_changed), pd); /* ================== panel 2 "folders" ==================== */ g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "updateAllFavicons")), "clicked", G_CALLBACK(on_updateallfavicons_clicked), pd); conf_get_int_value (FOLDER_DISPLAY_MODE, &iSetting); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "folderdisplaybtn")), iSetting?TRUE:FALSE); conf_get_bool_value (FOLDER_DISPLAY_HIDE_READ, &bSetting); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "hidereadbtn")), bSetting?TRUE:FALSE); /* ================== panel 3 "headlines" ==================== */ conf_get_int_value (BROWSE_KEY_SETTING, &iSetting); ui_common_setup_combo_menu (liferea_dialog_lookup (pd->dialog, "skimKeyCombo"), browser_skim_key_options, G_CALLBACK (on_skim_key_changed), iSetting); conf_get_int_value (DEFAULT_VIEW_MODE, &iSetting); ui_common_setup_combo_menu (liferea_dialog_lookup (pd->dialog, "defaultViewModeCombo"), default_view_mode_options, G_CALLBACK (on_default_view_mode_changed), iSetting); /* Setup social bookmarking list */ i = 0; conf_get_str_value (SOCIAL_BM_SITE, &name); store = gtk_list_store_new (1, G_TYPE_STRING); list = bookmarkSites; while (list) { socialSitePtr siter = list->data; if (name && !strcmp (siter->name, name)) tmp = i; gtk_list_store_append (store, &treeiter); gtk_list_store_set (store, &treeiter, 0, siter->name, -1); list = g_slist_next (list); i++; } combo = GTK_COMBO_BOX (liferea_dialog_lookup (pd->dialog, "socialpopup")); g_signal_connect (G_OBJECT (combo), "changed", G_CALLBACK (on_socialsite_changed), pd); gtk_combo_box_set_model (combo, GTK_TREE_MODEL (store)); ui_common_setup_combo_text (combo, 0); gtk_combo_box_set_active (combo, tmp); /* ================== panel 4 "browser" ==================== */ /* set the inside browsing flag */ widget = liferea_dialog_lookup(pd->dialog, "browseinwindow"); conf_get_bool_value(BROWSE_INSIDE_APPLICATION, &bSetting); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), bSetting); /* set the javascript-disabled flag */ widget = liferea_dialog_lookup(pd->dialog, "disablejavascript"); conf_get_bool_value(DISABLE_JAVASCRIPT, &bSetting); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), bSetting); /* set the enable Plugins flag */ widget = liferea_dialog_lookup(pd->dialog, "enableplugins"); conf_get_bool_value(ENABLE_PLUGINS, &bSetting); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), bSetting); conf_get_str_value (BROWSER_ID, &configuredBrowser); manualBrowser = !strcmp (configuredBrowser, "manual"); g_free (configuredBrowser); gtk_combo_box_set_active (GTK_COMBO_BOX (liferea_dialog_lookup (pd->dialog, "browserpopup")), manualBrowser); conf_get_str_value (BROWSER_COMMAND, &browser_command); entry = liferea_dialog_lookup (pd->dialog, "browsercmd"); gtk_entry_set_text (GTK_ENTRY(entry), browser_command); g_free (browser_command); gtk_widget_set_sensitive (GTK_WIDGET (entry), manualBrowser); gtk_widget_set_sensitive (liferea_dialog_lookup (pd->dialog, "manuallabel"), manualBrowser); gtk_widget_set_sensitive (liferea_dialog_lookup (pd->dialog, "urlhintlabel"), manualBrowser); /* ================== panel 4 "GUI" ================ */ /* tool bar settings */ widget = liferea_dialog_lookup (pd->dialog, "hidetoolbarbtn"); conf_get_bool_value(DISABLE_TOOLBAR, &bSetting); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), bSetting); /* select currently active toolbar style option */ conf_get_str_value (TOOLBAR_STYLE, &name); for (i = 0; gui_toolbar_style_values[i] != NULL; ++i) { if (strcmp (name, gui_toolbar_style_values[i]) == 0) break; } g_free (name); /* On invalid key value: revert to default */ if (gui_toolbar_style_values[i] == NULL) i = 0; /* create toolbar style menu */ ui_common_setup_combo_menu (liferea_dialog_lookup (pd->dialog, "toolbarCombo"), gui_toolbar_style_options, G_CALLBACK (on_gui_toolbar_style_changed), i); conf_bind (CONFIRM_MARK_ALL_READ, liferea_dialog_lookup (pd->dialog, "confirmMarkAllReadButton"), "active", G_SETTINGS_BIND_DEFAULT); /* ================= panel 5 "proxy" ======================== */ #if WEBKIT_CHECK_VERSION (2, 15, 3) gtk_widget_destroy (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "proxyDisabledInfobar"))); conf_get_str_value (PROXY_HOST, &proxy_host); gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (pd->dialog, "proxyhostentry")), proxy_host); g_free (proxy_host); conf_get_int_value (PROXY_PORT, &proxy_port); proxyport = g_strdup_printf ("%d", proxy_port); gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (pd->dialog, "proxyportentry")), proxyport); g_free (proxyport); conf_get_bool_value (PROXY_USEAUTH, &enabled); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "useProxyAuth")), enabled); conf_get_str_value (PROXY_USER, &proxy_user); gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (pd->dialog, "proxyusernameentry")), proxy_user); g_free (proxy_user); conf_get_str_value (PROXY_PASSWD, &proxy_passwd); gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (pd->dialog, "proxypasswordentry")), proxy_passwd); g_free (proxy_passwd); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup(pd->dialog, "proxyauthtable")), enabled); conf_get_int_value (PROXY_DETECT_MODE, &i); switch (i) { default: case 0: /* proxy auto detect */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "proxyAutoDetectRadio")), TRUE); enabled = FALSE; break; case 1: /* no proxy */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "noProxyRadio")), TRUE); enabled = FALSE; break; case 2: /* manual proxy */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (pd->dialog, "manualProxyRadio")), TRUE); enabled = TRUE; break; } gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "proxybox")), enabled); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "proxyAutoDetectRadio")), "clicked", G_CALLBACK (on_proxyAutoDetect_clicked), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "noProxyRadio")), "clicked", G_CALLBACK (on_noProxy_clicked), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "manualProxyRadio")), "clicked", G_CALLBACK (on_manualProxy_clicked), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "proxyhostentry")), "changed", G_CALLBACK (on_proxyhostentry_changed), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "proxyportentry")), "changed", G_CALLBACK (on_proxyportentry_changed), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "proxyusernameentry")), "changed", G_CALLBACK (on_proxyusernameentry_changed), pd); g_signal_connect (G_OBJECT (liferea_dialog_lookup (pd->dialog, "proxypasswordentry")), "changed", G_CALLBACK (on_proxypasswordentry_changed), pd); #else gtk_widget_show (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "proxyDisabledInfobar"))); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "proxybox")), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "proxyAutoDetectRadio")), TRUE); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "noProxyRadio")), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "manualProxyRadio")), FALSE); #endif /* ================= panel 6 "Privacy" ======================== */ widget = liferea_dialog_lookup (pd->dialog, "readermodebtn"); conf_get_bool_value (ENABLE_READER_MODE, &bSetting); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), bSetting); widget = liferea_dialog_lookup (pd->dialog, "donottrackbtn"); conf_get_bool_value (DO_NOT_TRACK, &bSetting); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), bSetting); #if WEBKIT_CHECK_VERSION (2, 30, 0) gtk_widget_destroy (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "itpInfoBar"))); widget = liferea_dialog_lookup (pd->dialog, "itpbtn"); conf_get_bool_value (ENABLE_ITP, &bSetting); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), bSetting); #else gtk_widget_set_sensitive (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "itpbtn")), FALSE); gtk_widget_show (GTK_WIDGET (liferea_dialog_lookup (pd->dialog, "itpInfoBar"))); #endif /* ================= panel 7 "Enclosures" ======================== */ /* menu for download tool */ conf_get_int_value (DOWNLOAD_TOOL, &iSetting); ui_common_setup_combo_menu (liferea_dialog_lookup (pd->dialog, "downloadToolCombo"), enclosure_download_tool_options, G_CALLBACK (on_enclosure_download_tool_changed), iSetting); /* restore toggle */ conf_get_bool_value (DOWNLOAD_USE_CUSTOM_COMMAND, &bSetting); widget = liferea_dialog_lookup (pd->dialog, "customDownload"); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget), bSetting); gtk_widget_set_sensitive (liferea_dialog_lookup (pd->dialog, "customDownloadEntry"), bSetting); gtk_widget_set_sensitive (liferea_dialog_lookup (pd->dialog, "downloadToolCombo"), !bSetting); /* restore custom command */ conf_get_str_value (DOWNLOAD_CUSTOM_COMMAND, &custom_download_command); widget = liferea_dialog_lookup (pd->dialog, "customDownloadEntry"); gtk_entry_set_text (GTK_ENTRY (widget), custom_download_command); g_free (custom_download_command); /* set up list of configured enclosure types */ treestore = gtk_tree_store_new (FTS_LEN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER); list = (GSList *)enclosure_mime_types_get (); while (list) { GtkTreeIter *newIter = g_new0 (GtkTreeIter, 1); gtk_tree_store_append (treestore, newIter, NULL); gtk_tree_store_set (treestore, newIter, FTS_TYPE, (NULL != ((encTypePtr)(list->data))->mime)?((encTypePtr)(list->data))->mime:((encTypePtr)(list->data))->extension, FTS_CMD, ((encTypePtr)(list->data))->cmd, FTS_PTR, list->data, -1); list = g_slist_next (list); } widget = liferea_dialog_lookup (pd->dialog, "enc_action_view"); gtk_tree_view_set_model (GTK_TREE_VIEW (widget), GTK_TREE_MODEL (treestore)); column = gtk_tree_view_column_new_with_attributes (_("Type"), gtk_cell_renderer_text_new (), "text", FTS_TYPE, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (widget), column); gtk_tree_view_column_set_sort_column_id (column, FTS_TYPE); column = gtk_tree_view_column_new_with_attributes (_("Program"), gtk_cell_renderer_text_new (), "text", FTS_CMD, NULL); gtk_tree_view_column_set_sort_column_id (column, FTS_CMD); gtk_tree_view_append_column (GTK_TREE_VIEW(widget), column); gtk_tree_selection_set_mode (gtk_tree_view_get_selection (GTK_TREE_VIEW(widget)), GTK_SELECTION_SINGLE); g_signal_connect_object (pd->dialog, "destroy", G_CALLBACK (preferences_dialog_destroy_cb), pd, 0); gtk_widget_show_all (pd->dialog); gtk_window_present (GTK_WINDOW (pd->dialog)); } void preferences_dialog_open (void) { if (prefdialog) { gtk_widget_show (prefdialog->dialog); return; } g_object_new (PREFERENCES_DIALOG_TYPE, NULL); } liferea-1.13.7/src/ui/preferences_dialog.h000066400000000000000000000053551415350204600204170ustar00rootroot00000000000000/** * @file preferences_dialog.h Liferea preferences * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _PREFERENCES_DIALOG_H #define _PREFERENCES_DIALOG_H #include G_BEGIN_DECLS #define PREFERENCES_DIALOG_TYPE (preferences_dialog_get_type ()) G_DECLARE_FINAL_TYPE (PreferencesDialog, preferences_dialog, PREFERENCES, DIALOG, GObject) /** * prefs_get_download_command: * * Returns: (transfer full): The download command. */ gchar * prefs_get_download_command (void); /** * preferences_dialog_open: * Show the preferences dialog. */ void preferences_dialog_open (void); /* functions used in glade/prefs.ui */ void on_folderdisplaybtn_toggled (GtkToggleButton *togglebutton, gpointer user_data); void on_folderhidereadbtn_toggled (GtkToggleButton *togglebutton, gpointer user_data); void on_popupwindowsoptionbtn_clicked (GtkButton *button, gpointer user_data); void on_startupactionbtn_toggled (GtkButton *button, gpointer user_data); void on_browsercmd_changed (GtkEditable *editable, gpointer user_data); void on_openlinksinsidebtn_clicked (GtkToggleButton *button, gpointer user_data); void on_disablejavascript_toggled (GtkToggleButton *togglebutton, gpointer user_data); void on_enableplugins_toggled (GtkToggleButton *togglebutton, gpointer user_data); void on_itemCountBtn_value_changed (GtkSpinButton *spinbutton, gpointer user_data); void on_default_update_interval_value_changed (GtkSpinButton *spinbutton, gpointer user_data); void on_useProxyAuth_toggled (GtkToggleButton *button, gpointer user_data); void on_enclosure_download_custom_command_changed (GtkEditable *entry, gpointer user_data); void on_enclosure_download_predefined_toggled (GtkToggleButton *button, gpointer user_data); void on_enc_action_change_btn_clicked (GtkButton *button, gpointer user_data); void on_enc_action_remove_btn_clicked (GtkButton *button, gpointer user_data); void on_hidetoolbar_toggled (GtkToggleButton *button, gpointer user_data); G_END_DECLS #endif liferea-1.13.7/src/ui/rule_editor.c000066400000000000000000000221761415350204600171070ustar00rootroot00000000000000/** * @file rule_editor.c rule editing dialog functionality * * Copyright (C) 2008-2020 Lars Windolf * Copyright (C) 2009 Hubert Figuiere * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/rule_editor.h" #include "ui/ui_common.h" #include "rule.h" /* A 'rule editor' is a dialog allowing editing arbitrary filtering 'rules'. The rules edited are loaded from an 'item set' which can belong to a 'search folder' or an 'item list' filter. The rule editing is independant of any search folder handling. */ struct _RuleEditor { GObject parentInstance; GtkWidget *root; /**< root widget */ GSList *newRules; /**< new list of rules currently in editing */ }; struct changeRequest { GtkWidget *hbox; /**< used for remove button (optional) */ RuleEditor *editor; /**< the rule editor */ gint rule; /**< used for rule type change (optional) */ GtkWidget *paramHBox; /**< used for rule type change (optional) */ }; G_DEFINE_TYPE (RuleEditor, rule_editor, G_TYPE_OBJECT); static void rule_editor_finalize (GObject *object) { RuleEditor *re = RULE_EDITOR (object); /* delete rules */ GSList *iter = re->newRules; while (iter) { rule_free ((rulePtr)iter->data); iter = g_slist_next (iter); } g_slist_free (re->newRules); re->newRules = NULL; } static void rule_editor_class_init (RuleEditorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = rule_editor_finalize; } static void rule_editor_init (RuleEditor *re) { } static void rule_editor_destroy_param_widget (GtkWidget *widget, gpointer data) { gtk_widget_destroy(widget); } static void on_rulevalue_changed (GtkEditable *editable, gpointer user_data) { rule_set_value ((rulePtr)user_data, gtk_editable_get_chars (editable, 0, -1)); } /* callback for rule additive option menu */ static void on_rule_changed_additive (GtkComboBox *optionmenu, gpointer user_data) { rulePtr rule = (rulePtr)user_data; gint active = gtk_combo_box_get_active (optionmenu); rule->additive = ((active==0) ? TRUE : FALSE); } /* sets up the widgets for a single rule */ static void rule_editor_setup_widgets (struct changeRequest *changeRequest, rulePtr rule) { GtkWidget *widget; ruleInfoPtr ruleInfo; ruleInfo = g_slist_nth_data (rule_get_available_rules (), changeRequest->rule); g_object_set_data (G_OBJECT (changeRequest->paramHBox), "rule", rule); /* remove of old widgets */ gtk_container_foreach (GTK_CONTAINER (changeRequest->paramHBox), rule_editor_destroy_param_widget, NULL); /* add popup menu for selection of positive or negative logic */ widget = gtk_combo_box_text_new (); gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (widget), ruleInfo->positive); gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (widget), ruleInfo->negative); gtk_combo_box_set_active ((GtkComboBox*)widget, (rule->additive)?0:1); g_signal_connect (G_OBJECT (widget), "changed", G_CALLBACK (on_rule_changed_additive), rule); gtk_widget_show_all (widget); gtk_box_pack_start (GTK_BOX (changeRequest->paramHBox), widget, FALSE, FALSE, 0); /* add new value entry if needed */ if (ruleInfo->needsParameter) { widget = gtk_entry_new (); gtk_entry_set_text (GTK_ENTRY (widget), rule->value); gtk_widget_show (widget); g_signal_connect (G_OBJECT (widget), "changed", G_CALLBACK(on_rulevalue_changed), rule); gtk_box_pack_start (GTK_BOX (changeRequest->paramHBox), widget, FALSE, FALSE, 0); } else { /* nothing needs to be added */ } } static void do_ruletype_changed (struct changeRequest *changeRequest) { ruleInfoPtr ruleInfo; rulePtr rule; rule = g_object_get_data (G_OBJECT (changeRequest->paramHBox), "rule"); if (rule) { changeRequest->editor->newRules = g_slist_remove (changeRequest->editor->newRules, rule); rule_free (rule); } ruleInfo = g_slist_nth_data (rule_get_available_rules (), changeRequest->rule); rule = rule_new (ruleInfo->ruleId, "", TRUE); changeRequest->editor->newRules = g_slist_append (changeRequest->editor->newRules, rule); rule_editor_setup_widgets (changeRequest, rule); } /* callback for rule type option menu */ static void on_ruletype_changed (GtkComboBox *optionmenu, gpointer user_data) { struct changeRequest *changeRequest = NULL; GtkTreeIter iter; if (gtk_combo_box_get_active_iter (optionmenu, &iter)) { gtk_tree_model_get (gtk_combo_box_get_model (optionmenu), &iter, 1, &changeRequest, -1); if (changeRequest) do_ruletype_changed (changeRequest); } } /* callback for each rules remove button */ static void on_ruleremove_clicked (GtkButton *button, gpointer user_data) { struct changeRequest *changeRequest = (struct changeRequest *)user_data; rulePtr rule; rule = g_object_get_data (G_OBJECT (changeRequest->paramHBox), "rule"); if (rule) { changeRequest->editor->newRules = g_slist_remove (changeRequest->editor->newRules, rule); rule_free(rule); } gtk_container_remove (GTK_CONTAINER (changeRequest->editor->root), changeRequest->hbox); g_free (changeRequest); } void rule_editor_add_rule (RuleEditor *re, rulePtr rule) { GSList *ruleIter; GtkWidget *hbox, *hbox2, *widget; GtkListStore *store; struct changeRequest *changeRequest, *selected = NULL; gint i = 0, active = 0; hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2); /* hbox to contain all rule widgets */ hbox2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2); /* another hbox where the rule specific widgets are added */ /* set up the rule type selection popup */ store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_POINTER); ruleIter = rule_get_available_rules (); while (ruleIter) { ruleInfoPtr ruleInfo = (ruleInfoPtr)ruleIter->data; GtkTreeIter iter; /* we add a change request to each popup option */ changeRequest = g_new0 (struct changeRequest, 1); changeRequest->paramHBox = hbox2; changeRequest->rule = i; changeRequest->editor = re; if (0 == i) selected = changeRequest; /* build the menu option */ gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, 0, ruleInfo->title, 1, changeRequest, -1); if (rule) { if (ruleInfo == rule->ruleInfo) { selected = changeRequest; active = i; } } ruleIter = g_slist_next (ruleIter); i++; } widget = gtk_combo_box_new (); ui_common_setup_combo_text (GTK_COMBO_BOX (widget), 0); gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (store)); gtk_combo_box_set_active (GTK_COMBO_BOX (widget), active); g_signal_connect (G_OBJECT (widget), "changed", G_CALLBACK (on_ruletype_changed), NULL); gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (hbox), hbox2, FALSE, FALSE, 0); if (!rule) { /* fake a rule type change to initialize parameter widgets */ do_ruletype_changed (selected); } else { rulePtr newRule = rule_new (rule->ruleInfo->ruleId, rule->value, rule->additive); /* set up widgets with existing rule type and value */ rule_editor_setup_widgets (selected, newRule); /* add the rule to the list of new rules */ re->newRules = g_slist_append (re->newRules, newRule); } /* add remove button */ changeRequest = g_new0 (struct changeRequest, 1); changeRequest->hbox = hbox; changeRequest->paramHBox = hbox2; changeRequest->editor = re; widget = gtk_button_new_with_label ("Remove"); gtk_box_pack_end (GTK_BOX (hbox), widget, FALSE, FALSE, 0); g_signal_connect (G_OBJECT (widget), "clicked", G_CALLBACK (on_ruleremove_clicked), changeRequest); /* and insert everything in the dialog */ gtk_widget_show_all (hbox); gtk_box_pack_start (GTK_BOX (re->root), hbox, FALSE, TRUE, 0); } RuleEditor * rule_editor_new (itemSetPtr itemset) { RuleEditor *re; GSList *iter; re = RULE_EDITOR (g_object_new (RULE_EDITOR_TYPE, NULL)); /* Set up rule list vbox */ re->root = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); /* load rules into dialog */ iter = itemset->rules; while (iter) { rule_editor_add_rule (re, (rulePtr)(iter->data)); iter = g_slist_next (iter); } gtk_widget_show_all (re->root); return re; } void rule_editor_save (RuleEditor *re, itemSetPtr itemset) { GSList *iter; /* delete all old rules */ iter = itemset->rules; while (iter) { rule_free ((rulePtr)iter->data); iter = g_slist_next (iter); } g_slist_free (itemset->rules); itemset->rules = NULL; /* and add all rules from editor */ iter = re->newRules; while (iter) { rulePtr rule = (rulePtr)iter->data; itemset_add_rule (itemset, rule->ruleInfo->ruleId, rule->value, rule->additive); iter = g_slist_next (iter); } } GtkWidget * rule_editor_get_widget (RuleEditor *re) { return re->root; } liferea-1.13.7/src/ui/rule_editor.h000066400000000000000000000041651415350204600171120ustar00rootroot00000000000000/** * @file rule_editor.h rule editing dialog functionality * * Copyright (C) 2008-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _RULE_EDITOR_H #define _RULE_EDITOR_H #include #include "rule.h" #include "itemset.h" G_BEGIN_DECLS #define RULE_EDITOR_TYPE (rule_editor_get_type ()) G_DECLARE_FINAL_TYPE (RuleEditor, rule_editor, RULE, EDITOR, GObject) /** * rule_editor_new: * Create a new rule editor widget set. Loads all rules * of the itemset passed. * * @itemset: the itemset with the rules to load * * Returns: (transfer none): a new RuleEditor instance */ RuleEditor * rule_editor_new (itemSetPtr itemset); /** * rule_editor_add_rule: * This method is used to add another rule to a rule editor. * The rule parameter might be NULL to create new rules or a * pointer to an existing rule to load it into the dialog. * * @re: the rule editor * @rule: the rule to add */ void rule_editor_add_rule (RuleEditor *re, rulePtr rule); /** * rule_editor_save: * Saves the editing state of the rule editor to * the given search folder. * * @re: the rule editor * @itemset: the item set to set the rules to */ void rule_editor_save (RuleEditor *re, itemSetPtr itemset); /** * rule_editor_get_widget: * Get the root widget of the rule editor. * * @re: the rule editor * * Returns: (transfer none): the root widget */ GtkWidget * rule_editor_get_widget (RuleEditor *re); G_END_DECLS #endif liferea-1.13.7/src/ui/search_dialog.c000066400000000000000000000200421415350204600173440ustar00rootroot00000000000000/** * @file search_dialog.c Search engine subscription dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/search_dialog.h" #include #include "common.h" #include "feed.h" #include "feedlist.h" #include "itemlist.h" #include "node.h" #include "node_view.h" #include "rule.h" #include "vfolder.h" #include "vfolder_loader.h" #include "ui/item_list_view.h" #include "ui/itemview.h" #include "ui/liferea_dialog.h" #include "ui/rule_editor.h" #include "ui/feed_list_view.h" /* shared functions */ static void search_clean_results (vfolderPtr vfolder) { if (!vfolder) return; /* Clean up old search result data and display... */ if (vfolder->node == itemlist_get_displayed_node ()) itemlist_unload (FALSE); /* FIXME: Don't simply free the result search folder as the search query might still be active. Instead g_object_unref() a search result object! For now we leak the node to avoid crashes. */ //node_free (vfolder->node); } static void search_load_results (vfolderPtr searchResult) { feed_list_view_select (NULL); itemlist_add_search_result (vfolder_loader_new (searchResult->node)); } /* complex search dialog */ static SearchDialog *search = NULL; struct _SearchDialog { GObject parentInstance; GtkWidget *dialog; /**< the dialog widget */ RuleEditor *re; /**< search folder rule editor widget set */ vfolderPtr vfolder; /**< temporary search folder representing the search result */ }; G_DEFINE_TYPE (SearchDialog, search_dialog, G_TYPE_OBJECT); static void search_dialog_finalize (GObject *object) { SearchDialog *sd = SEARCH_DIALOG (object); search = NULL; gtk_widget_destroy (sd->dialog); search_clean_results (sd->vfolder); } static void search_dialog_class_init (SearchDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = search_dialog_finalize; } static void search_dialog_init (SearchDialog *sd) { sd->vfolder = vfolder_new (node_new (vfolder_get_node_type ())); node_set_title (sd->vfolder->node, _("Saved Search")); } static void on_search_dialog_response (GtkDialog *dialog, gint responseId, gpointer user_data) { SearchDialog *sd = (SearchDialog *)user_data; vfolderPtr vfolder = sd->vfolder; if (1 == responseId) { /* Search */ search_clean_results (vfolder); sd->vfolder = vfolder = vfolder_new (node_new (vfolder_get_node_type ())); rule_editor_save (sd->re, vfolder->itemset); vfolder->itemset->anyMatch = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (sd->dialog, "anyRuleRadioBtn2"))); search_load_results (vfolder); } if (2 == responseId) { /* + Search Folder */ rule_editor_save (sd->re, vfolder->itemset); vfolder->itemset->anyMatch = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (sd->dialog, "anyRuleRadioBtn2"))); nodePtr node = vfolder->node; sd->vfolder = NULL; feedlist_node_added (node); } if (1 != responseId) g_object_unref (sd); } /* callback copied from search_folder_dialog.c */ static void on_addrulebtn_clicked (GtkButton *button, gpointer user_data) { SearchDialog *sd = SEARCH_DIALOG (user_data); rule_editor_add_rule (sd->re, NULL); } SearchDialog * search_dialog_open (const gchar *query) { SearchDialog *sd; if (search) return search; sd = SEARCH_DIALOG (g_object_new (SEARCH_DIALOG_TYPE, NULL)); sd->dialog = liferea_dialog_new ("search"); if (query) itemset_add_rule (sd->vfolder->itemset, "exact", query, TRUE); sd->re = rule_editor_new (sd->vfolder->itemset); /* Note: the following code is somewhat duplicated from search_folder_dialog.c */ /* Setting default rule match type */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (sd->dialog, "anyRuleRadioBtn2")), TRUE); /* Set up rule list vbox */ gtk_container_add (GTK_CONTAINER (liferea_dialog_lookup (sd->dialog, "ruleview_search_dialog")), rule_editor_get_widget (sd->re)); /* bind buttons */ g_signal_connect (liferea_dialog_lookup (sd->dialog, "addrulebtn2"), "clicked", G_CALLBACK (on_addrulebtn_clicked), sd); g_signal_connect (G_OBJECT (sd->dialog), "response", G_CALLBACK (on_search_dialog_response), sd); gtk_widget_show_all (sd->dialog); search = sd; return sd; } /* simple search dialog */ static SimpleSearchDialog *simpleSearch = NULL; struct _SimpleSearchDialog { GObject parentInstance; GtkWidget *dialog; /**< the dialog widget */ GtkWidget *query; /**< entry widget for the search query */ vfolderPtr vfolder; /**< temporary search folder representing the search result */ }; G_DEFINE_TYPE (SimpleSearchDialog, simple_search_dialog, G_TYPE_OBJECT); static void simple_search_dialog_finalize (GObject *object) { SimpleSearchDialog *ssd = SIMPLE_SEARCH_DIALOG (object); simpleSearch = NULL; gtk_widget_destroy (ssd->dialog); search_clean_results (ssd->vfolder); } static void simple_search_dialog_class_init (SimpleSearchDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = simple_search_dialog_finalize; } static void simple_search_dialog_init (SimpleSearchDialog *ssd) { } static void on_simple_search_dialog_response (GtkDialog *dialog, gint responseId, gpointer user_data) { SimpleSearchDialog *ssd = (SimpleSearchDialog *)user_data; const gchar *searchString; vfolderPtr vfolder = ssd->vfolder; searchString = gtk_entry_get_text (GTK_ENTRY (ssd->query)); if (1 == responseId) { /* Search */ search_clean_results (vfolder); /* Create new search... */ ssd->vfolder = vfolder = vfolder_new (node_new (vfolder_get_node_type ())); node_set_title (vfolder->node, searchString); itemset_add_rule (vfolder->itemset, "exact", searchString, TRUE); search_load_results (vfolder); } if (2 == responseId) /* Advanced... */ search_dialog_open (searchString); /* Do not close the dialog when "just" searching. The user should click "Close" to close the dialog to be able to do subsequent searches... */ if (1 != responseId) g_object_unref (ssd); } static void on_searchentry_activated (GtkEntry *entry, gpointer user_data) { SimpleSearchDialog *ssd = SIMPLE_SEARCH_DIALOG (user_data); /* simulate search response */ on_simple_search_dialog_response (GTK_DIALOG (ssd->dialog), 1, ssd); } static void on_searchentry_changed (GtkEditable *editable, gpointer user_data) { SimpleSearchDialog *ssd = SIMPLE_SEARCH_DIALOG (user_data); gchar *searchString; /* just to disable the start search button when search string is empty... */ searchString = gtk_editable_get_chars (editable, 0, -1); gtk_widget_set_sensitive (liferea_dialog_lookup (ssd->dialog, "searchstartbtn"), searchString && (0 < strlen (searchString))); } SimpleSearchDialog * simple_search_dialog_open (void) { SimpleSearchDialog *ssd; if (simpleSearch) return simpleSearch; ssd = SIMPLE_SEARCH_DIALOG (g_object_new (SIMPLE_SEARCH_DIALOG_TYPE, NULL)); ssd->dialog = liferea_dialog_new ("simple_search"); ssd->query = liferea_dialog_lookup (ssd->dialog, "searchentry"); gtk_window_set_focus (GTK_WINDOW (ssd->dialog), ssd->query); g_signal_connect (G_OBJECT (ssd->dialog), "response", G_CALLBACK (on_simple_search_dialog_response), ssd); g_signal_connect (G_OBJECT (ssd->query), "changed", G_CALLBACK (on_searchentry_changed), ssd); g_signal_connect (G_OBJECT (ssd->query), "activate", G_CALLBACK (on_searchentry_activated), ssd); gtk_widget_show_all (ssd->dialog); simpleSearch = ssd; return ssd; } liferea-1.13.7/src/ui/search_dialog.h000066400000000000000000000032441415350204600173560ustar00rootroot00000000000000/** * @file search_dialog.h Search engine subscription dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _SEARCH_DIALOG_H #define _SEARCH_DIALOG_H #include G_BEGIN_DECLS #define SEARCH_DIALOG_TYPE (search_dialog_get_type ()) G_DECLARE_FINAL_TYPE (SearchDialog, search_dialog, SEARCH, DIALOG, GObject) /** * search_dialog_open: * Open the complex singleton search dialog. * * @query: optional query string to create a rule for * * Returns: (transfer none): the new dialog */ SearchDialog * search_dialog_open (const gchar *query); #define SIMPLE_SEARCH_DIALOG_TYPE (simple_search_dialog_get_type ()) G_DECLARE_FINAL_TYPE (SimpleSearchDialog, simple_search_dialog, SIMPLE_SEARCH, DIALOG, GObject) /** * simple_search_dialog_open: * Open the simple (one keyword entry) singleton search dialog. * * Returns: (transfer none): the new dialog */ SimpleSearchDialog * simple_search_dialog_open (void); G_END_DECLS #endif liferea-1.13.7/src/ui/search_folder_dialog.c000066400000000000000000000105471415350204600207100ustar00rootroot00000000000000/** * @file search-folder-dialog.c Search folder properties dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/search_folder_dialog.h" #include "feedlist.h" #include "itemlist.h" #include "vfolder.h" #include "ui/feed_list_view.h" #include "ui/itemview.h" #include "ui/liferea_dialog.h" #include "ui/rule_editor.h" struct _SearchFolderDialog { GObject parentInstance; RuleEditor *re; /**< dynamically created rule editing widget subset */ GtkWidget *nameEntry; /**< search folder title entry */ nodePtr node; /**< search folder feed list node */ vfolderPtr vfolder; /**< the search folder */ }; G_DEFINE_TYPE (SearchFolderDialog, search_folder_dialog, G_TYPE_OBJECT); static void search_folder_dialog_finalize (GObject *object) { SearchFolderDialog *sfd = SEARCH_FOLDER_DIALOG (object); g_object_unref (sfd->re); } static void search_folder_dialog_class_init (SearchFolderDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = search_folder_dialog_finalize; } static void search_folder_dialog_init (SearchFolderDialog *sfd) { } static void on_propdialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { SearchFolderDialog *sfd = SEARCH_FOLDER_DIALOG (user_data); if (response_id == GTK_RESPONSE_OK) { /* save new search folder settings */ node_set_title (sfd->node, gtk_entry_get_text (GTK_ENTRY (sfd->nameEntry))); rule_editor_save (sfd->re, sfd->vfolder->itemset); sfd->vfolder->itemset->anyMatch = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "anyRuleRadioBtn"))); sfd->vfolder->unreadOnly = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "hideReadItemsBtn"))); /* update search folder */ itemview_clear (); vfolder_reset (sfd->vfolder); itemlist_unload (FALSE); /* If we are finished editing a new search folder add it to the feed list */ if (!sfd->node->parent) feedlist_node_added (sfd->node); feed_list_view_update_node (sfd->node->id); /* rebuild the search folder */ vfolder_rebuild (sfd->node); } gtk_widget_destroy (GTK_WIDGET (dialog)); } static void on_addrulebtn_clicked (GtkButton *button, gpointer user_data) { SearchFolderDialog *sfd = SEARCH_FOLDER_DIALOG (user_data); rule_editor_add_rule (sfd->re, NULL); } /** Use to create new search folders and to edit existing ones */ SearchFolderDialog * search_folder_dialog_new (nodePtr node) { GtkWidget *dialog; SearchFolderDialog *sfd; sfd = SEARCH_FOLDER_DIALOG (g_object_new (SEARCH_FOLDER_DIALOG_TYPE, NULL)); sfd->node = node; sfd->vfolder = (vfolderPtr)node->data; sfd->re = rule_editor_new (sfd->vfolder->itemset); /* Create the dialog */ dialog = liferea_dialog_new ("search_folder"); /* Setup search folder name */ sfd->nameEntry = liferea_dialog_lookup (dialog, "searchNameEntry"); gtk_entry_set_text (GTK_ENTRY (sfd->nameEntry), node_get_title (node)); /* Set up rule match type */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (dialog, sfd->vfolder->itemset->anyMatch?"anyRuleRadioBtn":"allRuleRadioBtn")), TRUE); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (dialog, "hideReadItemsBtn")), sfd->vfolder->unreadOnly); /* Set up rule list vbox */ gtk_container_add (GTK_CONTAINER (liferea_dialog_lookup (dialog, "ruleview_vfolder_dialog")), rule_editor_get_widget (sfd->re)); /* bind buttons */ g_signal_connect (liferea_dialog_lookup (dialog, "addrulebtn"), "clicked", G_CALLBACK (on_addrulebtn_clicked), sfd); g_signal_connect (G_OBJECT (dialog), "response", G_CALLBACK (on_propdialog_response), sfd); return sfd; } liferea-1.13.7/src/ui/search_folder_dialog.h000066400000000000000000000026201415350204600207060ustar00rootroot00000000000000/** * @file search_folder_dialog.h Search folder properties dialog * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _SEARCH_FOLDER_DIALOG_H #define _SEARCH_FOLDER_DIALOG_H #include #include "node.h" G_BEGIN_DECLS #define SEARCH_FOLDER_DIALOG_TYPE (search_folder_dialog_get_type ()) G_DECLARE_FINAL_TYPE (SearchFolderDialog, search_folder_dialog, SEARCH_FOLDER, DIALOG, GObject) /** * search_folder_dialog_new: * Open a new search folder properties dialog * for an existing search folder. * * @node: the search folder node * * Returns: (transfer none): a new dialog */ SearchFolderDialog * search_folder_dialog_new (nodePtr node); G_END_DECLS #endif liferea-1.13.7/src/ui/subscription_dialog.c000066400000000000000000000705601415350204600206350ustar00rootroot00000000000000/** * @file subscription_dialog.c property dialog for feed subscriptions * * Copyright (C) 2004-2018 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/subscription_dialog.h" #include #include #include "common.h" #include "conf.h" #include "db.h" #include "debug.h" #include "feed.h" #include "feedlist.h" #include "node.h" #include "update.h" #include "ui/feed_list_view.h" #include "ui/liferea_dialog.h" #include "ui/ui_common.h" /* Note: these update interval literals should be kept in sync with the ones in ui_prefs.c! */ static const gchar * default_update_interval_unit_options[] = { N_("minutes"), N_("hours"), N_("days"), NULL }; /* Common dialog data fields */ typedef struct ui_data { gint selector; /* Desiginates which fileselection dialog box is open. Set to 'u' for source Set to 'f' for filter */ GtkWidget *dialog; GtkWidget *feedNameEntry; GtkWidget *refreshInterval; GtkWidget *refreshIntervalUnit; GtkWidget *sourceEntry; GtkWidget *selectFile; GtkWidget *fileRadio; GtkWidget *urlRadio; GtkWidget *cmdRadio; GtkWidget *authcheckbox; GtkWidget *credTable; GtkWidget *username; GtkWidget *password; } dialogData; struct _SubscriptionPropDialog { GObject parentInstance; subscriptionPtr subscription; /** used only for "properties" dialog */ dialogData ui_data; }; struct _NewSubscriptionDialog { GObject parentInstance; subscriptionPtr subscription; /** used only for "properties" dialog */ dialogData ui_data; }; struct _SimpleSubscriptionDialog { GObject parentInstance; subscriptionPtr subscription; /** used only for "properties" dialog */ dialogData ui_data; }; /* properties dialog */ G_DEFINE_TYPE (SubscriptionPropDialog, subscription_prop_dialog, G_TYPE_OBJECT); static void subscription_prop_dialog_finalize (GObject *object) { SubscriptionPropDialog *spd = SUBSCRIPTION_PROP_DIALOG (object); gtk_widget_destroy (spd->ui_data.dialog); } static void subscription_prop_dialog_class_init (SubscriptionPropDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = subscription_prop_dialog_finalize; } static gchar * ui_subscription_create_url (gchar *url, gboolean auth, const gchar *username, const gchar *password) { gchar *source = NULL; gchar *str, *tmp2; /* First, strip leading and trailing whitespace */ str = g_strstrip(url); /* Add https:// if needed */ if (strstr(str, "://") == NULL) { tmp2 = g_strdup_printf("https://%s",str); g_free(str); str = tmp2; } /* Add trailing / if needed */ if (strstr(strstr(str, "://") + 3, "/") == NULL) { tmp2 = g_strdup_printf("%s/", str); g_free(str); str = tmp2; } /* Use the values in the textboxes if also specified in the URL! */ if (auth) { xmlURIPtr uri = xmlParseURI(str); if (uri != NULL) { xmlChar *sourceUrl; xmlFree(uri->user); uri->user = g_strdup_printf("%s:%s", username, password); sourceUrl = xmlSaveUri(uri); source = g_strdup((gchar *) sourceUrl); g_free(uri->user); uri->user = NULL; xmlFree(sourceUrl); xmlFreeURI(uri); } else source = g_strdup(str); } else { source = g_strdup(str); } g_free(str); return source; } static gchar * ui_subscription_dialog_decode_source (dialogData *ui_data) { gchar *source = NULL; if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->fileRadio))) source = g_strdup (gtk_entry_get_text (GTK_ENTRY (ui_data->sourceEntry))); else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->urlRadio))) source = ui_subscription_create_url (g_strdup (gtk_entry_get_text (GTK_ENTRY (ui_data->sourceEntry))), ui_data->authcheckbox && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->authcheckbox)), ui_data->username?gtk_entry_get_text (GTK_ENTRY (ui_data->username)):NULL, ui_data->password?gtk_entry_get_text (GTK_ENTRY (ui_data->password)):NULL); else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->cmdRadio))) source = g_strdup_printf ("|%s", gtk_entry_get_text (GTK_ENTRY (ui_data->sourceEntry))); return source; } static void on_propdialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { SubscriptionPropDialog *spd = (SubscriptionPropDialog *)user_data; if (response_id == GTK_RESPONSE_OK) { gchar *newSource; const gchar *newFilter; gboolean needsUpdate = FALSE; subscriptionPtr subscription = spd->subscription; nodePtr node = spd->subscription->node; feedPtr feed = (feedPtr)node->data; if (SUBSCRIPTION_TYPE(subscription) == feed_get_subscription_type ()) { /* "General" */ node_set_title(node, gtk_entry_get_text(GTK_ENTRY(spd->ui_data.feedNameEntry))); /* Source */ newSource = ui_subscription_dialog_decode_source(&(spd->ui_data)); /* Filter handling */ newFilter = gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (spd->ui_data.dialog, "filterEntry"))); if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "filterCheckbox"))) && strcmp(newFilter,"")) { /* Maybe this should be a test to see if the file exists? */ if (subscription_get_filter(subscription) == NULL || strcmp(newFilter, subscription_get_filter(subscription))) { subscription_set_filter(subscription, newFilter); needsUpdate = TRUE; } } else { if (subscription_get_filter(subscription)) { subscription_set_filter(subscription, NULL); needsUpdate = TRUE; } } /* if URL has changed... */ if (strcmp(newSource, subscription_get_source(subscription))) { subscription_set_source(subscription, newSource); needsUpdate = TRUE; } g_free(newSource); /* Update interval handling */ if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "updateIntervalNever")))) subscription_set_update_interval (subscription, -2); else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "updateIntervalDefault")))) subscription_set_update_interval (subscription, -1); else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "updateIntervalSpecific")))) { gint intervalUnit = gtk_combo_box_get_active (GTK_COMBO_BOX (spd->ui_data.refreshIntervalUnit)); gint updateInterval = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spd->ui_data.refreshInterval)); if (intervalUnit == 1) updateInterval *= 60; /* hours */ if (intervalUnit == 2) updateInterval *= 1440; /* days */ subscription_set_update_interval (subscription, updateInterval); db_subscription_update (subscription); } } /* "Archive" handling */ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "feedCacheDefault")))) feed->cacheLimit = CACHE_DEFAULT; else if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "feedCacheDisable")))) feed->cacheLimit = CACHE_DISABLE; else if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "feedCacheUnlimited")))) feed->cacheLimit = CACHE_UNLIMITED; else if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "feedCacheLimited")))) feed->cacheLimit = gtk_spin_button_get_value(GTK_SPIN_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "cacheItemLimit"))); if (SUBSCRIPTION_TYPE(subscription) == feed_get_subscription_type ()) { /* "Download" Options */ subscription->updateOptions->dontUseProxy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET(dialog), "dontUseProxyCheck"))); } /* "Advanced" options */ feed->encAutoDownload = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "enclosureDownloadCheck"))); node->loadItemLink = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "loadItemLinkCheck"))); feed->ignoreComments = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "ignoreCommentFeeds"))); feed->markAsRead = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "markAsReadCheck"))); feed->html5Extract = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (GTK_WIDGET (dialog), "html5ExtractCheck"))); feed_list_view_update_node (node->id); feedlist_schedule_save (); db_subscription_update (subscription); if (needsUpdate) subscription_update (subscription, FEED_REQ_PRIORITY_HIGH); } g_object_unref(spd); } static void on_feed_prop_filtercheck (GtkToggleButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gboolean filter = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (ui_data->dialog, "filterCheckbox"))); gtk_widget_set_sensitive (liferea_dialog_lookup (ui_data->dialog, "filterbox"), filter); } static void ui_subscription_prop_enable_httpauth (dialogData *ui_data, gboolean enable) { gboolean on; if (ui_data->authcheckbox) { on = enable && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->authcheckbox)); gtk_widget_set_sensitive (ui_data->authcheckbox, enable); gtk_widget_set_sensitive (ui_data->credTable, on); } } static void on_feed_prop_authcheck (GtkToggleButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gboolean url = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->urlRadio)); ui_subscription_prop_enable_httpauth (ui_data, url); } static void on_feed_prop_url_radio (GtkToggleButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gboolean url = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->urlRadio)); gboolean file = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->fileRadio)); gboolean cmd = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ui_data->cmdRadio)); ui_subscription_prop_enable_httpauth (ui_data, url); gtk_widget_set_sensitive (ui_data->selectFile, file || cmd); } static void on_selectfileok_clicked (const gchar *filename, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gchar *utfname; if (!filename) return; utfname = g_filename_to_utf8 (filename, -1, NULL, NULL, NULL); if (utfname) { if (ui_data->selector == 'u') gtk_entry_set_text (GTK_ENTRY (ui_data->sourceEntry), utfname); else gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (ui_data->dialog, "filterEntry")), utfname); } g_free (utfname); } static void on_selectfile_pressed (GtkButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; const gchar *utfname; gchar *name; if (GTK_WIDGET (button) == ui_data->selectFile) { ui_data->selector = 'u'; utfname = gtk_entry_get_text (GTK_ENTRY (ui_data->sourceEntry)); } else { ui_data->selector = 'f'; utfname = gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (ui_data->dialog, "filterEntry"))); } name = g_filename_from_utf8 (utfname, -1, NULL, NULL, NULL); ui_choose_file (_("Choose File"), _("_Open"), FALSE, on_selectfileok_clicked, name, NULL, NULL, NULL, ui_data); g_free (name); } static void on_feed_prop_cache_radio (GtkToggleButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gboolean limited = gtk_toggle_button_get_active (button); gtk_widget_set_sensitive (liferea_dialog_lookup (GTK_WIDGET (ui_data->dialog), "cacheItemLimit"), limited); } static void on_feed_prop_update_radio (GtkToggleButton *button, gpointer user_data) { dialogData *ui_data = (dialogData *)user_data; gboolean limited = gtk_toggle_button_get_active (button); gtk_widget_set_sensitive (ui_data->refreshInterval, limited); gtk_widget_set_sensitive (ui_data->refreshIntervalUnit, limited); } static void subscription_prop_dialog_load (SubscriptionPropDialog *spd, subscriptionPtr subscription) { gint interval; gint default_update_interval; gint defaultInterval, spinSetInterval; gchar *defaultIntervalStr; nodePtr node = subscription->node; feedPtr feed = (feedPtr)node->data; spd->subscription = subscription; /* General */ gtk_entry_set_text (GTK_ENTRY (spd->ui_data.feedNameEntry), node_get_title (node)); spd->ui_data.refreshInterval = liferea_dialog_lookup (spd->ui_data.dialog, "refreshIntervalSpinButton"); interval = subscription_get_update_interval (subscription); defaultInterval = subscription_get_default_update_interval (subscription); conf_get_int_value (DEFAULT_UPDATE_INTERVAL, &default_update_interval); spinSetInterval = defaultInterval > 0 ? defaultInterval : default_update_interval; if (-2 >= interval) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "updateIntervalNever")), TRUE); } else if (-1 == interval) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "updateIntervalDefault")), TRUE); } else { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "updateIntervalSpecific")), TRUE); spinSetInterval = interval; } /* Set refresh interval spin button and combo box */ if (spinSetInterval % 1440 == 0) { /* days */ gtk_combo_box_set_active (GTK_COMBO_BOX (spd->ui_data.refreshIntervalUnit), 2); spinSetInterval /= 1440; } else if (spinSetInterval % 60 == 0) { /* hours */ gtk_combo_box_set_active (GTK_COMBO_BOX (spd->ui_data.refreshIntervalUnit), 1); spinSetInterval /= 60; } else { gtk_combo_box_set_active (GTK_COMBO_BOX (spd->ui_data.refreshIntervalUnit), 0); } gtk_spin_button_set_value (GTK_SPIN_BUTTON (spd->ui_data.refreshInterval), spinSetInterval); gtk_widget_set_sensitive (spd->ui_data.refreshInterval, interval > 0); gtk_widget_set_sensitive (spd->ui_data.refreshIntervalUnit, interval > 0); /* setup info label about default update interval */ if (-1 != defaultInterval) defaultIntervalStr = g_strdup_printf (ngettext ("The provider of this feed suggests an update interval of %d minute.", "The provider of this feed suggests an update interval of %d minutes.", defaultInterval), defaultInterval); else defaultIntervalStr = g_strdup (_("This feed specifies no default update interval.")); gtk_label_set_text (GTK_LABEL (liferea_dialog_lookup (spd->ui_data.dialog, "feedUpdateInfo")), defaultIntervalStr); g_free (defaultIntervalStr); /* Source (only for feeds) */ if (SUBSCRIPTION_TYPE (subscription) == feed_get_subscription_type ()) { if (subscription_get_source (subscription)[0] == '|') { gtk_entry_set_text (GTK_ENTRY (spd->ui_data.sourceEntry), &(subscription_get_source (subscription)[1])); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (spd->ui_data.cmdRadio), TRUE); ui_subscription_prop_enable_httpauth (&(spd->ui_data), FALSE); gtk_widget_set_sensitive (spd->ui_data.selectFile, TRUE); } else if (strstr (subscription_get_source (subscription), "://") != NULL) { xmlURIPtr uri = xmlParseURI (subscription_get_source (subscription)); xmlChar *parsedUrl; if (uri) { if (uri->user) { gchar *user = uri->user; gchar *pass = strstr(user, ":"); if (pass) { pass[0] = '\0'; pass++; gtk_entry_set_text (GTK_ENTRY (spd->ui_data.password), pass); } gtk_entry_set_text (GTK_ENTRY (spd->ui_data.username), user); xmlFree (uri->user); uri->user = NULL; gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (spd->ui_data.authcheckbox), TRUE); } parsedUrl = xmlSaveUri (uri); gtk_entry_set_text (GTK_ENTRY(spd->ui_data.sourceEntry), (gchar *) parsedUrl); xmlFree (parsedUrl); xmlFreeURI (uri); } else { gtk_entry_set_text (GTK_ENTRY (spd->ui_data.sourceEntry), subscription_get_source (subscription)); } ui_subscription_prop_enable_httpauth (&(spd->ui_data), TRUE); gtk_widget_set_sensitive (spd->ui_data.selectFile, FALSE); } else { /* File */ gtk_entry_set_text (GTK_ENTRY (spd->ui_data.sourceEntry), subscription_get_source (subscription)); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (spd->ui_data.fileRadio), TRUE); ui_subscription_prop_enable_httpauth (&(spd->ui_data), FALSE); gtk_widget_set_sensitive (spd->ui_data.selectFile, TRUE); } if (subscription_get_filter (subscription)) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "filterCheckbox")), TRUE); gtk_entry_set_text (GTK_ENTRY (liferea_dialog_lookup (spd->ui_data.dialog, "filterEntry")), subscription_get_filter (subscription)); } } /* Archive */ if (feed->cacheLimit == CACHE_DISABLE) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "feedCacheDisable")), TRUE); } else if (feed->cacheLimit == CACHE_DEFAULT) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "feedCacheDefault")), TRUE); } else if (feed->cacheLimit == CACHE_UNLIMITED) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "feedCacheUnlimited")), TRUE); } else { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "feedCacheLimited")), TRUE); gtk_spin_button_set_value (GTK_SPIN_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "cacheItemLimit")), feed->cacheLimit); } gtk_widget_set_sensitive (liferea_dialog_lookup (spd->ui_data.dialog, "cacheItemLimit"), feed->cacheLimit > 0); on_feed_prop_filtercheck (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "filterCheckbox")), &(spd->ui_data)); /* Download */ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "dontUseProxyCheck")), subscription->updateOptions->dontUseProxy); /* Advanced */ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "enclosureDownloadCheck")), feed->encAutoDownload); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "loadItemLinkCheck")), node->loadItemLink); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "ignoreCommentFeeds")), feed->ignoreComments); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "markAsReadCheck")), feed->markAsRead); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (spd->ui_data.dialog, "html5ExtractCheck")), feed->html5Extract); /* Remove tabs we do not need... */ if (SUBSCRIPTION_TYPE (subscription) != feed_get_subscription_type ()) { /* Remove "General", "Source" and "Download" tab */ gtk_notebook_remove_page (GTK_NOTEBOOK (liferea_dialog_lookup (spd->ui_data.dialog, "subscriptionPropNotebook")), 0); gtk_notebook_remove_page (GTK_NOTEBOOK (liferea_dialog_lookup (spd->ui_data.dialog, "subscriptionPropNotebook")), 0); gtk_notebook_remove_page (GTK_NOTEBOOK (liferea_dialog_lookup (spd->ui_data.dialog, "subscriptionPropNotebook")), 1); } } static void subscription_prop_dialog_init (SubscriptionPropDialog *spd) { GtkWidget *propdialog; spd->ui_data.dialog = propdialog = liferea_dialog_new ("properties"); /* set default update interval spin button and unit combo box */ ui_common_setup_combo_menu (liferea_dialog_lookup (propdialog, "refreshIntervalUnitComboBox"), default_update_interval_unit_options, NULL /* no callback */, -1 /* default value */ ); spd->ui_data.feedNameEntry = liferea_dialog_lookup (propdialog, "feedNameEntry"); spd->ui_data.refreshInterval = liferea_dialog_lookup (propdialog, "refreshIntervalSpinButton"); spd->ui_data.refreshIntervalUnit = liferea_dialog_lookup (propdialog, "refreshIntervalUnitComboBox"); spd->ui_data.sourceEntry = liferea_dialog_lookup (propdialog, "sourceEntry"); spd->ui_data.selectFile = liferea_dialog_lookup (propdialog, "selectSourceFileButton"); spd->ui_data.fileRadio = liferea_dialog_lookup (propdialog, "feed_loc_file"); spd->ui_data.urlRadio = liferea_dialog_lookup (propdialog, "feed_loc_url"); spd->ui_data.cmdRadio = liferea_dialog_lookup (propdialog, "feed_loc_command"); spd->ui_data.authcheckbox = liferea_dialog_lookup (propdialog, "HTTPauthCheck"); spd->ui_data.username = liferea_dialog_lookup (propdialog, "usernameEntry"); spd->ui_data.password = liferea_dialog_lookup (propdialog, "passwordEntry"); spd->ui_data.credTable = liferea_dialog_lookup (propdialog, "httpAuthBox"); g_signal_connect (spd->ui_data.selectFile, "clicked", G_CALLBACK (on_selectfile_pressed), &(spd->ui_data)); g_signal_connect (spd->ui_data.urlRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(spd->ui_data)); g_signal_connect (spd->ui_data.fileRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(spd->ui_data)); g_signal_connect (spd->ui_data.cmdRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(spd->ui_data)); g_signal_connect (spd->ui_data.authcheckbox, "toggled", G_CALLBACK (on_feed_prop_authcheck), &(spd->ui_data)); g_signal_connect (liferea_dialog_lookup (propdialog, "filterCheckbox"), "toggled", G_CALLBACK (on_feed_prop_filtercheck), &(spd->ui_data)); g_signal_connect (liferea_dialog_lookup (propdialog, "filterSelectFile"), "clicked", G_CALLBACK (on_selectfile_pressed), &(spd->ui_data)); g_signal_connect (liferea_dialog_lookup (propdialog, "feedCacheLimited"), "toggled", G_CALLBACK (on_feed_prop_cache_radio), &(spd->ui_data)); g_signal_connect (liferea_dialog_lookup (propdialog, "updateIntervalSpecific"), "toggled", G_CALLBACK(on_feed_prop_update_radio), &(spd->ui_data)); g_signal_connect (G_OBJECT (propdialog), "response", G_CALLBACK (on_propdialog_response), spd); gtk_widget_show_all (propdialog); } SubscriptionPropDialog * subscription_prop_dialog_new (subscriptionPtr subscription) { SubscriptionPropDialog *spd; spd = SUBSCRIPTION_PROP_DIALOG (g_object_new (SUBSCRIPTION_PROP_DIALOG_TYPE, NULL)); subscription_prop_dialog_load(spd, subscription); return spd; } /* complex "New" dialog */ G_DEFINE_TYPE (NewSubscriptionDialog, new_subscription_dialog, G_TYPE_OBJECT); static void new_subscription_dialog_finalize (GObject *object) { NewSubscriptionDialog *nsd = NEW_SUBSCRIPTION_DIALOG (object); gtk_widget_destroy (nsd->ui_data.dialog); } static void new_subscription_dialog_class_init (NewSubscriptionDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = new_subscription_dialog_finalize; } static void on_newdialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { NewSubscriptionDialog *nsd = (NewSubscriptionDialog *)user_data; if (response_id == GTK_RESPONSE_OK) { gchar *source = NULL; const gchar *filter = NULL; updateOptionsPtr options; /* Source */ source = ui_subscription_dialog_decode_source (&(nsd->ui_data)); /* Filter handling */ filter = gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (nsd->ui_data.dialog, "filterEntry"))); if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (nsd->ui_data.dialog, "filterCheckbox"))) || !strcmp(filter,"")) { /* Maybe this should be a test to see if the file exists? */ filter = NULL; } options = g_new0 (struct updateOptions, 1); options->dontUseProxy = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (nsd->ui_data.dialog, "dontUseProxyCheck"))); feedlist_add_subscription_check_duplicate (source, filter, options, FEED_REQ_PRIORITY_HIGH); g_free (source); } g_object_unref (nsd); } static void new_subscription_dialog_init (NewSubscriptionDialog *nsd) { GtkWidget *newdialog; nsd->ui_data.dialog = newdialog = liferea_dialog_new ("new_subscription"); /* Setup source entry */ nsd->ui_data.sourceEntry = liferea_dialog_lookup (newdialog, "sourceEntry"); gtk_widget_grab_focus (GTK_WIDGET (nsd->ui_data.sourceEntry)); gtk_entry_set_activates_default (GTK_ENTRY (nsd->ui_data.sourceEntry), TRUE); nsd->ui_data.selectFile = liferea_dialog_lookup (newdialog, "selectSourceFileButton"); g_signal_connect (nsd->ui_data.selectFile, "clicked", G_CALLBACK (on_selectfile_pressed), &(nsd->ui_data)); /* Feed location radio buttons */ nsd->ui_data.fileRadio = liferea_dialog_lookup (newdialog, "feed_loc_file"); nsd->ui_data.urlRadio = liferea_dialog_lookup (newdialog, "feed_loc_url"); nsd->ui_data.cmdRadio = liferea_dialog_lookup (newdialog, "feed_loc_command"); g_signal_connect (nsd->ui_data.urlRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(nsd->ui_data)); g_signal_connect (nsd->ui_data.fileRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(nsd->ui_data)); g_signal_connect (nsd->ui_data.cmdRadio, "toggled", G_CALLBACK (on_feed_prop_url_radio), &(nsd->ui_data)); g_signal_connect (liferea_dialog_lookup (newdialog, "filterCheckbox"), "toggled", G_CALLBACK (on_feed_prop_filtercheck), &(nsd->ui_data)); g_signal_connect (liferea_dialog_lookup (newdialog, "filterSelectFile"), "clicked", G_CALLBACK (on_selectfile_pressed), &(nsd->ui_data)); gtk_widget_grab_default (liferea_dialog_lookup (newdialog, "newfeedbtn")); g_signal_connect (G_OBJECT (newdialog), "response", G_CALLBACK (on_newdialog_response), nsd); on_feed_prop_filtercheck (GTK_TOGGLE_BUTTON (liferea_dialog_lookup (newdialog, "filterCheckbox")), &(nsd->ui_data)); on_feed_prop_url_radio (GTK_TOGGLE_BUTTON (nsd->ui_data.urlRadio), &(nsd->ui_data)); gtk_widget_show_all (newdialog); } static NewSubscriptionDialog * ui_complex_subscription_dialog_new (void) { NewSubscriptionDialog *nsd; nsd = NEW_SUBSCRIPTION_DIALOG (g_object_new (NEW_SUBSCRIPTION_DIALOG_TYPE, NULL)); return nsd; } /* simple "New" dialog */ G_DEFINE_TYPE (SimpleSubscriptionDialog, simple_subscription_dialog, G_TYPE_OBJECT); static void simple_subscription_dialog_finalize (GObject *object) { SimpleSubscriptionDialog *ssd = SIMPLE_SUBSCRIPTION_DIALOG (object); gtk_widget_destroy (ssd->ui_data.dialog); } static void simple_subscription_dialog_class_init (SimpleSubscriptionDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = simple_subscription_dialog_finalize; } static void on_simple_newdialog_response (GtkDialog *dialog, gint response_id, gpointer user_data) { SimpleSubscriptionDialog *ssd = (SimpleSubscriptionDialog *) user_data; gchar *source = NULL; if (response_id == GTK_RESPONSE_OK) { source = ui_subscription_create_url (g_strdup (gtk_entry_get_text (GTK_ENTRY(ssd->ui_data.sourceEntry))), FALSE /* auth */, NULL /* user */, NULL /* passwd */); feedlist_add_subscription_check_duplicate (source, NULL, NULL, FEED_REQ_PRIORITY_HIGH); g_free (source); } if (response_id == GTK_RESPONSE_APPLY) /* misused for "Advanced" */ ui_complex_subscription_dialog_new (); g_object_unref (ssd); } static void simple_subscription_dialog_init (SimpleSubscriptionDialog *ssd) { GtkWidget *newdialog; ssd->ui_data.dialog = newdialog = liferea_dialog_new ("simple_subscription"); /* Setup source entry */ ssd->ui_data.sourceEntry = liferea_dialog_lookup (newdialog, "sourceEntry"); gtk_widget_grab_focus (GTK_WIDGET (ssd->ui_data.sourceEntry)); gtk_entry_set_activates_default (GTK_ENTRY (ssd->ui_data.sourceEntry), TRUE); g_signal_connect (G_OBJECT (newdialog), "response", G_CALLBACK (on_simple_newdialog_response), ssd); gtk_widget_show_all (newdialog); } SimpleSubscriptionDialog * subscription_dialog_new (void) { SimpleSubscriptionDialog *ssd; ssd = SIMPLE_SUBSCRIPTION_DIALOG (g_object_new (SIMPLE_SUBSCRIPTION_DIALOG_TYPE, NULL)); return ssd; } liferea-1.13.7/src/ui/subscription_dialog.h000066400000000000000000000040711415350204600206340ustar00rootroot00000000000000/** * @file subscription_dialog.h property dialog for feed subscriptions * * Copyright (C) 2007-2018 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _SUBSCRIPTION_DIALOG_H #define _SUBSCRIPTION_DIALOG_H #include #include "subscription.h" G_BEGIN_DECLS #define SUBSCRIPTION_PROP_DIALOG_TYPE (subscription_prop_dialog_get_type ()) G_DECLARE_FINAL_TYPE (SubscriptionPropDialog, subscription_prop_dialog, SUBSCRIPTION_PROP, DIALOG, GObject) /** * subscription_prop_dialog_new: * Creates a feed properties dialog (FIXME: handle * generic subscriptions too) * * @subscription: the subscription to load into the dialog * * Returns: (transfer none): a properties dialog */ SubscriptionPropDialog *subscription_prop_dialog_new (subscriptionPtr subscription); #define NEW_SUBSCRIPTION_DIALOG_TYPE (new_subscription_dialog_get_type ()) G_DECLARE_FINAL_TYPE (NewSubscriptionDialog, new_subscription_dialog, NEW_SUBSCRIPTION, DIALOG, GObject) #define SIMPLE_SUBSCRIPTION_DIALOG_TYPE (simple_subscription_dialog_get_type ()) G_DECLARE_FINAL_TYPE (SimpleSubscriptionDialog, simple_subscription_dialog, SIMPLE_SUBSCRIPTION, DIALOG, GObject) /** * subscription_dialog_new: * Create a simple subscription dialog. * * Returns: (transfer none): dialog instance */ SimpleSubscriptionDialog *subscription_dialog_new (void); G_END_DECLS #endif /* _SUBSCRIPTION_DIALOG_H */ liferea-1.13.7/src/ui/ui_common.c000066400000000000000000000165701415350204600165600ustar00rootroot00000000000000/** * @file ui_common.c UI helper functions * * Copyright (C) 2008-2014 Lars Windolf * Copyright (C) 2009 Hubert Figuiere * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/ui_common.h" #include "common.h" #include "conf.h" #include "ui/liferea_shell.h" void ui_common_setup_combo_menu (GtkWidget *widget, const gchar **options, GCallback callback, gint defaultValue) { GtkListStore *listStore; GtkTreeIter treeiter; guint i; listStore = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT); g_assert (NULL != widget); for (i = 0; options[i] != NULL; i++) { gtk_list_store_append (listStore, &treeiter); gtk_list_store_set (listStore, &treeiter, 0, _(options[i]), 1, i, -1); } gtk_combo_box_set_model (GTK_COMBO_BOX (widget), GTK_TREE_MODEL (listStore)); if (-1 <= defaultValue) gtk_combo_box_set_active (GTK_COMBO_BOX (widget), defaultValue); if (callback) g_signal_connect (G_OBJECT (widget), "changed", callback, widget); } void ui_common_setup_combo_text (GtkComboBox *combo, gint col) { GtkCellRenderer *rend = gtk_cell_renderer_text_new (); gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), rend, TRUE); gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo), rend, "text", col); } void ui_common_treeview_move_cursor (GtkTreeView *treeview, gint step) { gboolean ret; gtk_widget_grab_focus (GTK_WIDGET (treeview)); g_signal_emit_by_name (treeview, "move-cursor", GTK_MOVEMENT_DISPLAY_LINES, step, &ret); } void ui_common_treeview_move_cursor_to_first (GtkTreeView *treeview) { GtkTreePath *path; path = gtk_tree_path_new_first (); gtk_tree_view_set_cursor (treeview, path, NULL, FALSE); gtk_tree_path_free(path); } void ui_show_error_box (const char *format, ...) { GtkWidget *dialog; va_list args; gchar *msg; g_return_if_fail (format != NULL); va_start (args, format); msg = g_strdup_vprintf (format, args); va_end (args); dialog = gtk_message_dialog_new (GTK_WINDOW (liferea_shell_get_window ()), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg); (void)gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); g_free (msg); } void ui_show_info_box (const char *format, ...) { GtkWidget *dialog; va_list args; gchar *msg; g_return_if_fail (format != NULL); va_start (args, format); msg = g_strdup_vprintf (format, args); va_end (args); dialog = gtk_message_dialog_new (GTK_WINDOW (liferea_shell_get_window ()), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", msg); (void)gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); g_free (msg); } struct file_chooser_tuple { fileChoosenCallback func; gpointer user_data; }; static void ui_choose_file_save_cb (GtkNativeDialog *dialog, gint response_id, gpointer user_data) { struct file_chooser_tuple *tuple = (struct file_chooser_tuple*)user_data; gchar *filename; if (response_id == GTK_RESPONSE_ACCEPT) { filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); tuple->func (filename, tuple->user_data); g_free (filename); } else { tuple->func (NULL, tuple->user_data); } gtk_native_dialog_destroy (dialog); g_free (tuple); } static void ui_choose_file_or_dir(gchar *title, const gchar *buttonName, gboolean saving, gboolean directory, fileChoosenCallback callback, const gchar *currentPath, const gchar *defaultFilename, const char *filterstring, const char *filtername, gpointer user_data) { GtkFileChooserNative *native; GtkFileChooser *chooser; struct file_chooser_tuple *tuple; gchar *path = NULL; g_assert (!(saving & directory)); g_assert (!(defaultFilename && !saving)); if (!currentPath) path = g_strdup (g_get_home_dir ()); else path = g_strdup (currentPath); native = gtk_file_chooser_native_new (title, GTK_WINDOW (liferea_shell_get_window ()), (directory?GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER: (saving ? GTK_FILE_CHOOSER_ACTION_SAVE : GTK_FILE_CHOOSER_ACTION_OPEN)), buttonName, NULL); chooser = GTK_FILE_CHOOSER (native); if (saving) gtk_file_chooser_set_do_overwrite_confirmation (chooser, TRUE); gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (native), TRUE); gtk_native_dialog_set_transient_for (GTK_NATIVE_DIALOG (native), GTK_WINDOW (liferea_shell_get_window ())); tuple = g_new0 (struct file_chooser_tuple, 1); tuple->func = callback; tuple->user_data = user_data; g_signal_connect (G_OBJECT (native), "response", G_CALLBACK (ui_choose_file_save_cb), tuple); if (path && g_file_test (path, G_FILE_TEST_EXISTS)) { if (directory || defaultFilename) gtk_file_chooser_set_current_folder (chooser, path); else gtk_file_chooser_set_filename (chooser, path); } if (defaultFilename) gtk_file_chooser_set_current_name (chooser, defaultFilename); if (filterstring && filtername) { GtkFileFilter *filter, *allfiles; gchar **filterstrings, **f; filter = gtk_file_filter_new (); filterstrings = g_strsplit (filterstring, "|", 0); for (f = filterstrings; *f != NULL; f++) gtk_file_filter_add_pattern (filter, *f); g_strfreev (filterstrings); gtk_file_filter_set_name (filter, filtername); gtk_file_chooser_add_filter (chooser, filter); allfiles = gtk_file_filter_new (); gtk_file_filter_add_pattern (allfiles, "*"); gtk_file_filter_set_name (allfiles, _("All Files")); gtk_file_chooser_add_filter (chooser, allfiles); } gtk_native_dialog_show (GTK_NATIVE_DIALOG (native)); g_free (path); } void ui_choose_file (gchar *title, const gchar *buttonName, gboolean saving, fileChoosenCallback callback, const gchar *currentPath, const gchar *defaultFilename, const char *filterstring, const char *filtername, gpointer user_data) { ui_choose_file_or_dir (title, buttonName, saving, FALSE, callback, currentPath, defaultFilename, filterstring, filtername, user_data); } void ui_common_simple_action_group_set_enabled (GActionGroup *group, gboolean enabled) { gchar **actions_list = g_action_group_list_actions (group); gint i; for (i=0;actions_list[i] != NULL;i++) { g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (group), actions_list [i])), enabled); } g_strfreev (actions_list); } void ui_common_add_action_group_to_map (GActionGroup *group, GActionMap *map) { gchar **actions_list = g_action_group_list_actions (group); gint i; for (i=0;actions_list[i] != NULL;i++) { g_action_map_add_action (map, g_action_map_lookup_action (G_ACTION_MAP (group), actions_list [i])); } g_strfreev (actions_list); } liferea-1.13.7/src/ui/ui_common.h000066400000000000000000000076511415350204600165650ustar00rootroot00000000000000/** * @file ui_common.h UI helper functions * * Copyright (C) 2008-2011 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_COMMON_H #define _UI_COMMON_H #include /** * Helper function to set up a combo box option menu. * To be used to initialize dialogs. * * @param widget the option menu widget * @param options list of option literals * @param callback "changed" callback for the widget (or NULL) * @param defaultValue the default menu value */ void ui_common_setup_combo_menu (GtkWidget *widget, const gchar **options, GCallback callback, gint defaultValue); /** * Helper function to set up a combo to display the text from * a column of the model. * * @param combo the combo widget * @param col the column to use in the model */ void ui_common_setup_combo_text (GtkComboBox *combo, gint col); /** * Move cursor to nth next iter in a tree view. * * @param treeview the tree view * @param step how many iters to skip (negative values move backwards) */ void ui_common_treeview_move_cursor (GtkTreeView *treeview, gint step); /** * Move cursor to 1st iter in a tree view. * * @param treeview the tree view */ void ui_common_treeview_move_cursor_to_first (GtkTreeView *treeview); /** * Presents an "Info" type box with the given message. * * @param printf like format */ void ui_show_info_box (const char *format, ...); /** * Presents an "Error" type box with the given message. * Does not do error tracing or console output. * * @param printf like format */ void ui_show_error_box (const char *format, ...); /** Callback to be used with the filename choosing dialog */ typedef void (*fileChoosenCallback) (const gchar *title, gpointer user_data); /** * Open up a file selector * * @param title window title * @param buttonName Text to be used as the name of the accept button * @param saving TRUE if saving, FALSE if opening * @param callback that will be passed the filename (in the system's locale (NOT UTF-8), and some user data * @param currentPath This file or directory will be selected in the chooser * @param filename When saving, this is the suggested filename * @param filterstring a pattern for a GtkFileFilter or NULL * @param filtername a human readable name for the pattern or NULL (if pattern is NULL) * @param user_data user data passed to the callback */ void ui_choose_file (gchar *title, const gchar *buttonName, gboolean saving, fileChoosenCallback callback, const gchar *currentPath, const gchar *defaultFilename, const char *filterstring, const char *filtername, gpointer user_data); /** ui_common_simple_action_group_set_enabled: * @group: A GActionGroup containing only GSimpleActions. It must also implement * GActionMap in order to lookup the actions. * @enabled: TRUE to enable all actions in the group. * * Enable or disable all GSimpleActions in the group. */ void ui_common_simple_action_group_set_enabled (GActionGroup *group, gboolean enabled); /** ui_common_add_action_group_to_map: * @group: A GActionGroup which must also implement * GActionMap in order to lookup the actions. * @map: The GActionMap to which the actions will be added. * * Adds all actions from group to map. */ void ui_common_add_action_group_to_map (GActionGroup *group, GActionMap *map); #endif liferea-1.13.7/src/ui/ui_dnd.c000066400000000000000000000226441415350204600160340ustar00rootroot00000000000000/** * @file ui_dnd.c everything concerning Drag&Drop * * Copyright (C) 2003-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include /* For strcmp */ #include "common.h" #include "db.h" #include "feed.h" #include "feedlist.h" #include "folder.h" #include "debug.h" #include "ui/item_list_view.h" #include "ui/feed_list_view.h" #include "ui/liferea_shell.h" #include "ui/ui_dnd.h" #include "fl_sources/node_source.h" /* Why does Liferea need such a complex DnD handling (for the feed list)? -> Because parts of the feed list might be un-draggable. -> Because drag source and target might be different node sources with even incompatible subscription types. -> Because removal at drag source and insertion at drop target must be atomic to avoid subscription losses. For simplicity the DnD code reuses the UI node removal and insertion methods that asynchronously apply the actions at the node source. (FIXME: implement the last part) */ static gboolean (*old_feed_drop_possible)(GtkTreeDragDest *drag_dest, GtkTreePath *dest_path, GtkSelectionData *selection_data); static gboolean (*old_feed_drag_data_received)(GtkTreeDragDest *drag_dest, GtkTreePath *dest, GtkSelectionData *selection_data); /* GtkTreeDragSource/GtkTreeDragDest implementation */ /** decides whether a feed cannot be dragged or not */ static gboolean ui_dnd_feed_draggable (GtkTreeDragSource *drag_source, GtkTreePath *path) { GtkTreeIter iter; nodePtr node; debug1 (DEBUG_GUI, "DnD check if feed dragging is possible (%d)", path); if (gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_source), &iter, path)) { gtk_tree_model_get (GTK_TREE_MODEL (drag_source), &iter, FS_PTR, &node, -1); /* never drag "empty" entries or nodes of read-only subscription lists*/ if (!node || !(NODE_SOURCE_TYPE (node->parent)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST)) return FALSE; return TRUE; } else { g_warning ("fatal error! could not resolve tree path!"); return FALSE; } } static gboolean ui_dnd_feed_drop_possible (GtkTreeDragDest *drag_dest, GtkTreePath *dest_path, GtkSelectionData *selection_data) { GtkTreeModel *model = NULL; GtkTreePath *src_path = NULL; GtkTreeIter iter; nodePtr sourceNode, targetNode; debug1 (DEBUG_GUI, "DnD check if feed dropping is possible (%d)", dest_path); if (!(old_feed_drop_possible) (drag_dest, dest_path, selection_data)) return FALSE; if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_dest), &iter, dest_path)) return FALSE; /* Try to get an iterator, if we get none it means either feed list root or an "Empty" node. Both cases are fine */ gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter, FS_PTR, &targetNode, -1); if (!targetNode) return TRUE; /* If we got an iterator it's either a possible dropping candidate (a folder or source node to drop into, or a iterator to insert after). In any case we have to check if it is a writeable node source. */ /* Never drop into read-only subscription node sources */ if (!(NODE_SOURCE_TYPE (targetNode)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST)) return FALSE; /* never drag folders into non-hierarchic node sources */ if (!gtk_tree_get_row_drag_data (selection_data, &model, &src_path)) return TRUE; if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, src_path)) { gtk_tree_model_get (GTK_TREE_MODEL (model), &iter, FS_PTR, &sourceNode, -1); g_assert (sourceNode); /* Never drop into another node source as this arises to many problems (e.g. remote sync, different subscription type, e.g. SF #2855990) */ if (NODE_SOURCE_TYPE (targetNode) != NODE_SOURCE_TYPE (sourceNode)) return FALSE; if (IS_FOLDER(sourceNode) && !(NODE_SOURCE_TYPE (targetNode)->capabilities & NODE_SOURCE_CAPABILITY_HIERARCHIC_FEEDLIST)) return FALSE; } gtk_tree_path_free (src_path); return TRUE; } static gboolean ui_dnd_feed_drag_data_received (GtkTreeDragDest *drag_dest, GtkTreePath *dest, GtkSelectionData *selection_data) { GtkTreeIter iter, iter2, parentIter; nodePtr node, oldParent, newParent; gboolean result, valid, added; gint oldPos, pos; result = old_feed_drag_data_received (drag_dest, dest, selection_data); if (result) { if (gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_dest), &iter, dest)) { gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter, FS_PTR, &node, -1); /* If we don't do anything, then because DnD is implemented by removal and re-insertion, and the removed node is selected, the treeview selects the next row after the removal, which is supremely irritating. But setting a selection at this point is pointless, because the treeview will reset it as soon as the DnD callback returns. Instead, we set the cursor, which controls where treeview resets the selection later. */ gtk_tree_view_set_cursor(GTK_TREE_VIEW (liferea_shell_lookup ("feedlist")), dest, NULL, FALSE); /* remove from old parents child list */ oldParent = node->parent; g_assert (oldParent); oldPos = g_slist_index (oldParent->children, node); oldParent->children = g_slist_remove (oldParent->children, node); node_update_counters (oldParent); if (0 == g_slist_length (oldParent->children)) feed_list_view_add_empty_node (feed_list_view_to_iter (oldParent->id)); /* and rebuild new parents child list */ if (gtk_tree_model_iter_parent (GTK_TREE_MODEL (drag_dest), &parentIter, &iter)) { gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &parentIter, FS_PTR, &newParent, -1); } else { gtk_tree_model_get_iter_first (GTK_TREE_MODEL (drag_dest), &parentIter); newParent = feedlist_get_root (); } /* drop old list... */ debug3 (DEBUG_GUI, "old parent is %s (%d, position=%d)", oldParent->title, g_slist_length (oldParent->children), oldPos); debug2 (DEBUG_GUI, "new parent is %s (%d)", newParent->title, g_slist_length (newParent->children)); g_slist_free (newParent->children); newParent->children = NULL; node->parent = newParent; debug0 (DEBUG_GUI, "new newParent child list:"); /* and rebuild it from the tree model */ if (feedlist_get_root() != newParent) valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (drag_dest), &iter2, &parentIter); else valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (drag_dest), &iter2, NULL); pos = 0; added = FALSE; while (valid) { nodePtr child; gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter2, FS_PTR, &child, -1); if (child) { /* Well this is a bit complicated... If we move a feed inside a folder we need to skip the old insertion point (oldPos). This is easy if the feed is added behind this position. If it is dropped before the flag added is set once the new copy is encountered. The remaining copy is skipped automatically when the flag is set. */ /* check if this is a copy of the dragged node or the original itself */ if ((newParent == oldParent) && !strcmp(node->id, child->id)) { if ((pos == oldPos) || added) { /* it is the original */ debug2 (DEBUG_GUI, " -> %d: skipping old insertion point %s", pos, child->title); } else { /* it is a copy inserted before the original */ added = TRUE; debug2 (DEBUG_GUI, " -> %d: new insertion point of %s", pos, child->title); newParent->children = g_slist_append (newParent->children, child); } } else { /* all other nodes */ debug2 (DEBUG_GUI, " -> %d: adding %s", pos, child->title); newParent->children = g_slist_append (newParent->children, child); } } else { debug0 (DEBUG_GUI, " -> removing empty node"); /* remove possible existing "(empty)" node from newParent */ feed_list_view_remove_empty_node (&parentIter); } valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (drag_dest), &iter2); pos++; } db_node_update (node); node_update_counters (newParent); feedlist_schedule_save (); } } return result; } void ui_dnd_setup_feedlist (GtkTreeStore *feedstore) { GtkTreeDragSourceIface *drag_source_iface; GtkTreeDragDestIface *drag_dest_iface; drag_source_iface = GTK_TREE_DRAG_SOURCE_GET_IFACE (GTK_TREE_MODEL (feedstore)); drag_source_iface->row_draggable = ui_dnd_feed_draggable; drag_dest_iface = GTK_TREE_DRAG_DEST_GET_IFACE (GTK_TREE_MODEL (feedstore)); old_feed_drop_possible = drag_dest_iface->row_drop_possible; old_feed_drag_data_received = drag_dest_iface->drag_data_received; drag_dest_iface->row_drop_possible = ui_dnd_feed_drop_possible; drag_dest_iface->drag_data_received = ui_dnd_feed_drag_data_received; } liferea-1.13.7/src/ui/ui_dnd.h000066400000000000000000000017451415350204600160400ustar00rootroot00000000000000/** * @file ui_dnd.h everything concerning DnD * * Copyright (C) 2003, 2004 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_DND_H #define _UI_DND_H #include /** sets up DnD for the feedlist model */ void ui_dnd_setup_feedlist(GtkTreeStore *feedstore); #endif liferea-1.13.7/src/ui/ui_folder.c000066400000000000000000000030721415350204600165340ustar00rootroot00000000000000/** * @file ui_folder.c GUI folder handling * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2016 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/ui_folder.h" #include "feedlist.h" #include "ui/liferea_dialog.h" static GtkWidget *newfolderdialog = NULL; gboolean ui_folder_add (void) { GtkWidget *foldernameentry; if (!newfolderdialog || !G_IS_OBJECT (newfolderdialog)) newfolderdialog = liferea_dialog_new ("new_folder"); foldernameentry = liferea_dialog_lookup (newfolderdialog, "foldertitleentry"); gtk_entry_set_text (GTK_ENTRY (foldernameentry), ""); gtk_widget_show (newfolderdialog); return TRUE; } void on_newfolderbtn_clicked (GtkButton *button, gpointer user_data) { feedlist_add_folder (gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (newfolderdialog, "foldertitleentry")))); } liferea-1.13.7/src/ui/ui_folder.h000066400000000000000000000023401415350204600165360ustar00rootroot00000000000000/** * @file ui_folder.h GUI folder handling * * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2004-2009 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_FOLDER_H #define _UI_FOLDER_H #include #include "node.h" /** * Start interaction to create a new sub folder * attached to the given parent node. * * @returns TRUE on success */ gboolean ui_folder_add (void); /* menu callback */ void on_newfolderbtn_clicked(GtkButton *button, gpointer user_data); #endif liferea-1.13.7/src/ui/ui_update.c000066400000000000000000000131021415350204600165360ustar00rootroot00000000000000/** * @file ui_update.c GUI update monitor * * Copyright (C) 2006-2016 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "ui/ui_update.h" #include "common.h" #include "feedlist.h" #include "node.h" #include "subscription.h" #include "update.h" #include "ui/liferea_dialog.h" enum { UM_FAVICON, UM_REQUEST_TITLE, UM_LEN }; static GtkWidget *umdialog = NULL; static GtkTreeStore *um1store = NULL; static GtkTreeStore *um2store = NULL; static GHashTable *um1hash = NULL; static GHashTable *um2hash = NULL; static void ui_update_remove_request(nodePtr node, GtkTreeStore *store, GHashTable *hash) { GtkTreeIter *iter; iter = (GtkTreeIter *)g_hash_table_lookup(hash, node->id); if(iter) { gtk_tree_store_remove(store, iter); g_hash_table_remove(hash, (gpointer)node->id); g_free(iter); } } static void ui_update_merge_request(nodePtr node, GtkTreeStore *store, GHashTable *hash) { GtkTreeIter *iter; gchar *title; if(NULL != (iter = (GtkTreeIter *)g_hash_table_lookup(hash, (gpointer)node->id))) return; iter = g_new0(GtkTreeIter, 1); gtk_tree_store_append(store, iter, NULL); title = g_markup_escape_text (node_get_title (node), -1); gtk_tree_store_set(store, iter, UM_REQUEST_TITLE, title, UM_FAVICON, node_get_icon(node), -1); g_hash_table_insert(hash, (gpointer)node->id, (gpointer)iter); g_free (title); } static void ui_update_find_requests (nodePtr node) { if (node->children) node_foreach_child (node, ui_update_find_requests); if (!node->subscription) return; if (node->subscription->updateJob) { if (REQUEST_STATE_PROCESSING == update_job_get_state (node->subscription->updateJob)) { ui_update_merge_request (node, um1store, um1hash); ui_update_remove_request (node, um2store, um2hash); return; } if (REQUEST_STATE_PENDING == update_job_get_state (node->subscription->updateJob)) { ui_update_merge_request (node, um2store, um2hash); return; } } ui_update_remove_request (node, um1store, um1hash); ui_update_remove_request (node, um2store, um2hash); } static gboolean ui_update_monitor_update(void *data) { if(umdialog) { feedlist_foreach(ui_update_find_requests); return TRUE; } else { return FALSE; } } static void ui_update_cancel (nodePtr node) { if (node->children) node_foreach_child (node, ui_update_cancel); if (!node->subscription) return; subscription_cancel_update (node->subscription); } void on_cancel_all_requests_clicked(GtkButton *button, gpointer user_data) { feedlist_foreach(ui_update_cancel); } static void on_update_monitor_destroyed_cb(GtkWidget *widget, void *data) { g_hash_table_destroy(um1hash); g_hash_table_destroy(um2hash); um1hash = NULL; um2hash = NULL; umdialog = NULL; } void on_close_update_monitor_clicked(GtkButton *button, gpointer user_data) { gtk_widget_destroy(umdialog); } void on_menu_show_update_monitor(GSimpleAction *action, GVariant *parameter, gpointer user_data) { GtkCellRenderer *textRenderer, *iconRenderer; GtkTreeViewColumn *column; GtkTreeView *view; if(!umdialog) { umdialog = liferea_dialog_new ("update_monitor"); g_signal_connect (G_OBJECT (umdialog), "destroy", G_CALLBACK (on_update_monitor_destroyed_cb), NULL); /* Set up left store and view */ view = GTK_TREE_VIEW(liferea_dialog_lookup(umdialog, "left")); um1store = gtk_tree_store_new(UM_LEN, G_TYPE_ICON, G_TYPE_STRING); gtk_tree_view_set_model(view, GTK_TREE_MODEL(um1store)); textRenderer = gtk_cell_renderer_text_new(); iconRenderer = gtk_cell_renderer_pixbuf_new(); column = gtk_tree_view_column_new(); gtk_tree_view_column_pack_start(column, iconRenderer, FALSE); gtk_tree_view_column_pack_start(column, textRenderer, TRUE); gtk_tree_view_column_add_attribute(column, iconRenderer, "gicon", UM_FAVICON); gtk_tree_view_column_add_attribute(column, textRenderer, "markup", UM_REQUEST_TITLE); gtk_tree_view_append_column(view, column); /* Set up right store and view */ view = GTK_TREE_VIEW(liferea_dialog_lookup(umdialog, "right")); um2store = gtk_tree_store_new(UM_LEN, G_TYPE_ICON, G_TYPE_STRING); gtk_tree_view_set_model(view, GTK_TREE_MODEL(um2store)); textRenderer = gtk_cell_renderer_text_new(); iconRenderer = gtk_cell_renderer_pixbuf_new(); column = gtk_tree_view_column_new(); gtk_tree_view_column_pack_start(column, iconRenderer, FALSE); gtk_tree_view_column_pack_start(column, textRenderer, TRUE); gtk_tree_view_column_add_attribute(column, iconRenderer, "gicon", UM_FAVICON); gtk_tree_view_column_add_attribute(column, textRenderer, "markup", UM_REQUEST_TITLE); gtk_tree_view_append_column(view, column); /* Fill in data */ um1hash = g_hash_table_new(g_str_hash, g_str_equal); um2hash = g_hash_table_new(g_str_hash, g_str_equal); (void)g_timeout_add_seconds(1, ui_update_monitor_update, NULL); } gtk_window_present(GTK_WINDOW(umdialog)); } liferea-1.13.7/src/ui/ui_update.h000066400000000000000000000022041415350204600165440ustar00rootroot00000000000000/** * @file ui_update.h GUI update monitor * * Copyright (C) 2006 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UI_UPDATE_H #define _UI_UPDATE_H #include void on_close_update_monitor_clicked(GtkButton *button, gpointer user_data); void on_menu_show_update_monitor(GSimpleAction *action, GVariant *parameter, gpointer user_data); void on_cancel_all_requests_clicked(GtkButton *button, gpointer user_data); #endif liferea-1.13.7/src/update.c000066400000000000000000000504551415350204600154400ustar00rootroot00000000000000/** * @file update.c generic update request and state processing * * Copyright (C) 2003-2021 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * Copyright (C) 2009 Adrian Bunk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "update.h" #include #include #include #include #include #include #include #if !defined (G_OS_WIN32) || defined (HAVE_SYS_WAIT_H) #include #endif #include #include "auth_activatable.h" #include "common.h" #include "debug.h" #include "net.h" #include "plugins_engine.h" #include "xml.h" #include "ui/liferea_shell.h" #if defined (G_OS_WIN32) && !defined (WIFEXITED) && !defined (WEXITSTATUS) #define WIFEXITED(x) (x != 0) #define WEXITSTATUS(x) (x) #endif /** global update job list, used for lookups when cancelling */ static GSList *jobs = NULL; static GAsyncQueue *pendingHighPrioJobs = NULL; static GAsyncQueue *pendingJobs = NULL; static guint numberOfActiveJobs = 0; #define MAX_ACTIVE_JOBS 5 /* update state interface */ updateStatePtr update_state_new (void) { return g_new0 (struct updateState, 1); } glong update_state_get_lastmodified (updateStatePtr state) { return state->lastModified; } void update_state_set_lastmodified (updateStatePtr state, glong lastModified) { state->lastModified = lastModified; } const gchar * update_state_get_etag (updateStatePtr state) { return state->etag; } void update_state_set_etag (updateStatePtr state, const gchar *etag) { g_free (state->etag); state->etag = NULL; if (etag) state->etag = g_strdup(etag); } void update_state_set_cache_maxage (updateStatePtr state, const gint maxage) { if (0 < maxage) state->maxAgeMinutes = maxage; else state->maxAgeMinutes = -1; } gint update_state_get_cache_maxage (updateStatePtr state) { return state->maxAgeMinutes; } const gchar * update_state_get_cookies (updateStatePtr state) { return state->cookies; } void update_state_set_cookies (updateStatePtr state, const gchar *cookies) { g_free (state->cookies); state->cookies = NULL; if (cookies) state->cookies = g_strdup (cookies); } updateStatePtr update_state_copy (updateStatePtr state) { updateStatePtr newState; newState = update_state_new (); update_state_set_lastmodified (newState, update_state_get_lastmodified (state)); update_state_set_cookies (newState, update_state_get_cookies (state)); update_state_set_etag (newState, update_state_get_etag (state)); return newState; } void update_state_free (updateStatePtr updateState) { if (!updateState) return; g_free (updateState->cookies); g_free (updateState->etag); g_free (updateState); } /* update options */ updateOptionsPtr update_options_copy (updateOptionsPtr options) { updateOptionsPtr newOptions; newOptions = g_new0 (struct updateOptions, 1); newOptions->username = g_strdup (options->username); newOptions->password = g_strdup (options->password); newOptions->dontUseProxy = options->dontUseProxy; return newOptions; } void update_options_free (updateOptionsPtr options) { if (!options) return; g_free (options->username); g_free (options->password); g_free (options); } /* update request object */ G_DEFINE_TYPE (UpdateRequest, update_request, G_TYPE_OBJECT); static void update_request_finalize (GObject *obj) { UpdateRequest *request = UPDATE_REQUEST (obj); update_state_free (request->updateState); update_options_free (request->options); g_free (request->postdata); g_free (request->source); g_free (request->filtercmd); G_OBJECT_CLASS (update_request_parent_class)->finalize (obj); } static void update_request_class_init (UpdateRequestClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = update_request_finalize; } static void update_request_init (UpdateRequest *request) { } UpdateRequest * update_request_new (const gchar *source, updateStatePtr state, updateOptionsPtr options) { UpdateRequest *request = UPDATE_REQUEST (g_object_new (UPDATE_REQUEST_TYPE, NULL)); request->source = g_strdup (source); if (state) request->updateState = update_state_copy (state); else request->updateState = update_state_new (); if (options) request->options = update_options_copy (options); else request->options = g_new0 (struct updateOptions, 1); return request; } void update_request_set_source(UpdateRequest *request, const gchar* source) { g_free (request->source); request->source = g_strdup (source); } void update_request_set_auth_value (UpdateRequest *request, const gchar* authValue) { g_free (request->authValue); request->authValue = g_strdup (authValue); } /* update result object */ updateResultPtr update_result_new (void) { updateResultPtr result; result = g_new0 (struct updateResult, 1); result->updateState = update_state_new (); return result; } void update_result_free (updateResultPtr result) { if (!result) return; update_state_free (result->updateState); g_free (result->data); g_free (result->source); g_free (result->contentType); g_free (result->filterErrors); g_free (result); } /* update job handling */ static updateJobPtr update_job_new (gpointer owner, UpdateRequest *request, update_result_cb callback, gpointer user_data, updateFlags flags) { updateJobPtr job; job = g_new0 (struct updateJob, 1); job->owner = owner; job->request = UPDATE_REQUEST (request); job->result = update_result_new (); job->callback = callback; job->user_data = user_data; job->flags = flags; job->state = REQUEST_STATE_INITIALIZED; job->cmd.fd = -1; job->cmd.pid = 0; return job; } gint update_job_get_state (updateJobPtr job) { return job->state; } static void update_job_show_count_foreach_func (gpointer data, gpointer user_data) { updateJobPtr job = (updateJobPtr)data; guint *count = (guint *)user_data; // Count all subscription jobs (ignore HTML5 and favicon requests) if (!(job->flags & FEED_REQ_NO_FEED)) (*count)++; } static guint maxcount = 0; void update_jobs_get_count (guint *count, guint *max) { *count = 0; g_slist_foreach (jobs, update_job_show_count_foreach_func, count); if (*count > maxcount) maxcount = *count; *max = maxcount; } static void update_job_free (updateJobPtr job) { if (!job) return; jobs = g_slist_remove (jobs, job); g_object_unref (job->request); update_result_free (job->result); if (job->cmd.fd >= 0) { debug1 (DEBUG_UPDATE, "Found an open cmd.fd %d when freeing!", job->cmd.fd); close (job->cmd.fd); } if (job->cmd.timeout_id > 0) { g_source_remove (job->cmd.timeout_id); } if (job->cmd.io_watch_id > 0) { g_source_remove (job->cmd.io_watch_id); } if (job->cmd.child_watch_id > 0) { g_source_remove (job->cmd.child_watch_id); } if (job->cmd.stdout_ch) { g_io_channel_unref (job->cmd.stdout_ch); } g_free (job); } /* filter idea (and some of the code) was taken from Snownews */ static gchar * update_exec_filter_cmd (updateJobPtr job) { int fd, status; gchar *command; const gchar *tmpdir = g_get_tmp_dir(); char *tmpfilename; char *out = NULL; FILE *file, *p; size_t size = 0; tmpfilename = g_build_filename (tmpdir, "liferea-XXXXXX", NULL); fd = g_mkstemp (tmpfilename); if (fd == -1) { debug1 (DEBUG_UPDATE, "Error opening temp file %s to use for filtering!", tmpfilename); job->result->filterErrors = g_strdup_printf (_("Error opening temp file %s to use for filtering!"), tmpfilename); g_free (tmpfilename); return NULL; } file = fdopen (fd, "w"); fwrite (job->result->data, strlen (job->result->data), 1, file); fclose (file); command = g_strdup_printf("%s < %s", job->request->filtercmd, tmpfilename); p = popen (command, "r"); if (NULL != p) { while (!feof (p) && !ferror (p)) { size_t len; out = g_realloc (out, size + 1025); len = fread (&out[size], 1, 1024, p); if (len > 0) size += len; } status = pclose (p); if (!(WIFEXITED (status) && WEXITSTATUS (status) == 0)) { debug2 (DEBUG_UPDATE, "%s exited with status %d!", command, WEXITSTATUS(status)); job->result->filterErrors = g_strdup_printf (_("%s exited with status %d"), command, WEXITSTATUS(status)); size = 0; } if (out) out[size] = '\0'; } else { g_warning (_("Error: Could not open pipe \"%s\""), command); job->result->filterErrors = g_strdup_printf (_("Error: Could not open pipe \"%s\""), command); } /* Clean up. */ g_free (command); unlink (tmpfilename); g_free (tmpfilename); return out; } static gchar * update_apply_xslt (updateJobPtr job) { xsltStylesheetPtr xslt = NULL; xmlOutputBufferPtr buf; xmlDocPtr srcDoc = NULL, resDoc = NULL; gchar *output = NULL; g_assert (NULL != job->result); do { srcDoc = xml_parse (job->result->data, job->result->size, NULL); if (!srcDoc) { g_warning("fatal: parsing request result XML source failed (%s)!", job->request->filtercmd); break; } /* load localization stylesheet */ xslt = xsltParseStylesheetFile ((xmlChar *)job->request->filtercmd); if (!xslt) { g_warning ("fatal: could not load filter stylesheet \"%s\"!", job->request->filtercmd); break; } resDoc = xsltApplyStylesheet (xslt, srcDoc, NULL); if (!resDoc) { g_warning ("fatal: applying stylesheet \"%s\" failed!", job->request->filtercmd); break; } buf = xmlAllocOutputBuffer (NULL); if (-1 == xsltSaveResultTo (buf, resDoc, xslt)) { g_warning ("fatal: retrieving result of filter stylesheet failed (%s)!", job->request->filtercmd); break; } #ifdef LIBXML2_NEW_BUFFER if (xmlOutputBufferGetSize (buf) > 0) output = (gchar *)xmlCharStrdup ((char *)xmlOutputBufferGetContent (buf)); #else if (xmlBufferLength (buf->buffer) > 0) output = (gchar *)xmlCharStrdup ((char *)xmlBufferContent (buf->buffer)); #endif xmlOutputBufferClose (buf); } while (FALSE); if (srcDoc) xmlFreeDoc (srcDoc); if (resDoc) xmlFreeDoc (resDoc); if (xslt) xsltFreeStylesheet (xslt); return output; } static void update_apply_filter (updateJobPtr job) { gchar *filterResult; g_assert (NULL == job->result->filterErrors); /* we allow two types of filters: XSLT stylesheets and arbitrary commands */ if ((strlen (job->request->filtercmd) > 4) && (0 == strcmp (".xsl", job->request->filtercmd + strlen (job->request->filtercmd) - 4))) filterResult = update_apply_xslt (job); else filterResult = update_exec_filter_cmd (job); if (filterResult) { g_free (job->result->data); job->result->data = filterResult; job->result->size = strlen(filterResult); } } static void update_exec_cmd_cb_child_watch (GPid pid, gint status, gpointer user_data) { updateJobPtr job = (updateJobPtr) user_data; debug1 (DEBUG_UPDATE, "Child process %d terminated", job->cmd.pid); job->cmd.pid = 0; if (WIFEXITED (status) && WEXITSTATUS (status) == 0) { job->result->httpstatus = 200; } else { job->result->httpstatus = 404; /* FIXME: maybe setting request->returncode would be better */ } job->cmd.child_watch_id = 0; /* Caller will remove source. */ if (job->cmd.timeout_id > 0) { g_source_remove (job->cmd.timeout_id); job->cmd.timeout_id = 0; } if (job->cmd.io_watch_id > 0) { g_source_remove (job->cmd.io_watch_id); job->cmd.io_watch_id = 0; } update_process_finished_job (job); } static gboolean update_exec_cmd_cb_out_watch (GIOChannel *source, GIOCondition condition, gpointer user_data) { updateJobPtr job = (updateJobPtr) user_data; GError *err = NULL; gboolean ret = TRUE; /* Do not remove event source yet. */ GIOStatus st; gsize nread; if (condition == G_IO_HUP) { debug1 (DEBUG_UPDATE, "Pipe closed, child process %d is terminating", job->cmd.pid); ret = FALSE; } else if (condition == G_IO_IN) { while (TRUE) { job->result->data = g_realloc (job->result->data, job->result->size + 1025); nread = 0; st = g_io_channel_read_chars (source, job->result->data + job->result->size, 1024, &nread, &err); job->result->size += nread; job->result->data[job->result->size] = 0; if (err) { debug2 (DEBUG_UPDATE, "Error %d when reading from child %d", err->code, job->cmd.pid); g_error_free (err); err = NULL; ret = FALSE; /* remove event */ } if (nread == 0) { /* Finished reading */ break; } else if (st == G_IO_STATUS_AGAIN) { /* just try again */ } else if (st == G_IO_STATUS_EOF) { /* Pipe closed */ ret = FALSE; break; } else if (st == G_IO_STATUS_ERROR) { debug1 (DEBUG_UPDATE, "Got a G_IO_STATUS_ERROR from child %d", job->cmd.pid); ret = FALSE; break; } } } else { debug2 (DEBUG_UPDATE, "Unexpected condition %d for child process %d", condition, job->cmd.pid); ret = FALSE; } if (ret == FALSE) { close (job->cmd.fd); job->cmd.fd = -1; job->cmd.io_watch_id = 0; /* Caller will remove source. */ } return ret; } static gboolean update_exec_cmd_cb_timeout (gpointer user_data) { updateJobPtr job = (updateJobPtr) user_data; debug1 (DEBUG_UPDATE, "Child process %d timed out, killing.", job->cmd.pid); /* Kill child. Result will still be processed by update_exec_cmd_cb_child_watch */ kill((pid_t) job->cmd.pid, SIGKILL); job->cmd.timeout_id = 0; return FALSE; /* Remove timeout source */ } static void update_exec_cmd (updateJobPtr job) { gboolean ret; gchar *cmd = (job->request->source) + 1; /* Previous versions ran through popen() and a lot of users may be depending * on this behavior, so we run through a shell and keep compatibility. */ gchar *cmd_args[] = { "/bin/sh", "-c", cmd, NULL }; debug1 (DEBUG_UPDATE, "executing command \"%s\"...", cmd); ret = g_spawn_async_with_pipes (NULL, cmd_args, NULL, G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, &job->cmd.pid, NULL, &job->cmd.fd, NULL, NULL); if (!ret) { debug0 (DEBUG_UPDATE, "g_spawn_async_with_pipes failed"); liferea_shell_set_status_bar (_("Error: Could not open pipe \"%s\""), cmd); job->result->httpstatus = 404; /* FIXME: maybe setting request->returncode would be better */ return; } debug1 (DEBUG_UPDATE, "New child process launched with pid %d", job->cmd.pid); job->cmd.child_watch_id = g_child_watch_add (job->cmd.pid, (GChildWatchFunc) update_exec_cmd_cb_child_watch, job); job->cmd.stdout_ch = g_io_channel_unix_new (job->cmd.fd); job->cmd.io_watch_id = g_io_add_watch (job->cmd.stdout_ch, G_IO_IN | G_IO_HUP, (GIOFunc) update_exec_cmd_cb_out_watch, job); /* FIXME: timeout should be configurable */ job->cmd.timeout_id = g_timeout_add (30000, (GSourceFunc) update_exec_cmd_cb_timeout, job); } static void update_load_file (updateJobPtr job) { gchar *filename = job->request->source; gchar *anchor; if (!strncmp (filename, "file://",7)) filename += 7; anchor = strchr (filename, '#'); if (anchor) *anchor = 0; /* strip anchors from filenames */ if (g_file_test (filename, G_FILE_TEST_EXISTS)) { /* we have a file... */ if ((!g_file_get_contents (filename, &(job->result->data), &(job->result->size), NULL)) || (job->result->data[0] == '\0')) { job->result->httpstatus = 403; /* FIXME: maybe setting request->returncode would be better */ liferea_shell_set_status_bar (_("Error: Could not open file \"%s\""), filename); } else { job->result->httpstatus = 200; debug2 (DEBUG_UPDATE, "Successfully read %d bytes from file %s.", job->result->size, filename); } } else { liferea_shell_set_status_bar (_("Error: There is no file \"%s\""), filename); job->result->httpstatus = 404; /* FIXME: maybe setting request->returncode would be better */ } update_process_finished_job (job); } static void update_job_run (updateJobPtr job) { /* Here we decide on the source type and the proper execution methods which then do anything they want with the job and pass the processed job to update_process_finished_job() for result dequeuing */ /* everything starting with '|' is a local command */ if (*(job->request->source) == '|') { debug1 (DEBUG_UPDATE, "Recognized local command: %s", job->request->source); update_exec_cmd (job); return; } /* if it has a protocol "://" prefix, but not "file://" it is an URI */ if (strstr (job->request->source, "://") && strncmp (job->request->source, "file://", 7)) { network_process_request (job); return; } /* otherwise it must be a local file... */ { debug1 (DEBUG_UPDATE, "Recognized file URI: %s", job->request->source); update_load_file (job); return; } } static gboolean update_dequeue_job (gpointer user_data) { updateJobPtr job; if (!pendingJobs) return FALSE; /* we must be in shutdown */ if (numberOfActiveJobs >= MAX_ACTIVE_JOBS) return FALSE; /* we'll be called again when a job finishes */ job = (updateJobPtr)g_async_queue_try_pop(pendingHighPrioJobs); if (!job) job = (updateJobPtr)g_async_queue_try_pop(pendingJobs); if(!job) return FALSE; /* no request at the moment */ numberOfActiveJobs++; job->state = REQUEST_STATE_PROCESSING; debug1 (DEBUG_UPDATE, "processing request (%s)", job->request->source); if (job->callback == NULL) { update_process_finished_job (job); } else { update_job_run (job); } return FALSE; } updateJobPtr update_execute_request (gpointer owner, UpdateRequest *request, update_result_cb callback, gpointer user_data, updateFlags flags) { updateJobPtr job; g_assert (request->options != NULL); g_assert (request->source != NULL); job = update_job_new (owner, request, callback, user_data, flags); job->state = REQUEST_STATE_PENDING; jobs = g_slist_append (jobs, job); if (flags & FEED_REQ_PRIORITY_HIGH) { g_async_queue_push (pendingHighPrioJobs, (gpointer)job); } else { g_async_queue_push (pendingJobs, (gpointer)job); } g_idle_add (update_dequeue_job, NULL); return job; } void update_job_cancel_by_owner (gpointer owner) { GSList *iter = jobs; while (iter) { updateJobPtr job = (updateJobPtr)iter->data; if (job->owner == owner) job->callback = NULL; iter = g_slist_next (iter); } } static gboolean update_process_result_idle_cb (gpointer user_data) { updateJobPtr job = (updateJobPtr)user_data; if (job->callback) (job->callback) (job->result, job->user_data, job->flags); update_job_free (job); return FALSE; } static void update_apply_filter_async(GTask *task, gpointer src, gpointer tdata, GCancellable *ccan) { updateJobPtr job = tdata; update_apply_filter(job); g_task_return_int(task, 0); } static void update_apply_filter_finish(GObject *src, GAsyncResult *result, gpointer user_data) { updateJobPtr job = user_data; g_idle_add(update_process_result_idle_cb, job); } void update_process_finished_job (updateJobPtr job) { job->state = REQUEST_STATE_DEQUEUE; g_assert(numberOfActiveJobs > 0); numberOfActiveJobs--; g_idle_add (update_dequeue_job, NULL); /* Handling abandoned requests (e.g. after feed deletion) */ if (job->callback == NULL) { debug1 (DEBUG_UPDATE, "freeing cancelled request (%s)", job->request->source); update_job_free (job); return; } /* Finally execute the postfilter */ if (job->result->data && job->request->filtercmd) { GTask *task = g_task_new(NULL, NULL, update_apply_filter_finish, job); g_task_set_task_data(task, job, NULL); g_task_run_in_thread(task, update_apply_filter_async); g_object_unref(task); return; } g_idle_add (update_process_result_idle_cb, job); } void update_init (void) { pendingJobs = g_async_queue_new (); pendingHighPrioJobs = g_async_queue_new (); } void update_deinit (void) { GSList *iter = jobs; /* Cancel all jobs, to avoid async callbacks accessing the GUI */ while (iter) { updateJobPtr job = (updateJobPtr)iter->data; job->callback = NULL; iter = g_slist_next (iter); } g_async_queue_unref (pendingJobs); g_async_queue_unref (pendingHighPrioJobs); g_slist_free (jobs); jobs = NULL; } liferea-1.13.7/src/update.h000066400000000000000000000244611415350204600154430ustar00rootroot00000000000000/** * @file update.h generic update request and state processing * * Copyright (C) 2003-2020 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _UPDATE_H #define _UPDATE_H #include #include #include /* Update requests do represent feed updates, favicon and enclosure downloads. A request can be started synchronously or asynchronously. In the latter case it can be cancelled at any time. If the processing of a update request is done the request callback will be triggered. A request can have an update state assigned. This is to support the different bandwidth saving methods. For caching along feeds there are XML (de)serialization functions for the update state. For proxy support and authentication an update request can have update options assigned. Finally the request system has an on/offline state. When offline no new network requests are accepted. Filesystem and internal requests are still processed. Currently running downloads are not terminated. */ typedef enum { REQUEST_STATE_INITIALIZED = 0, /**< request struct newly created */ REQUEST_STATE_PENDING, /**< request added to download queue */ REQUEST_STATE_PROCESSING, /**< request currently in download */ REQUEST_STATE_DEQUEUE, /**< download finished, callback processing */ REQUEST_STATE_FINISHED /**< request processing finished */ } request_state; struct updateJob; struct updateResult; typedef guint32 updateFlags; /** * Generic update result processing callback type. * This callback must not free the result structure. It will be * free'd by the download system after the callback returns. * * @param result the update result * @param user_data update processing callback data * @param flags update processing flags */ typedef void (*update_result_cb) (const struct updateResult * const result, gpointer user_data, updateFlags flags); /** defines update options to be passed to an update request */ typedef struct updateOptions { gchar *username; /**< username for HTTP auth */ gchar *password; /**< password for HTTP auth */ gboolean dontUseProxy; /**< no proxy flag */ } *updateOptionsPtr; /** defines all state data an updatable object (e.g. a feed) needs */ typedef struct updateState { glong lastModified; /**< Last modified string as sent by the server */ guint64 lastPoll; /**< time at which the feed was last updated */ guint64 lastFaviconPoll; /**< time at which the feeds favicon was last updated */ gchar *cookies; /**< cookies to be used */ gchar *etag; /**< ETag sent by the server */ gint maxAgeMinutes; /**< default update interval, greatest value sourced from HTTP and XML */ gint synFrequency; /**< syn:updateFrequency */ gint synPeriod; /**< syn:updatePeriod */ gint timeToLive; /**< ttl */ } *updateStatePtr; G_BEGIN_DECLS #define UPDATE_REQUEST_TYPE (update_request_get_type ()) G_DECLARE_FINAL_TYPE (UpdateRequest, update_request, UPDATE, REQUEST, GObject) struct _UpdateRequest { GObject parent; gchar *source; /**< Location of the source. If it starts with '|', it is a command. If it contains "://", then it is parsed as a URL, otherwise it is a filename. */ gchar *postdata; /**< HTTP POST request data (NULL for non-POST requests) */ gchar *authValue; /**< Custom value for Authorization: header */ updateOptionsPtr options; /**< Update options for the request */ gchar *filtercmd; /**< Command will filter output of URL */ updateStatePtr updateState; /**< Update state of the requested object (etags, last modified...) */ }; /** structure to store results of the processing of an update request */ typedef struct updateResult { gchar *source; /**< Location of the downloaded document, in case of redirects different from the one given along with the update request */ int httpstatus; /**< HTTP status. Set to 200 for any valid command, file access, etc.... Set to 0 for unknown */ gchar *data; /**< Downloaded data */ size_t size; /**< Size of downloaded data */ gchar *contentType; /**< Content type of received data */ gchar *filterErrors; /**< Error messages from filter execution */ updateStatePtr updateState; /**< New update state of the requested object (etags, last modified...) */ } *updateResultPtr; /** structure to store state fo running command feeds */ typedef struct updateCommandState { GPid pid; /**< child PID */ guint timeout_id; /**< glib event source id for the timeout event */ guint io_watch_id; /**< glib event source id for stdout */ guint child_watch_id; /**< glib event source id for child termination */ gint fd; /**< fd for child stdout */ GIOChannel *stdout_ch; /**< child stdout as a channel */ } updateCommandState; /** structure describing an HTTP update job */ typedef struct updateJob { UpdateRequest *request; updateResultPtr result; gpointer owner; /**< owner of this job (used for matching when cancelling) */ update_result_cb callback; /**< result processing callback */ gpointer user_data; /**< result processing user data */ updateFlags flags; /**< request and result processing flags */ gint state; /**< State of the job (enum request_state) */ updateCommandState cmd; /**< values for command feeds */ } *updateJobPtr; /** * Create new update state */ updateStatePtr update_state_new (void); /** * Copy update state */ updateStatePtr update_state_copy (updateStatePtr state); glong update_state_get_lastmodified (updateStatePtr state); void update_state_set_lastmodified (updateStatePtr state, glong lastmodified); const gchar * update_state_get_etag (updateStatePtr state); void update_state_set_etag (updateStatePtr state, const gchar *etag); gint update_state_get_cache_maxage (updateStatePtr state); void update_state_set_cache_maxage (updateStatePtr state, const gint maxage); const gchar * update_state_get_cookies (updateStatePtr state); void update_state_set_cookies (updateStatePtr state, const gchar *cookies); /** * Frees the given update state. * * @param updateState the update state */ void update_state_free (updateStatePtr updateState); /** * Copies the given update options. * * @returns a new update options structure (to be free'd using update_options_free()) */ updateOptionsPtr update_options_copy (updateOptionsPtr options); /** * Frees the given update options * * @param options the update options */ void update_options_free (updateOptionsPtr options); /** * Initialises the download subsystem. * * Must be called before gtk_init() and after thread initialization * as threads are used and for proper network-manager initialization. */ void update_init (void); /** * Stops all update processing and frees all used memory. */ void update_deinit (void); /** * Creates a new request structure. * * @oaram source URI to download * @param state a previous update state of the requested URL (or NULL) * will not be owned, but copied! * @param options update options to be used (or NULL) * will not be owned but copied! * * @returns a new request GObject to be passed to update_execute_request() */ UpdateRequest * update_request_new (const gchar *source, updateStatePtr state, updateOptionsPtr options); /** * Sets the source for an updateRequest. Only use this when the source * is not known at update_request_new() calling time. * * @param request the update request * @param source the new source URL */ void update_request_set_source (UpdateRequest *request, const gchar* source); /** * Sets a custom authorization header value. * * @param request the update request * @param authValue the authorization header value */ void update_request_set_auth_value (UpdateRequest *request, const gchar* authValue); /** * Creates a new update result for the given update request. * * @returns update result (to be free'd using update_result_free()) */ updateResultPtr update_result_new (void); /** * Free's the given update result. * * @param result the result */ void update_result_free (updateResultPtr result); /** * Executes the given request. The request might be * delayed if other requests are pending. * * @param owner request owner (allows cancelling, can be NULL) * @param request the request to execute * @param callback result processing callback * @param user_data result processing callback parameters (or NULL) * @param flags request/result processing flags * * @returns the new update job */ updateJobPtr update_execute_request (gpointer owner, UpdateRequest *request, update_result_cb callback, gpointer user_data, updateFlags flags); /* Update job handling */ /** * To be called when an update job has been executed. Triggers * the job specific result processing callback. * * @param job the update job */ void update_process_finished_job (updateJobPtr job); /** * Cancel all pending requests for the given owner. * * @param owner pointer passed in update_request_new() */ void update_job_cancel_by_owner (gpointer owner); /** * Method to query the update state of currently processed jobs. * * @returns update job state (see enum request_state) */ gint update_job_get_state (updateJobPtr job); /** * update_jobs_get_count: * * Query current count and max count of subscriptions in update queue * * @count: gint ref to pass back nr of subscriptions in update * @maxcount: gint ref to pass back max nr of subscriptions in update */ void update_jobs_get_count (guint *count, guint *maxcount); G_END_DECLS #endif liferea-1.13.7/src/vfolder.c000066400000000000000000000166141415350204600156160ustar00rootroot00000000000000/** * @file vfolder.c search folder node type * * Copyright (C) 2003-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "vfolder.h" #include "common.h" #include "db.h" #include "debug.h" #include "feedlist.h" #include "itemset.h" #include "itemlist.h" #include "node.h" #include "rule.h" #include "vfolder_loader.h" #include "ui/icons.h" #include "ui/search_folder_dialog.h" /** The list of all existing vfolders. Used for updating vfolder information upon item changes */ static GSList *vfolders = NULL; vfolderPtr vfolder_new (nodePtr node) { vfolderPtr vfolder; debug_enter ("vfolder_new"); vfolder = g_new0 (struct vfolder, 1); vfolder->itemset = g_new0 (struct itemSet, 1); vfolder->itemset->nodeId = node->id; vfolder->itemset->ids = NULL; vfolder->itemset->anyMatch = TRUE; vfolder->node = node; vfolders = g_slist_append (vfolders, vfolder); if (!node->title) node_set_title (node, _("New Search Folder")); /* set default title */ node_set_data (node, (gpointer) vfolder); debug_exit ("vfolder_new"); return vfolder; } static void vfolder_import_rules (xmlNodePtr cur, vfolderPtr vfolder) { xmlChar *tmp, *type, *ruleId, *value, *additive; tmp = xmlGetProp (cur, BAD_CAST"matchType"); if (tmp) /* currently we only OR or AND the rules, "any" is the value for OR'ing, "all" for AND'ing */ vfolder->itemset->anyMatch = (0 != xmlStrcmp (tmp, BAD_CAST"all")); else vfolder->itemset->anyMatch = TRUE; xmlFree (tmp); tmp = xmlGetProp (cur, BAD_CAST"unreadOnly"); if (tmp) vfolder->unreadOnly = (0 == xmlStrcmp (tmp, BAD_CAST"true")); else vfolder->unreadOnly = FALSE; xmlFree (tmp); /* process any children */ cur = cur->xmlChildrenNode; while (cur) { if (!xmlStrcmp (cur->name, BAD_CAST"outline")) { type = xmlGetProp (cur, BAD_CAST"type"); if (type && !xmlStrcmp (type, BAD_CAST"rule")) { ruleId = xmlGetProp (cur, BAD_CAST"rule"); value = xmlGetProp (cur, BAD_CAST"value"); additive = xmlGetProp (cur, BAD_CAST"additive"); if (ruleId && value) { debug2 (DEBUG_CACHE, "loading rule \"%s\" \"%s\"", ruleId, value); if (additive && !xmlStrcmp (additive, BAD_CAST"true")) itemset_add_rule (vfolder->itemset, (gchar *)ruleId, (gchar *)value, TRUE); else itemset_add_rule (vfolder->itemset, (gchar *)ruleId, (gchar *)value, FALSE); } else { g_warning ("ignoring invalid rule entry for vfolder \"%s\"...\n", node_get_title (vfolder->node)); } xmlFree (ruleId); xmlFree (value); xmlFree (additive); } xmlFree (type); } cur = cur->next; } } static itemSetPtr vfolder_load (nodePtr node) { return db_search_folder_load (node->id); } void vfolder_foreach (nodeActionFunc func) { GSList *iter = vfolders; g_assert (NULL != func); while (iter) { vfolderPtr vfolder = (vfolderPtr)iter->data; (*func)(vfolder->node); iter = g_slist_next (iter); } } GSList * vfolder_get_all_with_item_id (itemPtr item) { GSList *result = NULL; GSList *iter = vfolders; while (iter) { vfolderPtr vfolder = (vfolderPtr)iter->data; if (itemset_check_item (vfolder->itemset, item)) result = g_slist_append (result, vfolder); iter = g_slist_next (iter); } return result; } GSList * vfolder_get_all_without_item_id (itemPtr item) { GSList *result = NULL; GSList *iter = vfolders; while (iter) { vfolderPtr vfolder = (vfolderPtr)iter->data; if (!itemset_check_item (vfolder->itemset, item)) result = g_slist_append (result, vfolder); iter = g_slist_next (iter); } return result; } static void vfolder_import (nodePtr node, nodePtr parent, xmlNodePtr cur, gboolean trusted) { vfolderPtr vfolder; debug1 (DEBUG_CACHE, "import vfolder: title=%s", node_get_title (node)); vfolder = vfolder_new (node); /* We use the itemset only to keep itemset rules, not to have the items in memory! Maybe the itemset<->filtering dependency is not a good idea... */ vfolder_import_rules (cur, vfolder); } static void vfolder_export (nodePtr node, xmlNodePtr cur, gboolean trusted) { vfolderPtr vfolder = (vfolderPtr) node->data; xmlNodePtr ruleNode; rulePtr rule; GSList *iter; debug_enter ("vfolder_export"); g_assert (TRUE == trusted); xmlNewProp (cur, BAD_CAST"matchType", BAD_CAST (vfolder->itemset->anyMatch?"any":"all")); xmlNewProp (cur, BAD_CAST"unreadOnly", BAD_CAST (vfolder->unreadOnly?"true":"false")); iter = vfolder->itemset->rules; while (iter) { rule = iter->data; ruleNode = xmlNewChild (cur, NULL, BAD_CAST"outline", NULL); xmlNewProp (ruleNode, BAD_CAST"type", BAD_CAST "rule"); xmlNewProp (ruleNode, BAD_CAST"text", BAD_CAST rule->ruleInfo->title); xmlNewProp (ruleNode, BAD_CAST"rule", BAD_CAST rule->ruleInfo->ruleId); xmlNewProp (ruleNode, BAD_CAST"value", BAD_CAST rule->value); if (rule->additive) xmlNewProp (ruleNode, BAD_CAST"additive", BAD_CAST "true"); else xmlNewProp (ruleNode, BAD_CAST"additive", BAD_CAST "false"); iter = g_slist_next (iter); } debug1 (DEBUG_CACHE, "adding vfolder: title=%s", node_get_title (node)); debug_exit ("vfolder_export"); } void vfolder_reset (vfolderPtr vfolder) { itemlist_unload (FALSE); g_list_free (vfolder->itemset->ids); vfolder->itemset->ids = NULL; db_search_folder_reset (vfolder->node->id); } void vfolder_rebuild (nodePtr node) { vfolderPtr vfolder = (vfolderPtr)node->data; vfolder_reset (vfolder); itemlist_add_search_result (vfolder_loader_new (node)); } static void vfolder_free (nodePtr node) { vfolderPtr vfolder = (vfolderPtr) node->data; debug_enter ("vfolder_free"); vfolders = g_slist_remove (vfolders, vfolder); itemset_free (vfolder->itemset); debug_exit ("vfolder_free"); } /* implementation of the node type interface */ static void vfolder_save (nodePtr node) { } static void vfolder_update_counters (nodePtr node) { node->needsUpdate = TRUE; node->unreadCount = db_search_folder_get_unread_count (node->id); node->itemCount = db_search_folder_get_item_count (node->id); } static void vfolder_remove (nodePtr node) { vfolder_reset (node->data); } static void vfolder_properties (nodePtr node) { search_folder_dialog_new (node); } static gboolean vfolder_add (void) { nodePtr node; node = node_new (vfolder_get_node_type ()); vfolder_new (node); vfolder_properties (node); return TRUE; } nodeTypePtr vfolder_get_node_type (void) { static struct nodeType nti = { NODE_CAPABILITY_SHOW_ITEM_FAVICONS | NODE_CAPABILITY_SHOW_UNREAD_COUNT, "vfolder", NULL, vfolder_import, vfolder_export, vfolder_load, vfolder_save, vfolder_update_counters, vfolder_remove, node_default_render, vfolder_add, vfolder_properties, vfolder_free }; nti.icon = icon_get (ICON_VFOLDER); return &nti; } liferea-1.13.7/src/vfolder.h000066400000000000000000000062711415350204600156210ustar00rootroot00000000000000/** * @file vfolder.h search folder node type * * Copyright (C) 2003-2020 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _VFOLDER_H #define _VFOLDER_H #include #include "itemset.h" #include "node_type.h" /* The search folder implementation of Liferea is similar to the one in Evolution. Search folders are effectivly permanent searches. As Liferea realizes filtered lists of items using rule based itemsets, search folders are effectively persistent rule based itemsets. GUI wise a search folder is a type of node in the subscription list. */ /** search folder data structure */ typedef struct vfolder { struct node *node; /**< the feed list node of this search folder */ itemSetPtr itemset; /**< the itemset with the rules and matching items */ gboolean unreadOnly; /**< TRUE if only unread items are to be shown in the item list */ gboolean reloading; /**< TRUE if the search folder is in async reloading */ gulong loadOffset; /**< when in reloading: current offset */ } *vfolderPtr; /** * Sets up a new search folder structure. * * @param node the feed list node of the search folder * * @returns a new search folder structure */ vfolderPtr vfolder_new (struct node *node); /** * Method to unconditionally invoke an node callback for all search folders. * * @param func callback */ void vfolder_foreach (nodeActionFunc func); typedef void (*vfolderActionDataFunc) (vfolderPtr vfolder, itemPtr item); /** * Returns a list of all search folders currently matching the given item. * * @param item the item * * @returns a list of vfolderPtr (to be free'd using g_slist_free()) */ GSList * vfolder_get_all_with_item_id (itemPtr item); /** * Returns a list of all search folders currently not matching the given item. * * @param item the item * * @returns a list of vfolderPtr (to be free'd using g_slist_free()) */ GSList * vfolder_get_all_without_item_id (itemPtr item); /** * Resets vfolder state. Drops all items from it. * To be called after vfolder_(add|remove)_rule(). * * @param vfolder search folder to reset */ void vfolder_reset (vfolderPtr vfolder); /** * Rebuilds a search folder by scanning all existing items. * * @param vfolder search folder to rebuild */ void vfolder_rebuild (nodePtr node); /* implementation of the node type interface */ #define IS_VFOLDER(node) (node->type == vfolder_get_node_type ()) /** * Returns the node type implementation for search folder nodes. */ nodeTypePtr vfolder_get_node_type (void); #endif liferea-1.13.7/src/vfolder_loader.c000066400000000000000000000051641415350204600171420ustar00rootroot00000000000000/** * @file vfolder_loader.c Loader for search folder items * * Copyright (C) 2011-2012 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "vfolder_loader.h" #include "db.h" #include "debug.h" #include "itemset.h" #include "node.h" #include "vfolder.h" #include "ui/feed_list_view.h" #define VFOLDER_LOADER_BATCH_SIZE 100 static gboolean vfolder_loader_fetch_cb (gpointer user_data, GSList **resultItems) { vfolderPtr vfolder = (vfolderPtr)user_data; itemSetPtr items = g_new0 (struct itemSet, 1); GList *iter; gboolean result; /* 1. Fetch a batch of items */ result = db_itemset_get (items, vfolder->loadOffset, VFOLDER_LOADER_BATCH_SIZE); vfolder->loadOffset += VFOLDER_LOADER_BATCH_SIZE; if (result) { /* 2. Match all items against search folder */ iter = items->ids; while (iter) { gulong id = GPOINTER_TO_UINT (iter->data); itemPtr item = db_item_load (id); if (itemset_check_item (vfolder->itemset, item)) *resultItems = g_slist_append (*resultItems, item); else item_unload (item); iter = g_list_next (iter); } } else { debug1 (DEBUG_CACHE, "search folder '%s' reload complete", vfolder->node->title); vfolder->reloading = FALSE; } itemset_free (items); /* 3. Save items to DB and update UI (except for search results) */ if (vfolder->node) { db_search_folder_add_items (vfolder->node->id, *resultItems); node_update_counters (vfolder->node); feed_list_view_update_node (vfolder->node->id); } return result; /* FALSE on last fetch */ } ItemLoader * vfolder_loader_new (nodePtr node) { vfolderPtr vfolder = (vfolderPtr)node->data; if(vfolder->reloading) { debug1 (DEBUG_CACHE, "search folder '%s' still reloading", node->title); return NULL; } debug1 (DEBUG_CACHE, "search folder '%s' reload started", node->title); vfolder_reset (vfolder); vfolder->reloading = TRUE; vfolder->loadOffset = 0; return item_loader_new (vfolder_loader_fetch_cb, node, vfolder); } liferea-1.13.7/src/vfolder_loader.h000066400000000000000000000022121415350204600171360ustar00rootroot00000000000000/** * @file vfolder_loader.h Loader for search folder items * * Copyright (C) 2011 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _VFOLDER_LOADER_H #define _VFOLDER_LOADER_H #include "item_loader.h" #include "node.h" /** * Create a new item loader for the given search folder node. * * @param vfolder the search folder to load * * @returns an ItemLoader instance */ ItemLoader * vfolder_loader_new (nodePtr vfolder); #endif liferea-1.13.7/src/webkit/000077500000000000000000000000001415350204600152665ustar00rootroot00000000000000liferea-1.13.7/src/webkit/Makefile.am000066400000000000000000000005561415350204600173300ustar00rootroot00000000000000## Process this file with automake to produce Makefile.in SUBDIRS = web_extension AM_CPPFLAGS = -I$(top_srcdir)/src $(PACKAGE_CFLAGS) $(WEBKIT_CFLAGS) -DWEB_EXTENSIONS_DIR=\""$(pkglibdir)/web-extension"\" noinst_LIBRARIES = libwebkit.a libwebkit_a_SOURCES = webkit.c liferea_web_view.c liferea_web_view.h libwebkit_a_CFLAGS = $(PACKAGE_CFLAGS) $(WEBKIT_CFLAGS) liferea-1.13.7/src/webkit/liferea_web_view.c000066400000000000000000000560501415350204600207360ustar00rootroot00000000000000/** * @file liferea_web_view.c Webkit2 widget for Liferea * * Copyright (C) 2016 Leiaz * Copyright (C) 2021 Lars Windolf * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "liferea_web_view.h" #include "../debug.h" #include "browser.h" #include "common.h" #include "enclosure.h" #include "feedlist.h" #include "social.h" #include "ui/browser_tabs.h" #include "ui/liferea_htmlview.h" #include "ui/item_list_view.h" #include "ui/itemview.h" #include "web_extension/liferea_web_extension_names.h" struct _LifereaWebView { WebKitWebView parent; GActionGroup *menu_action_group; GDBusConnection *dbus_connection; }; struct _LifereaWebViewClass { WebKitWebViewClass parent_class; }; G_DEFINE_TYPE (LifereaWebView, liferea_web_view, WEBKIT_TYPE_WEB_VIEW) static void liferea_web_view_finalize(GObject *gobject) { LifereaWebView *self = LIFEREA_WEB_VIEW(gobject); if (self->dbus_connection) { g_object_remove_weak_pointer (G_OBJECT (self->dbus_connection), (gpointer *) &self->dbus_connection); } /* Chaining finalize from parent class. */ G_OBJECT_CLASS(liferea_web_view_parent_class)->finalize(gobject); } static void liferea_web_view_class_init(LifereaWebViewClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->finalize = liferea_web_view_finalize; } static void can_copy_callback (GObject *web_view, GAsyncResult *result, gpointer user_data) { gboolean enabled; GError *error = NULL; GActionGroup *action_group; GSimpleAction *copy_action; enabled = webkit_web_view_can_execute_editing_command_finish (WEBKIT_WEB_VIEW (web_view), result, &error); if (error) { g_warning ("Error can_execute_editing_command callback : %s\n", error->message); g_error_free (error); return; } action_group = LIFEREA_WEB_VIEW (web_view)->menu_action_group; copy_action = G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (action_group), "copy-selection")); g_simple_action_set_enabled (copy_action, enabled); } static void liferea_web_view_update_actions_sensitivity (LifereaWebView *self) { webkit_web_view_can_execute_editing_command ( WEBKIT_WEB_VIEW (self), WEBKIT_EDITING_COMMAND_COPY, NULL, can_copy_callback, NULL); } /* * Copied from liferea_htmlview.c Perhaps could go in common.h ? */ static gboolean liferea_web_view_is_special_url (const gchar *url) { /* match against all special protocols, simple convention: all have to start with "liferea-" */ if (url == strstr (url, "liferea-")) return TRUE; return FALSE; } static void menu_add_item (GMenu *menu, const gchar *label, const gchar *action, const gchar *parameter) { GMenuItem *menu_item; menu_item = g_menu_item_new (label, NULL); g_menu_item_set_action_and_target (menu_item, action, "s", parameter); g_menu_append_item (menu, menu_item); g_object_unref (menu_item); } /** * Callback for WebKitWebView::context-menu signal. * * @view: the object on which the signal is emitted * @context_menu: the context menu proposed by WebKit * @event: the event that triggered the menu * @hit_result: result of hit test at that location. * * When a context menu is about to be displayed this signal is emitted. * */ static gboolean liferea_web_view_on_menu (WebKitWebView *view, WebKitContextMenu *context_menu, GdkEvent *event, WebKitHitTestResult *hit_result, gpointer user_data) { GtkWidget *menu; GMenu *menu_model,*section; GMenuItem *menu_item = NULL; gchar *image_uri = NULL; gchar *link_uri = NULL; gchar *link_title = NULL; gboolean link, image; if (webkit_hit_test_result_context_is_link (hit_result)) g_object_get (hit_result, "link-uri", &link_uri, "link-title", &link_title, NULL); if (webkit_hit_test_result_context_is_image (hit_result)) g_object_get (hit_result, "image-uri", &image_uri, NULL); if (webkit_hit_test_result_context_is_media (hit_result)) g_object_get (hit_result, "media-uri", &link_uri, NULL); /* treat media as normal link */ /* Making the menu */ link = (link_uri != NULL); image = (image_uri != NULL); /* do not expose internal links */ if (link_uri && liferea_web_view_is_special_url (link_uri) && !g_str_has_prefix (link_uri, "javascript:") && !g_str_has_prefix (link_uri, "data:")) link = FALSE; liferea_web_view_update_actions_sensitivity (LIFEREA_WEB_VIEW (view)); menu_model = g_menu_new (); section = g_menu_new (); /* Sections are used to get separators.*/ /* and now add all we want to see */ if (link) { gchar *path; menu_add_item (section, _("Open Link In _Tab"), "app.open-link-in-tab", link_uri); menu_add_item (section, _("Open Link In Browser"), "app.open-link-in-browser", link_uri); menu_add_item (section, _("Open Link In External Browser"), "app.open-link-in-external-browser", link_uri); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); path = g_strdup_printf (_("_Bookmark Link at %s"), social_get_bookmark_site ()); menu_item = g_menu_item_new (path, NULL); g_menu_item_set_action_and_target (menu_item, "app.social-bookmark-link", "(ss)", link_uri, link_title?link_title:""); g_menu_append_item (section, menu_item); g_object_unref (menu_item); g_free (path); menu_add_item (section, _("_Copy Link Location"), "app.copy-link-to-clipboard", link_uri); } if (image) { menu_add_item (section, _("_View Image"), "app.open-link-in-tab", image_uri); menu_add_item (section, _("_Copy Image Location"), "app.copy-link-to-clipboard", image_uri); } if (link) { menu_add_item (section, _("S_ave Link As"), "liferea_web_view.save-link", link_uri); } if (image) { menu_add_item (section, _("S_ave Image As"), "liferea_web_view.save-link", image_uri); } if (link) { g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); menu_add_item (section, _("_Subscribe..."), "liferea_web_view.subscribe-link", link_uri); } if(!link && !image) { g_menu_append (section, _("_Copy"), "liferea_web_view.copy-selection"); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("_Increase Text Size"), "liferea_web_view.zoom-in"); g_menu_append (section, _("_Decrease Text Size"), "liferea_web_view.zoom-out"); } g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); section = g_menu_new (); g_menu_append (section, _("_Reader Mode"), "liferea_web_view.toggle-reader-mode"); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); g_free (link_uri); g_free (image_uri); g_free (link_title); if(debug_level & DEBUG_HTML) { section = g_menu_new (); g_menu_append (section, "Inspect", "liferea_web_view.web-inspector"); g_menu_append_section (menu_model, NULL, G_MENU_MODEL (section)); g_object_unref (section); } menu = gtk_menu_new_from_model (G_MENU_MODEL (menu_model)); gtk_menu_attach_to_widget (GTK_MENU (menu), GTK_WIDGET (view), NULL); gtk_menu_popup_at_pointer (GTK_MENU (menu), event); return TRUE; // TRUE to ignore WebKit's menu as we make our own menu. } static void on_popup_copy_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (user_data), WEBKIT_EDITING_COMMAND_COPY); } static void on_popup_save_link_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { enclosure_download (NULL, g_variant_get_string (parameter, NULL), TRUE); } static void on_popup_subscribe_link_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { feedlist_add_subscription (g_variant_get_string (parameter, NULL), NULL, NULL, 0); } static void on_popup_zoomin_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { LifereaHtmlView *htmlview = g_object_get_data (G_OBJECT (user_data), "htmlview"); liferea_htmlview_do_zoom (htmlview, TRUE); } static void on_popup_zoomout_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { LifereaHtmlView *htmlview = g_object_get_data (G_OBJECT (user_data), "htmlview"); liferea_htmlview_do_zoom (htmlview, FALSE); } static void on_popup_webinspector_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { WebKitSettings *settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (user_data)); g_object_set (G_OBJECT(settings), "enable-developer-extras", TRUE, NULL); WebKitWebInspector *inspector = webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (user_data)); webkit_web_inspector_show (WEBKIT_WEB_INSPECTOR(inspector)); } static void on_popup_toggle_reader_mode_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data) { WebKitWebView *webview = WEBKIT_WEB_VIEW (user_data); gboolean reader = g_variant_get_boolean (parameter); g_warning("Change reader mode"); liferea_htmlview_set_reader_mode (g_object_get_data (G_OBJECT (webview), "htmlview"), reader); g_simple_action_set_state (action, g_variant_new_boolean (reader)); } static const GActionEntry liferea_web_view_gaction_entries[] = { {"save-link", on_popup_save_link_activate, "s", NULL, NULL}, {"subscribe-link", on_popup_subscribe_link_activate, "s", NULL, NULL}, {"copy-selection", on_popup_copy_activate, NULL, NULL, NULL}, {"zoom-in", on_popup_zoomin_activate, NULL, NULL, NULL}, {"zoom-out", on_popup_zoomout_activate, NULL, NULL, NULL}, {"web-inspector", on_popup_webinspector_activate, NULL, NULL, NULL}, {"toggle-reader-mode", NULL, NULL, "true", on_popup_toggle_reader_mode_activate} }; static void liferea_web_view_title_changed (WebKitWebView *view, GParamSpec *pspec, gpointer user_data) { LifereaHtmlView *htmlview; gchar *title; htmlview = g_object_get_data (G_OBJECT (view), "htmlview"); g_object_get (view, "title", &title, NULL); liferea_htmlview_title_changed (htmlview, title); g_free (title); } /* * Callback for the mouse-target-changed signal. * * Updates selected_url with hovered link. */ static void liferea_web_view_on_mouse_target_changed (WebKitWebView *view, WebKitHitTestResult *hit_result, guint modifiers, gpointer user_data) { LifereaHtmlView *htmlview; gchar *selected_url; htmlview = g_object_get_data (G_OBJECT (view), "htmlview"); selected_url = g_object_get_data (G_OBJECT (view), "selected_url"); if (selected_url) g_free (selected_url); if (webkit_hit_test_result_context_is_link (hit_result)) { g_object_get (hit_result, "link-uri", &selected_url, NULL); } else { selected_url = g_strdup (""); } /* overwrite or clear last status line text */ liferea_htmlview_on_url (htmlview, selected_url); g_object_set_data (G_OBJECT (view), "selected_url", selected_url); } struct FullscreenData { GtkWidget *me; gboolean visible; }; /** * callback for fullscreen mode gtk_container_foreach() */ static void fullscreen_toggle_widget_visible(GtkWidget *wid, gpointer user_data) { gchar* data_label; struct FullscreenData *fdata; gboolean old_v; gchar *propName; fdata = user_data; // remove shadow of scrolled window if (GTK_IS_SCROLLED_WINDOW(wid)) { GtkShadowType shadow_type; data_label = "fullscreen_shadow_type"; propName = "shadow-type"; if (fdata->visible == FALSE) { g_object_get(G_OBJECT(wid), propName, &shadow_type, NULL); g_object_set(G_OBJECT(wid), propName, GTK_SHADOW_NONE, NULL); g_object_set_data(G_OBJECT(wid), data_label, GINT_TO_POINTER(shadow_type)); } else { shadow_type = GPOINTER_TO_INT(g_object_steal_data( G_OBJECT(wid), data_label)); if (shadow_type && shadow_type != GTK_SHADOW_NONE) { g_object_set(G_OBJECT(wid), propName, shadow_type, NULL); } } } if (wid == fdata->me && !GTK_IS_NOTEBOOK(wid)) { return; } data_label = "fullscreen_visible"; if (GTK_IS_NOTEBOOK(wid)) { propName = "show-tabs"; } else { propName = "visible"; } if (fdata->visible == FALSE) { g_object_get(G_OBJECT(wid), propName, &old_v, NULL); g_object_set(G_OBJECT(wid), propName, FALSE, NULL); g_object_set_data(G_OBJECT(wid), data_label, GINT_TO_POINTER(old_v)); } else { old_v = GPOINTER_TO_INT(g_object_steal_data( G_OBJECT(wid), data_label)); if (old_v == TRUE) { g_object_set(G_OBJECT(wid), propName, TRUE, NULL); } } } /** * For fullscreen mode, hide everything except the current webview */ static void fullscreen_toggle_parent_visible(GtkWidget *me, gboolean visible) { GtkWidget *parent; struct FullscreenData *fdata; fdata = (struct FullscreenData *)g_new0(struct FullscreenData, 1); // Flag fullscreen status g_object_set_data(G_OBJECT(me), "fullscreen_on", GINT_TO_POINTER(!visible)); parent = gtk_widget_get_parent(me); fdata->visible = visible; while (parent != NULL) { fdata->me = me; gtk_container_foreach(GTK_CONTAINER(parent), (GtkCallback)fullscreen_toggle_widget_visible, (gpointer)fdata); me = parent; parent = gtk_widget_get_parent(me); } g_free(fdata); } /** * WebKitWebView "enter-fullscreen" signal * Hide all the widget except current WebView */ static gboolean liferea_web_view_entering_fullscreen (WebKitWebView *view, gpointer user_data) { fullscreen_toggle_parent_visible(GTK_WIDGET(view), FALSE); return FALSE; } /** * WebKitWebView "leave-fullscreen" signal * Restore visibility of hidden widgets */ static gboolean liferea_web_view_leaving_fullscreen (WebKitWebView *view, gpointer user_data) { fullscreen_toggle_parent_visible(GTK_WIDGET(view), TRUE); return FALSE; } /** * A link has been clicked * * When a link has been clicked the link management is dispatched to Liferea * core in order to manage the different filetypes, remote URLs. */ static gboolean liferea_web_view_link_clicked ( WebKitWebView *view, WebKitPolicyDecision *policy_decision) { const gchar *uri; WebKitNavigationAction *navigation_action; WebKitURIRequest *request; WebKitNavigationType reason; gboolean url_handled; g_return_val_if_fail (WEBKIT_IS_WEB_VIEW (view), FALSE); g_return_val_if_fail (WEBKIT_IS_POLICY_DECISION (policy_decision), FALSE); navigation_action = webkit_navigation_policy_decision_get_navigation_action (WEBKIT_NAVIGATION_POLICY_DECISION (policy_decision)); reason = webkit_navigation_action_get_navigation_type (navigation_action); /* iframes in items return WEBKIT_WEB_NAVIGATION_REASON_OTHER and shouldn't be handled as clicks */ if (reason != WEBKIT_NAVIGATION_TYPE_LINK_CLICKED) return FALSE; request = webkit_navigation_action_get_request (navigation_action); uri = webkit_uri_request_get_uri (request); if (webkit_navigation_action_get_mouse_button (navigation_action) == 2) { /* middle click */ browser_tabs_add_new (uri, uri, FALSE); webkit_policy_decision_ignore (policy_decision); return TRUE; } url_handled = liferea_htmlview_handle_URL (g_object_get_data (G_OBJECT (view), "htmlview"), uri); if (url_handled) webkit_policy_decision_ignore (policy_decision); return url_handled; } /** * A new window was requested. This is the case e.g. if the link * has target="_blank". In that case, we don't open the link in a new * tab, but do what the user requested as if it didn't have a target. */ static gboolean liferea_web_view_new_window_requested ( WebKitWebView *view, WebKitPolicyDecision *policy_decision) { WebKitNavigationAction *navigation_action; WebKitURIRequest *request; const gchar *uri; navigation_action = webkit_navigation_policy_decision_get_navigation_action (WEBKIT_NAVIGATION_POLICY_DECISION (policy_decision)); request = webkit_navigation_action_get_request (navigation_action); uri = webkit_uri_request_get_uri (request); if (webkit_navigation_action_get_mouse_button (navigation_action) == 2) { /* middle-click, let's open the link in a new tab */ browser_tabs_add_new (uri, uri, FALSE); } else if (liferea_htmlview_handle_URL (g_object_get_data (G_OBJECT (view), "htmlview"), uri)) { /* The link is to be opened externally, let's do nothing here */ } else { /* If the link is not to be opened in a new tab, nor externally, * it was likely a normal click on a target="_blank" link. * Let's open it in the current view to not disturb users */ webkit_web_view_load_uri (view, uri); } /* We handled the request ourselves */ webkit_policy_decision_ignore (policy_decision); return TRUE; } static gboolean liferea_web_view_response_decision_requested (WebKitWebView *view, WebKitPolicyDecision *decision) { g_return_val_if_fail (WEBKIT_IS_RESPONSE_POLICY_DECISION (decision), FALSE); if (!webkit_response_policy_decision_is_mime_type_supported (WEBKIT_RESPONSE_POLICY_DECISION (decision))) { webkit_policy_decision_download (decision); return TRUE; } return FALSE; } static gboolean liferea_web_view_decide_policy (WebKitWebView *view, WebKitPolicyDecision *decision, WebKitPolicyDecisionType type) { switch (type) { case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: return liferea_web_view_link_clicked (view, decision); case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION: return liferea_web_view_new_window_requested(view, decision); case WEBKIT_POLICY_DECISION_TYPE_RESPONSE: return liferea_web_view_response_decision_requested (view, decision); default: return FALSE; } return FALSE; } /** * e.g. after a click on javascript:openZoom() */ static WebKitWebView* liferea_web_view_create_web_view (WebKitWebView *view, WebKitNavigationAction *action, gpointer user_data) { LifereaHtmlView *htmlview; GtkWidget *container; GtkWidget *htmlwidget; GList *children; htmlview = browser_tabs_add_new (NULL, NULL, TRUE); container = liferea_htmlview_get_widget (htmlview); /* Ugly lookup of the webview. LifereaHtmlView uses a GtkBox with first a URL bar (sometimes invisble) and the HTML renderer as 2nd child */ children = gtk_container_get_children (GTK_CONTAINER (container)); htmlwidget = children->next->data; return WEBKIT_WEB_VIEW (htmlwidget); } static void liferea_web_view_load_status_changed (WebKitWebView *view, WebKitLoadEvent event, gpointer user_data) { LifereaHtmlView *htmlview; gboolean isFullscreen; switch (event) { case WEBKIT_LOAD_STARTED: // Hack to force webview exit from fullscreen mode on new page isFullscreen = GPOINTER_TO_INT(g_object_steal_data( G_OBJECT(view), "fullscreen_on")); if (isFullscreen == TRUE) { webkit_web_view_run_javascript (view, "document.webkitExitFullscreen();", NULL, NULL, NULL); } break; case WEBKIT_LOAD_COMMITTED: htmlview = g_object_get_data (G_OBJECT (view), "htmlview"); liferea_htmlview_location_changed (htmlview, webkit_web_view_get_uri (view)); break; case WEBKIT_LOAD_FINISHED: htmlview = g_object_get_data (G_OBJECT (view), "htmlview"); GActionGroup *action_group; action_group = LIFEREA_WEB_VIEW (view)->menu_action_group; GSimpleAction *reader_action; reader_action = G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (action_group), "toggle-reader-mode")); gboolean reader = liferea_htmlview_get_reader_mode(htmlview); g_simple_action_set_state (reader_action, g_variant_new_boolean (reader)); liferea_htmlview_load_finished (htmlview, webkit_web_view_get_uri (view)); break; default: break; } } static void liferea_web_view_progress_changed (GObject *webview, GParamSpec *pspec, gpointer user_data) { LifereaHtmlView *htmlview = g_object_get_data (G_OBJECT (webview), "htmlview"); liferea_htmlview_progress_changed (htmlview, webkit_web_view_get_estimated_load_progress (WEBKIT_WEB_VIEW (webview))); } static void liferea_web_view_init(LifereaWebView *self) { self->dbus_connection = NULL; g_signal_connect ( self, "context-menu", G_CALLBACK (liferea_web_view_on_menu), NULL ); /* Context menu actions */ self->menu_action_group = G_ACTION_GROUP (g_simple_action_group_new ()); g_action_map_add_action_entries (G_ACTION_MAP(self->menu_action_group), liferea_web_view_gaction_entries, G_N_ELEMENTS (liferea_web_view_gaction_entries), self); gtk_widget_insert_action_group (GTK_WIDGET (self), "liferea_web_view", self->menu_action_group); g_signal_connect ( self, "notify::title", G_CALLBACK (liferea_web_view_title_changed), NULL ); g_signal_connect ( self, "notify::estimated-load-progress", G_CALLBACK (liferea_web_view_progress_changed), NULL ); g_signal_connect ( self, "mouse-target-changed", G_CALLBACK (liferea_web_view_on_mouse_target_changed), NULL ); g_signal_connect ( self, "enter-fullscreen", G_CALLBACK (liferea_web_view_entering_fullscreen), NULL ); g_signal_connect ( self, "leave-fullscreen", G_CALLBACK (liferea_web_view_leaving_fullscreen), NULL ); g_signal_connect ( self, "decide-policy", G_CALLBACK (liferea_web_view_decide_policy), NULL ); g_signal_connect ( self, "create", G_CALLBACK (liferea_web_view_create_web_view), NULL ); g_signal_connect ( self, "load-changed", G_CALLBACK (liferea_web_view_load_status_changed), NULL ); } static void scroll_pagedown_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { GVariant *result = NULL; GError *error = NULL; gboolean scrolled; result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error); if (result == NULL) { g_warning ("Error invoking scrollPageDown: %s\n", error->message); g_error_free (error); return; } g_variant_get (result, "(b)", &scrolled); if (!scrolled) { on_next_unread_item_activate (NULL, NULL, NULL); } } void liferea_web_view_scroll_pagedown (LifereaWebView *self) { if (!self->dbus_connection) return; g_dbus_connection_call (self->dbus_connection, LIFEREA_WEB_EXTENSION_BUS_NAME, LIFEREA_WEB_EXTENSION_OBJECT_PATH, LIFEREA_WEB_EXTENSION_INTERFACE_NAME, "ScrollPageDown", g_variant_new ("(t)", webkit_web_view_get_page_id (WEBKIT_WEB_VIEW (self))), ((const GVariantType *) "(b)"), G_DBUS_CALL_FLAGS_NONE, -1, /* Default timeout */ NULL, scroll_pagedown_callback, NULL); } void liferea_web_view_set_dbus_connection (LifereaWebView *self, GDBusConnection *connection) { if (self->dbus_connection) { g_object_remove_weak_pointer (G_OBJECT (self->dbus_connection), (gpointer *) &self->dbus_connection); } self->dbus_connection = connection; g_object_add_weak_pointer (G_OBJECT (self->dbus_connection), (gpointer *) &self->dbus_connection); } LifereaWebView * liferea_web_view_new () { return g_object_new(LIFEREA_TYPE_WEB_VIEW, NULL); } liferea-1.13.7/src/webkit/liferea_web_view.h000066400000000000000000000035231415350204600207400ustar00rootroot00000000000000/** * @file liferea_web_view.h Webkit2 widget for Liferea * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_WEB_VIEW_H #define _LIFEREA_WEB_VIEW_H #include #define LIFEREA_TYPE_WEB_VIEW liferea_web_view_get_type () #define LIFEREA_WEB_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_TYPE_WEB_VIEW, LifereaWebView)) #define IS_LIFEREA_WEB_VIEW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_TYPE_WEB_VIEW)) #define LIFEREA_WEB_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), LIFEREA_TYPE_WEB_VIEW, LifereaWebViewClass)) #define IS_LIFEREA_WEB_VIEW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), LIFEREA_TYPE_WEB_VIEW)) #define LIFEREA_WEB_VIEW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), LIFEREA_TYPE_WEB_VIEW, LifereaWebViewClass)) typedef struct _LifereaWebView LifereaWebView; typedef struct _LifereaWebViewClass LifereaWebViewClass; GType liferea_web_view_get_type (void); LifereaWebView * liferea_web_view_new (void); void liferea_web_view_set_dbus_connection (LifereaWebView *self, GDBusConnection *connection); void liferea_web_view_scroll_pagedown (LifereaWebView *self); #endif liferea-1.13.7/src/webkit/web_extension/000077500000000000000000000000001415350204600201375ustar00rootroot00000000000000liferea-1.13.7/src/webkit/web_extension/Makefile.am000066400000000000000000000006501415350204600221740ustar00rootroot00000000000000webextension_LTLIBRARIES = liblifereawebextension.la webextensiondir = $(pkglibdir)/web-extension liblifereawebextension_la_SOURCES = web_extension_main.c liferea_web_extension.c liferea_web_extension.h liferea_web_extension_names.h liblifereawebextension_la_CFLAGS = $(WEB_EXTENSION_CFLAGS) liblifereawebextension_la_LIBADD = $(WEB_EXTENSION_LIBS) liblifereawebextension_la_LDFLAGS = -module -avoid-version -no-undefined liferea-1.13.7/src/webkit/web_extension/liferea_web_extension.c000066400000000000000000000221121415350204600246410ustar00rootroot00000000000000/** * @file liferea_web_extension.c Control WebKit2 via DBUS from Liferea * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #define WEBKIT_DOM_USE_UNSTABLE_API #include #include "liferea_web_extension.h" #include "liferea_web_extension_names.h" struct _LifereaWebExtension { GObject parent; GDBusConnection *connection; WebKitWebExtension *webkit_extension; GArray *pending_pages_created; gboolean initialized; GSettings *liferea_settings; }; struct _LifereaWebExtensionClass { GObjectClass parent_class; }; G_DEFINE_TYPE (LifereaWebExtension, liferea_web_extension, G_TYPE_OBJECT) static const char introspection_xml[] = "" " " " " " " " " " " " " " " " " " " ""; static void liferea_web_extension_dispose (GObject *object) { LifereaWebExtension *extension = LIFEREA_WEB_EXTENSION (object); g_clear_object (&extension->connection); g_clear_object (&extension->webkit_extension); g_clear_object (&extension->liferea_settings); } static void liferea_web_extension_class_init (LifereaWebExtensionClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = liferea_web_extension_dispose; } static void liferea_web_extension_init (LifereaWebExtension *self) { self->webkit_extension = NULL; self->connection = NULL; self->pending_pages_created = NULL; self->initialized = FALSE; self->liferea_settings = g_settings_new ("net.sf.liferea"); } static WebKitDOMDOMWindow* liferea_web_extension_get_dom_window (LifereaWebExtension *self, guint64 page_id) { WebKitWebPage *page; WebKitDOMDocument *document; WebKitDOMDOMWindow *window; page = webkit_web_extension_get_page (self->webkit_extension, page_id); document = webkit_web_page_get_dom_document (page); window = webkit_dom_document_get_default_view (document); return window; } /* * \returns TRUE if scrolling happened, FALSE if the end was reached */ static gboolean liferea_web_extension_scroll_page_down (LifereaWebExtension *self, guint64 page_id) { glong old_scroll_y, new_scroll_y, increment; WebKitDOMDOMWindow *window; window = liferea_web_extension_get_dom_window (self, page_id); old_scroll_y = webkit_dom_dom_window_get_scroll_y (window); increment = webkit_dom_dom_window_get_inner_height (window); webkit_dom_dom_window_scroll_by (window, 0, increment); new_scroll_y = webkit_dom_dom_window_get_scroll_y (window); return (new_scroll_y > old_scroll_y); } static gboolean on_authorize_authenticated_peer (GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials, gpointer extension) { gboolean authorized = FALSE; GCredentials *own_credentials = NULL; GError *error = NULL; if (!credentials) { g_warning ("No credentials received from Liferea.\n"); return FALSE; } own_credentials = g_credentials_new (); if (g_credentials_is_same_user (credentials, own_credentials, &error)) { authorized = TRUE; } else { g_warning ("Error authorizing connection to Liferea : %s\n", error->message); g_error_free (error); } g_object_unref (own_credentials); return authorized; } static void handle_dbus_method_call (GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { if (g_strcmp0 (method_name, "ScrollPageDown") == 0) { guint64 page_id; gboolean scrolled; g_variant_get (parameters, "(t)", &page_id); scrolled = liferea_web_extension_scroll_page_down (LIFEREA_WEB_EXTENSION (user_data), page_id); g_dbus_method_invocation_return_value (invocation, g_variant_new ("(b)", scrolled)); } } static const GDBusInterfaceVTable interface_vtable = { handle_dbus_method_call, NULL, NULL }; static void liferea_web_extension_emit_page_created (LifereaWebExtension *extension, guint64 page_id) { g_dbus_connection_emit_signal ( extension->connection, NULL, LIFEREA_WEB_EXTENSION_OBJECT_PATH, LIFEREA_WEB_EXTENSION_INTERFACE_NAME, "PageCreated", g_variant_new ("(t)", page_id), NULL); } static void liferea_web_extension_queue_page_created (LifereaWebExtension *extension, guint64 page_id) { if (!extension->pending_pages_created) { extension->pending_pages_created = g_array_new (FALSE, FALSE, sizeof (guint64)); } g_array_append_val (extension->pending_pages_created, page_id); } static void liferea_web_extension_emit_pending_pages_created (LifereaWebExtension *extension) { guint i; if (!extension->pending_pages_created) return; for (i = 0;ipending_pages_created->len;++i) { guint64 page_id = g_array_index (extension->pending_pages_created, guint64, i); liferea_web_extension_emit_page_created (extension, page_id); } g_array_free (extension->pending_pages_created, TRUE); extension->pending_pages_created = NULL; } static gboolean on_send_request (WebKitWebPage *web_page, WebKitURIRequest *request, WebKitURIResponse *redirected_response, gpointer web_extension) { SoupMessageHeaders *headers = webkit_uri_request_get_http_headers (request); gboolean do_not_track; do_not_track = g_settings_get_boolean ( LIFEREA_WEB_EXTENSION (web_extension)->liferea_settings, "do-not-track"); if (do_not_track && headers) { soup_message_headers_append (headers, "DNT", "1"); } return FALSE; } static void on_page_created (WebKitWebExtension *webkit_extension, WebKitWebPage *web_page, gpointer extension) { guint64 page_id; g_signal_connect ( web_page, "send-request", G_CALLBACK (on_send_request), extension ); page_id = webkit_web_page_get_id (web_page); if (LIFEREA_WEB_EXTENSION (extension)->connection) { liferea_web_extension_emit_page_created (LIFEREA_WEB_EXTENSION (extension), page_id); } else { liferea_web_extension_queue_page_created (LIFEREA_WEB_EXTENSION (extension), page_id); } } static void on_dbus_connection_created (GObject *source_object, GAsyncResult *result, gpointer user_data) { GDBusNodeInfo *introspection_data = NULL; GDBusConnection *connection = NULL; guint registration_id = 0; GError *error = NULL; LifereaWebExtension *extension = LIFEREA_WEB_EXTENSION (user_data); introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); connection = g_dbus_connection_new_for_address_finish (result, &error); if (error) { g_warning ("Extension failed to connect : %s", error->message); g_error_free (error); return; } registration_id = g_dbus_connection_register_object (connection, LIFEREA_WEB_EXTENSION_OBJECT_PATH, introspection_data->interfaces[0], &interface_vtable, extension, NULL, &error); g_dbus_node_info_unref (introspection_data); if (!registration_id) { g_warning ("Failed to register web extension object: %s\n", error->message); g_error_free (error); g_object_unref (connection); return; } extension->connection = connection; liferea_web_extension_emit_pending_pages_created (extension); } static gpointer liferea_web_extension_new (gpointer data) { return g_object_new (LIFEREA_TYPE_WEB_EXTENSION, NULL); } LifereaWebExtension * liferea_web_extension_get (void) { static GOnce init_once = G_ONCE_INIT; g_once (&init_once, liferea_web_extension_new, NULL); return init_once.retval; } void liferea_web_extension_initialize (LifereaWebExtension *extension, WebKitWebExtension *webkit_extension, const gchar *server_address) { if (extension->initialized) return; g_signal_connect ( webkit_extension, "page-created", G_CALLBACK (on_page_created), extension); GDBusAuthObserver *observer; extension->initialized = TRUE; extension->webkit_extension = g_object_ref (webkit_extension); observer = g_dbus_auth_observer_new (); g_signal_connect ( observer, "authorize-authenticated-peer", G_CALLBACK (on_authorize_authenticated_peer), extension); g_dbus_connection_new_for_address ( server_address, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, observer, NULL, (GAsyncReadyCallback)on_dbus_connection_created, extension); g_object_unref (observer); } liferea-1.13.7/src/webkit/web_extension/liferea_web_extension.h000066400000000000000000000037571415350204600246640ustar00rootroot00000000000000/** * @file liferea_web_extension.h Control WebKit2 via DBUS from Liferea * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_WEB_EXTENSION_H #define _LIFEREA_WEB_EXTENSION_H #include #include #define LIFEREA_TYPE_WEB_EXTENSION liferea_web_extension_get_type () #define LIFEREA_WEB_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_TYPE_WEB_EXTENSION, LifereaWebExtension)) #define IS_LIFEREA_WEB_EXTENSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_TYPE_WEB_EXTENSION)) #define LIFEREA_WEB_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), LIFEREA_TYPE_WEB_EXTENSION, LifereaWebExtensionClass)) #define IS_LIFEREA_WEB_EXTENSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), LIFEREA_TYPE_WEB_EXTENSION)) #define LIFEREA_WEB_EXTENSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), LIFEREA_TYPE_WEB_EXTENSION, LifereaWebExtensionClass)) typedef struct _LifereaWebExtension LifereaWebExtension; typedef struct _LifereaWebExtensionClass LifereaWebExtensionClass; GType liferea_web_extension_get_type (void); LifereaWebExtension* liferea_web_extension_get (void); void liferea_web_extension_initialize (LifereaWebExtension *extension, WebKitWebExtension *webkit_extension, const gchar *server_address); #endif liferea-1.13.7/src/webkit/web_extension/liferea_web_extension_names.h000066400000000000000000000022061415350204600260330ustar00rootroot00000000000000/** * @file liferea_web_extension_names.h Control WebKit2 via DBUS from Liferea * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _LIFEREA_WEB_EXTENSION_NAMES_H #define _LIFEREA_WEB_EXTENSION_NAMES_H #define LIFEREA_WEB_EXTENSION_OBJECT_PATH "/net/sf/liferea/WebExtension" #define LIFEREA_WEB_EXTENSION_BUS_NAME "net.sf.liferea.WebExtension" #define LIFEREA_WEB_EXTENSION_INTERFACE_NAME "net.sf.liferea.WebExtension" #endif liferea-1.13.7/src/webkit/web_extension/web_extension_main.c000066400000000000000000000025401415350204600241610ustar00rootroot00000000000000/** * @file web_extension_main.c Control WebKit2 via DBUS from Liferea * * Copyright (C) 2016 Leiaz * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include "liferea_web_extension.h" static LifereaWebExtension *extension = NULL; G_MODULE_EXPORT void webkit_web_extension_initialize_with_user_data (WebKitWebExtension *webkit_extension, GVariant *userdata) { extension = liferea_web_extension_get (); liferea_web_extension_initialize (extension, webkit_extension, g_variant_get_string (userdata, NULL)); } static void __attribute__((destructor)) web_extension_shutdown (void) { if (extension) g_object_unref (extension); } liferea-1.13.7/src/webkit/webkit.c000066400000000000000000000514711415350204600167270ustar00rootroot00000000000000/** * @file webkit.c WebKit2 browser module for Liferea * * Copyright (C) 2016-2019 Leiaz * Copyright (C) 2007-2021 Lars Windolf * Copyright (C) 2008 Lars Strojny * Copyright (C) 2009-2012 Emilio Pozuelo Monfort * Copyright (C) 2009 Adrian Bunk * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include "browser.h" #include "conf.h" #include "common.h" #include "enclosure.h" /* Only for enclosure_download */ #include "render.h" #include "ui/browser_tabs.h" #include "ui/liferea_htmlview.h" #include "web_extension/liferea_web_extension_names.h" #include "liferea_web_view.h" #define LIFEREA_TYPE_WEBKIT_IMPL liferea_webkit_impl_get_type () #define LIFEREA_WEBKIT_IMPL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), LIFEREA_TYPE_WEBKIT_IMPL, LifereaWebKitImpl)) #define IS_LIFEREA_WEBKIT_IMPL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), LIFEREA_TYPE_WEBKIT_IMPL)) #define LIFEREA_WEBKIT_IMPL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), LIFEREA_TYPE_WEBKIT_IMPL, LifereaWebKitImplClass)) #define IS_LIFEREA_WEBKIT_IMPL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), LIFEREA_TYPE_WEBKIT_IMPL)) #define LIFEREA_WEBKIT_IMPL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), LIFEREA_TYPE_WEBKIT_IMPL, LifereaWebKitImplClass)) typedef struct _LifereaWebKitImpl { GObject parent; GDBusServer *dbus_server; GList *dbus_connections; } LifereaWebKitImpl; typedef struct _LifereaWebKitImplClass { GObjectClass parent_class; } LifereaWebKitImplClass; G_DEFINE_TYPE (LifereaWebKitImpl, liferea_webkit_impl, G_TYPE_OBJECT) // singleton LifereaWebKitImpl *impl = NULL; enum { PAGE_CREATED_SIGNAL, N_SIGNALS }; static guint liferea_webkit_impl_signals [N_SIGNALS]; static void liferea_webkit_impl_dispose (GObject *gobject) { LifereaWebKitImpl *self = LIFEREA_WEBKIT_IMPL(gobject); g_clear_object (&self->dbus_server); g_list_free_full (self->dbus_connections, g_object_unref); /* Chaining dispose from parent class. */ G_OBJECT_CLASS(liferea_webkit_impl_parent_class)->dispose(gobject); } static void liferea_webkit_impl_class_init (LifereaWebKitImplClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); GType signal_params[2] = {G_TYPE_POINTER, G_TYPE_UINT64}; liferea_webkit_impl_signals[PAGE_CREATED_SIGNAL] = g_signal_newv ( "page-created", LIFEREA_TYPE_WEBKIT_IMPL, G_SIGNAL_RUN_FIRST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 2, signal_params); gobject_class->dispose = liferea_webkit_impl_dispose; } static LifereaWebKitImpl * liferea_webkit_impl_new (void) { return g_object_new (LIFEREA_TYPE_WEBKIT_IMPL, NULL); } /** * Update the settings object if the preferences change. * This will affect all the webviews as they all use the same * settings object. */ static void liferea_webkit_disable_javascript_cb (GSettings *gsettings, gchar *key, gpointer webkit_settings) { g_return_if_fail (key != NULL); g_object_set ( webkit_settings, "enable-javascript", !g_settings_get_boolean (gsettings, key), NULL ); } /** * Update the settings object if the preferences change. * This will affect all the webviews as they all use the same * settings object. */ static void liferea_webkit_enable_plugins_cb (GSettings *gsettings, gchar *key, gpointer webkit_settings) { g_return_if_fail (key != NULL); g_object_set ( webkit_settings, "enable-plugins", g_settings_get_boolean (gsettings, key), NULL ); } static void liferea_webkit_enable_itp_cb (GSettings *gsettings, gchar *key, gpointer user_data) { g_return_if_fail (key != NULL); #if WEBKIT_CHECK_VERSION (2, 30, 0) webkit_website_data_manager_set_itp_enabled ( webkit_web_context_get_website_data_manager (webkit_web_context_get_default()), g_settings_get_boolean (gsettings, key)); #endif } /* Font size math from Epiphany embed/ephy-embed-prefs.c to get font size in * pixels according to actual screen dpi. */ static gdouble get_screen_dpi (GdkMonitor *monitor) { gdouble dp, di; GdkRectangle rect; gdk_monitor_get_workarea (monitor, &rect); dp = hypot (rect.width, rect.height); di = hypot (gdk_monitor_get_width_mm (monitor), gdk_monitor_get_height_mm (monitor)) / 25.4; return dp / di; } static guint normalize_font_size (gdouble font_size, GtkWidget *widget) { /* WebKit2 uses font sizes in pixels. */ GdkDisplay *display; GdkMonitor *monitor; GdkScreen *screen; gdouble dpi; display = gtk_widget_get_display (widget); screen = gtk_widget_get_screen (widget); monitor = gdk_display_get_monitor_at_window (display, gtk_widget_get_window (widget)); if (screen) { dpi = gdk_screen_get_resolution (screen); if (dpi == -1) dpi = get_screen_dpi(monitor); } else dpi = 96; return font_size / 72.0 * dpi; } static gchar * webkit_get_font (guint *size) { gchar *font = NULL; *size = 11; /* default fallback */ /* font configuration support */ conf_get_str_value (USER_FONT, &font); if (NULL == font || 0 == strlen (font)) { if (NULL != font) { g_free (font); font = NULL; } conf_get_default_font_from_schema (DEFAULT_FONT, &font); } if (font) { /* The GTK2/GNOME font name format is " " */ gchar *tmp = strrchr(font, ' '); if (tmp) { *tmp++ = 0; *size = atoi(tmp); } } return font; } static gboolean liferea_webkit_authorize_authenticated_peer (GDBusAuthObserver *observer, GIOStream *stream, GCredentials *credentials, gpointer user_data) { gboolean authorized = FALSE; GCredentials *own_credentials = NULL; GError *error = NULL; if (!credentials) { g_printerr ("No credentials received from web extension.\n"); return FALSE; } own_credentials = g_credentials_new (); if (g_credentials_is_same_user (credentials, own_credentials, &error)) { authorized = TRUE; } else { g_printerr ("Error authorizing web extension : %s\n", error->message); g_error_free (error); } g_object_unref (own_credentials); return authorized; } static void liferea_webkit_on_dbus_connection_close (GDBusConnection *connection, gboolean remote_peer_vanished, GError *error, gpointer user_data) { LifereaWebKitImpl *webkit_impl = LIFEREA_WEBKIT_IMPL (user_data); if (!remote_peer_vanished && error) { g_warning ("DBus connection closed with error : %s", error->message); } webkit_impl->dbus_connections = g_list_remove (webkit_impl->dbus_connections, connection); g_object_unref (connection); } static void liferea_webkit_emit_page_created (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { guint64 page_id; LifereaWebKitImpl *webkit_impl = LIFEREA_WEBKIT_IMPL (user_data); g_variant_get (parameters, "(t)", &page_id); g_signal_emit (webkit_impl, liferea_webkit_impl_signals[PAGE_CREATED_SIGNAL], 0, (gpointer) connection, page_id); } static void on_page_created (LifereaWebKitImpl *instance, GDBusConnection *connection, guint64 page_id, gpointer web_view) { if (webkit_web_view_get_page_id (WEBKIT_WEB_VIEW (web_view)) == page_id) { liferea_web_view_set_dbus_connection (LIFEREA_WEB_VIEW (web_view), connection); } } static gboolean liferea_webkit_on_new_dbus_connection (GDBusServer *server, GDBusConnection *connection, gpointer user_data) { LifereaWebKitImpl *webkit_impl = LIFEREA_WEBKIT_IMPL (user_data); webkit_impl->dbus_connections = g_list_append (webkit_impl->dbus_connections, g_object_ref (connection)); g_signal_connect (connection, "closed", G_CALLBACK (liferea_webkit_on_dbus_connection_close), webkit_impl); g_dbus_connection_signal_subscribe ( connection, NULL, LIFEREA_WEB_EXTENSION_INTERFACE_NAME, "PageCreated", LIFEREA_WEB_EXTENSION_OBJECT_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, (GDBusSignalCallback)liferea_webkit_emit_page_created, webkit_impl, NULL); return TRUE; } static void liferea_webkit_initialize_web_extensions (WebKitWebContext *context, gpointer user_data) { gchar *guid = NULL; gchar *address = NULL; gchar *server_address = NULL; GError *error = NULL; GDBusAuthObserver *observer = NULL; LifereaWebKitImpl *webkit_impl = LIFEREA_WEBKIT_IMPL (user_data); guid = g_dbus_generate_guid (); address = g_strdup_printf ("unix:tmpdir=%s", g_get_tmp_dir ()); observer = g_dbus_auth_observer_new (); g_signal_connect (observer, "authorize-authenticated-peer", G_CALLBACK (liferea_webkit_authorize_authenticated_peer), NULL); webkit_impl->dbus_server = g_dbus_server_new_sync (address, G_DBUS_SERVER_FLAGS_NONE,//Flags guid, observer, NULL, //Cancellable &error); g_free (guid); g_free (address); g_object_unref (observer); if (webkit_impl->dbus_server == NULL) { g_printerr ("Error creating DBus server : %s\n", error->message); g_error_free (error); return; } g_dbus_server_start (webkit_impl->dbus_server); g_signal_connect (webkit_impl->dbus_server, "new-connection", G_CALLBACK (liferea_webkit_on_new_dbus_connection), webkit_impl); webkit_web_context_set_web_extensions_directory (context, WEB_EXTENSIONS_DIR); server_address = g_strdup (g_dbus_server_get_client_address (webkit_impl->dbus_server)); webkit_web_context_set_web_extensions_initialization_user_data (context, g_variant_new_take_string (server_address)); } static void liferea_webkit_impl_download_started (WebKitWebContext *context, WebKitDownload *download, gpointer user_data) { WebKitURIRequest *request = webkit_download_get_request (download); webkit_download_cancel (download); enclosure_download (NULL, webkit_uri_request_get_uri (request), TRUE); } static void liferea_webkit_handle_liferea_scheme (WebKitURISchemeRequest *request, gpointer user_data) { const gchar *uri = webkit_uri_scheme_request_get_uri (request); GInputStream *stream; gssize length; gchar *contents; contents = g_strdup_printf ("Placeholder handler for liferea scheme. URI requested : %s", uri); length = (gssize) strlen (contents); stream = g_memory_input_stream_new_from_data (contents, length, g_free); webkit_uri_scheme_request_finish (request, stream, length, "text/plain"); g_object_unref (stream); } static void liferea_webkit_impl_init (LifereaWebKitImpl *self) { gboolean enable_itp; WebKitSecurityManager *security_manager; WebKitWebsiteDataManager *website_data_manager; self->dbus_connections = NULL; webkit_web_context_register_uri_scheme (webkit_web_context_get_default(), "liferea", (WebKitURISchemeRequestCallback) liferea_webkit_handle_liferea_scheme,NULL,NULL); security_manager = webkit_web_context_get_security_manager (webkit_web_context_get_default ()); website_data_manager = webkit_web_context_get_website_data_manager (webkit_web_context_get_default ()); webkit_security_manager_register_uri_scheme_as_local (security_manager, "liferea"); conf_signal_connect ( "changed::" ENABLE_ITP, G_CALLBACK (liferea_webkit_enable_itp_cb), website_data_manager ); conf_get_bool_value (ENABLE_ITP, &enable_itp); #if WEBKIT_CHECK_VERSION (2, 30, 0) webkit_website_data_manager_set_itp_enabled (website_data_manager, enable_itp); #endif /* Webkit web extensions */ g_signal_connect ( webkit_web_context_get_default (), "initialize-web-extensions", G_CALLBACK (liferea_webkit_initialize_web_extensions), self); g_signal_connect ( webkit_web_context_get_default (), "download-started", G_CALLBACK (liferea_webkit_impl_download_started), self); } static LifereaWebKitImpl *liferea_webkit_impl = NULL; /** * HTML renderer init method */ static void liferea_webkit_init (void) { g_assert (!liferea_webkit_impl); liferea_webkit_impl = liferea_webkit_impl_new (); } /** * Load HTML string into the rendering scrollpane * * Load an HTML string into the web view. This is used to render * HTML documents created internally. */ static void liferea_webkit_write_html ( GtkWidget *webview, const gchar *string, const guint length, const gchar *base, const gchar *content_type ) { // FIXME Avoid doing a copy ? GBytes *string_bytes = g_bytes_new (string, length); /* Note: we explicitely ignore the passed base URL because we don't need it as Webkit supports
and throws a security exception when accessing file:// with a non-file:// base URL */ webkit_web_view_load_bytes ( WEBKIT_WEB_VIEW (webview), string_bytes, content_type, "UTF-8", "liferea://" ); g_bytes_unref (string_bytes); } static void liferea_webkit_run_js (GtkWidget *widget, gchar *js) { // No matter what was before we need JS now g_object_set (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (widget)), "enable-javascript", TRUE, NULL); webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (widget), js, NULL, NULL, NULL); g_free (js); } static void liferea_webkit_set_font_size (GtkWidget *widget, gpointer user_data) { WebKitSettings *settings = WEBKIT_SETTINGS(user_data); gchar *font; guint fontSize; if (!gtk_widget_get_realized (widget)) return; font = webkit_get_font (&fontSize); if (font) { g_object_set (settings, "default-font-family", font, NULL); fontSize = normalize_font_size (fontSize, widget); g_object_set (settings, "default-font-size", fontSize, NULL); g_free (font); } fontSize = normalize_font_size (7, widget); g_object_set (settings, "minimum-font-size", fontSize, NULL); } static void liferea_webkit_screen_changed (GtkWidget *widget, GdkScreen *previous_screen, gpointer user_data) { liferea_webkit_set_font_size (widget, user_data); } /** * Reset settings to safe preferences */ static void liferea_webkit_default_settings (WebKitSettings *settings) { gboolean disable_javascript, enable_plugins; conf_get_bool_value (DISABLE_JAVASCRIPT, &disable_javascript); g_object_set (settings, "enable-javascript", !disable_javascript, NULL); conf_get_bool_value (ENABLE_PLUGINS, &enable_plugins); g_object_set (settings, "enable-plugins", enable_plugins, NULL); webkit_settings_set_user_agent_with_application_details (settings, "Liferea", VERSION); conf_signal_connect ( "changed::" DISABLE_JAVASCRIPT, G_CALLBACK (liferea_webkit_disable_javascript_cb), settings ); conf_signal_connect ( "changed::" ENABLE_PLUGINS, G_CALLBACK (liferea_webkit_enable_plugins_cb), settings ); } /** * Initializes WebKit * * Initializes the WebKit HTML rendering engine. Creates a WebKitWebView. */ static GtkWidget * liferea_webkit_new (LifereaHtmlView *htmlview) { WebKitWebView *view; WebKitSettings *settings; view = WEBKIT_WEB_VIEW (liferea_web_view_new ()); settings = webkit_settings_new (); liferea_webkit_default_settings (settings); webkit_web_view_set_settings (view, settings); g_signal_connect_object ( liferea_webkit_impl, "page-created", G_CALLBACK (on_page_created), view, G_CONNECT_AFTER); /** Pass LifereaHtmlView into the WebKitWebView object */ g_object_set_data ( G_OBJECT (view), "htmlview", htmlview ); g_signal_connect (G_OBJECT (view), "screen_changed", G_CALLBACK (liferea_webkit_screen_changed), NULL); g_signal_connect (G_OBJECT (view), "realize", G_CALLBACK (liferea_webkit_set_font_size), settings); gtk_widget_show (GTK_WIDGET (view)); return GTK_WIDGET (view); } /** * Launch URL */ static void liferea_webkit_launch_url (GtkWidget *webview, const gchar *url) { // FIXME: hack to make URIs like "gnome.org" work // https://bugs.webkit.org/show_bug.cgi?id=24195 gchar *http_url; if (!strstr (url, "://")) { http_url = g_strdup_printf ("https://%s", url); } else { http_url = g_strdup (url); } // Force preference JS settings when launching external URL // needed, because we might be switching from internal reader mode liferea_webkit_default_settings (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (webview))); webkit_web_view_load_uri ( WEBKIT_WEB_VIEW (webview), http_url ); g_free (http_url); } /** * Change zoom level of the HTML scrollpane */ static void liferea_webkit_change_zoom_level (GtkWidget *webview, gfloat zoom_level) { webkit_web_view_set_zoom_level (WEBKIT_WEB_VIEW (webview), zoom_level); } /** * Copy selected text to the clipboard */ static void liferea_webkit_copy_selection (GtkWidget *webview) { webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (webview), WEBKIT_EDITING_COMMAND_COPY); } /** * Return current zoom level as a float */ static gfloat liferea_webkit_get_zoom_level (GtkWidget *webview) { return webkit_web_view_get_zoom_level (WEBKIT_WEB_VIEW (webview)); } /** * Scroll page down (via shortcut key) */ static void liferea_webkit_scroll_pagedown (GtkWidget *webview) { liferea_web_view_scroll_pagedown (LIFEREA_WEB_VIEW (webview)); } static void liferea_webkit_set_proxy (ProxyDetectMode mode, const gchar *host, guint port, const gchar *user, const gchar *pwd) { #if WEBKIT_CHECK_VERSION (2, 15, 3) WebKitNetworkProxySettings *proxy_settings = NULL; gchar *proxy_uri = NULL; gchar *user_pass = NULL, *host_port = NULL; switch (mode) { case PROXY_DETECT_MODE_AUTO: webkit_web_context_set_network_proxy_settings (webkit_web_context_get_default (), WEBKIT_NETWORK_PROXY_MODE_DEFAULT, NULL); break; case PROXY_DETECT_MODE_NONE: webkit_web_context_set_network_proxy_settings (webkit_web_context_get_default (), WEBKIT_NETWORK_PROXY_MODE_NO_PROXY, NULL); break; case PROXY_DETECT_MODE_MANUAL: /* Construct user:password part of the URI if specified. */ if (user) { user_pass = g_uri_escape_string (user, NULL, TRUE); if (pwd) { gchar *enc_user = user_pass; gchar *enc_pass = g_uri_escape_string (pwd, NULL, TRUE); user_pass = g_strdup_printf ("%s:%s", enc_user, enc_pass); g_free (enc_user); g_free (enc_pass); } } /* Construct the host:port part of the URI. */ if (port) { host_port = g_strdup_printf ("%s:%d", host, port); } else { host_port = g_strdup (host); } /* Construct proxy URI. */ if (user) { proxy_uri = g_strdup_printf("http://%s@%s", user_pass, host_port); } else { proxy_uri = g_strdup_printf("http://%s", host_port); } g_free (user_pass); g_free (host_port); proxy_settings = webkit_network_proxy_settings_new (proxy_uri, NULL); g_free (proxy_uri); webkit_web_context_set_network_proxy_settings (webkit_web_context_get_default (), WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy_settings); webkit_network_proxy_settings_free (proxy_settings); break; } #endif } /** * Load liferea.css via user style sheet */ static void liferea_webkit_set_style (GtkWidget *webview) { if (render_get_css () == NULL) return; WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager (WEBKIT_WEB_VIEW (webview)); webkit_user_content_manager_remove_all_style_sheets (manager); WebKitUserStyleSheet *stylesheet = webkit_user_style_sheet_new (render_get_css(), WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_STYLE_LEVEL_USER, NULL, NULL); webkit_user_content_manager_add_style_sheet (manager, stylesheet); webkit_user_style_sheet_unref (stylesheet); } /** * Reload the current contents of webview */ static void liferea_webkit_reload (GtkWidget *webview) { liferea_webkit_default_settings (webkit_web_view_get_settings (WEBKIT_WEB_VIEW (webview))); webkit_web_view_reload (webview); } static struct htmlviewImpl webkitImpl = { .init = liferea_webkit_init, .create = liferea_webkit_new, .write = liferea_webkit_write_html, .run_js = liferea_webkit_run_js, .launch = liferea_webkit_launch_url, .zoomLevelGet = liferea_webkit_get_zoom_level, .zoomLevelSet = liferea_webkit_change_zoom_level, .hasSelection = NULL, /* Was only useful for the context menu, can be removed */ .copySelection = liferea_webkit_copy_selection, /* Same. */ .scrollPagedown = liferea_webkit_scroll_pagedown, .setProxy = liferea_webkit_set_proxy, .setOffLine = NULL, // FIXME: blocked on https://bugs.webkit.org/show_bug.cgi?id=18893 .setStylesheet = liferea_webkit_set_style, .reload = liferea_webkit_reload }; DECLARE_HTMLVIEW_IMPL (webkitImpl); liferea-1.13.7/src/xml.c000066400000000000000000000416061415350204600147540ustar00rootroot00000000000000/** * @file xml.c XML helper methods for Liferea * * Copyright (C) 2003-2020 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "xml.h" #include #include #include #include #include #include #include #include #include #include "common.h" #include "debug.h" static void xml_buffer_parse_error(void *ctxt, const gchar * msg, ...); xmlDocPtr xhtml_parse (const gchar *html, gint len) { xmlDocPtr out = NULL; g_assert (html != NULL); g_assert (len >= 0); /* Note: NONET is not implemented so it will return an error because it doesn't know how to handle NONET. But, it might learn in the future. */ out = htmlReadMemory (html, len, NULL, "utf-8", HTML_PARSE_RECOVER | HTML_PARSE_NONET | ((debug_level & DEBUG_HTML)?0:(HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING))); return out; } static xmlNodePtr xhtml_find_body (xmlDocPtr doc) { xmlXPathContextPtr xpathCtxt = NULL; xmlXPathObjectPtr xpathObj = NULL; xmlNodePtr node = NULL; xpathCtxt = xmlXPathNewContext (doc); if (!xpathCtxt) goto error; xpathObj = xmlXPathEvalExpression (BAD_CAST "/html/body", xpathCtxt); if (!xpathObj) goto error; if (!xpathObj->nodesetval->nodeMax) goto error; node = xpathObj->nodesetval->nodeTab[0]; error: if (xpathObj) xmlXPathFreeObject (xpathObj); if (xpathCtxt) xmlXPathFreeContext (xpathCtxt); return node; } xmlDocPtr xhtml_extract_doc (xmlNodePtr xml, gint xhtmlMode, const gchar *defaultBase) { xmlChar *xml_base = NULL; xmlNs *ns; /* Create the new document and add the div tag*/ xmlDocPtr newDoc = xmlNewDoc (BAD_CAST "1.0" ); xmlNodePtr divNode = xmlNewNode (NULL, BAD_CAST "div"); xmlDocSetRootElement (newDoc, divNode); xmlNewNs (divNode, BAD_CAST "http://www.w3.org/1999/xhtml", NULL); /* Set the xml:base of the div tag */ xml_base = xmlNodeGetBase (xml->doc, xml); if (xml_base) { xmlNodeSetBase (divNode, xml_base ); xmlFree (xml_base); } else if (defaultBase) xmlNodeSetBase (divNode, BAD_CAST defaultBase); if (xhtmlMode == 0) { /* Read escaped HTML and convert to XHTML, placing in a div tag */ xmlDocPtr oldDoc; xmlNodePtr copiedNodes = NULL; gchar *escapedhtml; /* Parse the HTML into oldDoc*/ escapedhtml = (gchar *)xmlNodeListGetString (xml->doc, xml->xmlChildrenNode, 1); if (escapedhtml) { escapedhtml = g_strstrip (escapedhtml); /* stripping whitespaces to make empty string detection easier */ if (*escapedhtml) { /* never process empty content, xmlDocCopy() doesn't like it... */ xmlNodePtr body; oldDoc = xhtml_parse (escapedhtml, strlen (escapedhtml)); body = xhtml_find_body (oldDoc); /* Copy namespace from original documents root node. This is to determine additional namespaces for item content. For example to handle RSS 2.0 feeds as provided by LiveJournal: ... ... ... <span class='ljuser' lj:user='someone' style='white-space: nowrap;'><a href='http://community.livejournal.com/someone/profile'><img src='http://stat.livejournal.com/img/community.gif' alt='[info]' width='16' height='16' style='vertical-align: bottom; border: 0; padding-right: 2px;' /></a><a href='http://community.livejournal.com/someone/'><b>someone</b></a></span> ... ... ... Then we will want to extract and need to honour the xmlns:lj definition... */ ns = (xmlDocGetRootElement (xml->doc))->nsDef; while (ns) { xmlNewNs (divNode, ns->href, ns->prefix); ns = ns->next; } if (body) { /* Copy in the html tags */ copiedNodes = xmlDocCopyNodeList (newDoc, body->xmlChildrenNode); // FIXME: is the above correct? Why only operate on the first child node? // It might be unproblematic because all content is wrapped in a
... xmlAddChildList (divNode, copiedNodes); } xmlFreeDoc (oldDoc); } g_free (escapedhtml); } } else if (xhtmlMode == 1 || xhtmlMode == 2) { /* Read multiple XHTML tags and embed in div tag */ xmlNodePtr copiedNodes = xmlDocCopyNodeList (newDoc, xml->xmlChildrenNode); xmlAddChildList (divNode, copiedNodes); } return newDoc; } gchar * xhtml_extract (xmlNodePtr xml, gint xhtmlMode, const gchar *defaultBase) { xmlBufferPtr buf; xmlDocPtr newDoc; gchar *result = NULL; newDoc = xhtml_extract_doc (xml, xhtmlMode, defaultBase); if (!newDoc) return NULL; buf = xmlBufferCreate (); xmlNodeDump (buf, newDoc, xmlDocGetRootElement (newDoc), 0, 0 ); if (xmlBufferLength(buf) > 0) result = (gchar *)xmlCharStrdup ((gchar *)xmlBufferContent (buf)); xmlBufferFree (buf); xmlFreeDoc (newDoc); return result; } /* * Read HTML string and convert to XHTML, placing in a div tag */ gchar * xhtml_extract_from_string (const gchar *html, const gchar *defaultBase) { xmlDocPtr doc = NULL; xmlNodePtr body = NULL; gchar *result; /* never process empty content, xmlDocCopy() doesn't like it... */ if (html != NULL && !common_str_is_empty (html)) { doc = xhtml_parse (html, strlen (html)); body = xhtml_find_body (doc); if (body == NULL) body = xmlDocGetRootElement (doc); } if (body != NULL) result = xhtml_extract (body, 1, defaultBase); else result = g_strdup ("
"); xmlFreeDoc (doc); return result; } gboolean xhtml_is_well_formed (const gchar *data) { gchar *xml; gboolean result; errorCtxtPtr errors; xmlDocPtr doc; if (!data) return FALSE; errors = g_new0 (struct errorCtxt, 1); errors->msg = g_string_new (NULL); xml = g_strdup_printf ("\n\n%s", data); doc = xml_parse (xml, strlen (xml), errors); if (doc) xmlFreeDoc (doc); g_free (xml); g_string_free (errors->msg, TRUE); result = (0 == errors->errorCount); g_free (errors); return result; } typedef struct regex { GRegex *expr; const gchar *replace; } *regexPtr; static GSList *dhtml_strippers = NULL; static void xhtml_regex_add (GSList **regex, const gchar *pattern, const gchar *replace) { GError *err = NULL; regexPtr expr = g_new0 (struct regex, 1); expr->expr = g_regex_new (pattern, G_REGEX_CASELESS | G_REGEX_UNGREEDY | G_REGEX_DOTALL | G_REGEX_OPTIMIZE, 0, &err); expr->replace = replace; if (err) { g_warning ("xhtml_strip_setup: %s\n", err->message); g_error_free (err); return; } *regex = g_slist_append (*regex, expr); } static gchar * xhtml_regex (const gchar *html, GSList *strippers) { gchar *result = g_strdup (html); GSList *iter = strippers; while (iter) { GError *err = NULL; regexPtr expr = (regexPtr) iter->data; gchar *tmp = result; result = g_regex_replace_literal (expr->expr, tmp, -1, 0, expr->replace, 0, &err); if (err) { g_warning ("xhtml_strip: %s\n", err->message); g_error_free (err); err = NULL; } g_free (tmp); iter = g_slist_next (iter); } return result; } gchar * xhtml_strip_dhtml (const gchar *html) { if (!dhtml_strippers) { // Drop attribute xhtml_regex_add (&dhtml_strippers, "\\s+onload='[^']+'", ""); xhtml_regex_add (&dhtml_strippers, "\\s+onload=\"[^\"]+\"", ""); // Drop tags including content xhtml_regex_add (&dhtml_strippers, "<\\s*meta[^>]*>.*", ""); xhtml_regex_add (&dhtml_strippers, "<\\s*script[^>]*>.*", ""); xhtml_regex_add (&dhtml_strippers, "<\\s*iframe[^>]*>.*", ""); xhtml_regex_add (&dhtml_strippers, "<\\s*(meta|script|iframe)[^>]*/>", ""); // Drops tags but not their content xhtml_regex_add (&dhtml_strippers, "<\\s*/?wbr[^>]*/?\\s*>", ""); xhtml_regex_add (&dhtml_strippers, "<\\s*/?body[^>]*/?\\s*>", ""); } return xhtml_regex (html, dhtml_strippers); } typedef struct { gchar *data; gint length; } result_buffer; static void unhtmlizeHandleCharacters (void *user_data, const xmlChar *string, int length) { result_buffer *buffer = (result_buffer *)user_data; gint old_length; old_length = buffer->length; buffer->length += length; buffer->data = g_renew (gchar, buffer->data, buffer->length + 1); strncpy (buffer->data + old_length, (gchar *)string, length); buffer->data[buffer->length] = 0; } static void _unhtmlize (gchar *string, result_buffer *buffer) { htmlParserCtxtPtr ctxt; htmlSAXHandlerPtr sax_p; sax_p = g_new0 (htmlSAXHandler, 1); sax_p->characters = unhtmlizeHandleCharacters; ctxt = htmlCreatePushParserCtxt (sax_p, buffer, string, strlen (string), "", XML_CHAR_ENCODING_UTF8); htmlParseChunk (ctxt, string, 0, 1); htmlFreeParserCtxt (ctxt); g_free (sax_p); } static void _unxmlize (gchar *string, result_buffer *buffer) { xmlParserCtxtPtr ctxt; xmlSAXHandler *sax_p; sax_p = g_new0 (xmlSAXHandler, 1); sax_p->characters = unhtmlizeHandleCharacters; ctxt = xmlCreatePushParserCtxt (sax_p, buffer, string, strlen (string), ""); xmlParseChunk (ctxt, string, 0, 1); xmlFreeParserCtxt (ctxt); g_free(sax_p); } /* Converts a UTF-8 strings containing any XML stuff to a string without any entities or tags containing all text nodes of the given HTML string. The original string will be freed. */ static gchar * unmarkupize (gchar *string, void(*parse)(gchar *string, result_buffer *buffer)) { gchar *result; result_buffer *buffer; if (!string) return NULL; /* only do something if there are any entities or tags */ if(NULL == (strpbrk (string, "&<>"))) return string; buffer = g_new0 (result_buffer, 1); parse (string, buffer); result = buffer->data; g_free (buffer); if (result == NULL || !g_utf8_strlen (result, -1)) { /* Something went wrong in the parsing. * Use original string instead */ g_free (result); return string; } else { g_free (string); return result; } } gchar * unhtmlize (gchar * string) { return unmarkupize (string, _unhtmlize); } gchar * unxmlize (gchar * string) { return unmarkupize (string, _unxmlize); } #define MAX_PARSE_ERROR_LINES 10 /** * Error buffering function to be registered by * xmlSetGenericErrorFunc(). This function is called on * each libxml2 error output and collects all output as * HTML in the buffer ctxt points to. * * @param ctxt error context * @param msg printf like format string */ static void xml_buffer_parse_error (void *ctxt, const gchar * msg, ...) { va_list params; errorCtxtPtr errors = (errorCtxtPtr)ctxt; gchar *newmsg; gchar *tmp; if (MAX_PARSE_ERROR_LINES > errors->errorCount++) { va_start (params, msg); newmsg = g_strdup_vprintf (msg, params); va_end (params); /* Do never encode any invalid characters from error messages */ if (g_utf8_validate (newmsg, -1, NULL)) { tmp = g_markup_escape_text (newmsg, -1); g_free (newmsg); newmsg = tmp; g_string_append_printf(errors->msg, "%s\n", newmsg); } g_free(newmsg); } if (MAX_PARSE_ERROR_LINES == errors->errorCount) g_string_append (errors->msg, _("[There were more errors. Output was truncated!]")); } static xmlDocPtr entities = NULL; static xmlEntityPtr xml_process_entities (void *ctxt, const xmlChar *name) { xmlEntityPtr entity, found; xmlChar *tmp; entity = xmlGetPredefinedEntity (name); if (!entity) { if(!entities) { /* loading HTML entities from external DTD file */ entities = xmlNewDoc (BAD_CAST "1.0"); xmlCreateIntSubset (entities, BAD_CAST "HTML entities", NULL, BAD_CAST PACKAGE_DATA_DIR "/" PACKAGE "/dtd/html.ent"); entities->extSubset = xmlParseDTD (entities->intSubset->ExternalID, entities->intSubset->SystemID); } if (NULL != (found = xmlGetDocEntity (entities, name))) { /* returning as faked predefined entity... */ tmp = xmlStrdup (found->content); tmp = BAD_CAST unhtmlize ((gchar *)tmp); /* arghh ... slow... */ entity = g_new0 (xmlEntity, 1); entity->type = XML_ENTITY_DECL; entity->name = name; entity->orig = NULL; entity->content = tmp; entity->length = g_utf8_strlen ((gchar *)tmp, -1); entity->etype = XML_INTERNAL_PREDEFINED_ENTITY; } } if (!entity) { g_print("unsupported entity: %s\n", name); } return entity; } xmlNodePtr xpath_find (xmlNodePtr node, const gchar *expr) { xmlNodePtr result = NULL; if (node && node->doc) { xmlXPathContextPtr xpathCtxt = NULL; xmlXPathObjectPtr xpathObj = NULL; if (NULL != (xpathCtxt = xmlXPathNewContext (node->doc))) { xpathCtxt->node = node; xpathObj = xmlXPathEval (BAD_CAST expr, xpathCtxt); } if (xpathObj && !xmlXPathNodeSetIsEmpty (xpathObj->nodesetval)) result = xpathObj->nodesetval->nodeTab[0]; if (xpathObj) xmlXPathFreeObject(xpathObj); if (xpathCtxt) xmlXPathFreeContext(xpathCtxt); } return result; } gboolean xpath_foreach_match (xmlNodePtr node, const gchar *expr, xpathMatchFunc func, gpointer user_data) { if (node && node->doc) { xmlXPathContextPtr xpathCtxt = NULL; xmlXPathObjectPtr xpathObj = NULL; if (NULL != (xpathCtxt = xmlXPathNewContext (node->doc))) { xpathCtxt->node = node; xpathObj = xmlXPathEval (BAD_CAST expr, xpathCtxt); } if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeMax) { int i; for (i = 0; i < xpathObj->nodesetval->nodeNr; i++) (*func) (xpathObj->nodesetval->nodeTab[i], user_data); } if (xpathObj) xmlXPathFreeObject (xpathObj); if (xpathCtxt) xmlXPathFreeContext (xpathCtxt); return TRUE; } return FALSE; } gchar * xml_get_attribute (xmlNodePtr node, const gchar *name) { return (gchar *)xmlGetProp (node, BAD_CAST name); } gchar * xml_get_ns_attribute (xmlNodePtr node, const gchar *name, const gchar *namespace) { return (gchar *)xmlGetNsProp (node, BAD_CAST name, BAD_CAST namespace); } static void liferea_xml_errorSAXFunc (void * ctx, const char * msg,...) { va_list valist; gchar *parser_error = NULL; va_start(valist,msg); parser_error = g_strdup_vprintf (msg, valist); va_end(valist); debug1 (DEBUG_PARSING, "SAX parser error : %s", parser_error); g_free (parser_error); } xmlDocPtr xml_parse (const gchar *data, size_t length, errorCtxtPtr errCtx) { xmlParserCtxtPtr ctxt; xmlDocPtr doc; g_assert (NULL != data); ctxt = xmlNewParserCtxt (); ctxt->sax->getEntity = xml_process_entities; ctxt->sax->error = liferea_xml_errorSAXFunc; if (errCtx) xmlSetGenericErrorFunc (errCtx, (xmlGenericErrorFunc)xml_buffer_parse_error); doc = xmlSAXParseMemory (ctxt->sax, data, length, 0); /* This seems to reset the errorfunc to its default, so that the GtkHTML2 module is not unhappy because it also tries to call the errorfunc on occasion. */ xmlSetGenericErrorFunc (NULL, NULL); xmlFreeParserCtxt (ctxt); return doc; } xmlDocPtr xml_parse_feed (feedParserCtxtPtr fpc) { errorCtxtPtr errors; xmlDocPtr doc = NULL; g_assert (NULL != fpc->data); g_assert (NULL != fpc->feed); g_assert (NULL != fpc->feed->parseErrors); fpc->feed->valid = FALSE; /* we don't like no data */ if (0 == fpc->dataLength) { debug1 (DEBUG_PARSING, "xml_parse_feed(): empty input while parsing \"%s\"!", fpc->subscription->node->title); g_string_append (fpc->feed->parseErrors, "Empty input!\n"); return NULL; } errors = g_new0 (struct errorCtxt, 1); errors->msg = fpc->feed->parseErrors; doc = xml_parse (fpc->data, (size_t)fpc->dataLength, errors); if (!doc) { debug1 (DEBUG_PARSING, "xml_parse_feed(): could not parse feed \"%s\"!", fpc->subscription->node->title); g_string_prepend (fpc->feed->parseErrors, _("XML Parser: Could not parse document:\n")); g_string_append (fpc->feed->parseErrors, "\n"); } fpc->feed->valid = !(errors->errorCount > 0); g_free (errors); return doc; } void xml_init (void) { /* set libxml2 to use glib allocation, so that we can free() and reuse libxml2 allocated memory chunks */ xmlMemSetup (g_free, g_malloc, g_realloc, g_strdup); /* has to be called for multithreaded programs */ xmlInitParser (); } void xml_deinit (void) { GSList *iter = dhtml_strippers; while (iter) { g_regex_unref ((((regexPtr)iter->data))->expr); iter = g_slist_next (iter); } g_slist_free_full (dhtml_strippers, g_free); dhtml_strippers = NULL; } liferea-1.13.7/src/xml.h000066400000000000000000000140661415350204600147610ustar00rootroot00000000000000/** * @file xml.h XML helper methods for Liferea * * Copyright (C) 2003-2020 Lars Windolf * Copyright (C) 2004-2006 Nathan J. Conrad * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef _XML_H #define _XML_H #include #include #include #include "feed_parser.h" /** * Initialize XML parsing. */ void xml_init (void); /** * Deinit XML parsing */ void xml_deinit (void); /** * Retrieves the text content of an HTML chunk. All entities * will be replaced. All HTML tags are stripped. The passed * string will be freed. * * @param string the string to strip * * @returns stripped UTF-8 plain text string */ gchar * unhtmlize (gchar *string); /** * Retrieves the text content of an XML chunk. All entities * will be replaced. All XML tags are stripped. The passed * string will be freed. * * @param string the chunk to strip * * @returns stripped UTF-8 XHTML string */ gchar * unxmlize (gchar *string); /** * xhtml_parse: * * DOM parse an XHTML string. * * @html: The HTML * @nodeBase: An URI to set as xml:base, or #NULL * * Returns: XHTML version of the HTML */ xmlDocPtr xhtml_parse (const gchar *html, gint len); /** * Extract XHTML from a string of HTML and place it in a div tag. * * @param html The HTML * @param nodeBase An URI to set as xml:base, or #NULL * * @returns XHTML version of the HTML */ gchar * xhtml_extract_from_string (const gchar *html, const gchar *nodeBase); /** * Extract XHTML document from the children of the passed node. * * @param cur parent of the nodes that will be returned * @param xhtmlMode If 0, reads escaped HTML. * If 1, reads XHTML nodes as children, and wrap in div tag * If 2, Find a div tag, and return it as a string * @param defaultBase * @returns XHTML document containing children of passed node */ xmlDocPtr xhtml_extract_doc (xmlNodePtr cur, gint xhtmlMode, const gchar *defaultBase); /** * Extract XHTML string from the children of the passed node. * * @param cur parent of the nodes that will be returned * @param xhtmlMode If 0, reads escaped HTML. * If 1, reads XHTML nodes as children, and wrap in div tag * If 2, Find a div tag, and return it as a string * @param defaultBase * @returns XHTML version of children of passed node */ gchar * xhtml_extract (xmlNodePtr cur, gint xhtmlMode, const gchar *defaultBase); /** * Strips some DHTML constructs from the given HTML string. * * @param html some HTML content * * @return newly allocated stripped HTML string */ gchar * xhtml_strip_dhtml (const gchar *html); /** * Removes self closing tags (on one line) from HTML so that it renders correctly in the browser. * * @param html some HTML content * * @return newly allocated stripped HTML string */ gchar * xhtml_expand_self_closing_tag (const gchar *html); /** * Checks the given string for XHTML well formedness. * * @returns TRUE if the string is well formed XHTML */ gboolean xhtml_is_well_formed (const gchar *text); /** * Find the first XML node matching an XPath expression. * * @param node node to apply the XPath expression to * @param expr an XPath expression string * * @return first node found that matches expr (or NULL) */ xmlNodePtr xpath_find (xmlNodePtr node, const gchar *expr); /** Function type used by xpath_foreach_match() */ typedef void (*xpathMatchFunc)(xmlNodePtr match, gpointer user_data); /** * Executes an XPath expression and calls the given function for each matching node. * * @param node node to apply the XPath expression to * @param expr an XPath expression string * @param func the function to call for each result * * @return TRUE if result set was not empty */ gboolean xpath_foreach_match (xmlNodePtr node, const gchar *expr, xpathMatchFunc func, gpointer user_data); /** * Return the value of a attribute. * * @param node XML node * @param name attribute name * * @returns the attribute value (or NULL) to be free'd with g_free */ gchar * xml_get_attribute (xmlNodePtr node, const gchar *name); /** * Return the value of a attribute. * This is the namespace sensitive version of xml_get_attribute(). * * @param node XML node * @param name attribute name * @param namespace attribute namespace * * @returns the attribute value (or NULL) to be free'd with g_free */ gchar * xml_get_ns_attribute (xmlNodePtr node, const gchar *name, const gchar *namespace); /** used to keep track of error messages during parsing */ typedef struct errorCtxt { GString *msg; /**< message buffer */ gint errorCount; /**< error counter */ } *errorCtxtPtr; /** * Common function to create a XML DOM object from a given string * * @param data XML document buffer * @param length length of buffer * @param errors parser error context (can be NULL) * * @return XML document */ xmlDocPtr xml_parse (const gchar *data, size_t length, errorCtxtPtr errors); /** * Common function to create a XML DOM object from a given * XML buffer. This function sets up a parser context * and sets up the error handler. * * The function returns a XML document pointer or NULL * if the document could not be read. It also sets * errormsg to the last error messages on parsing * errors. * * @param fpc feed parsing context with valid data * * @return XML document */ xmlDocPtr xml_parse_feed (feedParserCtxtPtr fpc); #endif liferea-1.13.7/xslt/000077500000000000000000000000001415350204600142045ustar00rootroot00000000000000liferea-1.13.7/xslt/Makefile.am000066400000000000000000000006601415350204600162420ustar00rootroot00000000000000 xslt_in_files = item.xml.in \ feed.xml.in \ html5-extract.xml.in \ source.xml.in \ folder.xml.in \ newsbin.xml.in \ vfolder.xml.in xslt_files = $(xslt_in_files:.xml.in=.xml) i18n-filter.xslt xsltdir = $(pkgdatadir)/xslt xslt_DATA = $(xslt_files) @INTLTOOL_XML_RULE@ EXTRA_DIST = \ $(xslt_DATA) $(xslt_in_files) DISTCLEANFILES = $(xslt_in_files:.xml.in=.xml) liferea-1.13.7/xslt/feed.xml.in000066400000000000000000000165741415350204600162530ustar00rootroot00000000000000
<_span>Feed:
<_span>Source:
<_span>Publisher
<_span>There was a problem when fetching this subscription!
  • <_span>1. Authentication
  • <_span>2. Download
  • <_span>3. Feed Discovery
  • <_span>4. Parsing
<_span>Details:

<_span>Authentication failed. Please check the credentials and try again!

HTTP :

<_span>There was an error when downloading the feed source:

<_span>There was an error when running the feed filter command:

<_span>The source does not point directly to a feed or a webpage with a link to a feed!

<_span>Sorry, the feed could not be parsed!

<_span>You may want to contact the author/webmaster of the feed about this!

://
liferea-1.13.7/xslt/folder.xml.in000066400000000000000000000036671415350204600166220ustar00rootroot00000000000000
<_span>Folder:

<_span>children with <_span>unread headlines

liferea-1.13.7/xslt/html5-extract.xml.in000066400000000000000000000043461415350204600200430ustar00rootroot00000000000000 liferea-1.13.7/xslt/i18n-filter.xslt000066400000000000000000000037301415350204600171650ustar00rootroot00000000000000 liferea-1.13.7/xslt/item.xml.in000066400000000000000000000276251415350204600163050ustar00rootroot00000000000000
<_span>Source
<_span>Feed
<_span>Filed under ,
<_span>Author
<_span>Shared by
<_span>Via
<_span>Related
<_span>Also posted in
<_span>Creator
<_span>Coordinates ,
<_span>Map OpenStreeMap
<_span>View count
<_span>Rating / ( votes)

<_span>Comments ( <_span>Updating... )

<_span>Section <_span>Department

liferea-1.13.7/xslt/newsbin.xml.in000066400000000000000000000036731415350204600170110ustar00rootroot00000000000000
<_span>News Bin:

<_span>Add items to this news bin by selecting "Copy to News Bin" from the item list context menu.

liferea-1.13.7/xslt/reader.xml.in000066400000000000000000000042331415350204600165770ustar00rootroot00000000000000

Fetching content...

liferea-1.13.7/xslt/source.xml.in000066400000000000000000000036741415350204600166450ustar00rootroot00000000000000
<_span>Source:

<_span>children with <_span>unread headlines

liferea-1.13.7/xslt/vfolder.xml.in000066400000000000000000000035701415350204600170010ustar00rootroot00000000000000
<_span>Search Folder:

<_span>unread headlines